package main import ( "encoding/json" "errors" "os" "strings" "testing" ) // askingAController answers the controller's verbs as the seat does: `tools` in-process, the rest as the // command's output and its JSON. func askingAController(t *testing.T) Ask { t.Helper() wrap := func(output string, answer any) json.RawMessage { raw, _ := json.Marshal(map[string]any{"ok": true, "output": output, "answer": answer}) return raw } return func(address string, args any) (json.RawMessage, error) { switch address { case "seat:mesh-controller.tools": return json.RawMessage(`{"seats":[ {"seat":"mesh-controller","scope":"mesh","tools":[{"name":"node","description":"What one machine reported it can do, what it is assigned, and why.","replaces":["hostnamectl","uptime"]},{"name":"nodes","description":"Every machine."}]}, {"seat":"node-service-manager","scope":"node","tools":[ {"name":"status","description":"One unit as the service manager sees it now: its states, whether it starts at boot.","replaces":["systemctl status","systemctl is-active"]}, {"name":"journal","description":"The last lines of one unit's journal.","replaces":["journalctl"]}]}]}`), nil case "seat:mesh-controller.modules": return wrap("[...]", []any{map[string]any{"module": "docker", "on": []string{"anchor"}, "replaces": map[string][]string{"docker_logs": {"docker logs"}}}, map[string]any{"module": "zsh", "on": []string{"anchor"}}}), nil case "seat:mesh-controller.nodes": return wrap("", []any{map[string]any{"name": "anchor"}, map[string]any{"name": "laptop"}}), nil case "seat:mesh-controller.node": node, _ := args.(map[string]any)["node"].(string) if node == "anchor" { return wrap("anchor\n mode converged\n public domain Anchor.Example\n\n what it runs\n", nil), nil } return wrap("laptop\n mode converged\n", nil), nil } return nil, errors.New("no such verb") } } func resolving(name string) []string { return map[string][]string{"anchor": {"10.10.0.1"}, "anchor.internal": {"10.10.0.1"}, "anchor.example": {"192.0.2.10"}, "laptop": {"127.0.0.1", "10.10.0.4"}}[name] } // What the controller says of its tools and machines is read whole: the seats' verbs, the modules' own tools, // every machine with its domains and addresses — a loopback address never one of them. func TestTheMeshsToolsAreAskedOfTheController(t *testing.T) { m, err := AskMeshTools(askingAController(t), resolving) if err != nil { t.Fatal(err) } var addresses []string for _, r := range m.Replacements { addresses = append(addresses, r.Address) } if got := strings.Join(addresses, ","); got != "mesh-controller.node,node-service-manager.status,node-service-manager.journal,docker.docker_logs" { t.Errorf("replacements: %s", got) } if len(m.Machines) != 2 || strings.Join(m.Machines[0].Domains, ",") != "anchor.internal,anchor.example" || strings.Join(m.Machines[0].Addresses, ",") != "10.10.0.1,192.0.2.10" || strings.Join(m.Machines[1].Addresses, ",") != "10.10.0.4" { t.Errorf("machines: %+v", m.Machines) } if m.Replacements[3].Does != "logs" || !m.Replacements[2].NodeScoped || m.Replacements[0].NodeScoped { t.Errorf("the rows: %+v", m.Replacements) } } // **The table is generated, not written** (novox/hq ADR 0245): what the records say a verb replaces is a row, // the machines' seats first, and nothing is rendered where the records say nothing. func TestTheInsteadOfTableIsGeneratedFromTheRecords(t *testing.T) { m, _ := AskMeshTools(askingAController(t), resolving) table := InsteadOf(&m) for _, want := range []string{ "| `/node-service-manager.` | `status` — `systemctl status`, `systemctl is-active` · `journal` — `journalctl` |", "| `/docker.` | `docker_logs` — `docker logs` |", "| `mesh-controller.` | `node` — `hostnamectl`, `uptime` |", "Never `ssh` to a mesh machine", } { if !strings.Contains(table, want) { t.Errorf("the table lacks %q:\n%s", want, table) } } if strings.Index(table, "node-service-manager.") > strings.Index(table, "docker.") || strings.Index(table, "docker.") > strings.Index(table, "`mesh-controller.`") { t.Errorf("not the machines' seats, then modules, then the mesh's:\n%s", table) } // What the guard here named most comes first: its row, and in its row its verb. m.Refused = map[string]int{"node-service-manager.journal": 3, "mesh-controller.node": 1} ranked := InsteadOf(&m) if !strings.Contains(ranked, "| `/node-service-manager.` | `journal` — `journalctl` · `status`") || strings.Index(ranked, "`mesh-controller.`") > strings.Index(ranked, "`/docker.`") { t.Errorf("not ranked by the guard's record:\n%s", ranked) } m.Refused = nil if InsteadOf(nil) != "" || InsteadOf(&MeshTools{}) != "" { t.Error("a table from nothing") } // Into the instructions after the mesh's own text, before what was registered. out := RenderWithMesh(Facts{Node: "w", Console: "x"}, Settings{}, nil, "/h", nil, Config{Mesh: []Item{{Kind: KindInstructions, Name: "conventions", Scope: ScopeMesh, Files: map[string]string{"conventions.md": "Be brief."}}}}, &m) md := out["CLAUDE.md"] if i, j, k := strings.Index(md, "## Conventions"), strings.Index(md, "## Instead of a shell command"), strings.Index(md, "### conventions"); !(i < j && j < k && i >= 0) { t.Errorf("the table is not between the mesh's text and the registered sections:\n%s", md) } if os.Getenv("SHOW_TABLE") != "" { t.Log(table) } // A long list is cut, and says how the rest is found. var many MeshTools for i := 0; i < InsteadOfRows+3; i++ { many.Replacements = append(many.Replacements, Replacement{Address: "s" + string(rune('a'+i)) + ".v", Seat: true, Replaces: []string{"c"}}) } if t2 := InsteadOf(&many); strings.Count(t2, "| `s") != InsteadOfRows || !strings.Contains(t2, "3 more seats and modules") { t.Errorf("not cut to %d rows:\n%s", InsteadOfRows, t2) } } // Asked again with the same answer, nothing is written and nothing renders. func TestTheMeshsToolsAreKeptOnlyWhenTheyChange(t *testing.T) { p := Paths{State: t.TempDir()} m, _ := AskMeshTools(askingAController(t), resolving) if !KeepMeshTools(p, m) { t.Fatal("the first answer was not kept") } if KeepMeshTools(p, m) { t.Fatal("the same answer was kept again") } m.Machines = m.Machines[:1] if !KeepMeshTools(p, m) || len(ReadMeshTools(p).Machines) != 1 { t.Fatal("a changed answer was not kept") } d := GuardDataOf(Paths{State: p.State, Node: "laptop"}, ReadMeshTools(p)) if d.Node != "laptop" || len(d.Replacements) != 4 || !strings.HasSuffix(d.Log, "guard.log") { t.Errorf("the guard's data: %+v", d) } } // Where claude-code runs is read from the controller's `modules` answer as it comes — JSON, from `module list // --json` — and from the printed list too. func TestTheMachinesRunningTheModuleAreReadFromTheControllersAnswer(t *testing.T) { asJSON := json.RawMessage(`{"ok":true,"output":"[...]","answer":[{"module":"zsh","on":["a"]},{"module":"claude-code","on":["a","b"]}]}`) if got := strings.Join(NodesRunning(asJSON, "claude-code"), ","); got != "a,b" { t.Errorf("from JSON: %q", got) } printed := json.RawMessage(`{"ok":true,"output":"zsh 1 built x on a\nclaude-code 1 built y on a, b\n"}`) if got := strings.Join(NodesRunning(printed, "claude-code"), ","); got != "a,b" { t.Errorf("from the printed list: %q", got) } if got := NodesRunning(json.RawMessage(`{"ok":true,"answer":[{"module":"zsh","on":["a"]}]}`), "claude-code"); got != nil { t.Errorf("a module that runs nowhere: %v", got) } }