157 lines
8.9 KiB
Go
157 lines
8.9 KiB
Go
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)
|
|
}
|
|
}
|