package main import ( "reflect" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/inventory" ) // novox/hq issue 249, ADR 0218: a plan rolls a module out to one machine first and the rest only // once that machine has reported it applied; a module whose policy says together goes everywhere at // once, as before. func TestAPlanSendsOneMachineFirstAndTheRestAfterItsReport(t *testing.T) { running := []string{"novox", "ace", "g14"} sentAt := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) now := sentAt.Add(5 * time.Minute) bound := 30 * time.Minute after := sentAt.Add(time.Minute) next := func(s inventory.PlanModule, running []string, together bool, reports []inventory.Reported) rolloutStep { return nextRollout(s, running, together, reports, now, bound) } // Together: every machine at once. if step := next(inventory.PlanModule{}, running, true, nil); !reflect.DeepEqual(step.send, running) || step.first { t.Fatalf("a together policy did not send every machine at once: %+v", step) } // Otherwise the first by name, alone, when none has reported lately. step := next(inventory.PlanModule{}, running, false, nil) if !step.first || !reflect.DeepEqual(step.send, []string{"ace"}) { t.Fatalf("the first send was %+v, wanted ace alone", step) } state := inventory.PlanModule{First: []string{"ace"}, FirstAt: &sentAt} report := func(outcome string, current bool) []inventory.Reported { return []inventory.Reported{{Node: "ace", At: &after, Outcome: outcome, Current: current}, {Node: "g14", At: &after, Outcome: inventory.OutcomeApplied, Current: true}} } // No report yet, or one about an older declaration than it was last sent: wait. for what, reports := range map[string][]inventory.Reported{ "no report": nil, "a report about older": report(inventory.OutcomeApplied, false), } { step := next(state, running, false, reports) if len(step.send) != 0 || step.waiting != "ace" || step.failed != "" { t.Errorf("%s: %+v, wanted to wait for ace", what, step) } } // Applied what it was last sent: the rest, and only the rest. step = next(state, running, false, report(inventory.OutcomeApplied, true)) if step.first || !reflect.DeepEqual(step.send, []string{"novox", "g14"}) { t.Fatalf("after ace applied it the plan sent %+v, wanted novox and g14", step) } // Failed or refused: stop, the rest untouched. for _, outcome := range []string{inventory.OutcomeFailed, inventory.OutcomeRefused} { step := next(state, running, false, report(outcome, true)) if len(step.send) != 0 || !strings.Contains(step.failed, "ace "+outcome) || !reflect.DeepEqual(step.rest, []string{"novox", "g14"}) { t.Errorf("a first machine that %s it: %+v", outcome, step) } } // The machine holding the bus went with the first send: the rest wait for it too, and it is // not sent again. both := inventory.PlanModule{First: []string{"novox", "ace"}, FirstAt: &sentAt} half := report(inventory.OutcomeApplied, true) if step := next(both, running, false, half); step.waiting != "novox" { t.Fatalf("the plan did not wait for the bus's machine sent first: %+v", step) } all := append(half, inventory.Reported{Node: "novox", At: &after, Outcome: inventory.OutcomeApplied, Current: true}) if step := next(both, running, false, all); !reflect.DeepEqual(step.send, []string{"g14"}) { t.Fatalf("after both applied it the plan sent %+v, wanted g14 alone", step) } // One machine, or none: nothing is waited for that cannot come. if step := next(inventory.PlanModule{}, nil, false, nil); len(step.send) != 0 || step.first { t.Fatalf("a module nothing runs was sent: %+v", step) } if step := next(state, []string{"ace"}, false, report(inventory.OutcomeApplied, true)); len(step.send) != 0 || step.waiting != "" { t.Fatalf("a module on one machine waited for more: %+v", step) } } // ADR 0218 §2: a first machine that does not report within the bound stops the rollout there, // naming the machine and the bound; the rest are left alone. func TestAFirstMachineThatDoesNotReportStopsTheRollout(t *testing.T) { sentAt := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) state := inventory.PlanModule{First: []string{"ace"}, FirstAt: &sentAt} step := nextRollout(state, []string{"ace", "g14"}, false, nil, sentAt.Add(31*time.Minute), 30*time.Minute) if !strings.Contains(step.failed, "ace did not report it applied within 30m") || !reflect.DeepEqual(step.rest, []string{"g14"}) || len(step.send) != 0 { t.Fatalf("a silent first machine: %+v", step) } } // The first machine is the first by name among those heard from lately: a laptop that is away is // not the one the rest wait on. When none has been heard from, the first by name. func TestTheFirstMachineIsOneThatHasReportedLately(t *testing.T) { now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) lately, long := now.Add(-time.Minute), now.Add(-3*time.Hour) reports := []inventory.Reported{ {Node: "ace", At: &long}, {Node: "g14", At: &lately}, {Node: "novox", At: &lately}, } step := nextRollout(inventory.PlanModule{}, []string{"novox", "ace", "g14"}, false, reports, now, 30*time.Minute) if !step.first || !reflect.DeepEqual(step.send, []string{"g14"}) { t.Fatalf("the first send was %+v, wanted g14, the first heard from lately", step) } } // An announced move of a module an open plan is still rolling out is left to the plan: sending it // here as well put the bundle on every machine at once (novox/hq issue 249). func TestAnAnnouncedMoveIsLeftToThePlanRollingItOut(t *testing.T) { sent := time.Now() plans := []inventory.Plan{ {ID: "plan-done", State: inventory.PlanDone, Modules: map[string]*inventory.PlanModule{"agent": {}}}, {ID: "plan-1", State: inventory.PlanRolling, Modules: map[string]*inventory.PlanModule{ "agent": {State: "built", First: []string{"ace"}, FirstAt: &sent}, "gitea": {State: "built", SentAt: &sent}}}, } if got := rolledOutByAPlan(plans, "agent"); got != "plan-1" { t.Fatalf("a module the plan is rolling out was not left to it: %q", got) } for _, m := range []string{"gitea", "keycloak"} { if got := rolledOutByAPlan(plans, m); got != "" { t.Errorf("%s, which no plan will send, was left to %s", m, got) } } } // A plan that ends without sending what it built says so, with the remedy; a module whose rollout // stopped at its first machine is not among them. func TestAnEndedPlanSaysWhatItBuiltAndNeverSent(t *testing.T) { at := time.Now() p := inventory.Plan{State: inventory.PlanFailed, Note: "closed by hand at tier 1", Modules: map[string]*inventory.PlanModule{ "agent": {State: "built"}, "stopped": {State: "built", First: []string{"ace"}, FirstAt: &at}, "sent": {State: "built", SentAt: &at}, "notes": {State: "built"}, "later": {}, }} rollsOut := func(m string) bool { return m != "notes" } sayUnsent(&p, rollsOut) sayUnsent(&p, rollsOut) if p.Note != "closed by hand at tier 1; built and never sent: agent — `push --behind` sends them" { t.Fatalf("the note reads %q", p.Note) } }