Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/kit_test.go
T
jochen ca3c3e6dfd mesh-knowledge tests: set the local zone once before any goroutine, not under a running bus server (race found by the repository check on #237)
inZone wrote time.Local while the bus test's nats server read it from a
goroutine that outlives its test; the race detector the repository check
now runs failed TestAReviewOwedOutlivesARestartAndItsLastTryIsSaid.
2026-10-12 13:25:09 +02:00

507 lines
16 KiB
Go

package main
// What the tests stand on: the knowledge base over each store — in memory always, and on a real postgres when one
// can be had (MESH_TEST_POSTGRES, or a private server started here from the machine's initdb on a socket of its own,
// listening on no port) — hq as real files copied from it at a commit (testdata/hq, and testdata/hq-main.tar.gz,
// the whole of main), and the issue tracker, the agent's judgement and the router played here.
import (
"archive/tar"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
const bundleVar = "MESH_KNOWLEDGE_TEST_BUNDLE"
// bundleDay is the bundle's clock: the day the fixture's proposals are dated (valid's Date), so a reservation and the
// proposal after it are of one day whatever day the test runs on (issue 540).
var bundleDay = time.Date(2026, 10, 11, 9, 0, 0, 0, time.UTC)
func TestMain(m *testing.M) {
if os.Getenv(bundleVar) == "1" {
// The bundle the bus test starts: the knowledge base as the runtime launches it, its store in memory.
k := newKB(newMemStore())
k.Now = func() time.Time { return bundleDay }
serve(k, os.Getenv("MESH_KNOWLEDGE_STATUS"))
os.Exit(0)
}
// Every test runs in a zone that is not UTC, so a time said locally is proven to be, wherever the tests run. It
// is set once, before any goroutine starts: a test that set it while a bus test's server read it raced.
time.Local = testZone
code := m.Run()
stopPostgres()
os.Exit(code)
}
// ---- postgres ------------------------------------------------------------------------------------------------
var (
pgOnce sync.Once
pgBase string
pgDir string
pgWhy string
pgCount int
pgMu sync.Mutex
)
func startPostgres() {
if url := os.Getenv("MESH_TEST_POSTGRES"); url != "" {
pgBase = url
return
}
initdb, err := exec.LookPath("initdb")
if err != nil {
pgWhy = "no postgres: MESH_TEST_POSTGRES is unset and the machine has no initdb"
return
}
pgctl := filepath.Join(filepath.Dir(initdb), "pg_ctl")
dir, err := os.MkdirTemp("", "mkpg")
if err != nil {
pgWhy = err.Error()
return
}
pgDir = dir
if out, err := exec.Command(initdb, "-D", dir+"/data", "-U", "postgres", "-A", "trust", "-E", "UTF8", "--no-locale").CombinedOutput(); err != nil {
pgWhy = fmt.Sprintf("initdb: %v: %s", err, out)
return
}
// A socket in its own directory and no port: nothing on this machine can reach it but these tests.
if out, err := exec.Command(pgctl, "-D", dir+"/data", "-l", dir+"/log", "-w", "-o",
"-k "+dir+" -c listen_addresses='' -F", "start").CombinedOutput(); err != nil {
pgWhy = fmt.Sprintf("pg_ctl start: %v: %s", err, out)
return
}
pgBase = "postgres://postgres@/postgres?host=" + dir
}
func stopPostgres() {
if pgDir == "" {
return
}
if initdb, err := exec.LookPath("initdb"); err == nil {
_ = exec.Command(filepath.Join(filepath.Dir(initdb), "pg_ctl"), "-D", pgDir+"/data", "-m", "immediate", "stop").Run()
}
_ = os.RemoveAll(pgDir)
}
// freshDatabase is a database of its own for one test, on the test postgres, or a skip saying why there is none.
func freshDatabase(t *testing.T) string {
t.Helper()
pgOnce.Do(startPostgres)
if pgBase == "" {
t.Skipf("NOT TESTED ON POSTGRES: %s", pgWhy)
}
pgMu.Lock()
pgCount++
name := fmt.Sprintf("kb_%d_%d", os.Getpid(), pgCount)
pgMu.Unlock()
ctx := context.Background()
admin, err := OpenPgStore(ctx, pgBase)
if err != nil {
t.Fatal(err)
}
defer admin.Close()
if _, err := admin.pool.Exec(ctx, "create database "+name); err != nil {
t.Fatal(err)
}
return replaceDatabase(pgBase, name)
}
func replaceDatabase(base, name string) string {
u, err := url.Parse(base)
if err != nil {
return base
}
u.Path = "/" + name
return u.String()
}
// pgStore is a prepared store on a fresh database.
func pgStore(t *testing.T) *PgStore {
t.Helper()
url := freshDatabase(t)
s, err := OpenPgStore(context.Background(), url)
if err != nil {
t.Fatal(err)
}
t.Cleanup(s.Close)
if _, err := s.Prepare(context.Background()); err != nil {
t.Fatal(err)
}
return s
}
// eachStore runs a test over the store in memory and over postgres.
func eachStore(t *testing.T, run func(t *testing.T, s Store)) {
t.Run("memory", func(t *testing.T) { run(t, newMemStore()) })
t.Run("postgres", func(t *testing.T) { run(t, pgStore(t)) })
}
// ---- hq ---------------------------------------------------------------------------------------------------------
// fixture is the handful of real hq files copied at one commit.
func fixture() dirRecords { return dirRecords{dir: filepath.Join("testdata", "hq")} }
var (
mainOnce sync.Once
mainDir string
mainErr error
)
// hqMain is the whole of hq's main at the commit testdata/hq-main.commit names, unpacked once.
func hqMain(t *testing.T) dirRecords {
t.Helper()
mainOnce.Do(func() {
dir, err := os.MkdirTemp("", "kb-hq-main")
if err != nil {
mainErr = err
return
}
mainDir = dir
f, err := os.Open(filepath.Join("testdata", "hq-main.tar.gz"))
if err != nil {
mainErr = err
return
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
mainErr = err
return
}
tr := tar.NewReader(gz)
for {
h, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
mainErr = err
return
}
path := filepath.Join(dir, filepath.FromSlash(h.Name))
if h.Typeflag == tar.TypeDir {
_ = os.MkdirAll(path, 0o755)
continue
}
_ = os.MkdirAll(filepath.Dir(path), 0o755)
raw, err := io.ReadAll(tr)
if err != nil {
mainErr = err
return
}
if err := os.WriteFile(path, raw, 0o644); err != nil {
mainErr = err
return
}
}
commit, _ := os.ReadFile(filepath.Join("testdata", "hq-main.commit"))
mainErr = os.WriteFile(filepath.Join(dir, "hq", "COMMIT"), commit, 0o644)
})
if mainErr != nil {
t.Fatal(mainErr)
}
return dirRecords{dir: filepath.Join(mainDir, "hq")}
}
// ---- what the knowledge base reaches, played here ------------------------------------------------------------
// tracker is the issue tracker's counters, played: the next number of a kind, from where hq's main stands.
type tracker struct {
mu sync.Mutex
next map[string]int
asked []map[string]string
}
func newTracker() *tracker {
return &tracker{next: map[string]int{KindDecision: 300, KindResearch: 50}}
}
func (tr *tracker) reserve(kind, title string) (int, error) {
tr.mu.Lock()
defer tr.mu.Unlock()
tr.asked = append(tr.asked, map[string]string{"kind": kind, "for": title})
n := tr.next[kind]
tr.next[kind]++
return n, nil
}
// judge is the agent's judgement, played: it drafts a clause from each sentence of a section that says "is", "are"
// or "decid", confirms every clause but those whose text holds "NOT EXACT", names clause 1 for a change, and
// approves every record it reviews unless told otherwise.
type judge struct {
mu sync.Mutex
questions []string
fail bool
// review is the answer a review gets, raw; approve when empty. reviewFail fails every review. during runs
// inside a review's judgement, while the knowledge base waits for it.
review string
reviewFail bool
reviews []string
during func()
}
func (j *judge) judge(question string, schema map[string]any, model string) (json.RawMessage, string, error) {
j.mu.Lock()
defer j.mu.Unlock()
j.questions = append(j.questions, question)
if j.fail {
return nil, "", fmt.Errorf("the agent holds no licence")
}
props := schema["properties"].(map[string]any)
switch {
case props["verdict"] != nil:
j.reviews = append(j.reviews, question)
if during := j.during; during != nil {
j.during = nil
j.mu.Unlock()
during()
j.mu.Lock()
}
if j.reviewFail {
return nil, "", fmt.Errorf("nothing answers claude-code.claude_code_judge")
}
if j.review != "" {
return json.RawMessage(j.review), "the agent on test (" + model + ")", nil
}
return json.RawMessage(`{"verdict":"approve","findings":[]}`), "the agent on test (" + model + ")", nil
case props["verdicts"] != nil:
var verdicts []map[string]any
for _, line := range strings.Split(question[strings.Index(question, "The drafted clauses:"):], "\n") {
m := regexp.MustCompile(`^(\d+)\. `).FindStringSubmatch(line)
if m == nil {
continue
}
exact := !strings.Contains(line, "NOT EXACT")
v := map[string]any{"clause": atoi(m[1]), "exact": exact}
if !exact {
v["reason"] = "the section says it of the controller only"
}
verdicts = append(verdicts, v)
}
raw, _ := json.Marshal(map[string]any{"verdicts": verdicts})
return raw, "the agent on test (" + model + ")", nil
case props["clauses"] != nil && props["reason"] != nil:
return json.RawMessage(`{"clauses":[1],"reason":"it replaces the first"}`), "the agent on test (" + model + ")", nil
case props["exact"] != nil:
return json.RawMessage(`{"exact":true,"reason":"it does"}`), "the agent on test (" + model + ")", nil
}
marker := "The Decision section:"
if !strings.Contains(question, marker) {
marker = "The record's sections:"
}
section := question[strings.Index(question, marker)+len(marker):]
var clauses []map[string]string
for _, s := range regexp.MustCompile(`[^.]+\.`).FindAllString(strings.Join(strings.Fields(section), " "), -1) {
s = strings.TrimSpace(s)
if len(clauses) < 4 && len(s) > 20 && regexp.MustCompile(`(?i)\b(is|are|decid)`).MatchString(s) {
clauses = append(clauses, map[string]string{"lead": "Drafted " + fmt.Sprint(len(clauses)+1), "text": s})
}
}
if len(clauses) == 0 {
clauses = append(clauses, map[string]string{"lead": "Drafted 1", "text": "what the section decides."})
}
raw, _ := json.Marshal(map[string]any{"clauses": clauses})
return raw, "the agent on test (" + model + ")", nil
}
// router is the operator channel, played: the asks published, and its own record of each, which only it writes.
type router struct {
mu sync.Mutex
asked []asks.Ask
records map[string]routerRecord
}
func newRouter() *router { return &router{records: map[string]routerRecord{}} }
func (r *router) Publish(subject string, body any, id string) error {
r.mu.Lock()
defer r.mu.Unlock()
if subject != asks.AskSubject(Module) {
return fmt.Errorf("published on %s", subject)
}
a := body.(asks.Ask)
r.asked = append(r.asked, a)
cp := a
r.records[a.ID] = routerRecord{State: "open", Ask: &cp}
return nil
}
func (r *router) Record(id string) (*stdio.StateEntry, error) {
r.mu.Lock()
defer r.mu.Unlock()
rec, ok := r.records[id]
if !ok {
return nil, nil
}
raw, _ := json.Marshal(rec)
return &stdio.StateEntry{Key: id, Value: raw, Revision: 1}, nil
}
// answer closes an ask as the router does when the operator taps an option on a channel that proves who answered.
func (r *router) answer(id, option string, at time.Time) asks.Warrant {
r.mu.Lock()
defer r.mu.Unlock()
rec := r.records[id]
o, _ := rec.Ask.Option(option)
w := asks.Warrant{Ask: id, Asker: Module, About: rec.Ask.About, Outcome: asks.OutcomeChosen, Option: o.ID, Label: o.Label,
Level: o.Level, By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "1", Verified: "user id verified"},
Channel: "telegram", At: at, AskDigest: rec.Ask.Digest()}
rec.State, rec.Warrant = "closed", &w
r.records[id] = rec
return w
}
// said is the events said.
type said struct {
mu sync.Mutex
events []map[string]any
}
func (s *said) emit(event string, body any) error {
s.mu.Lock()
defer s.mu.Unlock()
b := body.(map[string]any)
cp := map[string]any{"event": event}
for k, v := range b {
cp[k] = v
}
s.events = append(s.events, cp)
return nil
}
func (s *said) of(event string) []map[string]any {
s.mu.Lock()
defer s.mu.Unlock()
var out []map[string]any
for _, e := range s.events {
if e["event"] == event {
out = append(out, e)
}
}
return out
}
// world is one knowledge base and everything it reaches, played.
type world struct {
k *KB
tracker *tracker
judge *judge
router *router
said *said
told []string
// toldKeys are the keys told under, and cleared what was cleared: key, then its resolved line.
toldKeys []string
cleared [][2]string
resolved []string // the resolved line given with each notice raised
clock time.Time
}
func newWorld(t *testing.T, s Store, records Records) *world {
t.Helper()
w := &world{tracker: newTracker(), judge: &judge{}, router: newRouter(), said: &said{},
clock: time.Date(2026, 10, 11, 9, 0, 0, 0, time.UTC)}
w.k = &KB{Store: s, Records: records, Reserve: w.tracker.reserve, Judge: w.judge.judge, Asks: w.router,
Emit: w.said.emit, Settings: func() (Settings, error) { return Settings{}, nil }, Now: func() time.Time { return w.clock },
Logf: func(f string, a ...any) { t.Logf(f, a...) },
Notify: func(key, summary, headline, explanation, resolved string) error {
w.told, w.toldKeys, w.resolved = append(w.told, summary), append(w.toldKeys, key), append(w.resolved, resolved)
return nil
},
Clear: func(key, resolved string) error { w.cleared = append(w.cleared, [2]string{key, resolved}); return nil }}
return w
}
// valid is a proposal every rule takes, for the record numbered n, against the fixture's records.
func valid(n int) ProposeAsk {
return ProposeAsk{Kind: KindDecision, Number: n, Title: "A merge window closes after ninety seconds of quiet",
Topic: "the mesh", Date: "2026-10-11", Deciders: []string{"jochen"},
Clauses: []string{"**The window closes after ninety seconds of quiet.** A merge opens or extends it.",
"**A batch is delivered by one walk.** One commit per repository."},
Sections: []Section{
{Name: "Context", Text: "The merge window of [ADR 0276](0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md) waits too long."},
{Name: "Considered Options", Text: "1. **Keep it.** Rejected."},
{Name: "Consequences", Text: "Merges land sooner."},
{Name: "How it is checked", Text: "The controller's merge-window tests."},
},
Links: []LinkAsk{{Kind: LinkExtends, To: "0276"}, {Kind: LinkChanges, To: "0276 §1", From: 1}},
Homes: []string{testHome}}
}
// testHome is the hq design valid names as its home: a decision is accepted only cited (novox/hq issue 504).
const testHome = "03-DESIGN/01-to-be/55-the-knowledge-base.md"
// imported is a world whose knowledge base holds the fixture's records.
func imported(t *testing.T, s Store) *world {
t.Helper()
w := newWorld(t, s, fixture())
if _, err := w.k.Import("import@test", ImportAsk{}, fixture()); err != nil {
t.Fatal(err)
}
return w
}
// reviewAll takes every review waiting, as the background work does, and answers what each step said.
func (w *world) reviewAll(t *testing.T) []string {
t.Helper()
var said []string
for i := 0; i < 20; i++ {
s, err := w.k.ReviewStep()
if err != nil {
t.Fatal(err)
}
if s == "" {
return said
}
said = append(said, s)
}
t.Fatalf("the reviews never ended: %v", said)
return nil
}
// accepted proposes a valid record under a reserved number and has the knowledge base's review approve it.
func (w *world) accepted(t *testing.T, author string, p ProposeAsk) {
t.Helper()
if _, err := w.k.Propose(author, p); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
if e := mustGet(t, w.k.Store, Key{p.Kind, p.Number}); e.Pending != nil || !e.Current.Accepted() {
t.Fatalf("%s not accepted after its review: %s, %+v", Key{p.Kind, p.Number}, e.Current.Status, e.Working().Judging)
}
}
// reserved reserves a decision number and answers it.
func (w *world) reserved(t *testing.T, caller string) int {
t.Helper()
r, err := w.k.ReserveNumber(caller, KindDecision, "a test record")
if err != nil {
t.Fatal(err)
}
return r.Number
}
func mustGet(t *testing.T, s Store, k Key) *Entry {
t.Helper()
e, err := s.Get(context.Background(), k)
if err != nil || e == nil {
t.Fatalf("%s: %v %v", k, e, err)
}
return e
}