289 lines
14 KiB
Go
289 lines
14 KiB
Go
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)
|
|
}
|
|
}
|