Two halves of novox/hq ADR 0131. Migration 0040 moves the mesh-broker row from `amqp` to `mesh-bus`, so the seat's holder answers for the mesh's bus and not for the wire protocol the old broker spoke — which is what let only the retiring broker hold the seat that names the bus. Safe under the current holder: the control plane composes its own address through the seat by name and the overview derives holders by name; only registration and provision resolution read the column. What must not happen in between is re-registering the current holder. And the parser refuses a manifest that provides or requires `amqp`, each refusal saying what to do instead: a module reaches the mesh's bus through the sdk and depends on the seat, not on a protocol. A whole-catalogue test asserts nothing beside this checkout names it; the three modules that did are removed there. Two tests that used the old broker as a fixture now use the module that replaces it or a manifest this package owns.
147 lines
6.4 KiB
Go
147 lines
6.4 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func problemsFor(t *testing.T, shelf Shelf) string {
|
|
t.Helper()
|
|
return strings.Join(CatalogueProblems(shelf), "; ")
|
|
}
|
|
|
|
func telegram() Manifest {
|
|
return Manifest{Module: "telegram", Tools: []string{"status"}, DefinesSeats: []SeatDeclaration{{
|
|
Name: "telegram-sender", Scope: ScopeMesh,
|
|
Accepts: []string{"send"}, Emits: []string{"delivered", "failed"}, Serves: []string{"status"},
|
|
}}, Claims: []Claim{{Name: "telegram-sender", Scope: ScopeMesh}}}
|
|
}
|
|
|
|
// The whole point: a module contributes a capability without the mesh being changed.
|
|
func TestAModuleDeclaresItsOwnSeatAndHoldsIt(t *testing.T) {
|
|
shop := Manifest{Module: "shop", Uses: []string{"telegram-sender"}}
|
|
if got := problemsFor(t, Shelf{"telegram": telegram(), "shop": shop}); got != "" {
|
|
t.Fatalf("a declared seat and its caller were refused: %s", got)
|
|
}
|
|
}
|
|
|
|
// The prefix is the reservation rule, so there is no list to maintain and none to drift.
|
|
func TestAModuleCannotDeclareAMeshSeat(t *testing.T) {
|
|
for _, n := range []string{"mesh-broker", "mesh-anything", "mesh-store"} {
|
|
m := Manifest{Module: "impostor", DefinesSeats: []SeatDeclaration{{Name: n, Accepts: []string{"x"}}}}
|
|
got := strings.Join(declaredSeatProblems(m), "; ")
|
|
if !strings.Contains(got, "reserved to the mesh") {
|
|
t.Fatalf("%q was accepted as a module's seat: %q", n, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A seat name meaning two protocols is the failure nobody could diagnose afterwards.
|
|
func TestTwoModulesCannotDeclareTheSameSeat(t *testing.T) {
|
|
other := Manifest{Module: "aardvark", DefinesSeats: []SeatDeclaration{{
|
|
Name: "telegram-sender", Scope: ScopeMesh, Accepts: []string{"something-else"}}}}
|
|
got := problemsFor(t, Shelf{"telegram": telegram(), "aardvark": other})
|
|
if !strings.Contains(got, "already declares") {
|
|
t.Fatalf("both declarations stood: %s", got)
|
|
}
|
|
// The first declarer keeps it; only the second is refused.
|
|
if strings.Count(got, "already declares") != 1 {
|
|
t.Fatalf("expected exactly one refusal: %s", got)
|
|
}
|
|
}
|
|
|
|
// Where ADR 0110's guarantee lands under a derived set: a typo is refused, not resolved to
|
|
// nothing at runtime.
|
|
func TestUsingASeatNobodyDeclaresIsRefused(t *testing.T) {
|
|
shop := Manifest{Module: "shop", Uses: []string{"telegram-sendr"}}
|
|
got := problemsFor(t, Shelf{"telegram": telegram(), "shop": shop})
|
|
if !strings.Contains(got, "telegram-sendr") || !strings.Contains(got, "no module declares") {
|
|
t.Fatalf("a misspelled seat was accepted: %s", got)
|
|
}
|
|
}
|
|
|
|
// A holder that does not answer what the seat promises is a caller's timeout, found here instead.
|
|
func TestAHolderMustServeWhatItsSeatPromises(t *testing.T) {
|
|
m := telegram()
|
|
m.Tools = nil // declares the seat, serves none of it
|
|
got := problemsFor(t, Shelf{"telegram": m})
|
|
if !strings.Contains(got, "does not serve status") {
|
|
t.Fatalf("a holder was accepted that answers nothing its seat promises: %s", got)
|
|
}
|
|
}
|
|
|
|
// A seat with no protocol is a marker: which module is this node's showcase, or its packet filter.
|
|
// Most node-scoped seats are markers, so refusing one would refuse the majority of the set.
|
|
func TestASeatWithoutAProtocolIsAMarkerNotAMistake(t *testing.T) {
|
|
m := Manifest{Module: "vague", DefinesSeats: []SeatDeclaration{{Name: "something", Scope: ScopeNode}}}
|
|
if got := strings.Join(declaredSeatProblems(m), "; "); got != "" {
|
|
t.Fatalf("a marker seat was refused: %s", got)
|
|
}
|
|
}
|
|
|
|
// A claim at the wrong scope is a different seat than the one declared.
|
|
func TestAClaimMustMatchTheDeclaredScope(t *testing.T) {
|
|
m := telegram()
|
|
m.Claims = []Claim{{Name: "telegram-sender", Scope: ScopeNode}}
|
|
got := problemsFor(t, Shelf{"telegram": m})
|
|
if !strings.Contains(got, "scope") {
|
|
t.Fatalf("a claim at the wrong scope was accepted: %s", got)
|
|
}
|
|
}
|
|
|
|
// The mesh's own seats still work, and are not shadowed by the derived half.
|
|
func TestTheMeshsOwnSeatsAreStillClaimable(t *testing.T) {
|
|
// It delivers the bus, so its holder provides the bus — the rule this check now enforces.
|
|
m := Manifest{Module: "nats",
|
|
Provides: []Offer{{Name: "mesh-bus", Scope: ScopeMesh}},
|
|
Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}}}
|
|
if got := problemsFor(t, Shelf{"nats": m}); got != "" {
|
|
t.Fatalf("a mesh seat was refused by the derived check: %s", got)
|
|
}
|
|
}
|
|
|
|
// A refusal that reorders itself between runs is a refusal nobody can diff.
|
|
func TestTheProblemsAreStable(t *testing.T) {
|
|
shelf := Shelf{"telegram": telegram(), "shop": {Module: "shop", Uses: []string{"nope"}},
|
|
"other": {Module: "other", Uses: []string{"also-nope"}}}
|
|
first, second := problemsFor(t, shelf), problemsFor(t, shelf)
|
|
if first != second {
|
|
t.Fatalf("unstable:\n%s\n%s", first, second)
|
|
}
|
|
}
|
|
|
|
// **A build machine has no store, so it may not judge a seat.** The set is data the control plane
|
|
// owns (novox/hq ADR 0122), and `ParseManifest` runs on the build machine too, against whatever set
|
|
// that binary was compiled with. When the two disagreed, a valid holder of the bus seat was refused
|
|
// mid-rollout — the compiled row said the seat delivered one provision, the store's row said
|
|
// another, and the build failed on the copy rather than the truth. The parser judges the manifest;
|
|
// the seat set judges the claim, where it is loaded.
|
|
func TestTheParserDoesNotJudgeWhatOnlyTheStoreKnows(t *testing.T) {
|
|
was := Seats()
|
|
t.Cleanup(func() { UseSeats(was) })
|
|
|
|
// A store whose bus seat delivers something this module does provide.
|
|
UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "mesh-bus", Decision: "test"}})
|
|
raw := []byte(`{"module":"a-bus","version":"1",` +
|
|
`"provides":[{"name":"mesh-bus","scope":"mesh"}],` +
|
|
`"claims":[{"name":"mesh-broker","scope":"mesh"}]}`)
|
|
|
|
m, err := ParseManifest(raw)
|
|
if err != nil {
|
|
t.Fatalf("the parser refused a claim only the seat set can judge: %v", err)
|
|
}
|
|
if got := CatalogueProblems(Shelf{m.Module: m}); len(got) != 0 {
|
|
t.Fatalf("a holder that provides what the store says the seat delivers was refused: %v", got)
|
|
}
|
|
|
|
// And with the store saying the seat delivers something else, registration is what refuses it.
|
|
UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: "other-bus", Decision: "test"}})
|
|
if _, err := ParseManifest(raw); err != nil {
|
|
t.Fatalf("the parser judged it the second time: %v", err)
|
|
}
|
|
got := strings.Join(CatalogueProblems(Shelf{m.Module: m}), "; ")
|
|
if !strings.Contains(got, `does not provide "other-bus"`) {
|
|
t.Fatalf("registration did not refuse a holder that cannot answer for the seat: %q", got)
|
|
}
|
|
}
|