mesh/delivery-group group feat/sdk1 rejected: composed together: the checker's verdict at the members' heads (428d048dd50a): unit: git.novox.be/novox/mesh-sdk/…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-sdk/go/process.TestStatePutWritesAKeyWhole: a Put through a short blackout: 0, mesh-mail's state send…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
91 lines
4.3 KiB
Go
91 lines
4.3 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 newBroker() Manifest {
|
|
return Manifest{Module: "new-broker", Provides: []Offer{{Name: "mesh-bus", Scope: ScopeMesh}},
|
|
Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// 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: "node-build-go", Scope: ScopeNode, Decision: "row"}})
|
|
got, ok := SeatNamed("node-build-go")
|
|
if !ok || len(got.Accepts) == 0 || !got.PulledByKind || !got.Yields {
|
|
t.Fatalf("the Go build 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)
|
|
}
|
|
}
|