From 5baf8ad1d76623daf55c522a0524e46ee81bfe02 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 16:39:19 +0100 Subject: [PATCH] Knowledge base: progress is proven over the bus with the module started whole (issue 1947) The verb asks three seats and the records module; only a run over a real bus shows its grants and subjects line up with what the manifest invokes. --- .../cmd/mesh-knowledge/bus_test.go | 2 +- .../cmd/mesh-knowledge/progress_bus_test.go | 69 +++++++++++++++++++ .../cmd/mesh-knowledge/progress_test.go | 22 ++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/bus_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/bus_test.go index 0e47d456..e0b3249c 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/bus_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/bus_test.go @@ -72,7 +72,7 @@ func newBusWorld(t *testing.T) *busWorld { t.Fatal(err) } // What stands on the other side: the issue tracker, the records module, the agent's judgement. - records := fixture() + records := withDesigns{fixture(), progressRecords()} answer := func(key string, do func(body json.RawMessage) (any, error)) { if _, err := nc.Subscribe("ask."+key, func(m *nats.Msg) { w.mu.Lock() diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go new file mode 100644 index 00000000..95d3ffca --- /dev/null +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_bus_test.go @@ -0,0 +1,69 @@ +//go:build integration + +package main + +import ( + "encoding/json" + "strconv" + "strings" + "testing" + + "github.com/nats-io/nats.go" +) + +// 1947/AC1: the knowledge base running whole, as the runtime launches it, answers progress over the bus: it reads +// design 47 through the records module and asks the issue tracker and mesh-delivery on their seats' subjects, +// played here from what the live seats answered (testdata/progress). +func TestProgressOverTheBus(t *testing.T) { + w := newBusWorld(t) + seats := newSeatsWorld(t) + for _, key := range []string{"seat:issue-tracker.list", "seat:issue-tracker.get", "seat:mesh-delivery.deliveries", + "seat:mesh-delivery.groups", "seat:mesh-delivery.show"} { + key := key + if _, err := w.nc.Subscribe("ask."+key, func(m *nats.Msg) { + var body map[string]any + _ = json.Unmarshal(m.Data, &body) + out, err := seats.ask(key, body) + if err != nil { + _ = m.Respond([]byte(`{"error":` + strconv.Quote(err.Error()) + `}`)) + return + } + raw, _ := json.Marshal(map[string]any{"result": json.RawMessage(out)}) + _ = m.Respond(raw) + }); err != nil { + t.Fatal(err) + } + } + b, _ := w.start(t) + raw, err := b.call(Seat+".progress", map[string]any{"of": "design 47"}) + if err != nil { + t.Fatal(err) + } + var p Progress + if err := json.Unmarshal(raw, &p); err != nil { + t.Fatal(err) + } + lines := strings.Split(p.Tree, "\n") + if len(raw) > DefaultBound || p.Detail != DetailMinimal || !strings.HasPrefix(lines[0], "✗ design 47 · Delivery: from a commit to delivered — ") || + !strings.Contains(p.Tree, "▶ R6 6/7 1945 fixing since") || !strings.Contains(strings.Join(p.Warnings, "\n"), "issue 1209 is in milestones R3b and R3c") { + t.Fatalf("%d bytes:\n%s\n%v", len(raw), p.Tree, p.Warnings) + } + raw, err = b.call(Seat+".progress", map[string]any{"of": "milestone 47/R6", "detail": "detailed"}) + if err != nil { + t.Fatal(err) + } + _ = json.Unmarshal(raw, &p) + for _, want := range []string{"▶ milestone 47/R6 · ", "▶ 1945 ", "jschoubben/car-hunter@cc4c8ce12901 ready since", "depends on 1943", "✓ 1946 "} { + if !strings.Contains(p.Tree, want) { + t.Errorf("detailed: no %q in\n%s", want, p.Tree) + } + } + if _, err := b.call(Seat+".progress", map[string]any{"of": "design 47", "by": "someone@else"}); err == nil { + t.Error("an argument naming the caller was taken") + } + seats.mu.Lock() + defer seats.mu.Unlock() + if seats.asked["seat:issue-tracker.list"] != 2 || seats.asked["seat:mesh-delivery.groups"] != 1 { + t.Fatalf("asked %v", seats.asked) + } +} diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go index 9e00ae37..b4cd61b6 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/progress_test.go @@ -18,6 +18,28 @@ func progressRecords() dirRecords { return dirRecords{dir: filepath.Join("testdata", "progress", "hq")} } +// withDesigns is the fixture's records with the captured designs beside them, as the bus test's records module +// serves them: a folder or document the first does not hold is read from the second. +type withDesigns struct{ base, designs dirRecords } + +func (w withDesigns) Standing() (string, error) { return w.base.Standing() } + +func (w withDesigns) List(folder string) ([]string, []string, error) { + if f, d, err := w.base.List(folder); err == nil { + return f, d, nil + } + return w.designs.List(folder) +} + +func (w withDesigns) Read(path string) (string, string, error) { + if c, commit, err := w.base.Read(path); err == nil { + return c, commit, nil + } + c, _, err := w.designs.Read(path) + commit, _ := w.base.Standing() + return c, commit, err +} + // seatsWorld plays the issue tracker and mesh-delivery from the captured answers, and counts what it is asked. type seatsWorld struct { mu sync.Mutex