`assign ace postgres` made the control plane's own store unresolvable: postgres's manifest claims mesh-store, nobody was ever recorded as its holder, and with two eligible assignments and nothing on record both claimed and both were refused — novox's included. ADR 0110 says the assignment holds, by a deliberate act; ADR 0131 gave the record its force but left a seat nobody handed over held by whichever assignment happened to be alone. Before acting on an assignment the controller now writes the derived answer down: every mesh-scoped seat the store knows, resolved to exactly one holder with nothing on record, gets that holder recorded — the same record `seat <name> --to <node>/<module>` makes by hand. The next assignment able to hold the seat then stands beside the holder, eligible and silent. A seat with two derived claimants is left for a person; a seat on record is never rewritten; seats the store does not list stay held by derivation as before. And the refusal, when it still happens, names the handover that records the holder. Verified: catalogue tests against the real catalogue; the cmd suite against a store (the only failure, TestConvergingPreviewsThenChanges…, fails identically on main).
174 lines
7.4 KiB
Go
174 lines
7.4 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// **A seat's holder on record settles who holds it, and lets the next holder stand beside the
|
|
// current one** (novox/hq ADR 0131, design 28 task 5.3). Until the record existed, two assignments
|
|
// whose modules both claimed a seat were refused outright — which left no way to hand a seat over
|
|
// without a moment where nobody held it, and the control plane finds its own bus through one of
|
|
// these seats. That moment was the outage of 2026-09-27.
|
|
|
|
func busSeatDelivering(t *testing.T, delivers string) {
|
|
t.Helper()
|
|
was := Seats()
|
|
t.Cleanup(func() { UseSeats(was) })
|
|
UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: delivers, Decision: "test"}})
|
|
}
|
|
|
|
func oldBroker() Manifest {
|
|
return Manifest{Module: "old-broker", Provides: []Offer{{Name: "mesh-bus", Scope: ScopeMesh}},
|
|
Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}}
|
|
}
|
|
|
|
func newBroker() Manifest {
|
|
return Manifest{Module: "new-broker", Provides: []Offer{{Name: "mesh-bus", Scope: ScopeMesh}},
|
|
Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}}
|
|
}
|
|
|
|
// Nothing on record: exactly the old rule. One claimant holds; two are refused.
|
|
func TestWithNoHolderOnRecordTheSoleClaimantHoldsAndTwoAreRefused(t *testing.T) {
|
|
busSeatDelivering(t, "mesh-bus")
|
|
node := Node{Name: "anchor"}
|
|
|
|
held, problems := checkClaims([]Manifest{oldBroker()}, node, nil, nil)
|
|
if len(problems) != 0 || len(held) != 1 || held[0].Module != "old-broker" {
|
|
t.Fatalf("a sole claimant did not hold the seat: held=%v problems=%v", held, problems)
|
|
}
|
|
_, problems = checkClaims([]Manifest{oldBroker(), newBroker()}, node, nil, nil)
|
|
if len(problems) != 1 || !strings.Contains(problems[0], "both claim") {
|
|
t.Fatalf("two claimants with nothing on record were not refused: %v", problems)
|
|
}
|
|
}
|
|
|
|
// With a holder on record, the other eligible assignment is silent: not refused, and not holding.
|
|
func TestTheHolderOnRecordHoldsAndTheOtherClaimantStandsBesideIt(t *testing.T) {
|
|
busSeatDelivering(t, "mesh-bus")
|
|
node := Node{Name: "anchor"}
|
|
record := []Held{{Claim: "mesh-broker", Scope: ScopeMesh, Node: "anchor", Module: "new-broker"}}
|
|
|
|
held, problems := checkClaims([]Manifest{oldBroker(), newBroker()}, node, nil, record)
|
|
if len(problems) != 0 {
|
|
t.Fatalf("the assignment beside the holder was refused: %v", problems)
|
|
}
|
|
if len(held) != 1 || held[0].Module != "new-broker" {
|
|
t.Fatalf("the holder on record is not the one holding: %v", held)
|
|
}
|
|
}
|
|
|
|
// The record names a node too: an eligible module on another machine holds nothing, and its
|
|
// machine's set still resolves.
|
|
func TestAHolderOnRecordElsewhereLeavesThisMachinesClaimantSilent(t *testing.T) {
|
|
busSeatDelivering(t, "mesh-bus")
|
|
record := []Held{{Claim: "mesh-broker", Scope: ScopeMesh, Node: "anchor", Module: "new-broker"}}
|
|
|
|
held, problems := checkClaims([]Manifest{oldBroker()}, Node{Name: "laptop"}, nil, record)
|
|
if len(problems) != 0 || len(held) != 0 {
|
|
t.Fatalf("a claimant elsewhere than the recorded holder was not simply silent: held=%v problems=%v",
|
|
held, problems)
|
|
}
|
|
}
|
|
|
|
// A record naming a seat's former name still applies to it after a rename (ADR 0122).
|
|
func TestAHolderRecordedUnderAFormerNameStillHolds(t *testing.T) {
|
|
busSeatDelivering(t, "mesh-bus")
|
|
wasAliases := aliases
|
|
t.Cleanup(func() { UseAliases(wasAliases) })
|
|
UseAliases(map[string]string{"the-broker": "mesh-broker"})
|
|
record := []Held{{Claim: "the-broker", Scope: ScopeMesh, Node: "anchor", Module: "new-broker"}}
|
|
|
|
held, problems := checkClaims([]Manifest{oldBroker(), newBroker()}, Node{Name: "anchor"}, nil, record)
|
|
if len(problems) != 0 || len(held) != 1 || held[0].Module != "new-broker" {
|
|
t.Fatalf("a record under the former name did not settle the seat: held=%v problems=%v", held, problems)
|
|
}
|
|
}
|
|
|
|
// CanHold is the one judgement registration and the handover share, against the store's row.
|
|
func TestCanHoldJudgesClaimScopeAndWhatTheSeatDelivers(t *testing.T) {
|
|
busSeatDelivering(t, "mesh-bus")
|
|
seat, _ := SeatNamed("mesh-broker")
|
|
|
|
if err := CanHold(newBroker(), seat); err != nil {
|
|
t.Fatalf("a module that claims the seat and provides what it delivers was refused: %v", err)
|
|
}
|
|
noClaim := Manifest{Module: "quiet", Provides: []Offer{{Name: "mesh-bus", Scope: ScopeMesh}}}
|
|
if err := CanHold(noClaim, seat); err == nil || !strings.Contains(err.Error(), "does not claim") {
|
|
t.Fatalf("a module that never claimed the seat was allowed to hold it: %v", err)
|
|
}
|
|
wrongScope := newBroker()
|
|
wrongScope.Claims[0].Scope = ScopeNode
|
|
if err := CanHold(wrongScope, seat); err == nil || !strings.Contains(err.Error(), "scope") {
|
|
t.Fatalf("a claim at the wrong scope was allowed: %v", err)
|
|
}
|
|
cannotAnswer := Manifest{Module: "amqp-only", Provides: []Offer{{Name: "amqp", Scope: ScopeMesh}},
|
|
Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}}
|
|
if err := CanHold(cannotAnswer, seat); err == nil || !strings.Contains(err.Error(), `does not provide "mesh-bus"`) {
|
|
t.Fatalf("a holder that cannot answer for the seat was allowed: %v", err)
|
|
}
|
|
// And the judgement follows the store's row, not a compiled copy.
|
|
busSeatDelivering(t, "amqp")
|
|
seat, _ = SeatNamed("mesh-broker")
|
|
if err := CanHold(cannotAnswer, seat); err != nil {
|
|
t.Fatalf("with the row saying amqp, an amqp provider was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// A machine being moved is handed its membership for the new bus as a sealed file in its own
|
|
// declaration — the machine's, not any module's (design 28, task 5.2).
|
|
func TestAMembershipForTheNewBusIsComposedAsASealedFile(t *testing.T) {
|
|
r := Resolution{Node: "anchor"}
|
|
got, err := r.Compose(Rendering{BusMembership: "sealed-blob"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var found map[string]any
|
|
for _, res := range got.Resources {
|
|
if res["id"] == BusMembershipID() {
|
|
found = res
|
|
}
|
|
}
|
|
if found == nil {
|
|
t.Fatalf("no membership resource in %v", got.Resources)
|
|
}
|
|
if found["path"] != BusMembershipPath || found["sealed"] != "sealed-blob" || found["mode"] != "0600" {
|
|
t.Fatalf("the membership is not a sealed 0600 file where the host reads it: %v", found)
|
|
}
|
|
// And a machine not being moved is handed nothing.
|
|
got, _ = r.Compose(Rendering{})
|
|
for _, res := range got.Resources {
|
|
if res["id"] == BusMembershipID() {
|
|
t.Fatal("a machine with no membership on record was handed one")
|
|
}
|
|
}
|
|
}
|
|
|
|
// The store's seat rows have no protocol columns yet; loading them must not drop the protocol the
|
|
// bus is derived from, or no role's work queue is ever raised (found live, 2026-09-28).
|
|
func TestAStoreRowWithoutAProtocolKeepsTheCompiledOne(t *testing.T) {
|
|
was := Seats()
|
|
t.Cleanup(func() { UseSeats(was) })
|
|
UseSeats([]Seat{{Name: "mesh-build-machine", Scope: ScopeMesh, Decision: "row"}})
|
|
got, ok := SeatNamed("mesh-build-machine")
|
|
if !ok || len(got.Accepts) == 0 {
|
|
t.Fatalf("the build machine's seat lost what it accepts when loaded from the store: %+v", got)
|
|
}
|
|
if got.Decision != "row" {
|
|
t.Fatalf("the store's own columns were not kept: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestARefusalWithNothingOnRecordNamesTheHandover(t *testing.T) {
|
|
busSeatDelivering(t, "mesh-bus")
|
|
elsewhere := []Held{{Claim: "mesh-broker", Scope: ScopeMesh, Node: "anchor", Module: "old-broker"}}
|
|
|
|
_, problems := checkClaims([]Manifest{newBroker()}, Node{Name: "laptop"}, elsewhere, nil)
|
|
if len(problems) != 1 {
|
|
t.Fatalf("two derived claimants across machines were not refused: %v", problems)
|
|
}
|
|
if !strings.Contains(problems[0], "`seat mesh-broker --to anchor/old-broker`") {
|
|
t.Fatalf("the refusal does not name the handover that records the holder: %s", problems[0])
|
|
}
|
|
}
|