package main import ( "context" "reflect" "strings" "testing" "github.com/novox/mesh-controller/internal/inventory" ) // novox/hq ADR 0344, issue 641: the operator's one Approve on a settings proposal is the act that makes it take // effect. The warrant sets the layer and opens a settings walk, which sends it to the machines it reaches one at a // time, judging the module on each; the ask says so before the Approve, and nothing names a push. // The order: every machine running the module, or the one the layer names, the control node last. func TestASettingsWalkGoesToTheMachinesTheLayerReachesTheControlNodeLast(t *testing.T) { running := []string{"shanks", "novox", "g14", "ace"} for _, c := range []struct { node string want []string }{ {"", []string{"ace", "g14", "shanks", "novox"}}, {"g14", []string{"g14"}}, {"novox", []string{"novox"}}, {"elsewhere", nil}, } { if got := settingsWalkOrder(c.node, running, []string{"novox"}); !reflect.DeepEqual(got, c.want) { t.Errorf("a layer on %q goes to %v, want %v", c.node, got, c.want) } } } // A settings walk sends a machine only when no other walk judges there. func TestASettingsWalkWaitsWhileAnotherWalkJudgesOnTheMachine(t *testing.T) { judging := &inventory.PlanGate{Machines: []string{"g14"}} passed := &inventory.PlanGate{Machines: []string{"g14"}, Verdict: inventory.GatePassed} plans := []inventory.Plan{ {ID: "settings-1", State: inventory.PlanRolling, Release: &inventory.PlanRelease{Gate: judging}}, {ID: "release-2", State: inventory.PlanRolling, Release: &inventory.PlanRelease{Gate: passed}}, {ID: "plan-3", State: inventory.PlanRolling, Modules: map[string]*inventory.PlanModule{"app": {Gate: judging}}}, {ID: "plan-4", State: inventory.PlanDone, Modules: map[string]*inventory.PlanModule{"app": {Gate: judging}}}, } if id := judgingIn(plans, "settings-1", "g14"); id != "plan-3" { t.Errorf("on g14 %q judges, want plan-3 (itself, a passed gate and an ended walk are not judging)", id) } if id := judgingIn(plans[1:2], "x", "g14"); id != "" { t.Errorf("a passed gate judges: %q", id) } if id := judgingIn(plans, "x", "shanks"); id != "" { t.Errorf("on shanks %q judges", id) } } // The ask says, before the Approve, that the layer is sent at once and judged, and that its end is told as every // walk's is; neither the ask nor the proposer's answer names a push any more. func TestTheAskSaysAnApprovedLayerIsSentAtOnce(t *testing.T) { for _, c := range []struct { in proposeInput where string had bool }{ {proposeInput{module: "mounts", node: "shanks", values: mountsSources}, "shanks", false}, {proposeInput{module: "notes", values: map[string]any{"x": "1"}}, "every machine running notes", true}, {proposeInput{module: "notes", node: "laptop", clear: true}, "laptop", true}, } { r := newProposerRig(t) if c.had { r.before, r.had = map[string]any{"x": "0"}, true } words, err := r.pr.propose(context.Background(), c.in) if err != nil { t.Fatal(err) } q := r.askSent(t) if err := q.Check(r.now); err != nil { t.Fatalf("the ask is refused: %v", err) } // At a glance (ADR 0345): the summary says it is sent at once by a walk, one machine at a time; the rest of // what Approve does is the Details', which the router notes after the answers. if !strings.Contains(q.Explanation, "sent at once by a walk to "+c.where+", one machine at a time, each judged") || !strings.Contains(q.Details, "and sent at once to "+c.where+", one machine at a time, each judged, "+ "every build there kept; its end is told as every walk's is") { t.Errorf("the ask does not say the layer is sent at once to %s:\n%s", c.where, q.Explanation) } approve := q.Options[0] if approve.Label != "Approve" || !strings.Contains(approve.Does, "sent to "+c.where+" at once") { t.Errorf("Approve does %q", approve.Does) } if !strings.Contains(words, "a walk sends it to "+c.where+" at once") { t.Errorf("the proposer's answer:\n%s", words) } for _, said := range []string{q.Explanation, q.Whole, approve.Does, words} { if strings.Contains(said, "push") { t.Errorf("a push is still named, as if a second act were needed:\n%s", said) } } } } // ---- on the real stores ----------------------------------------------------------------------------- // The warrant sets the layer and opens the walk that sends it, naming the machines it reaches; a layer for a // machine that does not run the module opens none and says so. // walkMesh is a mesh where `app` runs c1 on anchor and laptop and nothing waits for a gate: every send applied at // once, recorded, and judged from the reports, with a machine named broken failing what it was sent. type walkMesh struct { open *stores sent [][]string broken map[string]bool // kept says, per send, whether it kept every build of its machine; moves is the data a machine's send would move. kept []bool moves map[string][]dataMove heard map[string]bool } func (w *walkMesh) walk(t *testing.T) inventory.Plan { t.Helper() plans, err := w.open.inventory.RecentPlans(t.Context(), 10) if err != nil { t.Fatal(err) } for _, p := range plans { if p.Release != nil && p.Release.Settings != nil { return p } } t.Fatal("no settings walk") return inventory.Plan{} } // The walk sends the first machine alone and judges the module there before it sends the next, and ends // delivered on both; nothing waited for a gate, so each send carried the layer alone. // A machine that fails what it was sent stops the walk there: the next is not sent, the layer stays, and the // failure holds back no walk of waiting builds. // Each send of a settings walk keeps every build of its machine: the layer goes, and a build held for a person or // waiting for a gate stays where it is (ADR 0344 §3). // A machine whose send would move a running module's data stops the walk there, failed and said with the push that // sends it on a person's word (ADR 0217 §3); one not heard from is passed over and named; and a walk that reached // no machine is not said delivered. // The gate keeps the module's build its send kept, so a crossing send — a merge's walk sending the machine a new // build of the module while the settings gate judges there — supersedes it, and its report is never read as the // settings send's (issue 641 review, finding 1). // A send refusal that cannot clear by itself — what the machine was last sent is not known, or a build it runs is // gone — fails the walk with why, rather than keeping it open and holding the walk of waiting builds; and a // failure names the machines passed over as away (issue 641 review, findings 2 and 3).