package main import ( "context" "fmt" "github.com/novox/mesh-controller/internal/link" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" ) // `builds` given a build's id reads that build's log from the bus rather than listing builds // (novox/hq ADR 0157). func TestBuildsWithAnIdReadsThatBuildsLog(t *testing.T) { argv, err := argvFor("builds", map[string]any{"log": "build-17"}) if err != nil { t.Fatal(err) } if strings.Join(argv, " ") != "builds --log build-17" { t.Fatalf("builds with a log id became %q", strings.Join(argv, " ")) } } // The build tool takes a repository as a URL or as its path on the forge holding the git seat, and // says which it was given, so the command reads the path as a seat source rather than handing it to // git as written (novox/hq issue 176). And it never waits: the id follows the build. func TestTheBuildToolTellsAForgePathFromAURL(t *testing.T) { argv, _ := argvFor("build", map[string]any{"repository": "novox/mesh-catalog", "path": "modules/x"}) if line := strings.Join(argv, " "); !strings.Contains(line, "--self") || !strings.Contains(line, "--wait 0") { t.Fatalf("a forge path is a seat source, not waited for; got %q", line) } argv, _ = argvFor("build", map[string]any{"repository": "https://example.tld/o/r.git"}) if line := strings.Join(argv, " "); strings.Contains(line, "--self") { t.Fatalf("a URL is cloned as given; got %q", line) } } // The build tool has the command's three shapes: a repository, a base whose dependents are rebuilt // (`--on`), or everything behind its source (`--behind`) — each asked, not waited for, one per call. // Given neither, the command line is what it was: nothing new reaches it. func TestTheBuildToolTakesOnAndBehind(t *testing.T) { cases := []struct { args map[string]any want string }{ {map[string]any{"on": "mesh-tools"}, "build --on mesh-tools --wait 0"}, {map[string]any{"behind": "true"}, "build --behind --wait 0"}, {map[string]any{"behind": true}, "build --behind --wait 0"}, {map[string]any{"repository": "novox/x"}, "build novox/x --wait 0 --self"}, {map[string]any{"repository": "novox/x", "behind": "false"}, "build novox/x --wait 0 --self"}, {map[string]any{"repository": "novox/x", "path": "modules/x", "ref": "main"}, "build novox/x --wait 0 --self --path modules/x --ref main"}, } for _, c := range cases { argv, err := argvFor("build", c.args) if err != nil { t.Errorf("%v: %v", c.args, err) continue } if got := strings.Join(argv, " "); got != c.want { t.Errorf("%v became %q, not %q", c.args, got, c.want) } } // Naming no shape runs the bare command, which answers its usage naming all three. if argv, err := argvFor("build", map[string]any{}); err != nil || strings.Join(argv, " ") != "build" { t.Errorf("a build naming nothing became %v, %v", argv, err) } // One shape per call: a second beside it is refused, not dropped, and so is half of one. for _, refused := range []map[string]any{ {"path": "modules/x"}, {"on": "mesh-tools", "repository": "novox/x"}, {"on": "mesh-tools", "behind": "true"}, {"behind": "true", "repository": "novox/x"}, {"behind": "true", "path": "modules/x"}, {"on": "mesh-tools", "ref": "main"}, {"behind": "yes"}, } { if argv, err := argvFor("build", refused); err == nil { t.Errorf("%v was accepted as %v", refused, argv) } } } // `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` is `token issue` at a shell, with the machine's tunnel key (novox/hq ADR 0169). func TestTokenIssuesForAMachineAndItsTunnelKey(t *testing.T) { argv, err := argvFor("token", map[string]any{"new": "laptop", "overlay_key": "k", "for": "2h"}) if err != nil || strings.Join(argv, " ") != "token issue --new laptop --overlay-key k --for 2h" { t.Fatalf("token: %v %v", 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") } } // A push and a build are sent, not waited for: the asker reads status, or the build's log by its // id, for what happened. A repository given as a forge path is said to be one (issue 176). func TestActsDoNotBlockTheCall(t *testing.T) { argv, _ := argvFor("push", map[string]any{"node": "one", "why": "w"}) if strings.Join(argv, " ") != "push one --wait 0 --why w" { t.Fatalf("push waits: %v", argv) } argv, _ = argvFor("build", map[string]any{"repository": "novox/x", "path": "modules/x"}) if strings.Join(argv, " ") != "build novox/x --wait 0 --self --path modules/x" { t.Fatalf("build: %v", argv) } } // What `tools` answers is the seats' records, with each verb's schema. func TestToolsAnswersTheSeatsRecords(t *testing.T) { handlers, behind, err := seatToolHandlers() if err != nil { t.Fatal(err) } if len(behind) != 0 { t.Fatalf("this build cannot run %v of its own seat's verbs", behind) } if len(handlers) != len(catalogue.ControllerVerbs) { t.Fatalf("%d handlers for %d verbs", len(handlers), len(catalogue.ControllerVerbs)) } answer := seatTools() seats, _ := answer["seats"].([]map[string]any) var found bool for _, s := range seats { if s["seat"] == catalogue.ControllerSeatName { found = true tools, _ := s["tools"].([]map[string]any) if len(tools) != len(catalogue.ControllerVerbs) || tools[0]["input"] == nil { t.Fatalf("the controller seat's tools are not listed in full: %v", tools) } } } if !found { t.Fatal("the mesh-controller seat is not in the listing") } // And what a verb replaces travels with it (novox/hq ADR 0245): the console and the agent's // instructions read it from here. var journal []string for _, s := range seats { if s["seat"] != catalogue.ServiceManagerSeat { continue } tools, _ := s["tools"].([]map[string]any) for _, tool := range tools { if tool["name"] == "journal" { journal, _ = tool["replaces"].([]string) } } } if len(journal) == 0 || journal[0] != "journalctl" { t.Fatalf("the service manager's journal does not say it replaces journalctl: %v", journal) } } // A JSON verb's answer is parsed from what the command wrote to standard output alone; a warning it // printed beside the document does not take the document away. The test binary stands in for the // controller: `-test.run` with a name that matches nothing prints `ok` and a warning about no tests. func TestAJSONVerbsAnswerIsItsStandardOutput(t *testing.T) { jsonVerbs["-test.run"] = true t.Cleanup(func() { delete(jsonVerbs, "-test.run") }) answer, err := runVerb(t.Context(), []string{"-test.run", "TestAnswerEcho", "-test.v"}) if err != nil { t.Fatal(err) } if !answer.OK { t.Fatalf("the command failed: %s", answer.Output) } if !strings.Contains(answer.Output, "PASS") { t.Fatalf("stderr and stdout are both what the command said: %s", answer.Output) } } // `command` is the generic verb: the command line as given, split as a shell would, nothing added — // so an operator's `node account g14 jochen` is one call through the console rather than a shell on // the control node (novox/hq ADR 0154, ADR 0175). func TestCommandRunsTheLineAsGiven(t *testing.T) { argv, err := argvFor("command", map[string]any{"command": "node account g14 jochen"}) if err != nil || strings.Join(argv, " ") != "node account g14 jochen" { t.Fatalf("a plain line: %v %v", argv, err) } argv, err = argvFor("command", map[string]any{"command": `settings set 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": `node add "the box" --adopted`}) if err != nil || len(argv) != 4 || argv[2] != "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. func TestARowAheadOfThisBuildIsServedAnyway(t *testing.T) { seat, known := catalogue.SeatNamed(catalogue.ControllerSeatName) if !known { t.Fatal("no controller seat") } // The row as a newer control plane would have written it: every verb this build knows, and one // it does not. widened := seat widened.Serves = append(append([]catalogue.Verb{}, seat.Serves...), catalogue.Verb{Name: "teleport", Description: "a verb from a build that does not exist yet"}) rows := catalogue.DefaultSeats() for i := range rows { if rows[i].Name == catalogue.ControllerSeatName { rows[i] = widened } } catalogue.UseSeats(rows) t.Cleanup(func() { catalogue.UseSeats(catalogue.DefaultSeats()) }) handlers, behind, err := seatToolHandlers() if err != nil { t.Fatalf("a row with one unknown verb refused to serve at all: %v", err) } if len(behind) != 1 || behind[0] != "teleport" { t.Fatalf("the verbs this build cannot run were reported as %v", behind) } if len(handlers) != len(widened.Serves) { t.Fatalf("%d handlers for %d verbs in the row", len(handlers), len(widened.Serves)) } for _, known := range []string{"status", "nodes", "push"} { if handlers[known] == nil { t.Errorf("%s is not served although this build knows it", known) } } _, err = handlers["teleport"](context.Background(), nil) if err == nil { t.Fatal("the unknown verb answered as though it had run") } for _, want := range []string{"teleport", "cannot run it", "behind"} { if !strings.Contains(err.Error(), want) { t.Errorf("the answer does not say %q: %v", want, err) } } } // 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. func TestTheControllerAnnouncesTheVerbsItServes(t *testing.T) { handlers, _, err := seatToolHandlers() if err != nil { t.Fatal(err) } info := seatAnnouncement(handlers) if info.Name != catalogue.ControllerSeatName || info.ID == "" || info.Version == "" { t.Fatalf("the service is not named for the seat: %+v", info.ServiceIdentity) } if len(info.Endpoints) != len(handlers) { t.Fatalf("%d endpoints announced for %d verbs served", len(info.Endpoints), len(handlers)) } for _, e := range info.Endpoints { verb := e.Metadata["tool"] if _, served := handlers[verb]; !served || e.Name != catalogue.ControllerSeatName+"__"+verb { t.Errorf("%s (%s) is announced and not served under that name", e.Name, verb) } if e.Metadata["kind"] != "seat" || e.Metadata["seat"] != catalogue.ControllerSeatName { t.Errorf("%s is not announced as the seat's verb: %v", e.Name, e.Metadata) } if e.Subject != link.SeatToolSubject(catalogue.ControllerSeatName, verb) || e.QueueGroup != "seat."+catalogue.ControllerSeatName { t.Errorf("%s is announced on %s/%s, not where it is served", e.Name, e.Subject, e.QueueGroup) } if e.Metadata["description"] == "" || e.Metadata["schema"] == "" || e.Metadata["scope"] != "mesh" { t.Errorf("%s is announced without its description, schema or scope: %v", e.Name, e.Metadata) } } }