package inventory import ( "reflect" "sort" "strings" "testing" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/catalogue" ) // The tool runner's manifest with the families novox/hq ADR 0367 §5 starts the grant with, and one more. const runtimeWaiting = `{"module": "node-tools", "version": "1", "invokes": ["*"], "waits": [%s]}` var adr0367Families = []string{`"mesh-knowledge.reviewed"`, `"mesh-delivery.transition"`, `"gitea.forge.status"`, `"mesh-controller.checked"`, `"operator-channel.decided.claude-code"`} // composeWaits composes the bus's users from manifests as BusRecords does — the router declaring the operator // channel, the mesh's own seats, a person granted every tool — and answers the tool runner's account's // subscriptions on events, and the subjects its membership names. func composeWaits(t *testing.T, families []string, people map[string][]string) (granted, issued []string) { t.Helper() parse := func(s string) catalogue.Manifest { m, err := catalogue.ParseManifest([]byte(s)) if err != nil { t.Fatal(err) } return m } router := parse(`{"module": "messenger", "version": "1", "runs-as": "messenger", "seats": [{"name": "operator-channel", "scope": "mesh", "serves": ["open"], "accepts": ["ask"], "emits": ["decided"], "by-caller": ["ask", "decided"]}], "claims": [{"name": "operator-channel", "scope": "mesh", "serves": ["open"]}], "own-secrets": {"broker": "${dir:state}/broker"}, "secrets-owner": "messenger", "resources": [{"id": "account", "type": "user", "name": "messenger", "shell": "/usr/bin/nologin", "home": "/var/lib/messenger"}, {"id": "state", "type": "directory", "mode": "0700", "place": ".", "owner": "messenger"}]}`) asker := parse(`{"module": "claude-code", "version": "1", "consumes": ["mesh-knowledge.reviewed"], "own-secrets": {"broker": "${dir:state}/broker"}, "resources": [{"id": "state", "type": "directory", "mode": "0700", "place": "."}]}`) runtime := parse(strings.Replace(runtimeWaiting, "%s", strings.Join(families, ", "), 1)) manifests := []catalogue.Manifest{router, asker, runtime} seats := map[string]catalogue.SeatDeclaration{} declarers := map[string]string{} for _, m := range manifests { for _, s := range m.DefinesSeats { seats[s.Name], declarers[s.Name] = s, m.Module } } for _, own := range catalogue.SeatsWithAProtocol() { seats[own.Name] = catalogue.SeatDeclaration{Name: own.Name, Scope: own.Scope, Accepts: own.Accepts, Emits: own.Emits, Serves: own.Serves} } records := broker.Records{Nodes: []string{"laptop"}, Assigned: map[string][]broker.Declared{}, People: people, Interchangeable: map[string]bool{}} for _, m := range manifests { records.Assigned["laptop"] = append(records.Assigned["laptop"], declaredFor(m, seats, declarers)) } users, err := broker.Users(records) if err != nil { t.Fatal(err) } for _, u := range users { if u.Kind != broker.KindNodeTools { continue } p, err := broker.PermissionsFor(u) if err != nil { t.Fatal(err) } for _, s := range p.Subscribe { if strings.Contains(s, ".event.") { granted = append(granted, s) } } } for _, d := range records.Assigned["laptop"] { for _, w := range broker.MembershipFor("laptop", d, broker.PlacementsOf(records, nil)).Waits { issued = append(issued, w.Subject) } } sort.Strings(granted) sort.Strings(issued) return granted, issued } // novox/hq ADR 0367 "How it is checked": the account's wait grant equals the manifest's families — a seat's // event under the seat, the asker's own `decided` alone, a module's under the module — and the membership // names the same; no person's grant, however wide, widens it; a family is granted only once the manifest // that adds it is composed. func TestTheWaitGrantIsTheToolRunnersManifestAndNothingElse(t *testing.T) { want := []string{ "mesh.mod.gitea.event.forge.status", "mesh.mod.mesh-delivery.event.transition", "mesh.mod.mesh-knowledge.event.reviewed", "mesh.seat.mesh-controller.event.checked", "mesh.seat.operator-channel.event.decided.claude-code", } granted, issued := composeWaits(t, adr0367Families, map[string][]string{}) if !reflect.DeepEqual(granted, want) { t.Errorf("the tool runner hears %v, want the manifest's families %v", granted, want) } if !reflect.DeepEqual(issued, want) { t.Errorf("its membership names %v, want the grant %v", issued, want) } // No call or setting widens it: a person granted every tool, an operator's account, changes nothing. if wide, _ := composeWaits(t, adr0367Families, map[string][]string{"operator": {"*"}}); !reflect.DeepEqual(wide, want) { t.Errorf("a person's grant widened the wait grant: %v", wide) } // And without the families it hears no event at all (ADR 0043 §3): a module beside it consuming one // lends it nothing. if none, issuedNone := composeWaits(t, nil, map[string][]string{}); len(none)+len(issuedNone) > 0 { t.Errorf("a tool runner declaring no waits hears %v, issued %v", none, issuedNone) } // A family is granted only after the manifest change that adds it is composed. more := append(append([]string{}, adr0367Families...), `"mesh-knowledge.accepted"`) grown, _ := composeWaits(t, more, map[string][]string{}) if added := without(grown, want); !reflect.DeepEqual(added, []string{"mesh.mod.mesh-knowledge.event.accepted"}) { t.Errorf("adding one family to the manifest granted %v", added) } } func without(all, some []string) []string { drop := map[string]bool{} for _, s := range some { drop[s] = true } var out []string for _, s := range all { if !drop[s] { out = append(out, s) } } return out } // A family of a seat whose event subjects end in a kind (a kinded bench) or in the asker (a seat naming its // caller) is composed with that last token, and refused without it, naming the family: a subject without it // is one nothing publishes on, and a wait on it would never hear (the review of issue 745). func TestAWaitOnASeatNamingAKindOrAnAskerNeedsThatName(t *testing.T) { seats := map[string]catalogue.SeatDeclaration{ "intake": {Name: "intake", Kinded: true, Emits: []string{"choice", "failed"}}, "operator-channel": {Name: "operator-channel", Emits: []string{"decided"}, ByCaller: []string{"ask", "decided"}}, } compose := func(family string) (string, error) { m := catalogue.Manifest{Module: broker.RuntimeModule, Waits: []string{family}} waits := waitsOf(m, seats) return broker.WaitSubject(waits[0]) } if s, err := compose("intake.choice.telegram"); err != nil || s != "mesh.seat.intake.event.choice.telegram" { t.Errorf("a kind's event: %q, %v", s, err) } if s, err := compose("operator-channel.decided.claude-code"); err != nil || s != "mesh.seat.operator-channel.event.decided.claude-code" { t.Errorf("an asker's event: %q, %v", s, err) } for family, want := range map[string]string{"intake.choice": "kind", "operator-channel.decided": "asker"} { s, err := compose(family) if err == nil || !strings.Contains(err.Error(), family) || !strings.Contains(err.Error(), want) { t.Errorf("%s composed as %q (%v), want it refused for want of its %s", family, s, err, want) } } // Refused where the bus's users are composed, naming it, so the merge gate is red before it lands. p := broker.Principal{Kind: broker.KindNodeTools, Node: "laptop", Module: broker.RuntimeModule, Carries: []broker.Declared{ {Module: broker.RuntimeModule, Waits: waitsOf(catalogue.Manifest{Module: broker.RuntimeModule, Waits: []string{"intake.choice"}}, seats)}}} if _, err := broker.PermissionsFor(p); err == nil || !strings.Contains(err.Error(), "intake.choice") { t.Errorf("the runtime was composed with a family no subject carries: %v", err) } }