From 7597294ff86383c171b8bec3920b5472fbce00e9 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 7 Oct 2026 23:38:29 +0200 Subject: [PATCH] Replay issues 296, 299 and 300 as tests the commit before each fix fails (hq ADR 0237) --- cmd/mesh-controller/replays_test.go | 181 ++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/cmd/mesh-controller/replays_test.go b/cmd/mesh-controller/replays_test.go index aeaabe1b..2ba09fd0 100644 --- a/cmd/mesh-controller/replays_test.go +++ b/cmd/mesh-controller/replays_test.go @@ -6,13 +6,16 @@ import ( "fmt" "os" "slices" + "strings" "testing" "time" + "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" + "github.com/novox/mesh-controller/internal/testbus" ) // The replays of the controller's incidents (novox/hq to-be 45 §9, M9): each a scripted replay of what @@ -341,3 +344,181 @@ func TestReplaySilentWebAppIsRaisedWithinTwoLooks(t *testing.T) { } } } + +// **R296 — a plan's clock counts from its tier, not from its last save.** On 2026-10-07 a plan for a +// merge to the catalogue, its one module's build asked at 19:48:41 and queued at the build seat for +// minutes, read "tier 1 of 1, building for 1s", then 4s, then 9s: the line counted from the plan's last +// save, and every advance — on the 30-second tick and after every build outcome on the mesh — saved it +// whether or not anything moved. A clock reset by each save is never LATE, so `plans` and `status` could +// not say a plan was stuck. The outcome asserted: a plan in its tier for forty minutes, saved seconds ago, +// says forty minutes and LATE. +func TestReplay296APlansLineCountsFromWhenItEnteredItsTier(t *testing.T) { + now := time.Date(2026, 10, 7, 20, 30, 0, 0, time.UTC) + entered := time.Date(2026, 10, 7, 19, 48, 41, 0, time.UTC) + asked := entered + p := inventory.Plan{ID: "plan-296", Repository: "novox/mesh-catalog", Branch: "main", Commit: "3ad22356", + State: inventory.PlanBuilding, Created: entered, TierEntered: entered, Updated: now.Add(-4 * time.Second), + Tiers: [][]string{{"app"}}, Modules: map[string]*inventory.PlanModule{"app": {State: "asked", AskedAt: &asked}}} + line := planLine(p, now) + if !strings.Contains(line, "for 41m19s") || !strings.Contains(line, "LATE") { + t.Fatalf("a plan in its tier since 19:48:41, read at 20:30:00 and saved 4s before, reads %q", line) + } +} + +// **R296, the save.** The same live case from the store's side: a plan whose only build was asked and +// still waits is advanced as the tick and the mesh's build outcomes advance it, and is not written again — +// its revision and its last save stay where its last change left them. Before the fix each advance saved +// it, and its revision went from 3 to 6. +func TestReplay296APlanStandingStillInItsTierIsNotSavedAgain(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + inv := open.inventory + asked := asksRecorded(t) + if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "app", Version: "1"}, + inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/app", Ref: "main", + BuiltFrom: "c0", Head: "c0"}); err != nil { + t.Fatal(err) + } + m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "3ad22356aa", + Paths: []string{"modules/app/index.ts"}, ModuleDirs: []string{"modules/app"}, ModuleDirsSaid: true} + if err := (following{open: open}).SourceMoved(ctx, m); err != nil { + t.Fatal(err) + } + advanceHeld(ctx, open) + recent, err := inv.RecentPlans(ctx, 1) + if err != nil || len(recent) != 1 || len(*asked) != 1 { + t.Fatalf("no plan waiting on its build: %v %v, asked %v", recent, err, *asked) + } + before := recent[0] + if before.State != inventory.PlanBuilding || before.Modules["app"] == nil || before.Modules["app"].State != "asked" { + t.Fatalf("the plan is not waiting on its build: %+v", before) + } + for range 3 { + advanceHeld(ctx, open) + } + after, err := inv.PlanByID(ctx, before.ID) + if err != nil { + t.Fatal(err) + } + if after.Revision != before.Revision || !after.Updated.Equal(before.Updated) { + t.Fatalf("a plan that did not move was saved again: revision %d → %d, saved %s → %s", + before.Revision, after.Revision, before.Updated, after.Updated) + } +} + +// **R299 — a holder of a seat whose verbs are all optional is not silent.** On 2026-10-07 the +// node-uplink seat gained its first verbs, `resolvers` and `links`, both optional while its holders catch +// up (ADR 0246, step 1); its holders served neither, so they registered nothing on the bus's discovery for +// it, and the self-check's D3 raised `seat.node-uplink..silent` on every machine of the mesh, +// which the healer then acted on. The outcome asserted: through D3 itself — the seats read back from the +// store as the controller reads them, a holder on record on every machine heard from, and nothing +// answering the discovery — no holder of a seat whose verbs are all optional is said silent. +// +// The live seat is replayed while its verbs are all optional; beside it, a seat whose verbs are all +// optional by definition, so the outcome is still held once node-uplink's verbs are required (step 3). +func TestReplay299AHolderOfASeatWhoseVerbsAreAllOptionalIsNotSilent(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + inv := open.inventory + catchingUp := catalogue.Seat{Name: "node-catching-up", Scope: catalogue.ScopeNode, Decision: "novox/hq issue 299", + Serves: []catalogue.Verb{{Name: "first", Optional: true}, {Name: "second", Optional: true}}} + if _, err := inv.SeedSeats(ctx, append(catalogue.DefaultSeats(), catchingUp)); err != nil { + t.Fatal(err) + } + rows, err := inv.Seats(ctx) + if err != nil { + t.Fatal(err) + } + // The working set as the serving controller holds it: the store's rows, the compiled seats' marks. + // A seat the compiled set does not know keeps its mark only as defined here. + for i := range rows { + if rows[i].Name == catchingUp.Name { + rows[i].Serves = catchingUp.Serves + } + } + catalogue.UseSeats(rows) + t.Cleanup(func() { catalogue.UseSeats(catalogue.DefaultSeats()) }) + + seats := []string{catchingUp.Name} + if uplink, ok := catalogue.SeatNamed("node-uplink"); ok && len(uplink.Serves) > 0 { + allOptional := true + for _, v := range uplink.Serves { + allOptional = allOptional && v.Optional + } + if allOptional { + seats = append(seats, "node-uplink") + } else { + t.Logf("node-uplink's verbs are required at this commit; the live seat is not replayed, its kind is") + } + } + + register(t, open, catalogue.Manifest{Module: "networkmanager", Version: "1"}) + now := time.Now() + var heard []machineFacts + for _, node := range []string{"anchor", "laptop"} { + if _, err := inv.Assign(ctx, node, "networkmanager"); err != nil { + t.Fatal(err) + } + for _, seat := range seats { + if err := inv.HoldSeat(ctx, seat, catalogue.ScopeNode, node, "networkmanager"); err != nil { + t.Fatal(err) + } + } + heard = append(heard, machineFacts{name: node, lastHeard: now}) + } + js, err := broker.Dial(testbus.URL(t)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(js.Close) + d := &doctor{open: open, js: js, watchdogs: &watchdogs{last: &signalFacts{now: now, machines: heard}}} + + found, err := probeHolders(ctx, d) + if err != nil { + t.Fatal(err) + } + for _, o := range found { + for _, seat := range seats { + if strings.HasPrefix(o.ID, seat+".") { + t.Errorf("a holder serving none of a seat's optional verbs was said silent: %s (%s)", o.Summary, o.Said) + } + } + } +} + +// **R300 — a merge that adds a module builds it.** On 2026-10-07 a pull request to the catalogue added +// one module, systemd-resolved; its delivery plan said "builds new: modules/systemd-resolved, sent +// nowhere", and at the merge the controller said it "changed nothing any module the mesh holds is built +// from" and built nothing, so the module was never registered and `assign` refused it until a person +// built it by hand. The outcome asserted: the merge asks the build seat for the new module's directory, at +// the branch merged into, from the repository as the mesh spells it, and opens no plan for it. +func TestReplay300AMergeAddingAModuleAsksForItsBuild(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + inv := open.inventory + var asked [][3]string + was := askABuild + askABuild = func(_ context.Context, source buildSource, path, ref string) (string, error) { + asked = append(asked, [3]string{source.Repository, path, ref}) + return "build-" + path, nil + } + t.Cleanup(func() { askABuild = was }) + if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "networkmanager", Version: "1"}, + inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/networkmanager", Ref: "main", + BuiltFrom: "c0", Head: "c0"}); err != nil { + t.Fatal(err) + } + m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "3da80a4b00", + Paths: []string{"modules/systemd-resolved/module.json", "modules/systemd-resolved/cmd/main.go"}, + ModuleDirs: []string{"modules/systemd-resolved"}, ModuleDirsSaid: true} + if err := (following{open: open}).SourceMoved(ctx, m); err != nil { + t.Fatal(err) + } + want := [3]string{"novox/mesh-catalog", "modules/systemd-resolved", "main"} + if len(asked) != 1 || asked[0] != want { + t.Fatalf("the merge adding modules/systemd-resolved asked the build seat for %v, not %v", asked, want) + } + if plans, err := inv.OpenPlans(ctx); err != nil || len(plans) != 0 { + t.Fatalf("a merge moving nothing the mesh holds opened a plan: %+v %v", plans, err) + } +}