Moving the build outcome onto its role broke the one module that consumes it, and my own agreement check passed anyway. The catalogue's subscription derived `mesh.mod.mesh-build-machine.event.built` — a module namespace for a role's event, which no such module owns — so it started, connected, and its graph stayed empty. The check compared names, and the names agreed: the build machine does emit `built`. Only the subjects disagreed, and a subscription that matches nothing is silence. A consumed name is a module's event unless it names a role, and this package cannot tell by looking — so whoever resolved the declaration says which, the way it already does for a seat held or used. A module that watches a role gets the role's event subject and a consumer filtered on it; watching grants subscribe and nothing else, because hearing what a role announced is not taking part in it. The check now compares the two halves that actually have to match — the subject a consumer subscribes against the subject an emitter publishes — with a case pinning that it catches this exact confusion. Comparing names was checking the easy half. **And that answered the open question about catch-up: there is nothing to build.** The mechanism exists because a queue on the old bus receives only what is published after it is bound, so everything built before the catalogue existed was announced to nobody. A stream is a log and a consumer is a position in it: a consumer created afterwards starts at the beginning, so the builds are simply there. Asked of a real server, since the whole decision rested on it — three builds published with nothing listening, then a consumer created, and all three waiting for it.
237 lines
8.7 KiB
Go
237 lines
8.7 KiB
Go
package broker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// **Do the catalogue's emitters and consumers agree?**
|
|
//
|
|
// This is the check whose absence let issue 127 stand: every manifest was individually well-formed,
|
|
// every derivation individually correct, and no cross-module subscription in the mesh matched
|
|
// anything. A subscription that matches nothing is not an error — it is silence — so nothing
|
|
// anywhere reported it.
|
|
//
|
|
// It compares what one manifest asks to hear against what another says it emits. It cannot demand
|
|
// that every consumed event have a live emitter, because a module lives in its own repository and
|
|
// may be registered long before the one whose events it wants. Where the emitter *is* here, it must
|
|
// emit what the consumer asked for.
|
|
func TestTheCataloguesEmittersAndConsumersAgree(t *testing.T) {
|
|
emitters, consumers, seats := theCataloguesEvents(t)
|
|
|
|
if bad := Disagreements(emitters, consumers, seats); len(bad) > 0 {
|
|
t.Fatalf("%d subscription(s) in the catalogue would match nothing:\n %s",
|
|
len(bad), strings.Join(bad, "\n "))
|
|
}
|
|
}
|
|
|
|
// And the check itself catches the thing it exists for, so it cannot pass by doing nothing.
|
|
func TestTheAgreementCheckCatchesASubscriptionThatMatchesNothing(t *testing.T) {
|
|
bad := Disagreements(
|
|
[]AnEmitter{{Module: "builder", Emits: []string{"built"}}},
|
|
[]AConsumer{{Module: "mesh-catalog", Consumes: []string{"builder.finished"}}},
|
|
nil)
|
|
if len(bad) != 1 {
|
|
t.Fatalf("a consumer asking for an event its emitter does not emit was not caught: %v", bad)
|
|
}
|
|
if !strings.Contains(bad[0], "builder.finished") || !strings.Contains(bad[0], "built") {
|
|
t.Fatalf("the report names neither what was asked for nor what is emitted: %s", bad[0])
|
|
}
|
|
|
|
// A module that is not here is not a disagreement: it may be registered later.
|
|
if bad := Disagreements(nil,
|
|
[]AConsumer{{Module: "plex", Consumes: []string{"sonarr.download.completed"}}}, nil); len(bad) != 0 {
|
|
t.Fatalf("a consumer whose emitter is not installed was reported: %v", bad)
|
|
}
|
|
|
|
// A wildcard over emitters is deliberate and names no particular event to check.
|
|
if bad := Disagreements([]AnEmitter{{Module: "sonarr", Emits: []string{"download.completed"}}},
|
|
[]AConsumer{{Module: "plex", Consumes: []string{"*.download.completed"}}}, nil); len(bad) != 0 {
|
|
t.Fatalf("a wildcard over emitters was reported: %v", bad)
|
|
}
|
|
|
|
// A consumer of a role's event whose role does not emit it is caught, which is what stops the
|
|
// catalogue check above from passing by knowing nothing about roles.
|
|
if bad := Disagreements(nil,
|
|
[]AConsumer{{Module: "mesh-catalog", Consumes: []string{"mesh-build-machine.finished"}}},
|
|
[]DeclaredSeat{{Name: "mesh-build-machine", Emits: []string{"built"}}}); len(bad) != 1 {
|
|
t.Fatalf("a consumer of a role event the role does not emit was not caught: %v", bad)
|
|
}
|
|
|
|
// An event published under a seat's name is real even though no module declares it as its own.
|
|
if bad := Disagreements(nil,
|
|
[]AConsumer{{Module: "watcher", Consumes: []string{"mesh-artifact-store.image.pushed"}}},
|
|
[]DeclaredSeat{{Name: "mesh-artifact-store", Emits: []string{"image.pushed"}}}); len(bad) != 0 {
|
|
t.Fatalf("an event a seat emits was reported as matching nothing: %v", bad)
|
|
}
|
|
}
|
|
|
|
func theCataloguesEvents(t *testing.T) ([]AnEmitter, []AConsumer, []DeclaredSeat) {
|
|
t.Helper()
|
|
root := filepath.Join("..", "..", "..", "mesh-catalog", "modules")
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
t.Skipf("catalogue sibling not present: %v", err)
|
|
}
|
|
var emitters []AnEmitter
|
|
var consumers []AConsumer
|
|
// The mesh's own roles, which emit under the seat's name rather than any module's (novox/hq
|
|
// ADR 0121). Without these the check skips every consumer of a role's event as "the emitter is
|
|
// not installed" — which is how it passed vacuously the first time one existed.
|
|
var seats []DeclaredSeat
|
|
for _, own := range catalogue.SeatsWithAProtocol() {
|
|
seats = append(seats, DeclaredSeat{Name: own.Name, Accepts: own.Accepts, Emits: own.Emits})
|
|
}
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(root, e.Name(), "module.json"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var m struct {
|
|
Module string `json:"module"`
|
|
Emits []string `json:"emits"`
|
|
Consumes []string `json:"consumes"`
|
|
Seats []struct {
|
|
Name string `json:"name"`
|
|
Emits []string `json:"emits"`
|
|
} `json:"seats"`
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatalf("%s: %v", e.Name(), err)
|
|
}
|
|
if len(m.Emits) > 0 {
|
|
emitters = append(emitters, AnEmitter{Module: m.Module, Emits: m.Emits})
|
|
}
|
|
if len(m.Consumes) > 0 {
|
|
consumers = append(consumers, AConsumer{Module: m.Module, Consumes: m.Consumes})
|
|
}
|
|
for _, s := range m.Seats {
|
|
seats = append(seats, DeclaredSeat{Name: s.Name, Emits: s.Emits})
|
|
}
|
|
}
|
|
if len(emitters) == 0 {
|
|
t.Skip("no manifests found beside this checkout")
|
|
}
|
|
return emitters, consumers, seats
|
|
}
|
|
|
|
// **Do the derived subjects meet, not just the names?**
|
|
//
|
|
// The check above compares what a consumer asks for against what an emitter says it emits, by name. It
|
|
// passed while the catalogue's subscription pointed at `mesh.mod.mesh-build-machine.event.built` — a
|
|
// module namespace for a role's event, which no emitter owns. The names agreed; the subjects did not,
|
|
// and the graph stayed empty.
|
|
//
|
|
// So this compares the thing that actually has to match: the subject a consumer subscribes against the
|
|
// subject an emitter publishes. It is the last place the two halves can be held together, because
|
|
// after this the server is the only thing that knows and it says nothing — a subscription that matches
|
|
// nothing is silence.
|
|
func TestTheCataloguesDerivedSubjectsMeet(t *testing.T) {
|
|
emitters, consumers, seats := theCataloguesEvents(t)
|
|
|
|
// Every subject something publishes: a module's own events, and the events of every role.
|
|
published := map[string]bool{}
|
|
for _, e := range emitters {
|
|
for _, name := range e.Emits {
|
|
published["mesh.mod."+e.Module+".event."+name] = true
|
|
}
|
|
}
|
|
for _, s := range seats {
|
|
for _, name := range s.Emits {
|
|
published["mesh.seat."+s.Name+".event."+name] = true
|
|
}
|
|
}
|
|
|
|
byName := map[string]DeclaredSeat{}
|
|
for _, s := range seats {
|
|
byName[s.Name] = s
|
|
}
|
|
|
|
var lonely []string
|
|
for _, c := range consumers {
|
|
principal := Principal{Kind: KindModule, Node: "one", Module: c.Module, PasswordHash: "x"}
|
|
for _, want := range c.Consumes {
|
|
emitter, event, named := strings.Cut(want, ".")
|
|
if named {
|
|
if s, isASeat := byName[emitter]; isASeat {
|
|
principal.Watches = append(principal.Watches,
|
|
Seat{Name: s.Name, Emits: []string{event}})
|
|
continue
|
|
}
|
|
}
|
|
principal.Consumes = append(principal.Consumes, want)
|
|
}
|
|
perms, err := PermissionsFor(principal)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", c.Module, err)
|
|
}
|
|
for _, subject := range perms.Subscribe {
|
|
if !strings.Contains(subject, ".event.") {
|
|
continue
|
|
}
|
|
if reaches(subject, published) {
|
|
continue
|
|
}
|
|
// A wildcard over emitters reaches whatever arrives later, and an emitter that is not
|
|
// installed is ordinary — both are already excused by the check above, so only a subject
|
|
// that can never match anything gets here.
|
|
if strings.Contains(subject, "*") || strings.Contains(subject, ">") {
|
|
continue
|
|
}
|
|
lonely = append(lonely, c.Module+" subscribes "+subject+", which nothing publishes")
|
|
}
|
|
}
|
|
if len(lonely) > 0 {
|
|
sort.Strings(lonely)
|
|
t.Fatalf("%d subscription(s) derive to a subject no emitter owns:\n %s",
|
|
len(lonely), strings.Join(lonely, "\n "))
|
|
}
|
|
}
|
|
|
|
// And it catches the thing it exists for: a role's event read as a module's.
|
|
func TestTheDerivedSubjectCheckCatchesARolesEventReadAsAModules(t *testing.T) {
|
|
published := map[string]bool{"mesh.seat.mesh-build-machine.event.built": true}
|
|
// What the derivation produced before a consumed seat name was resolved as one.
|
|
if reaches("mesh.mod.mesh-build-machine.event.built", published) {
|
|
t.Fatal("a module namespace was treated as reaching a role's event, which is the bug")
|
|
}
|
|
// And the corrected one does reach it.
|
|
if !reaches("mesh.seat.mesh-build-machine.event.built", published) {
|
|
t.Fatal("the role's own subject does not reach the role's event")
|
|
}
|
|
}
|
|
|
|
// reaches says whether a subscribed subject admits any published one.
|
|
func reaches(subject string, published map[string]bool) bool {
|
|
for p := range published {
|
|
if admitsSubject(strings.Split(subject, "."), strings.Split(p, ".")) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func admitsSubject(pattern, subject []string) bool {
|
|
for i, token := range pattern {
|
|
if token == ">" {
|
|
return i < len(subject)
|
|
}
|
|
if i >= len(subject) {
|
|
return false
|
|
}
|
|
if token != "*" && token != subject[i] {
|
|
return false
|
|
}
|
|
}
|
|
return len(pattern) == len(subject)
|
|
}
|