package main import ( "strings" "testing" "time" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/lease" ) // novox/hq issue 352: on 2026-10-09 a release's gate on the control node read the machine's report against a // newer send another plan had just made there — not against its own send — and failed three builds the // machine had reported healthy ("has not reported on what it was sent"), put them back on every machine, // to a controller older than the store's schema, and that controller then judged the newer plan's // controller passed from the put-back build's health. // TestReplay352 replays the walk on the backlog fixture: the release sends anchor and anchor reports; // another send reaches anchor, unreported; the gate still passes. And a send that moves a judged module // to another build supersedes the judging: no verdict, nothing put back. // A plan's own first send waits while a release judges the same module on that machine with another build. // A merge plan's judging is superseded the same way: another send moved its module on the first machine. // A report is on the gate's own send: the declaration itself, or one sequenced after it; a gate kept // without its send reads the report against the send made last, as before. Pure. func TestAReportIsHeldAgainstTheGatesOwnSend(t *testing.T) { withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks) sent := inventory.SentDeclaration{Digest: "d-490", Sequence: 490} for _, c := range []struct { r inventory.Reported want bool }{ {inventory.Reported{Declared: "d-490", Current: false}, true}, {inventory.Reported{Declared: "d-491", ReportedSequence: 491, Current: true}, true}, {inventory.Reported{Declared: "d-489", ReportedSequence: 489, Current: false}, false}, {inventory.Reported{Declared: "other", ReportedSequence: 490}, true}, // the same sequence, said by another digest {inventory.Reported{Declared: "d-495", ReportedSequence: 495, Current: true}, true}, // the last send: this one or a later one {inventory.Reported{Declared: "", Current: false}, false}, } { if got := sent.ReportsOn(c.r); got != c.want { t.Errorf("%+v on %+v: %v", c.r, sent, got) } } byDigest := inventory.SentDeclaration{Digest: "d-1"} if !byDigest.ReportsOn(inventory.Reported{Declared: "d-1"}) || byDigest.ReportsOn(inventory.Reported{ReportedSequence: 5}) || !byDigest.ReportsOn(inventory.Reported{Current: true}) { t.Fatal("a send kept without a sequence is matched by its digest and by the last send alone") } f := gateFacts{sent: map[string]inventory.SentDeclaration{"anchor": sent}} if !f.reportedOn("anchor", inventory.Reported{Declared: "d-490"}) || f.reportedOn("anchor", inventory.Reported{Declared: "d-1"}) { t.Fatal("a gate that kept its send read the report against something other than it") } if !f.reportedOn("laptop", inventory.Reported{Current: true}) || f.reportedOn("laptop", inventory.Reported{Current: false}) { t.Fatal("a gate that did not keep its send does not read the report against the send made last") } // Through the merge plan's first-machine wait too. at := time.Now() state := inventory.PlanModule{First: []string{"anchor"}, FirstAt: &at, Gate: &inventory.PlanGate{Machines: []string{"anchor"}, Sent: map[string]inventory.SentDeclaration{"anchor": sent}}} reports := []inventory.Reported{{Node: "anchor", At: &at, Outcome: inventory.OutcomeApplied, Current: false, Declared: "d-490"}} if step := nextRollout(state, []string{"anchor", "laptop"}, false, reports, at.Add(time.Minute), time.Hour); step.waiting != "" || step.failed != "" { t.Fatalf("the first machine's report on the plan's own send read as none: %+v", step) } reports[0].Declared = "d-480" if step := nextRollout(state, []string{"anchor", "laptop"}, false, reports, at.Add(time.Minute), time.Hour); step.waiting == "" { t.Fatalf("a report on an older send read as the plan's: %+v", step) } } // A gate judges only the build the machine was last sent: last sent another build of the module, the // judging is superseded, whatever the machine reports. Pure. func TestAGateJudgesOnlyTheBuildTheMachineWasLastSent(t *testing.T) { withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks) at := time.Now() f := gateFacts{now: at, reports: map[string]inventory.Reported{"anchor": {Node: "anchor", Outcome: inventory.OutcomeApplied, At: &at, Current: true}}, engines: map[string]string{}, served: map[string]served{}, rolledBack: map[string][]lease.Rollback{}, commits: map[string]string{"mesh-controller": "e6b00e2e"}, sentBuilds: map[string]map[string]string{"anchor": {"mesh-controller": "ef26d4cb"}}} taken := at.Add(-30 * time.Second) f.holder = &lease.Holder{Taken: taken, Health: &lease.Health{Ready: true}} h, why := judgeHealth("mesh-controller", lease.ComponentController, catalogue.Manifest{}, "anchor", at.Add(-time.Minute), f) if h != healthSuperseded || !strings.Contains(why, "ef26d4cb") || !strings.Contains(why, "e6b00e2e") { t.Fatalf("a controller build the machine no longer runs: %v %q", h, why) } f.sentBuilds["anchor"]["mesh-controller"] = "e6b00e2e" if h, why := judgeHealth("mesh-controller", lease.ComponentController, catalogue.Manifest{}, "anchor", at.Add(-time.Minute), f); h == healthSuperseded { t.Fatalf("the build sent read as another: %q", why) } delete(f.sentBuilds, "anchor") if h, why := judgeHealth("mesh-controller", lease.ComponentController, catalogue.Manifest{}, "anchor", at.Add(-time.Minute), f); h == healthSuperseded { t.Fatalf("a machine whose send is not known read as superseded: %q", why) } g := &inventory.PlanGate{To: "c2", Carried: []inventory.CarriedMove{{Module: "late", Node: "anchor", To: "c5"}}} if got := judgedCommits(g, []judged{{"app", "anchor"}, {"late", "anchor"}}); got["app"] != "c2" || got["late"] != "c5" { t.Fatalf("judged commits %v", got) } } // A move of another build of a module to a machine where a release or a plan is judging that module // waits for that judging; the same build to that machine is already there. Pure. func TestAWalkWaitsForAJudgingOfTheSameModuleOnThatMachine(t *testing.T) { withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks) at := time.Now() release := inventory.Plan{ID: "release-1", State: inventory.PlanRolling, Release: &inventory.PlanRelease{ Gate: &inventory.PlanGate{Machines: []string{"novox"}, Carried: []inventory.CarriedMove{ {Module: "mesh-controller", Node: "novox", From: "ef26d4cb", To: "2913c54c"}}}}} merge := inventory.Plan{ID: "plan-1", State: inventory.PlanRolling, Modules: map[string]*inventory.PlanModule{ "app": {First: []string{"anchor"}, FirstAt: &at, Commit: "c2", Gate: &inventory.PlanGate{Machines: []string{"anchor"}}}}} f := moveFacts{plans: []inventory.Plan{release, merge}} for _, c := range []struct { module, node, to, want string }{ {"mesh-controller", "novox", "e6b00e2e", "release-1"}, // the day's case: a newer controller to the judged machine {"mesh-controller", "novox", "2913c54c", ""}, // the same build: already there {"mesh-controller", "ace", "2913c54c", "release-1"}, // another machine while the first is judged {"mesh-host", "novox", "x", ""}, // a module the release does not carry {"app", "anchor", "c2", ""}, {"app", "anchor", "c3", "plan-1"}, {"app", "laptop", "c2", "plan-1"}, } { if got := f.walkedBy(c.module, c.node, c.to); got != c.want { t.Errorf("%s %s to %s: walked by %q, want %q", c.module, c.to, c.node, got, c.want) } } release.Release.Gate.Verdict = inventory.GatePassed merge.Modules["app"].Gate.Verdict = inventory.GatePassed if f.walkedBy("mesh-controller", "novox", "e6b00e2e") != "" || f.walkedBy("app", "laptop", "c3") != "" { t.Fatal("a passed judging still holds a move") } release.Release.Gate.Verdict = "" f.plans[0].State = inventory.PlanSuperseded if f.walkedBy("mesh-controller", "novox", "e6b00e2e") != "" { t.Fatal("a closed release still holds a move") } } // The controller is never put back to a build that reaches less of the store's schema than the store // has, or to one that never said what it reaches: the current build is kept, and the condition says so. // The lease's holder names the build the declaration told it it is, and the version stamp only without one. func TestTheHolderNamesTheBuildTheDeclarationToldIt(t *testing.T) { withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks) t.Setenv(RunningBuildVar, " ad62528c47c7 ") if h := holderOf("x"); h.Build != "ad62528c47c7" { t.Fatalf("the holder's build is %q", h.Build) } t.Setenv(RunningBuildVar, "") if h := holderOf("x"); h.Build != version { t.Fatalf("without a declared version the holder's build is %q", h.Build) } if reach, err := schemaReach(); err != nil || reach < 87 { t.Fatalf("this build's reach is %d (%v)", reach, err) } }