package main import ( "context" "encoding/json" "errors" "fmt" "reflect" "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 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) } if !strings.Contains(q.Explanation, "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, 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. func TestAnApprovedLayerOpensTheWalkThatSendsIt(t *testing.T) { open := aMesh(t) ctx := t.Context() register(t, open, catalogue.Manifest{Module: "notes", Version: "1", Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "content": "x = ${setting:x}\n"}}}) if _, err := assign(ctx, open, "laptop", "notes"); err != nil { t.Fatal(err) } // A layer set by hand opens no walk: it still waits for a push (ADR 0344 §7). if err := atTheTerminal(t, "settings", "set", "notes", `{"x":"0"}`, "--node", "laptop"); err != nil { t.Fatal(err) } if plans, _ := open.inventory.OpenPlans(ctx); len(plans) != 0 { t.Fatalf("a layer set at the terminal opened %d walk(s)", len(plans)) } set := setLayerIn(open) now := time.Now() values := map[string]any{"x": "1"} p := settingsProposal{Module: "notes", Values: values, From: "g14/claude-code", At: now, Digest: layerDigest(values), BeforeDigest: layerDigest(nil)} words, err := set(ctx, p, "s9", "approved by the operator (ask s9)") if err != nil { t.Fatal(err) } if !strings.Contains(words, "+ x (ask s9); settings-") || !strings.Contains(words, "sends it to laptop, one machine at a time") { t.Errorf("the warrant's words: %q", words) } plans, err := open.inventory.OpenPlans(ctx) if err != nil || len(plans) != 1 { t.Fatalf("open walks: %+v %v", plans, err) } w := plans[0] if w.Release == nil || w.Release.Settings == nil || w.Release.Settings.Module != "notes" || w.Release.Settings.Ask != "s9" || !reflect.DeepEqual(w.Release.Order, []string{"laptop"}) || w.Repository != "the settings of notes on the whole mesh" || !reflect.DeepEqual(w.Tiers, [][]string{{"notes"}}) { t.Fatalf("the settings walk: %+v %+v", w, w.Release) } // A machine's layer where the module does not run: set, and nothing to send. q := settingsProposal{Module: "notes", Node: "anchor", Values: values, From: "g14/claude-code", At: now, Digest: layerDigest(values), BeforeDigest: layerDigest(nil)} words, err = set(ctx, q, "s10", "approved by the operator (ask s10)") if err != nil || !strings.Contains(words, "no machine runs notes there, so there is nothing to send") { t.Fatalf("a layer nobody runs: %q %v", words, err) } if plans, _ := open.inventory.OpenPlans(ctx); len(plans) != 1 { t.Fatalf("a walk was opened for a machine that does not run the module: %d open", len(plans)) } // A walk that cannot be opened is the act's failure, said with the push that sends it by hand. was := openSettingsWalk openSettingsWalk = func(context.Context, *stores, inventory.PlanSettings) (string, error) { return "", fmt.Errorf("the plans are not to be had") } t.Cleanup(func() { openSettingsWalk = was }) again := map[string]any{"x": "2"} byHand := map[string]any{"x": "0"} r := settingsProposal{Module: "notes", Node: "laptop", Values: again, Before: byHand, HadLayer: true, From: "g14/claude-code", At: now, Digest: layerDigest(again), BeforeDigest: layerDigest(byHand)} if _, err := set(ctx, r, "s11", "approved"); err == nil || !strings.Contains(err.Error(), "is set, and the walk that "+ "sends it could not be opened: the plans are not to be had — `push laptop` sends it") { t.Errorf("an unopened walk: %v", err) } } // 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 aSettledMesh(t *testing.T) *walkMesh { t.Helper() open := aMesh(t) ctx := t.Context() inv := open.inventory w := &walkMesh{open: open, broken: map[string]bool{}, moves: map[string][]dataMove{}, heard: map[string]bool{"anchor": true, "laptop": true}} withConditionsInMemory(t) was := doctorFrom doctorFrom = nil t.Cleanup(func() { doctorFrom = was }) old := time.Now().Add(-2 * time.Hour) manifest, _ := json.Marshal(catalogue.Manifest{Module: "app", Version: "1"}) if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-app-c1", Module: "app", Commit: "c1", Repository: "novox/mesh-catalog", Path: "modules/app", Manifest: manifest, Asked: old, At: old, Made: []inventory.Artifact{{Name: "x", Kind: "bundle", Reference: "sha256:app-1"}}}); err != nil { t.Fatal(err) } if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "app", Version: "1"}, inventory.Source{ Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/app", BuiltFrom: "c1", Head: "c1", Asked: old}); err != nil { t.Fatal(err) } for _, n := range []string{"anchor", "laptop"} { if _, err := inv.Assign(ctx, n, "app"); err != nil { t.Fatal(err) } if err := inv.RecordSent(ctx, nodeID(t, open, n), "d-"+n, map[string]string{"app": "c1"}); err != nil { t.Fatal(err) } } wasMoves, wasHeard, wasSend, wasGather, wasData := machineMoves, releaseHeard, sendRollout, gatherGateFacts, settingsDataMoves settingsDataMoves = func(_ context.Context, _ *stores, node string) ([]dataMove, error) { return w.moves[node], nil } machineMoves = func(ctx context.Context, open *stores, f moveFacts, node string, all bool) ([]inventory.CarriedMove, error) { modules, err := open.inventory.Assigned(ctx, node) if err != nil { return nil, err } sent, known, err := open.inventory.SentBuilds(ctx, node) if err != nil { return nil, err } return f.moves(node, modules, sent, known, all), nil } releaseHeard = func(context.Context, *stores) (map[string]bool, error) { return w.heard, nil } n := 0 sendRollout = func(ctx context.Context, open *stores, names []string) ([]string, error) { w.sent = append(w.sent, append([]string(nil), names...)) w.kept = append(w.kept, len(names) == 1 && settingsKeptOn(ctx, names[0])) current, err := open.inventory.CurrentBuilds(ctx) if err != nil { return nil, err } for _, node := range names { modules, _ := open.inventory.Assigned(ctx, node) carried := map[string]string{} for _, m := range modules { carried[m] = current[m].Commit } n++ digest := fmt.Sprintf("d-%s-%d", node, n) if err := open.inventory.RecordSent(ctx, nodeID(t, open, node), digest, carried); err != nil { return nil, err } if _, err := open.inventory.RecordDoing(ctx, nodeID(t, open, node), inventory.Doing{Node: node, Outcome: inventory.OutcomeApplied, Declared: digest, Applied: 1, At: time.Now()}); err != nil { return nil, err } } return names, nil } gatherGateFacts = func(ctx context.Context, open *stores, component string) (gateFacts, error) { f := gateFacts{now: time.Now(), reports: map[string]inventory.Reported{}, engines: map[string]string{}, rolledBack: map[string][]lease.Rollback{}, served: map[string]served{}} reports, err := open.inventory.LastReports(ctx) if err != nil { return f, err } for _, r := range reports { if w.broken[r.Node] { r.Outcome = inventory.OutcomeFailed } f.reports[r.Node] = r } return f, nil } wasSettle, wasEvery, wasBound := gateSettle, gateEvery, gateBound gateSettle, gateEvery = 0, time.Hour t.Cleanup(func() { machineMoves, releaseHeard, sendRollout, gatherGateFacts = wasMoves, wasHeard, wasSend, wasGather settingsDataMoves = wasData gateSettle, gateEvery, gateBound = wasSettle, wasEvery, wasBound }) return w } 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. func TestASettingsWalkSendsOneMachineAtATimeEachJudged(t *testing.T) { judgeLeavesAsTheCore(t) w := aSettledMesh(t) ctx := t.Context() words, err := openSettingsWalk(ctx, w.open, inventory.PlanSettings{Module: "app", Ask: "s1", Change: "~ x"}) if err != nil || !strings.Contains(words, "sends it to anchor, laptop") { t.Fatalf("%q %v", words, err) } advancePlans(ctx, w.open) if !reflect.DeepEqual(w.sent, [][]string{{"anchor"}}) { t.Fatalf("sent %v: the first machine alone, before it is judged", w.sent) } p := w.walk(t) if p.Release.Gate == nil || p.Release.Next != 0 || !strings.Contains(p.Note, "judging app on anchor") { t.Fatalf("the walk after its first send: %s %+v", p.Note, p.Release) } gateEvery = 0 for i := 0; i < 6; i++ { advancePlans(ctx, w.open) } if !reflect.DeepEqual(w.sent, [][]string{{"anchor"}, {"laptop"}}) { t.Fatalf("sent %v", w.sent) } p = w.walk(t) if p.State != inventory.PlanDone || !reflect.DeepEqual(p.Release.Done, []string{"anchor", "laptop"}) || p.Outcome() != inventory.WalkDelivered || !strings.Contains(p.Note, "delivered on anchor, laptop") { t.Fatalf("the walk is %s: %s %+v", p.State, p.Note, p.Release) } // It said its start and its end, so the operator is told once (ADR 0308). if p.Times == nil || p.Times.Started == nil || p.Times.Ended == nil { t.Fatalf("the walk's start and end are not marked: %+v", p.Times) } // Nothing waits, and no walk of waiting builds opened beside it. if r, err := releaseBacklog(ctx, w.open, ""); err != nil || r != nil { t.Fatalf("%+v %v", r, err) } } // 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. func TestAFailedSettingsWalkStopsAndHoldsNothingBack(t *testing.T) { w := aSettledMesh(t) ctx := t.Context() inv := w.open.inventory w.broken["anchor"] = true if err := inv.KeepSettings(ctx, "", "app", map[string]any{"x": "1"}); err != nil { t.Fatal(err) } if _, err := openSettingsWalk(ctx, w.open, inventory.PlanSettings{Module: "app", Ask: "s2"}); err != nil { t.Fatal(err) } for i := 0; i < 4; i++ { advancePlans(ctx, w.open) } p := w.walk(t) if p.State != inventory.PlanFailed || p.Outcome() != inventory.WalkFailed || !strings.Contains(p.Note, "the settings of app on the whole mesh failed its gate on anchor") || !strings.Contains(p.Note, "the layer stays as the operator approved it") || !strings.Contains(p.Note, "not sent: laptop") { t.Fatalf("the walk is %s: %s", p.State, p.Note) } if !reflect.DeepEqual(w.sent, [][]string{{"anchor"}}) { t.Fatalf("sent %v: anchor alone, nothing to laptop", w.sent) } if layer, has, err := inv.Layer(ctx, "", "app"); err != nil || !has || layer["x"] != "1" { t.Fatalf("the layer after the failure: %v %v %v", layer, has, err) } // A build that waits for a gate is still walked: the failed settings walk holds nothing back. w.broken["anchor"] = false now := time.Now().Add(-time.Minute) manifest, _ := json.Marshal(catalogue.Manifest{Module: "app", Version: "1"}) if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-app-c2", Module: "app", Commit: "c2", Repository: "novox/mesh-catalog", Path: "modules/app", Manifest: manifest, Asked: now, At: now, Made: []inventory.Artifact{{Name: "x", Kind: "bundle", Reference: "sha256:app-2"}}}); err != nil { t.Fatal(err) } if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "app", Version: "1"}, inventory.Source{ Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/app", BuiltFrom: "c2", Head: "c2", Asked: now}); err != nil { t.Fatal(err) } if r, err := releaseBacklog(ctx, w.open, ""); err != nil || r == nil { t.Fatalf("the waiting build is held back by a failed settings walk: %+v %v", r, err) } } // 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). func TestASettingsWalkSendsTheLayerAloneEveryBuildKept(t *testing.T) { w := aSettledMesh(t) ctx := t.Context() inv := w.open.inventory gateEvery = 0 if _, err := openSettingsWalk(ctx, w.open, inventory.PlanSettings{Module: "app", Ask: "s3"}); err != nil { t.Fatal(err) } for i := 0; i < 4; i++ { advancePlans(ctx, w.open) } if !reflect.DeepEqual(w.kept, []bool{true, true}) || w.walk(t).State != inventory.PlanDone { t.Fatalf("the sends kept every build: %v; the walk: %s", w.kept, w.walk(t).Note) } // What such a send keeps, on the store: a rolled-out build waiting there is kept at the one last sent. now := time.Now().Add(-time.Minute) manifest, _ := json.Marshal(catalogue.Manifest{Module: "app", Version: "1"}) if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-app-c2", Module: "app", Commit: "c2", Repository: "novox/mesh-catalog", Path: "modules/app", Manifest: manifest, Asked: now, At: now, Made: []inventory.Artifact{{Name: "x", Kind: "bundle", Reference: "sha256:app-2"}}}); err != nil { t.Fatal(err) } if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "app", Version: "1"}, inventory.Source{ Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/app", BuiltFrom: "c2", Head: "c2", Asked: now}); err != nil { t.Fatal(err) } kept, err := recordedKept(keepingBuildsForSettings(ctx, "anchor"), w.open, "anchor") if err != nil || kept["app"] != "c1" { t.Fatalf("a settings send to anchor keeps %v (%v), want app at c1", kept, err) } if kept, _ := recordedKept(ctx, w.open, "anchor"); kept["app"] != "" { t.Fatalf("an ordinary send keeps a rolled-out build: %v", kept) } // And so the ungated build waiting on anchor is no move of the settings send's: nothing for a gate to see. f, err := readMoveFacts(ctx, inv) if err != nil { t.Fatal(err) } waiting, _ := machineMoves(ctx, w.open, f, "anchor", false) if len(waiting) != 1 || waiting[0].Module != "app" { t.Fatalf("anchor waits for %+v, want app's c2", waiting) } if moved, err := withoutKept(keepingBuildsForSettings(ctx, "anchor"), w.open, "anchor", waiting); err != nil || len(moved) != 0 { t.Fatalf("a settings send to anchor would move %+v (%v)", moved, err) } } // 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. func TestASettingsWalkStopsBeforeADataMoveAndPassesOverAMachineAway(t *testing.T) { w := aSettledMesh(t) ctx := t.Context() gateEvery = 0 w.moves["laptop"] = []dataMove{{Container: "app-server", Inside: "/data", From: "/srv/a", To: "/srv/b"}} if _, err := openSettingsWalk(ctx, w.open, inventory.PlanSettings{Module: "app", Ask: "s4"}); err != nil { t.Fatal(err) } for i := 0; i < 4; i++ { advancePlans(ctx, w.open) } p := w.walk(t) if p.State != inventory.PlanFailed || !reflect.DeepEqual(w.sent, [][]string{{"anchor"}}) || !strings.Contains(p.Note, "not sent to laptop: a running module's data would move there") || !strings.Contains(p.Note, "push laptop --move app") { t.Fatalf("sent %v; the walk is %s: %s", w.sent, p.State, p.Note) } v := aSettledMesh(t) v.heard = map[string]bool{"laptop": true} gateEvery = 0 if _, err := openSettingsWalk(ctx, v.open, inventory.PlanSettings{Module: "app", Ask: "s5"}); err != nil { t.Fatal(err) } for i := 0; i < 4; i++ { advancePlans(ctx, v.open) } p = v.walk(t) if p.State != inventory.PlanDone || !reflect.DeepEqual(p.Release.Skipped, []string{"anchor"}) || !strings.Contains(p.Note, "delivered on laptop; not heard from, left as they were, their next send carries it: anchor") { t.Fatalf("the walk with anchor away is %s: %s", p.State, p.Note) } u := aSettledMesh(t) u.heard = map[string]bool{} if _, err := openSettingsWalk(ctx, u.open, inventory.PlanSettings{Module: "app", Ask: "s6"}); err != nil { t.Fatal(err) } advancePlans(ctx, u.open) if p := u.walk(t); p.State != inventory.PlanFailed || p.Outcome() != inventory.WalkFailed || !strings.Contains(p.Note, "was sent to no machine") || len(u.sent) != 0 { t.Fatalf("a walk that reached nobody is %s: %s", p.State, p.Note) } } // 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). func TestACrossingSendSupersedesASettingsGate(t *testing.T) { judgeLeavesAsTheCore(t) w := aSettledMesh(t) ctx := t.Context() inv := w.open.inventory if _, err := openSettingsWalk(ctx, w.open, inventory.PlanSettings{Module: "app", Ask: "s7"}); err != nil { t.Fatal(err) } advancePlans(ctx, w.open) p := w.walk(t) if g := p.Release.Gate; g == nil || g.To != "c1" || g.From != "c1" { t.Fatalf("the settings gate judges no build: %+v", p.Release.Gate) } // Another walk sends anchor app's c2 while the settings gate judges there. now := time.Now().Add(-time.Minute) manifest, _ := json.Marshal(catalogue.Manifest{Module: "app", Version: "1"}) if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-app-c2", Module: "app", Commit: "c2", Repository: "novox/mesh-catalog", Path: "modules/app", Manifest: manifest, Asked: now, At: now, Made: []inventory.Artifact{{Name: "x", Kind: "bundle", Reference: "sha256:app-2"}}}); err != nil { t.Fatal(err) } if err := inv.RecordSent(ctx, nodeID(t, w.open, "anchor"), "d-merge", map[string]string{"app": "c2"}); err != nil { t.Fatal(err) } if _, err := inv.RecordDoing(ctx, nodeID(t, w.open, "anchor"), inventory.Doing{Node: "anchor", Outcome: inventory.OutcomeApplied, Declared: "d-merge", Applied: 1, At: time.Now()}); err != nil { t.Fatal(err) } gateEvery = 0 advancePlans(ctx, w.open) p = w.walk(t) if p.State != inventory.PlanSuperseded || p.Release.Gate == nil || p.Release.Gate.Verdict != inventory.GateSuperseded || !strings.Contains(p.Note, "superseded on anchor") || len(p.Release.Done) != 0 { t.Fatalf("the settings walk after a crossing send is %s: %s %+v", p.State, p.Note, p.Release) } if !reflect.DeepEqual(w.sent, [][]string{{"anchor"}}) { t.Fatalf("sent %v: laptop was sent after a superseded judging", w.sent) } } // 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). func TestASettingsWalkFailsOnARefusalThatCannotClear(t *testing.T) { w := aSettledMesh(t) ctx := t.Context() inv := w.open.inventory // What a send to a machine never sent can keep: nothing, and said as the refusal the walk fails on. if _, err := inv.AddNode(ctx, "fresh"); err != nil { t.Fatal(err) } if _, err := recordedKept(keepingBuildsForSettings(ctx, "fresh"), w.open, "fresh"); !errors.Is(err, errSettingsCannotKeep) { t.Fatalf("a settings send to a machine never sent: %v", err) } w.heard = map[string]bool{"laptop": true} settingsDataMoves = func(_ context.Context, _ *stores, node string) ([]dataMove, error) { return nil, fmt.Errorf("%w: what %s was last sent is not known", errSettingsCannotKeep, node) } if _, err := openSettingsWalk(ctx, w.open, inventory.PlanSettings{Module: "app", Ask: "s8"}); err != nil { t.Fatal(err) } advancePlans(ctx, w.open) p := w.walk(t) if p.State != inventory.PlanFailed || len(w.sent) != 0 || !strings.Contains(p.Note, "not sent to laptop: the settings cannot be sent with every build kept: what laptop was last sent is not known") || !strings.Contains(p.Note, "not heard from, left as they were: anchor") { t.Fatalf("the walk is %s: %s", p.State, p.Note) } // It is not open, so a build waiting for a gate is walked. now := time.Now().Add(-time.Minute) manifest, _ := json.Marshal(catalogue.Manifest{Module: "app", Version: "1"}) if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-app-c2", Module: "app", Commit: "c2", Repository: "novox/mesh-catalog", Path: "modules/app", Manifest: manifest, Asked: now, At: now, Made: []inventory.Artifact{{Name: "x", Kind: "bundle", Reference: "sha256:app-2"}}}); err != nil { t.Fatal(err) } if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "app", Version: "1"}, inventory.Source{ Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/app", BuiltFrom: "c2", Head: "c2", Asked: now}); err != nil { t.Fatal(err) } if r, err := releaseBacklog(ctx, w.open, ""); err != nil || r == nil { t.Fatalf("the waiting build is held by the failed settings walk: %+v %v", r, err) } }