diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compose_integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compose_integration_test.go index 9e60c43..580bfe3 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compose_integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/compose_integration_test.go @@ -91,7 +91,7 @@ func (b *bed) hold(t *testing.T) *holder { // 1622/AC1-AC3 and 1623 on the bus: started whole, the assigner composes a group holding a mesh-sdk pull request by // asking the Go build seat for the composer at the member's head, and for the composer at the mesh-sdk the running -// controller pins; it waits naming the asks while they run, writes the verdict from their answers naming the mesh-sdk the +// node-engine pins; it waits naming the asks while they run, writes the verdict from their answers naming the mesh-sdk the // head's composer was built with, and asks nothing more on a later pass of the same heads. func TestAGroupWithAnSDKMemberIsComposedByTheComposerAtItsHead(t *testing.T) { b := startBed(t, "") @@ -104,8 +104,8 @@ func TestAGroupWithAnSDKMemberIsComposedByTheComposerAtItsHead(t *testing.T) { b.putJSON(t, delivery.ModulesBucket, "web", mod) b.place(t, "g14", "web") running := strings.Repeat("3", 40) - b.place(t, "novox", "mesh-controller") - b.built(t, "mesh-controller", "novox/mesh-controller", running) + b.place(t, "novox", "mesh-host") + b.built(t, "mesh-host", "novox/mesh-host", running) sdkHead := strings.Repeat("4", 40) heads := []delivery.Head{{Repository: "novox/mesh-sdk", Number: 80, Head: sdkHead}, {Repository: "novox/web", Number: 7, Head: strings.Repeat("2", 40)}} hash, _ := delivery.HeadsHash(heads) @@ -121,8 +121,8 @@ func TestAGroupWithAnSDKMemberIsComposedByTheComposerAtItsHead(t *testing.T) { key, _ := delivery.CompositionKey("feat/x", hash) // No holder takes the asks yet: the composition waits, naming them. waits := b.verdict(t, key, func(v delivery.CompositionVerdict) bool { return v.State == delivery.CompositionWaits }) - if !strings.Contains(waits.Words, "mesh-sdk "+sdkHead) || !strings.Contains(waits.Words, "mesh-controller at "+running) { - t.Fatalf("the waiting composition says %q, want the head's and the running controller's asks", waits.Words) + if !strings.Contains(waits.Words, "mesh-sdk "+sdkHead) || !strings.Contains(waits.Words, "mesh-host at "+running) { + t.Fatalf("the waiting composition says %q, want the head's and the running node-engine's asks", waits.Words) } h := b.hold(t) v := b.verdict(t, key, func(v delivery.CompositionVerdict) bool { return v.State != delivery.CompositionWaits }) @@ -131,7 +131,7 @@ func TestAGroupWithAnSDKMemberIsComposedByTheComposerAtItsHead(t *testing.T) { } asks := h.taken() if len(asks) != 2 { - t.Fatalf("the seat was asked %d composes, want two (the head, the running controller)", len(asks)) + t.Fatalf("the seat was asked %d composes, want two (the head, the running node-engine)", len(asks)) } byRepo := map[string]delivery.StepAsk{} for _, a := range asks { @@ -143,8 +143,8 @@ func TestAGroupWithAnSDKMemberIsComposedByTheComposerAtItsHead(t *testing.T) { if a := byRepo["novox/mesh-sdk"]; a.Compose == nil || a.Compose.Fetch["commit"] != sdkHead || a.Compose.Dir != "go" { t.Errorf("the head's ask = %+v", a.Compose) } - if a := byRepo["novox/mesh-controller"]; a.Compose == nil || a.Compose.Fetch["commit"] != running || a.Compose.Dir != delivery.RootPath { - t.Errorf("the running controller's ask = %+v", a.Compose) + if a := byRepo["novox/mesh-host"]; a.Compose == nil || a.Compose.Fetch["commit"] != running || a.Compose.Dir != delivery.RootPath { + t.Errorf("the running node-engine's ask = %+v", a.Compose) } // A later pass of the same heads (woken by any change) asks nothing more. on, _ := delivery.EncodePresence(delivery.Presence{Online: true}) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index 0c02eb4..382d1f4 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -30,10 +30,8 @@ type passer struct { // pass says a condition once and a restart says it again (the router keeps one per key). say func(asks.Condition) error said map[string]bool - // findings is the assigner's findings bucket (its said conditions and policy-split, ADR 0564 §2, ADR 0565 §8); splits the - // modules whose policy-split finding stands, nil until the first pass, which deletes any a previous process left. + // findings is the assigner's findings bucket (its said conditions, ADR 0564 §2). findings assign.Findings - splits map[string]bool // failedProvisions are the failed provisions whose finding this process wrote (ADR 0570 §4). failedProvisions map[string]bool // missing are the required provisions with no key this process pulsed, and missingSaid those whose finding it wrote. @@ -138,7 +136,6 @@ func (p *passer) wants(ctx context.Context) { defer func() { p.provisionsMissing(ctx, missing) }() defer p.unplacedFindings() defer p.seatHolders(ctx) - defer p.policySplits(manifests, chosen) p.clearStaleCalls() placed := p.w.placedNodes() puts := p.putsByModule() @@ -159,7 +156,7 @@ func (p *passer) wants(ctx context.Context) { stale := map[string]neitherOne{} left := map[string]string{} defer func() { p.sayNeither(neither, stale, left) }() - // Where grants are read from, once for the pass: the grants part's while the lease is handed away (issue 774). + // Where grants are read from, once for the pass: the grants part's (issue 774, ADR 0571 clause 9). grants := p.w.grantsBucket() for name := range names { fp := trunk[name].FP2 @@ -925,55 +922,6 @@ func chosenOf(chosen map[string]catalogue.PersonPolicy, module string) *catalogu return nil } -// policySplits writes the finding policy-split for each module whose policy at the assigner differs from one the controller -// writes beside a composition of it, and deletes it once they agree (ADR 0565 §8): written when a split begins, never again -// while it stands. The first pass after a start deletes every one a previous process left that no longer splits. -func (p *passer) policySplits(manifests map[string]catalogue.Manifest, chosen map[string]catalogue.PersonPolicy) { - if p.findings.Put == nil { - return - } - now := time.Now - if p.now != nil { - now = p.now - } - split := map[string]bool{} - // The controller writes no policy any more (ADR 0571 §6): nothing is compared with. - for _, s := range assign.Splits(manifests, chosen, nil) { - split[s.Module] = true - if p.splits[s.Module] { - continue - } - value, err := assign.PolicySplitFinding(s, now()) - if err == nil { - err = p.findings.Put(assign.PolicySplitKey(s.Module), value) - } - if err != nil { - p.logf("the policy split of %s is not written: %v", s.Module, err) - delete(split, s.Module) - } - } - gone := p.splits - if gone == nil && p.findings.Keys != nil { - gone = map[string]bool{} - if keys, err := p.findings.Keys(); err == nil { - for _, k := range keys { - if m, ok := strings.CutSuffix(strings.TrimPrefix(k, "module."), "."+assign.KindPolicySplit); ok && strings.HasPrefix(k, "module.") { - gone[m] = true - } - } - } - } - for m := range gone { - if !split[m] { - if err := p.findings.Delete(assign.PolicySplitKey(m)); err != nil { - p.logf("the policy split of %s is not deleted: %v", m, err) - split[m] = true // tried again at the next pass - } - } - } - p.splits = split -} - // provisionsOf answers, per module, the provisions it consumes on a node, by key (ADR 0570 §4), from the seat // mesh-provisioner's state; nil while the state is not read (the seat not delivered yet), so no want waits on it. A failed // provision is also said as the assigner's finding `provider..provision-failed` (a waiting one is the watchman's diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go index 8f944dd..030cea1 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools.go @@ -312,17 +312,6 @@ func (s *seatTools) doAgain(args map[string]any) (any, error) { return map[string]any{"proposal": p.ID, "again": p.Again, "follow": followProposal(p.ID)}, nil } -// doAdopt is the verb adopt (§7): gone since the lease write. The seed was the controller's export proposed whole, and the -// mesh has no controller export to take over (ADR 0547 §11, ADR 0567 §11); nothing is written. -func (s *seatTools) doAdopt(args map[string]any, by string) (any, error) { - return nil, errors.New("adopt is gone: placement is the mesh's alone since the lease write, and there is no controller to take it over from (ADR 0547 §11, ADR 0567 §11)") -} - -// doHandBack is the verb hand-back (§7): gone since the lease write, as adopt is; nothing is deleted. -func (s *seatTools) doHandBack(args map[string]any, by string) (any, error) { - return nil, errors.New("hand-back is gone: placement is the mesh's alone since the lease write, and there is no controller to hand it back to (ADR 0547 §11, ADR 0567 §11)") -} - // placementAnswer is what the verb placement answers. type placementAnswer struct { Keys map[string]keyed `json:"keys,omitempty"` @@ -496,8 +485,6 @@ func (s *seatTools) tools() []stdio.Tool { catalogue.PlacementVerbPropose: func(a map[string]any) (any, error) { return s.doPropose(a, stdio.CallerOf(a)) }, catalogue.PlacementVerbWithdraw: func(a map[string]any) (any, error) { return s.doWithdraw(a, stdio.CallerOf(a)) }, catalogue.PlacementVerbAgain: s.doAgain, - catalogue.PlacementVerbAdopt: func(a map[string]any) (any, error) { return s.doAdopt(a, stdio.CallerOf(a)) }, - catalogue.PlacementVerbHandBack: func(a map[string]any) (any, error) { return s.doHandBack(a, stdio.CallerOf(a)) }, } var out []stdio.Tool for _, v := range seat.Serves { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go index 2636592..64df14c 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go @@ -35,9 +35,8 @@ func (s memStore) history(state, key string) ([]stdio.StateRevision, error) { return []stdio.StateRevision{{Key: key, Value: v, Revision: uint64(len(v)), Op: "put"}}, nil } -// ADR 0567 §2, §7, §10: the seat's seven verbs over the states. propose lays a proposal and writes it with its candidates, -// or refuses naming every finding and writes nothing; withdraw and again mark it for the holder; adopt and hand-back are -// answered as gone and change nothing (ADR 0547 §11, ADR 0567 §11); placement and +// ADR 0567 §2, §7, §10: the seat's five verbs over the states. propose lays a proposal and writes it with its candidates, +// or refuses naming every finding and writes nothing; withdraw and again mark it for the holder; placement and // proposals read every state the seat keeps, and history names the stream for what the state no longer keeps. func TestTheSeatsVerbs(t *testing.T) { anchor := delivery.Placement{V: delivery.Version, Node: "anchor", Modules: []delivery.PlacedModule{{Module: "web"}}} @@ -59,21 +58,8 @@ func TestTheSeatsVerbs(t *testing.T) { }) s.now = func() time.Time { return time.Date(2026, 10, 28, 20, 0, 0, 0, time.UTC) } - // 774/S28/AC1: adopt and hand-back are answered as gone, naming their records, and change nothing and ask nobody. raw, _ := delivery.EncodePlacement(anchor) st[delivery.PlacementState].values["placement.anchor"] = raw - for verb, call := range map[string]func() (any, error){ - "adopt": func() (any, error) { return s.doAdopt(map[string]any{"why": "take over"}, "operator@laptop") }, - "hand-back": func() (any, error) { return s.doHandBack(map[string]any{"why": "back"}, "op") }, - } { - if _, err := call(); err == nil || !strings.Contains(err.Error(), verb+" is gone") || !strings.Contains(err.Error(), "ADR 0567 §11") { - t.Errorf("%s: %v", verb, err) - } - } - if len(asked) != 0 || len(st[delivery.PlacementProposalsState].values) != 0 || len(st[delivery.PlacementCandidatesState].values) != 0 || - len(st[delivery.PlacementState].values) != 1 { - t.Fatalf("adopt or hand-back changed something: asked %v", asked) - } // withdraw and again mark the proposal; the holder ends or judges it. got, err := s.doPropose(map[string]any{"why": "hold", "changes": `[{"op":"hold","node":"anchor","module":"web","claim":"node-shell","scope":"node"}]`}, "op") @@ -194,10 +180,8 @@ func TestTheSeatsVerbs(t *testing.T) { } else if rm := got.(proposed).Proposal; !slices.Equal(rm.Nodes(), []string{"joiner"}) || !slices.Equal(rm.RemovedNodes(), []string{"joiner"}) { t.Errorf("the remove-node proposed %+v", rm) } - if _, err := s.doHandBack(map[string]any{"why": "back"}, "op"); err == nil || !strings.Contains(err.Error(), "since the lease write") { - t.Errorf("a hand-back: %v", err) - } - if tools := s.tools(); len(tools) != 7 || tools[2].Name != "mesh-placement.propose" || tools[2].Run == nil || tools[2].Input == nil { + // 1126/B8: the seat serves its five verbs; adopt and hand-back went with the controller (ADR 0590). + if tools := s.tools(); len(tools) != 5 || tools[2].Name != "mesh-placement.propose" || tools[2].Run == nil || tools[2].Input == nil { t.Errorf("the seat's verbs as served: %+v", tools) } t.Run("1953/D4-D6: the seat mesh-wants' upgrade and bus", wantsVerbs) @@ -379,8 +363,7 @@ func TestTheSeatsVerbs(t *testing.T) { // wantsVerbs (novox/hq ADR 0565 §7, §8, §9): upgrade answers each module's policy and its source; a person's choice is // written with who and why, recorded as a hand act in the seat's log and said as policy-changed, and default takes it back; -// the bus is refused; compare lists the modules the controller writes another policy for; the process writes policy-split -// for a split and deletes it once they agree; bus names the node behind the indexer's newest. +// the bus is refused; compare lists each node's composition and the findings; bus names the node behind the indexer's newest. func wantsVerbs(t *testing.T) { fpOld, fpNew := delivery.SourcePrefix2+strings.Repeat("1", 64), delivery.SourcePrefix2+strings.Repeat("2", 64) st := memStore{} @@ -419,9 +402,10 @@ func wantsVerbs(t *testing.T) { if p := policyOf("nats"); p.Policy != catalogue.PolicyRecord || p.From != catalogue.FromBus { t.Errorf("nats = %+v", p) } - // compare: the controller writes no policy, so no module splits; each node's own composition is the digest its have names. + // compare: each node's own composition is the digest its have names, and the assigner's findings; nothing splits, the + // controller being gone (1126/B8). out, _ := w.doUpgrade(map[string]any{"compare": "true"}, "op") - if got := out.(map[string]any); len(got["splits"].([]assign.Split)) != 0 || got["empty"] != true || got["findings"] == nil || + if got := out.(map[string]any); got["splits"] != nil || got["findings"] == nil || got["composed"].(map[string]string)["anchor"] != "sha256:"+strings.Repeat("c", 64) || len(got["composed"].(map[string]string)) != 1 { t.Errorf("compare = %+v", got) } @@ -466,27 +450,7 @@ func wantsVerbs(t *testing.T) { if watches[policiesState] != 0 { t.Errorf("the seat's own small policies state was watched: %v", watches) } - // The process says the split as its finding, and deletes it once they agree. - findings := map[string][]byte{} - p := &passer{w: newWorld(), logf: func(string, ...any) {}, findings: assign.Findings{ - Put: func(k string, v []byte) error { findings[k] = v; return nil }, - Delete: func(k string) error { delete(findings, k); return nil }, - Keys: func() ([]string, error) { return []string{"module.gone.policy-split"}, nil }}} - man := map[string]catalogue.Manifest{} - for _, name := range []string{"web", "photos"} { - raw, _, _, _ := st.get(modulesState, name) - mod, _ := delivery.DecodeModule(raw) - mraw, _, _, _ := st.get(manifestsState, mod.Manifest) - kept, _ := delivery.DecodeManifest(mraw) - man[name], _ = catalogue.Decode([]byte(kept.JSON)) - } - findings["module.gone.policy-split"] = []byte("{}") - // The controller writes no policy now (ADR 0571 §6): the process compares with nothing, and deletes a finding left. - p.policySplits(man, nil) - if len(findings) != 0 { - t.Errorf("findings = %v, want a stale one deleted", findings) - } - // a person's record for web: written, recorded, said; the split is gone. + // a person's record for web: written, recorded, said. if _, err := w.doUpgrade(map[string]any{"module": "web", "policy": "record", "why": "the operator checks it"}, "person.jochen"); err != nil { t.Fatal(err) } @@ -495,10 +459,6 @@ func wantsVerbs(t *testing.T) { len(said) != 1 || !strings.HasPrefix(said[0], "policy-changed policy-changed.web.") { t.Errorf("chosen %+v, asked %v, said %v", c, asked, said) } - p.policySplits(man, chosen) - if len(findings) != 0 { - t.Errorf("findings after the choice = %v", findings) - } if _, err := w.doUpgrade(map[string]any{"module": "nats", "policy": "roll", "why": "x"}, "op"); err == nil { t.Error("the bus was given a policy") } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go index 2a45f88..febfd0b 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/wants_tools.go @@ -133,7 +133,7 @@ type policyRow struct { } // doUpgrade is the verb upgrade: read every module's policy or one's; with policy and why set a person's choice, or take it -// back with default; with compare list the splits. +// back with default; with compare each node's composition and the assigner's findings. func (s *wantsTools) doUpgrade(args map[string]any, by string) (any, error) { manifests, _, err := s.manifests() if err != nil { @@ -157,12 +157,8 @@ func (s *wantsTools) doUpgrade(args map[string]any, by string) (any, error) { } } } - splits := assign.Splits(manifests, chosen, nil) // the controller writes no policy to differ from (ADR 0571 §6) - if splits == nil { - splits = []assign.Split{} - } - out := map[string]any{"splits": splits, "empty": len(splits) == 0, "composed": composed} - // The assigner's findings, its said conditions and its policy splits as the condition keeper reads them (§8, §15). + out := map[string]any{"composed": composed} + // The assigner's findings, its said conditions as the condition keeper reads them (§15). if found, err := s.values(health.FindingsState); err != nil { out["findings-unread"] = err.Error() } else { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go index d335a7f..e21e828 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/world_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-assigner/internal/assign" @@ -128,27 +127,19 @@ func theWorldCountsTheValuesAKeyTakes(t *testing.T) { if got := w.changed(delivery.HaveBucket, "gone"); got != 0 { t.Errorf("changed = %d for a key never seen", got) } - // ADR 0571 clause 9: grants are read from the grants part's bucket in every case, a lease held, handed away, absent or - // unreadable alike, and the controller's own grants count for nothing. + // ADR 0571 clause 9: grants are read from the grants part's bucket, and the controller's own grants count for nothing. fp := delivery.SourcePrefix2 + strings.Repeat("d", 64) gkey, _ := delivery.ControllerGrantKey("web", fp) grant, _ := delivery.EncodeControllerGrant(delivery.ControllerGrant{Accounts: []string{"ace.web"}}) for _, row := range []struct { - name, lease string + name string inPart, inController bool granted bool }{ - {"handed away, the part's grant", `{"handed-to":"the operator's hand-over"}`, true, false, true}, - {"handed away, only the controller's grant", `{"handed-to":"x"}`, false, true, false}, - {"a controller holds it, the part's grant", `{"instance":"novox/1","epoch":3}`, true, false, true}, - {"a controller holds it, only its own grant", `{"instance":"novox/1"}`, false, true, false}, - {"no lease read", ``, true, false, true}, - {"an unreadable lease", `[1]`, true, false, true}, + {"the part's grant", true, false, true}, + {"only the controller's grant", false, true, false}, } { p := newPasser(buckets{}, time.Now()) - if row.lease != "" { - p.w.put(buscompose.LeaseBucket, buscompose.ControllerLeaseKey, []byte(row.lease)) - } if row.inPart { p.w.put(delivery.PartGrantsBucket, gkey, grant) } diff --git a/modules/mesh-delivery-assigner/internal/assign/composer.go b/modules/mesh-delivery-assigner/internal/assign/composer.go index 5abbc2c..a4cedf8 100644 --- a/modules/mesh-delivery-assigner/internal/assign/composer.go +++ b/modules/mesh-delivery-assigner/internal/assign/composer.go @@ -23,9 +23,9 @@ const ( // composer is built at its repository's root, at the mesh-sdk its go.mod pins. const SDKDir = "go" -// The modules whose mesh-sdk reads the manifests a plan sends (ADR 0482 clause 5): the controller composes and sends -// them, the node-engine applies them. Each is judged at its repository. -var ReaderModules = map[string]string{"mesh-controller": "novox/mesh-controller", "mesh-host": "novox/mesh-host"} +// The modules whose mesh-sdk reads the manifests a plan sends (ADR 0482 clause 5): the node-engine composes and applies +// them, the controller being gone (novox/hq ADR 0590, issue 1126). Each is judged at its repository. +var ReaderModules = map[string]string{"mesh-host": "novox/mesh-host"} // HeadJob is the compose job of a group with a mesh-sdk member (ADR 0482 clauses 3 and 4, issue 1622): the composer // built at the member's head composes the machines the change touches, as the trunk holds them and as the change leaves diff --git a/modules/mesh-delivery-assigner/internal/assign/policy.go b/modules/mesh-delivery-assigner/internal/assign/policy.go index 9d3b5e5..c3d9456 100644 --- a/modules/mesh-delivery-assigner/internal/assign/policy.go +++ b/modules/mesh-delivery-assigner/internal/assign/policy.go @@ -17,8 +17,6 @@ import ( // A person's upgrade policy is the assigner's (novox/hq ADR 0565 §7, §8, issue 1953 D4, D5): kept in its state `policies`, one // key per module, set and taken back through `mesh-wants.upgrade`. Every reader takes one derivation, mesh-sdk's // catalogue.Upgrade: a person's choice over the module's word, its irreplaceable data and the default, the bus never rolled. -// Until the lease write the controller still writes a policy beside each composition's digest; where it differs from the -// assigner's, the assigner says so as its finding `policy-split`, naming mesh-wants.upgrade as the remedy. // PoliciesBucket is the assigner's state `policies`. var PoliciesBucket = delivery.AssignerModule + "_" + delivery.AssignerPoliciesState @@ -57,64 +55,6 @@ func contains(list []string, s string) bool { return false } -// Split is a module whose policy at the assigner differs from one the controller writes beside a composition of it. -type Split struct { - Module string `json:"module"` - Policy string `json:"policy"` - From string `json:"from"` - Controller []string `json:"controller"` -} - -// Splits is every module of manifests the controller writes a policy for that differs from the assigner's, sorted. -func Splits(manifests map[string]catalogue.Manifest, chosen map[string]catalogue.PersonPolicy, controller map[string][]string) []Split { - var out []Split - for module, theirs := range controller { - m, ok := manifests[module] - if !ok { - continue - } - policy, from, _, _ := PolicyOf(m, chosen) - for _, p := range theirs { - if p != policy { - out = append(out, Split{Module: module, Policy: policy, From: from, Controller: theirs}) - break - } - } - } - sort.Slice(out, func(i, j int) bool { return out[i].Module < out[j].Module }) - return out -} - -// KindPolicySplit is the assigner's finding of a split policy (ADR 0565 §8). -const KindPolicySplit = "policy-split" - -// PolicySplitKey is a module's policy-split finding's key. -func PolicySplitKey(module string) string { - return health.FindingKey(health.ScopeModule, module, KindPolicySplit) -} - -// PolicySplitFinding is the finding of one split, in mesh-sdk's layout. -func PolicySplitFinding(s Split, at time.Time) ([]byte, error) { - summary := fmt.Sprintf("%s's upgrade policy is %s at the assigner (%s) and %s at the controller", s.Module, s.Policy, s.From, - strings.Join(s.Controller, " and ")) - return health.EncodeFinding(health.Finding{Key: PolicySplitKey(s.Module), Kind: KindPolicySplit, - Subject: health.Subject{Scope: health.ScopeModule, ID: s.Module}, Severity: health.Warning, Summary: summary, - Headline: clipHeadline(s.Module + "'s upgrade policy is split"), - Needs: "set it with mesh-wants.upgrade, so the assigner and the controller agree", - Explanation: asks.NeedsYou + "set it with mesh-wants.upgrade, so the assigner and the controller agree." + " The controller still " + - "writes a policy beside each composition until it hands its lease away; a choice made at its upgrade verb after the copy " + - "is not the assigner's.", - Resolved: s.Module + "'s upgrade policy agrees again", Evidence: []health.Evidence{{At: at.UTC(), Said: summary}}, - Source: "assigner." + KindPolicySplit, Observer: delivery.AssignerModule, At: at.UTC()}) -} - -func clipHeadline(s string) string { - if len([]rune(s)) <= 60 { - return s - } - return string([]rune(s)[:60]) -} - // KindProvisionFailed is the assigner's finding of a provision its module's want waits for that failed (ADR 0570 §4). const KindProvisionFailed = "provision-failed" @@ -186,3 +126,10 @@ func UnplacedFinding(node string, at time.Time) ([]byte, error) { Resolved: node + " is placed", Evidence: []health.Evidence{{At: at.UTC(), Said: summary}}, Source: "assigner." + KindUnplaced, Observer: delivery.AssignerModule, At: at.UTC()}) } + +func clipHeadline(s string) string { + if len([]rune(s)) <= 60 { + return s + } + return string([]rune(s)[:60]) +} diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index bb2b26d..674eef8 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -60,14 +60,14 @@ func TestManifest(t *testing.T) { } // ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the // module), says each put's step as the event put, and calls the hand-act seat and the bus node's backup holder. - // ADR 0567 §1, §6-§10: it holds the seat mesh-placement, serving its seven verbs; keeps the seat's events on its stream - // placement for 30 days; calls the controller's export, its four verbs that keep its assignments in step and its - // hand-act log; and reads mesh-config's resolved values to compose every node a proposal touches. + // ADR 0567 §1, §6-§10: it holds the seat mesh-placement, serving its five verbs (adopt and hand-back went with the + // controller, ADR 0590); keeps the seat's events on its stream placement for 30 days; calls the hand-act log; and reads + // mesh-config's resolved values to compose every node a proposal touches. // ADR 0565 §7, §9 (issue 1953 D4): and the seat mesh-wants, serving upgrade and bus. if len(m.Claims) != 2 || m.Claims[0].Name != catalogue.PlacementSeat || m.Claims[0].At() != catalogue.ScopeMesh || - !slices.Equal(m.Claims[0].Serves, []string{"placement", "proposals", "propose", "withdraw", "again", "adopt", "hand-back"}) || + !slices.Equal(m.Claims[0].Serves, []string{"placement", "proposals", "propose", "withdraw", "again"}) || m.Claims[1].Name != catalogue.WantsSeat || m.Claims[1].At() != catalogue.ScopeMesh || !slices.Equal(m.Claims[1].Serves, []string{"upgrade", "bus"}) { - t.Errorf("the assigner claims %+v, want mesh-placement with its seven verbs and mesh-wants with its two", m.Claims) + t.Errorf("the assigner claims %+v, want mesh-placement with its five verbs and mesh-wants with its two", m.Claims) } if seat, _ := catalogue.SeatNamed(catalogue.WantsSeat); catalogue.CanHold(m, seat) != nil { t.Errorf("the assigner cannot hold mesh-wants: %v", catalogue.CanHold(m, seat)) diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 7dc241c..2a918d9 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -11,9 +11,7 @@ "proposals", "propose", "withdraw", - "again", - "adopt", - "hand-back" + "again" ] }, { diff --git a/modules/mesh-delivery-checker/internal/check/beside.go b/modules/mesh-delivery-checker/internal/check/beside.go index 43c0b8c..2f92eb1 100644 --- a/modules/mesh-delivery-checker/internal/check/beside.go +++ b/modules/mesh-delivery-checker/internal/check/beside.go @@ -2,7 +2,6 @@ package check import ( "context" - "fmt" "strconv" ) @@ -33,12 +32,13 @@ const ( // besideRules are the repositories each core repository's tests read, as the tests state it (mesh-sdk go/internal/beside // beside.go): the catalogue, the node-engine, the SDK, and the delivery's pinned forge dependencies (mesh-catalog -// checks/deps). Which commit of each is besideCommit's rule. +// checks/deps). Which commit of each is besideCommit's rule. No repository's tests read the controller, which ended by its +// unassignment (novox/hq ADR 0590, issue 1126); its own repository keeps its siblings while it holds a module. var besideRules = map[string][]string{ controllerRepo: {catalogueRepository, nodeEngineRepo, SDKRepository}, - nodeEngineRepo: {catalogueRepository, SDKRepository, controllerRepo}, + nodeEngineRepo: {catalogueRepository, SDKRepository}, SDKRepository: {catalogueRepository, nodeEngineRepo}, - catalogueRepository: {SDKRepository, controllerRepo}, + catalogueRepository: {SDKRepository}, "novox/mesh-delivery": {SDKRepository, catalogueRepository}, } @@ -82,10 +82,6 @@ func (j *Examiner) besideFor(ctx context.Context, c *Case, p Pull) ([]BesideRef, out = append(out, BesideRef{Repository: repo, Commit: member.Head, Ref: refOf(member.Number)}) } else if head := heads[repo]; head != "" { out = append(out, BesideRef{Repository: repo, Commit: head, atTrunk: true}) - } else if repo == controllerRepo && p.Repository == nodeEngineRepo { - // The node-engine's step-nine test judges the controller's own manifest and fails without it (issue 1295): a - // checker that cannot place it says so, where placing none left that test to skip. - return nil, fmt.Errorf("the trunk head of %s is not known, so the controller the tests of %s#%d read cannot be placed beside them", repo, p.Repository, p.Number) } } } diff --git a/modules/mesh-delivery-checker/internal/check/pins_test.go b/modules/mesh-delivery-checker/internal/check/pins_test.go index 878d720..ac6ac17 100644 --- a/modules/mesh-delivery-checker/internal/check/pins_test.go +++ b/modules/mesh-delivery-checker/internal/check/pins_test.go @@ -2,6 +2,7 @@ package check import ( "maps" + "slices" "strings" "testing" @@ -106,34 +107,31 @@ func TestTheSiblingsAreWhereTheTestsStateThem(t *testing.T) { // review D2, ADR } } -// 1295/R4g: the node-engine's tests read the controller's manifest, so a mesh-host pull request has mesh-controller beside it: -// at the group member's head when the group holds one, else at its trunk. -func hostCase(t *testing.T, pulls ...delivery.CandidatePull) *Case { - t.Helper() - c, err := delivery.DecodeCandidate(candidateOf(t, "feat/x", pulls...)) - if err != nil { - t.Fatal(err) - } - cs := CaseOf(c, map[string]string{"novox/mesh-host": trunkA, controllerRepo: trunkA2}) - if cs == nil { - t.Fatal("no case") - } - return cs -} - -func TestTheNodeEnginesTestsHaveTheControllerBesideThem(t *testing.T) { +// 1126/B8 (novox/hq ADR 0590): the controller is gone, so no repository's tests read it: a mesh-host pull request has +// the catalogue and mesh-sdk beside it and no mesh-controller, even where its trunk is known and the group holds one. +func TestNoRepositorysTestsHaveTheControllerBesideThem(t *testing.T) { const host = "novox/mesh-host" b := newBench(t) b.ex.Trunks = trunksOf(t, map[string]string{host: trunkA, controllerRepo: trunkA2, catalogueRepository: trunkS, SDKRepository: trunkS}) - b.judge(t, hostCase(t, pullOf(host, 5, head1))) - if c := besideOf(t, b, host)[controllerRepo]; c.Commit != trunkA2 || c.Ref != "" { - t.Errorf("the controller beside the node-engine alone = %+v, want its trunk %s", c, trunkA2) + c, err := delivery.DecodeCandidate(candidateOf(t, "feat/x", pullOf(host, 5, head1), pullOf(controllerRepo, 9, head2))) + if err != nil { + t.Fatal(err) } - b = newBench(t) - b.ex.Trunks = trunksOf(t, map[string]string{host: trunkA, controllerRepo: trunkA2, catalogueRepository: trunkS, SDKRepository: trunkS}) - b.judge(t, hostCase(t, pullOf(host, 5, head1), pullOf(controllerRepo, 9, head2))) - if c := besideOf(t, b, host)[controllerRepo]; c.Commit != head2 || c.Ref != "refs/pull/9/head" { - t.Errorf("the controller beside the node-engine in a group = %+v, want the member's head %s", c, head2) + if v := b.judge(t, CaseOf(c, map[string]string{host: trunkA, controllerRepo: trunkA2})).Verdict; v.State == delivery.VerdictFail && + strings.Contains(v.Failed.Words, "mesh-controller") { + t.Fatalf("verdict = %+v", v) + } + got := besideOf(t, b, host) + if _, ok := got[controllerRepo]; ok { + t.Errorf("the controller is beside the node-engine: %+v", got) + } + if _, ok := got[catalogueRepository]; !ok { + t.Errorf("the catalogue is not beside the node-engine: %+v", got) + } + for repo, siblings := range besideRules { + if repo != controllerRepo && slices.Contains(siblings, controllerRepo) { + t.Errorf("the tests of %s read the controller", repo) + } } } @@ -148,15 +146,15 @@ func TestTheChangesOwnRepositoryIsBesideItAtItsTrunk(t *testing.T) { } } -// 1887/A12: a catalogue change's siblings hold the catalogue itself at the trunk it is merged onto, beside mesh-sdk and -// mesh-controller, so the catalogue's shrink-only list tests read $MESH_CHECK_BESIDE/mesh-catalog. +// 1887/A12: a catalogue change's siblings hold the catalogue itself at the trunk it is merged onto, beside mesh-sdk, so +// the catalogue's shrink-only list tests read $MESH_CHECK_BESIDE/mesh-catalog. func TestACatalogueChangeHasTheCatalogueBesideItAtItsTrunk(t *testing.T) { b := newBench(t) b.ex.Trunks = trunksOf(t, map[string]string{catalogueRepository: trunkA, SDKRepository: trunkS, controllerRepo: trunkA2}) out := b.judge(t, caseOf(t, pullOf(catalogueRepository, 12, head1))) // ADR 0548 §6: the outcome names the siblings read at their trunk's head, never the repository it merges. - if out.Beside[SDKRepository] != trunkS || out.Beside[controllerRepo] != trunkA2 || out.Beside[catalogueRepository] != "" { - t.Errorf("the siblings named for the verdict = %v, want mesh-sdk and mesh-controller at their trunks and not the catalogue", out.Beside) + if out.Beside[SDKRepository] != trunkS || out.Beside[controllerRepo] != "" || out.Beside[catalogueRepository] != "" { + t.Errorf("the siblings named for the verdict = %v, want mesh-sdk at its trunk and neither the controller nor the catalogue", out.Beside) } got := besideOf(t, b, catalogueRepository) if c := got[catalogueRepository]; c.Commit != trunkA || c.Ref != "" { @@ -188,15 +186,3 @@ func TestAMeshLabHeadGetsACheck(t *testing.T) { t.Fatalf("mesh-lab was not fetched: %+v", fetched) } } - -// 1295: the node-engine's step-nine test fails without the controller beside it, so a checker that knows no trunk head of the -// controller fails the check at the placing, loudly, and does not place none. -func TestTheNodeEnginesCheckFailsWhereTheControllersTrunkIsNotKnown(t *testing.T) { - const host = "novox/mesh-host" - b := newBench(t) - b.ex.Trunks = trunksOf(t, map[string]string{host: trunkA, catalogueRepository: trunkS, SDKRepository: trunkS}) - v := b.judge(t, hostCase(t, pullOf(host, 5, head1))).Verdict - if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "mesh-controller") || !strings.Contains(v.Failed.Words, "cannot be placed") { - t.Fatalf("verdict = %+v", v) - } -} diff --git a/modules/mesh-delivery-indexer/internal/beside/beside.go b/modules/mesh-delivery-indexer/internal/beside/beside.go index 0bf4a8b..d1cd8ff 100644 --- a/modules/mesh-delivery-indexer/internal/beside/beside.go +++ b/modules/mesh-delivery-indexer/internal/beside/beside.go @@ -16,11 +16,10 @@ // check gets is chosen from the inventory: mesh-catalog by the source of the `nats` module, mesh-host // by the source of `mesh-host`, and mesh-sdk as the controller's sibling at the commit the controller's // go.mod pins. Each repository's tests read these siblings, and the checker places exactly these -// (mesh-delivery's checker, besideRules, follows this list): mesh-controller's read mesh-catalog, -// mesh-host and mesh-sdk; mesh-host's read mesh-catalog, mesh-sdk and mesh-controller (its step-nine -// test reads the controller's own manifest, novox/hq issue 1295); mesh-sdk's read mesh-catalog and -// mesh-host; mesh-catalog's read mesh-sdk and mesh-controller; mesh-delivery's read mesh-sdk and -// mesh-catalog. In a mesh where either module has no source repository nothing is +// (mesh-delivery's checker, besideRules, follows this list): mesh-host's read mesh-catalog and +// mesh-sdk; mesh-sdk's read mesh-catalog and mesh-host; mesh-catalog's read mesh-sdk; mesh-delivery's +// read mesh-sdk and mesh-catalog. None reads mesh-controller, which ended by its unassignment (novox/hq +// ADR 0590). In a mesh where either module has no source repository nothing is // cloned, and these tests fail loudly with "not beside this check": a cause in the setup, not in the // change. And in a delivery group that holds a mesh-catalog pull request, these tests read the // catalogue's main, not the group's head. diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index fa3ef9e..fc74f5b 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -4005,7 +4005,9 @@ func toolsSecretProblems(m catalogue.Manifest) []string { return problems } -var BusRemovals = []string{catalogue.RemovesControllerDeadLetterConsumer, catalogue.RemovesEngineActs, catalogue.RemovesAssignmentsStream} +// BusRemovals are what a bus account may remove, the closed set of novox/hq ADR 0564 Limits 4 as ADR 0590 clause 6 +// changes it: the node-engine's acts and the ended controller's leftovers. +var BusRemovals = []string{catalogue.RemovesEngineActs, catalogue.RemovesControllerLeftovers} // ReadsHolderProblems is what a holder of node-reads is held to (novox/hq ADR 0310 §3): it reads as an // account of its own, so the node's file modes bound what it sees. Carried by the machine's runtime it would @@ -7251,37 +7253,6 @@ func namesOfAccessIDs(accesses map[string]string) []string { var renderFuncs = template.FuncMap{"quote": quote} var defaultSeats = append([]catalogue.Seat{ - // The control plane states what it did under the seat it holds (novox/hq ADR 0134): a role's - // events belong to the role, so they keep their address while the holder is replaced. No accepts, - // so no work queue is raised for it — only what its holder may say. - // And it serves the mesh's own verbs as the seat's tools (novox/hq ADR 0154): `status`, `push`, - // `assign` and the rest are a role's interface, not a container's, and stay addressable while - // the control plane is replaced. - {Name: catalogue.ControllerSeatName, Scope: catalogue.ScopeMesh, Decision: "novox/hq ADR 0079", - // And what is wrong, as it changes, and the self-check's heartbeat (novox/hq to-be 45 §2, §4). - Emits: []string{"applied", "refused", "built-before", - "condition-raised", "condition-changed", "condition-cleared", "doctor-heartbeat", - // A value given by hand, replaced after its module's first good start (novox/hq ADR 0228). - "secret-replaced", - // Every act a healer takes (novox/hq to-be 45 §7). - "healer-acted", - // A build put back after its gate failed (novox/hq ADR 0236, to-be 45 §8). - "rolled-back", - // A pull request's merge check, judged (novox/hq to-be 45 §9). - "checked", - // A walk kept in a new state, and a tier sent to a machine: kept until the catalogue's main consumes them no - // more; nothing emits them (ADR 0539). - "plan-moved", - "sent", - // A key of a record mirror written or removed (novox/hq ADR 0289, design 54 §3; design 57 F3). - "assignment-changed", "declared-config-changed", "predefined-changed", - // A builder's key not taken in because the artifact store does not hold an artifact it names (novox/hq - // issue 1937): the builder builds it again. - "artifacts-gone", - // A bucket of a state this release deleted, deleted at the controller's first start (novox/hq ADR 0539 - // clause 13): the merge window's, the silent sends' and the building plans' hold, each said once. - "bucket-deleted"}, - Serves: catalogue.ControllerVerbs}, // The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable, // served by whichever module holds the seat with tools of these names. {Name: "mesh-store", Scope: catalogue.ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079", @@ -7957,14 +7928,6 @@ func placementVerbs() []catalogue.Verb { "stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. " + "Nothing more: the recovery of a proposal whose judgement or ask was lost.", Input: schema(map[string]string{"id": "the proposal's id"}, []string{"id"})}, - {Name: catalogue.PlacementVerbAdopt, Optional: true, Description: "Take placement over from the controller: its " + - "placement-export is proposed whole as the seed, every node's key, which every node must answer as the same. " + - "Its Approve is the take-over; at the warrant the export is read again and a changed one is proposed anew.", - Input: schema(map[string]string{"why": "why, shown to the operator"}, []string{"why"})}, - {Name: catalogue.PlacementVerbHandBack, Optional: true, Description: "The operator's alone, recorded as a hand act: " + - "delete every placement key, the history kept, so every node returns to the controller's compositions. " + - "Refused after the lease write.", - Input: schema(map[string]string{"why": "why, recorded with the hand act"}, []string{"why"})}, } } diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 75bd736..9a7288b 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -2588,7 +2588,7 @@ func TestAProcessDeclaresItsBusAndIsAccepted(t *testing.T) { for _, c := range []struct { name, manifest, refused string }{ - {name: "the bus module's tools removing", manifest: tools(broker, `{"removes":["DEAD_LETTER_NOTICES.controller","mesh-host_acts","ASSIGNMENTS"]}`)}, + {name: "the bus module's tools removing", manifest: tools(broker, `{"removes":["mesh-host_acts","controller-leftovers"]}`)}, {name: "a removal outside the set", manifest: tools(broker, `{"removes":["EVENTS.a_m"]}`), refused: "not one of"}, {name: "a removal beside a grant", manifest: tools(broker, `{"removes":["mesh-host_acts"],"invokes":["nats.nats_streams"]}`), refused: "with nothing else"}, {name: "another module's tools removing", manifest: tools(`[]`, `{"removes":["mesh-host_acts"]}`), refused: "only the bus module"}, @@ -2613,7 +2613,7 @@ func TestAProcessDeclaresItsBusAndIsAccepted(t *testing.T) { } accounts := m.BusAccounts() if len(accounts) != 1 || accounts[0].ID != "nats-tools" || !accounts[0].Tools || - !strSliceEqual(accounts[0].Bus.Removes, []string{catalogue.RemovesControllerDeadLetterConsumer, catalogue.RemovesEngineActs, catalogue.RemovesAssignmentsStream}) { + !strSliceEqual(accounts[0].Bus.Removes, []string{catalogue.RemovesEngineActs, catalogue.RemovesControllerLeftovers}) { t.Errorf("%s: the accounts are %+v", c.name, accounts) } // Resolved, the build goes and its bundles keep the bus (build.go). @@ -4357,7 +4357,7 @@ func TestTheBussAccountSaysNothingOnTheBus(t *testing.T) { "invokes":["seat:mesh-hand-acts.hand-act"],"consumes":[], "tools":["nats_server","nats_bucket_expiry"], "build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go","system":"arch","from":"cmd/nats-tools", - "binary":"nats-tools","loads":["nats-tools"],"bus":{"removes":["DEAD_LETTER_NOTICES.controller","ASSIGNMENTS"]}}]}}`) + "binary":"nats-tools","loads":["nats-tools"],"bus":{"removes":["controller-leftovers"]}}]}}`) nm, err := ParseManifest(nats) if err != nil { t.Fatalf("the bus module whose tools bundle invokes a seat verb was refused: %v", err) @@ -4970,9 +4970,10 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { delivered[s.Delivers] = s.Name } } - // Forty-seven with mesh-placement (novox/hq ADR 0567), serving its seven verbs. + // Forty-seven with mesh-placement (novox/hq ADR 0567), serving its five verbs: adopt and hand-back went with the + // controller (novox/hq ADR 0590, issue 1126). if s, ok := catalogue.SeatNamed(catalogue.PlacementSeat); !ok || s.Scope != catalogue.ScopeMesh || !reflect.DeepEqual(catalogue.VerbNames(s.Serves), - []string{"placement", "proposals", "propose", "withdraw", "again", "adopt", "hand-back"}) { + []string{"placement", "proposals", "propose", "withdraw", "again"}) { t.Errorf("the placement seat is %v, %v", ok, s) } // The checker's seat serves the try, which replaces the shell's merge check (ADR 0547 §10, ADR 0245). @@ -5049,9 +5050,10 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { // Forty-eight with mesh-delivery-checker (novox/hq ADR 0547 §10, issue 774); forty-nine with node-engine (novox/hq ADR 0563 §1); // fifty-three with the four health seats (novox/hq ADR 0564 clause 1); fifty-five with mesh-wants and mesh-hand-acts // (novox/hq ADR 0565 clause 1); fifty-six with mesh-artifact-sweep (novox/hq ADR 0571 clause 2); - // fifty-seven with mesh-grants (ADR 0582 clause 1); fifty-eight with mesh-enrolment (novox/hq ADR 0587 §1). - if len(catalogue.Seats()) != 58 { - t.Errorf("the mesh defines %d seats rather than 58; the set is closed, so a change here is "+ + // fifty-seven with mesh-grants (ADR 0582 clause 1); fifty-eight with mesh-enrolment (novox/hq ADR 0587 §1); fifty-six + // without the controller's seat and mesh-grants, which served its lease write alone (novox/hq ADR 0590, issue 1126). + if len(catalogue.Seats()) != 56 { + t.Errorf("the mesh defines %d seats rather than 56; the set is closed, so a change here is "+ "a decision (novox/hq ADR 0110): %s", len(catalogue.Seats()), seatNames()) } } @@ -5286,10 +5288,10 @@ func TestUsingASeatNobodyDeclaresIsRefused(t *testing.T) { catalogue.Manifest{Module: "shop", Invokes: []string{"seat:telegram-sender.status"}}, unclaimed, true}, {"a verb of a mesh seat whose holder lives in another repository", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-watchdogs.signals"}}, telegram(), false}, - {"a verb a mesh seat does not promise", `the seat mesh-controller serves no verb "root-freed"`, - catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-controller.root-freed"}}, telegram(), false}, - {"the controller's verb: the controller answers its seat", "", - catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-controller.root-free"}}, telegram(), false}, + {"a verb a mesh seat does not promise", `the seat mesh-placement serves no verb "adopt"`, + catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-placement.adopt"}}, telegram(), false}, + {"the controller's seat is gone (ADR 0590)", `a verb of the seat "mesh-controller", ` + catalogue.UndeclaredSeat, + catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-controller.root-free"}}, telegram(), true}, {"the engine's verb: every node's engine holds its seat", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-engine.root-free"}}, telegram(), false}, {"a verb the engine's seat does not promise", `the seat node-engine serves no verb "root-freed"`,