diff --git a/cmd/mesh-controller/replay201_test.go b/cmd/mesh-controller/replay201_test.go new file mode 100644 index 00000000..c852ef20 --- /dev/null +++ b/cmd/mesh-controller/replay201_test.go @@ -0,0 +1,90 @@ +package main + +import ( + "encoding/json" + "testing" + "time" + + "github.com/novox/mesh-controller/internal/catalogue" + "github.com/novox/mesh-controller/internal/inventory" +) + +// TestReplay201ThePlansSend replays novox/hq issue 201 (2026-10-02): two merges a minute apart made two plans; the +// controller's plan rolled the newer controller onto the control node, and the node-engine's plan then sent that +// machine the controller build before it, whose binary refused the seat row the newer one had widened. Here the +// control node, anchor, was last sent the newer controller c2 while the mesh's record of the controller is the +// build before it, c1, which passed its gate. The outcome asserted: a send that is no person's push — composed as a +// plan's send composes it (keepingRecorded, what sendKeeps gives every send through sendToEach) — carries the +// controller at c2, the build the machine was last sent, and not the older record; a person's push still carries +// the record. Written with only what the controller had before its fix (mesh-controller #273) — the inventory's +// build, gate, assignment and send records, keepingRecorded, planFor and declarationFor — so it is laid over the +// commit before; the fix's own TestReplay201 reads what the fix added. Needs the store (MESH_TEST_POSTGRES), and +// is named in testdata/store-tests. +func TestReplay201ThePlansSend(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + inv := open.inventory + const older, newer = "c1aaaaaaaaaa", "c2bbbbbbbbbb" + old, now := time.Now().Add(-2*time.Hour), time.Now().Add(-time.Minute) + record := func(commit string, asked time.Time) inventory.Build { + t.Helper() + manifest, _ := json.Marshal(catalogue.Manifest{Module: "mesh-controller", Version: "1"}) + b := inventory.Build{ID: "build-mesh-controller-" + commit, Module: "mesh-controller", Commit: commit, + Repository: "novox/mesh-controller", Manifest: manifest, Asked: asked, At: asked, + Made: []inventory.Artifact{{Name: "x", Kind: "bundle", Reference: "sha256:" + commit + "0000000000000000"}}} + if err := inv.RecordBuild(ctx, b); err != nil { + t.Fatal(err) + } + return b + } + + // The older controller rolled out and passed its gate; the newer one was asked after it and registered. + before := record(older, old) + if err := inv.RecordGate(ctx, inventory.GateVerdict{Build: before.ID, Module: "mesh-controller", Commit: older, + Machines: []string{"anchor"}, Verdict: inventory.GatePassed}); err != nil { + t.Fatal(err) + } + record(newer, now) + if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-controller", Version: "1"}, inventory.Source{ + Repository: "novox/mesh-controller", BuiltFrom: newer, Head: newer, Asked: now}); err != nil { + t.Fatal(err) + } + if _, err := inv.Assign(ctx, "anchor", "mesh-controller"); err != nil { + t.Fatal(err) + } + // The controller's plan sent anchor the newer controller. + if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor", map[string]string{"mesh-controller": newer}); err != nil { + t.Fatal(err) + } + // And the mesh's record of the controller is the build before it — whatever made it so. + if err := inv.RestoreModule(ctx, before); err != nil { + t.Fatal(err) + } + + // The controller each send to anchor carries: a plan's send keeps recorded builds (keepingRecorded, what + // sendKeeps gives every send but a person's push); a person's push composes the mesh's record. + controllerSent := func(sends string, keeping bool) string { + t.Helper() + c := ctx + if keeping { + c = keepingRecorded(ctx) + } + plan, settings, err := planFor(c, open, "anchor") + if err != nil { + t.Fatalf("%s to anchor did not compose: %v", sends, err) + } + declared, err := declarationFor(c, open, "anchor", plan, settings) + if err != nil { + t.Fatalf("%s to anchor did not compose: %v", sends, err) + } + return declared.Builds["mesh-controller"] + } + + if got := controllerSent("a plan's send", true); got != newer { + t.Fatalf("a plan's send carried anchor the controller %s, older than the %s anchor was last sent: the "+ + "older binary would read the row its successor widened", short(got), short(newer)) + } + if got := controllerSent("a person's push", false); got != older { + t.Fatalf("a person's push carried the controller %s, not the mesh's record %s", short(got), short(older)) + } +} diff --git a/testdata/store-tests b/testdata/store-tests index 7586e2d0..b9f34a82 100644 --- a/testdata/store-tests +++ b/testdata/store-tests @@ -35,3 +35,4 @@ ./cmd/mesh-controller TestARollbackWhoseSendWasRefusedIsCompletedByTheNextSend ./cmd/mesh-controller TestAGateJudgesTheOtherMovesBesideAKeptCore ./cmd/mesh-controller TestReplay401 +./cmd/mesh-controller TestReplay201ThePlansSend