catalogue's working set starts empty and a host gives it the seats it read (UseSeats over SeatsFromDefinitions); the node-engine's verbs and events take the seat's name from the caller. Tests take their seats from seatlisttest. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
90 lines
4.1 KiB
Go
90 lines
4.1 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", Serves: verbsNamed(brokerVerbs())}})
|
|
}
|
|
|
|
// brokerVerbs are the bus seat's verbs as a literal: mesh-sdk imports no seat's contract module, a test neither
|
|
// (ADR 0625 §3).
|
|
func brokerVerbs() []string {
|
|
return []string{"list", "show", "check", "orphans", "create", "apply", "recreate", "drop"}
|
|
}
|
|
|
|
func newBroker() Manifest {
|
|
return Manifest{Module: "new-broker", Provides: []Offer{{Name: "mesh-bus", Scope: ScopeMesh}},
|
|
Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh, Serves: brokerVerbs()}}}
|
|
}
|
|
|
|
// 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, Serves: brokerVerbs()}}}
|
|
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)
|
|
}
|
|
// A holder's own tools need not be the seat's verbs: the claim may name what it serves for the
|
|
// role (ADR 0160), and then only those count — and only the seat's verbs may be named.
|
|
promising := seat
|
|
promising.Serves = []Verb{{Name: "databases"}, {Name: "query"}}
|
|
engine := newBroker()
|
|
engine.Tools = []string{"engine_list_databases", "engine_query"}
|
|
if err := CanHold(engine, promising); err == nil || !strings.Contains(err.Error(), "does not serve databases, query") {
|
|
t.Fatalf("a holder whose tools are not the seat's verbs was allowed without saying what it serves: %v", err)
|
|
}
|
|
engine.Claims[0].Serves = []string{"databases", "query"}
|
|
if err := CanHold(engine, promising); err != nil {
|
|
t.Fatalf("a claim naming the seat's verbs was refused: %v", err)
|
|
}
|
|
engine.Claims[0].Serves = []string{"databases"}
|
|
if err := CanHold(engine, promising); err == nil || !strings.Contains(err.Error(), "does not serve query") {
|
|
t.Fatalf("a claim naming half the verbs was allowed: %v", err)
|
|
}
|
|
engine.Claims[0].Serves = []string{"databases", "query", "engine_query"}
|
|
if err := CanHold(engine, promising); err == nil || !strings.Contains(err.Error(), "does not promise") {
|
|
t.Fatalf("a claim naming a verb the seat never promised 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)
|
|
}
|
|
}
|
|
|
|
func verbsNamed(names []string) []Verb {
|
|
var out []Verb
|
|
for _, n := range names {
|
|
out = append(out, Verb{Name: n})
|
|
}
|
|
return out
|
|
}
|