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).
168 lines
7.6 KiB
Go
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)
|
|
}
|
|
}
|