Files
mesh-controller/internal/inventory/waits_grants_test.go
T
mesh-tools 97a2d841c7 Land feature 57's controller work on its release branch
The controller plans config, templates and moved providers' provisions,
hands bus accounts to the vault and judges the new manifest words, on the
feature's SDK and node-engine validator (feature 57, config and provisioning).
2026-10-15 21:27:33 +00:00

168 lines
7.6 KiB
Go

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)
}
}