package main import ( "errors" "fmt" "io/fs" "os" "path/filepath" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" ) // `builds` given a build's id no longer reads a log from the bus: the build seat is retired (ADR 0525 clause 7). func TestBuildsWithAnIdIsRefusedForTheRetiredSeat(t *testing.T) { if _, err := argvFor("builds", map[string]any{"log": "build-17"}); err == nil || !strings.Contains(err.Error(), "build seat is retired") { t.Fatalf("builds with a log id: %v", err) } } // `rotate` is one verb with two shapes (ADR 0114, issue 180): a pair credential by provision, or a // module's own secret by machine, module and name. func TestRotateTakesAProvisionOrAnOwnSecret(t *testing.T) { argv, _ := argvFor("rotate", map[string]any{"provision": "postgres-database", "consumer": "ace"}) if strings.Join(argv, " ") != "rotate postgres-database --consumer ace" { t.Fatalf("a pair credential: %v", argv) } argv, _ = argvFor("rotate", map[string]any{"provision": "postgres-database", "consumer": "ace", "module": "letta"}) if strings.Join(argv, " ") != "rotate postgres-database --consumer ace --module letta" { t.Fatalf("one consuming module's pair credential: %v", argv) } argv, _ = argvFor("rotate", map[string]any{"node": "ace", "module": "nodered", "secret": "api-token"}) if strings.Join(argv, " ") != "secret rotate ace nodered api-token" { t.Fatalf("an own secret: %v", argv) } if _, err := argvFor("rotate", map[string]any{"node": "ace"}); err == nil || !strings.Contains(err.Error(), `"module"`) { t.Fatalf("half an own secret is refused, naming what it lacks: %v", err) } if argv, _ := argvFor("rotate", nil); strings.Join(argv, " ") != "rotate" { t.Fatalf("neither shape falls to the command's usage: %v", argv) } if _, err := argvFor("rotate", map[string]any{"provision": "p", "node": "ace", "module": "m", "secret": "s"}); err == nil { t.Fatal("both shapes at once were taken, and one of them passed over") } } // `token` answers a joining token to its caller, and whoever may call a verb includes agents: it is the // controller's terminal's alone (novox/hq ADR 0266), refused through the verb whatever it is given. func TestTokenIsRefusedThroughAVerb(t *testing.T) { for _, args := range []map[string]any{{"new": "laptop", "overlay_key": "k", "for": "2h"}, {"node": "ace"}} { argv, err := argvFor("token", args) if err == nil || !strings.Contains(err.Error(), "controller's terminal only") { t.Fatalf("token %v: %v %v", args, argv, err) } } } // `settings` is `settings set|clear` at a shell, with the values passed inline (novox/hq issue 198). func TestSettingsSetsOrClearsALayer(t *testing.T) { argv, err := argvFor("settings", map[string]any{"module": "dnsmasq", "values": `{"a":1}`, "node": "ace"}) if err != nil || strings.Join(argv, " ") != `settings set dnsmasq {"a":1} --node ace` { t.Fatalf("set on a machine: %v %v", argv, err) } argv, _ = argvFor("settings", map[string]any{"module": "dnsmasq", "clear": "true"}) if strings.Join(argv, " ") != "settings clear dnsmasq" { t.Fatalf("clear for the mesh: %v", argv) } // Neither values nor clear reads the layer as it stands (novox/hq ADR 0217): what a caller reads // before replacing it, where it used to fall to the command's usage. argv, _ = argvFor("settings", map[string]any{"module": "dnsmasq"}) if strings.Join(argv, " ") != "settings show dnsmasq" { t.Fatalf("a call with no values reads the layer: %v", argv) } } // `issue` is `module issue` at a shell: the module and the machine, and nothing that would push. A // module's bus account was mintable only from the controller's command line, so an agent working // through the tools could not finish a rollout that gave a module one (novox/hq issue 191). func TestIssueGivesAModuleItsAccountOnAMachine(t *testing.T) { argv, err := argvFor("issue", map[string]any{"node": "ace", "module": "route-proxy"}) if err != nil { t.Fatal(err) } if strings.Join(argv, " ") != "module issue route-proxy --node ace" { t.Fatalf("issue runs %v", argv) } if _, err := argvFor("issue", map[string]any{"module": "route-proxy"}); err == nil { t.Error("an account was issued without saying which machine reads it") } } // A required argument missing is refused in the verb's own words, before anything runs. func TestAVerbMissingWhatItNeedsIsRefused(t *testing.T) { if _, err := argvFor("node", map[string]any{}); err == nil || !strings.Contains(err.Error(), `node needs "node"`) { t.Fatalf("node without a machine was accepted: %v", err) } if _, err := argvFor("no-such-verb", map[string]any{}); err == nil { t.Fatal("a verb the seat does not serve was accepted") } } // `command` is the generic verb: the command line as given, split as a shell would, nothing added // (novox/hq ADR 0154, ADR 0175). It once carried an operator's `node account g14 jochen` too; a node's // accounts are the controller's terminal's alone since ADR 0266 (TestNoVerbSetsANodesAccounts). func TestCommandRunsTheLineAsGiven(t *testing.T) { argv, err := argvFor("command", map[string]any{"command": "node show g14"}) if err != nil || strings.Join(argv, " ") != "node show g14" { t.Fatalf("a plain line: %v %v", argv, err) } argv, err = argvFor("command", map[string]any{"command": `settings show dnsmasq '{"a": "b c"}' --node ace`}) if err != nil || len(argv) != 6 || argv[3] != `{"a": "b c"}` { t.Fatalf("a quoted word stays one word: %q %v", argv, err) } argv, err = argvFor("command", map[string]any{"command": `plan "the box" --json`}) if err != nil || len(argv) != 3 || argv[1] != "the box" { t.Fatalf("double quotes group: %q %v", argv, err) } if _, err := argvFor("command", map[string]any{"command": " "}); err == nil { t.Fatal("an empty line was accepted") } if _, err := argvFor("command", map[string]any{"command": `node "unclosed`}); err == nil { t.Fatal("an unclosed quote was accepted") } } // A verb in the row that this binary cannot run does not take the control plane off the bus: the // rest are served, the unknown one answers the reason, and the start-up names it (novox/hq ADR // 0185). One unknown word cost the mesh ten minutes of silence on 2026-10-02, recoverable only by // a person running the binary by hand — the push that would have repaired it needs the control // plane that was down. // novox/hq ADR 0195: the console's discovery reads the machines and the modules; they answer as JSON, // as status and seats do, so nothing parses a printed column. func TestTheNodesAndModulesVerbsAnswerAsJSON(t *testing.T) { for verb, want := range map[string]string{"nodes": "[node list --json]", "modules": "[module list --json]"} { argv, err := argvFor(verb, map[string]any{}) if err != nil { t.Fatal(err) } if fmt.Sprint(argv) != want { t.Errorf("%s runs %v, want %s", verb, argv, want) } } } // novox/hq ADR 0197: the controller announces exactly the verbs it serves, each on the subject and // queue it serves it on, with the seat's own description and schema, in NATS's services format. // The generic verb only reads (novox/hq ADR 0266): an allow list of reading forms, and every line that // writes, issues, sets a key or reveals a secret refused — the chain a review found ran through it: set the // operator's key to one the caller holds, rotate secrets sealed to it, open them. func TestTheCommandVerbOnlyReads(t *testing.T) { for _, line := range []string{ "operator key set --replace k", "operator issue", "secret accept a b", "secret rotate a b c", "secret recover a", "secret export a", "token issue --new x", "identity show", "broker users", "api key", "licence show", "push anchor --why w", "assign novox m", "settings set m {}", "settings clear m", "module add f", "module check /etc", "module forget m", "module issue m --node a", "seat rename a b", "plans close p --why w", "plans go p", "plans retry p", "plans stop p", "plans --json go p", "plans -n 3 close p", "plans --what-if r retry p", "doctor run", "conditions silence c --why w", "retire approve x", "cleanup delete x", "delivery check", "delivery go x", "bus upgrade", "mirrors --record x", "mirrors --confirm", "hand-act record x --why y --cause z", "serve", "migrate", "prepare", "declare x", "overlay x", "facts", "merge-gate", "check-here", "build x", "rebuild x", "cancel x", "kill x", "clear x", "replay x", "pause", "resume", "pin a b", "unpin a", "take x", "converge", "adopt x", "rollout x", "upgrade x", "collect", "board", "builder", "ask x", "frobnicate", } { argv, err := argvFor("command", map[string]any{"command": line}) var policy *heldAtTheTerminal if err == nil || !errors.As(err, &policy) { t.Errorf("%q ran as %v (%v); the generic verb only reads", line, argv, err) } } for _, line := range []string{ "status --json", "version", "seats --json", "healers", "hand-acts --days 3", "durations", "collection", "images", "artifacts --collected", "data --machine a", "builds", "plan ace --diff", "plans", "plans plan-1", "doctor", "doctor probes", "doctor signals", "conditions", "conditions list", "conditions show c", "conditions history", "node list", "node show ace", "module list", "settings show m", "settings preferences", "retire list", "cleanup list", "delivery plan --repository r", "delivery walks", "bus", "mirrors --json", } { if _, err := argvFor("command", map[string]any{"command": line}); err != nil { t.Errorf("%q, a read, was refused: %v", line, err) } } } // What hands a caller a key, a credential or a secret is refused whichever verb composed it. // A value with a quote in it can be said on a `command` line, as in a shell, and a line whose quote // is left open is refused naming where it opened and how a quote is written, quoting none of it (novox/hq issue 294: an // apostrophe inside a single-quoted JSON value cut the line, and the refusal named no cause). func TestAQuotedValueCanHoldAQuote(t *testing.T) { for _, c := range []struct{ line, want string }{ {`settings set claude-code '{"role":"the operator'\''s laptop"}'`, `{"role":"the operator's laptop"}`}, {`settings set claude-code "{\"role\":\"the operator's laptop\"}"`, `{"role":"the operator's laptop"}`}, {"x \"a \\\\ b\nc\"", "a \\ b\nc"}, {`x 'a\b'`, `a\b`}, } { argv, err := splitCommandLine(c.line) if err != nil || argv[len(argv)-1] != c.want { t.Errorf("%s: read back %q %v, want %q", c.line, argv, err, c.want) } } _, err := splitCommandLine(`settings set claude-code '{"role":"the operator's laptop"}' --node g14`) if err == nil { t.Fatal("an apostrophe that leaves a quote open was accepted") } for _, want := range []string{"character 57", `'\''`, `\"`} { if !strings.Contains(err.Error(), want) { t.Errorf("the refusal does not say %q: %v", want, err) } } if strings.Contains(err.Error(), "laptop") || strings.Contains(err.Error(), "g14") { t.Errorf("the refusal quotes the line, which may carry a setting's value: %v", err) } } // Every value reads back as it was when written the way the refusal says: single-quoted with '\” // for each quote, or double-quoted with \ before each " and \ — newlines and backslashes included. func TestAQuotedValueRoundTrips(t *testing.T) { for _, v := range []string{`plain`, `it's`, `say "hi"`, `back\slash\`, "two\nlines", `'"\'\"`, `''`, ``} { single := "x '" + strings.ReplaceAll(v, "'", `'\''`) + "'" double := `x "` + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(v) + `"` for _, line := range []string{single, double} { argv, err := splitCommandLine(line) if err != nil || len(argv) != 2 || argv[1] != v { t.Errorf("%s: read back %q %v, want %q", line, argv, err, v) } } } } // **A verb release 6 removed is refused by name, with what replaces it** (ADR 0539, ADR 0429 §8, issue 1945 B10): every // verb of catalogue.RemovedControllerVerbs is answered "no longer exists" naming itself and its replacement, whatever // arguments it is given, and the tool-call path and the terminal's command line give the same words. func TestTheVerbsReleaseSixRemovedAreRefusedByName(t *testing.T) { if len(catalogue.RemovedControllerVerbs) == 0 { t.Fatal("the SDK lists no removed controller verb") } for verb, why := range catalogue.RemovedControllerVerbs { argv, err := argvFor(verb, map[string]any{"node": "anchor", "why": "w"}) if err == nil || len(argv) != 0 { t.Errorf("%s was composed into %v", verb, argv) continue } for _, want := range []string{"`" + verb + "`", "no longer exists", why} { if !strings.Contains(err.Error(), want) { t.Errorf("%s: the refusal %q lacks %q", verb, err, want) } } if cmd := removedByName(verb); cmd == nil || cmd.Error() != err.Error() { t.Errorf("%s: the terminal says %v, the tool says %v", verb, cmd, err) } } } // The controller's size, printed against the 111,386 non-test lines its repository held before release 6 (acceptance test 20, ADR // 0539 §17): raw lines of every non-test Go file of the module, which the pull request reports. func TestTheControllersNonTestLinesArePrinted(t *testing.T) { const before = 111386 total := 0 err := filepath.WalkDir("../..", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() && (d.Name() == ".git" || d.Name() == "testdata") { return filepath.SkipDir } if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { return nil } raw, err := os.ReadFile(path) if err != nil { return err } total += strings.Count(string(raw), "\n") return nil }) if err != nil { t.Fatal(err) } t.Logf("non-test Go lines: %d against %d before release 6 (%+d)", total, before, total-before) if total >= before { t.Errorf("the controller holds %d non-test lines, no fewer than the %d it held before release 6", total, before) } }