package console import ( "encoding/json" "strings" "testing" "github.com/novox/mesh-tools/node-tools/internal/announce" ) // meshToSearch is a mesh as discovery and the controller's records describe it: the service manager's // verbs on two machines, the controller's own, a docker module with its tools, and what each replaces // as the records say it — the seats' from `tools`, the module's from `modules`. func meshToSearch(t *testing.T) *index { t.Helper() var endpoints []announce.Endpoint verbs := map[string]string{ "units": "The units the service manager knows in a scope, each with its load, active and sub state.", "status": "One unit as the service manager sees it now: its states, whether it starts at boot.", "restart": "Restart one unit.", "journal": "The last lines of one unit's journal (at most 2000).", } for _, node := range []string{"anchor", "laptop"} { for verb, does := range verbs { endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "systemd", Seat: "node-service-manager", Tool: verb, Scope: "node", Node: node, Description: does, Subject: "mesh.seat.node-service-manager.tool." + verb + "." + node}) } } for verb, does := range map[string]string{ "node": "What one machine reported it can do, what it is assigned, and why.", "nodes": "Every machine the mesh knows, with whether it is converged or adopted.", } { endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "mesh-controller", Seat: "mesh-controller", Tool: verb, Scope: "mesh", Description: does, Subject: "mesh.seat.mesh-controller.tool." + verb}) } for tool, does := range map[string]string{ "docker_list": "Every container on this machine: name, image, state, ports.", "docker_logs": "The last lines a container wrote.", "docker_secrets_in_logs": "Whether a container's logs hold a secret.", "docker_restart": "Restart one container.", } { endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindTool, Module: "docker", Tool: tool, Node: "anchor", Description: does, Subject: "mesh.mod.docker.tool." + tool + ".anchor"}) } x, _, _ := indexOfEndpoints(endpoints) tools := json.RawMessage(`{"seats":[{"seat":"node-service-manager","scope":"node","tools":[ {"name":"units","replaces":["systemctl list-units","systemctl --failed"]}, {"name":"status","replaces":["systemctl status","systemctl is-active"]}, {"name":"restart","replaces":["systemctl restart"]}, {"name":"journal","replaces":["journalctl"]}]}, {"seat":"mesh-controller","scope":"mesh","tools":[{"name":"node","replaces":["hostnamectl","uptime"]},{"name":"nodes"}]}]}`) x.Replaces = replacesOf(tools, []recordedModule{{Module: "docker", On: []string{"anchor"}, Tools: true, Replaces: map[string][]string{"docker_list": {"docker ps"}, "docker_logs": {"docker logs"}, "docker_restart": {"docker restart"}}}}) return x } // **The words an agent has find the verb first** (novox/hq ADR 0242): the command it would have run, or // the word it would have used, puts the mesh's tool for it at the top. func TestSearchFindsTheToolThatReplacesACommandFirst(t *testing.T) { x := meshToSearch(t) for query, want := range map[string]string{ "journalctl": "anchor/node-service-manager.journal", "journalctl -u mesh-controller -n 50": "anchor/node-service-manager.journal", "sudo journalctl -fu sshd": "anchor/node-service-manager.journal", "/usr/bin/journalctl": "anchor/node-service-manager.journal", "logs": "anchor/node-service-manager.journal", "systemctl status": "anchor/node-service-manager.status", "systemctl status nats.service": "anchor/node-service-manager.status", "systemctl restart mesh-controller": "anchor/node-service-manager.restart", "systemctl --failed": "anchor/node-service-manager.units", "docker ps": "anchor/docker.docker_list", "docker ps -a": "anchor/docker.docker_list", "docker logs": "anchor/docker.docker_logs", "docker logs --tail 100 nats": "anchor/docker.docker_logs", "docker restart nats": "anchor/docker.docker_restart", "container logs": "anchor/docker.docker_logs", "uptime": "mesh-controller.node", } { hits := x.search(query) if len(hits) == 0 { t.Errorf("%q found nothing; want %s first", query, want) continue } if hits[0].Address != want { var got []string for _, h := range hits { got = append(got, h.Address) } t.Errorf("%q found %s first; want %s (all: %s)", query, hits[0].Address, want, strings.Join(got, ", ")) } } } // A match on a replaced command says which command it replaced, and the most specific one wins. func TestASearchByCommandSaysWhatItReplaces(t *testing.T) { x := meshToSearch(t) hits := x.search("systemctl status sshd") if hits[0].InsteadOf != "systemctl status" { t.Errorf("the match does not say what it replaces: %+v", hits[0]) } if hits[0].Also != "also on laptop" { t.Errorf("a node seat's verb does not say its other machines: %+v", hits[0]) } // `logs` also finds the container's logs, after the journal — both are answers. var docker bool for _, h := range x.search("logs")[:3] { docker = docker || h.Address == "anchor/docker.docker_logs" } if !docker { t.Errorf("`logs` does not find docker_logs among the first: %+v", x.search("logs")) } } // What search did before stays: every word in a name or description, and nothing for a word nothing has. // A synonym is a word, never part of one: `logs` matches through `log`, and finds no `login`. func TestSearchStillMatchesEveryWord(t *testing.T) { x := meshToSearch(t) if hits := x.search("machine converged"); len(hits) != 1 || hits[0].Address != "mesh-controller.nodes" { t.Errorf("words in a description: %+v", hits) } if hits := x.search("journalctl kubernetes"); len(hits) != 1 || hits[0].Address != "anchor/node-service-manager.journal" { t.Errorf("a command with arguments nothing has is still that command: %+v", hits) } if hits := x.search("kubernetes"); len(hits) != 0 { t.Errorf("a word nothing has matched: %+v", hits) } x.Modules["shell"] = &moduleInfo{Module: "shell", On: []string{"anchor"}, Tools: []Tool{{Module: "shell", Name: "shell_login", Description: "The account's login shell."}}} for _, h := range x.search("logs") { if h.Address == "anchor/shell.shell_login" { t.Errorf("`logs` found `login` through its synonym `log`: %+v", h) } } } // A controller older than `replaces` answers without it: search matches names and descriptions as before. func TestSearchWithoutReplacesMatchesAsBefore(t *testing.T) { x := meshToSearch(t) x.Replaces = replacesOf(json.RawMessage(`{"seats":[{"seat":"node-service-manager","tools":[{"name":"journal"}]}]}`), []recordedModule{{Module: "docker"}}) if len(x.Replaces) != 0 { t.Fatalf("replaces from records that have none: %v", x.Replaces) } if hits := x.search("journal"); len(hits) == 0 || hits[0].Address != "anchor/node-service-manager.journal" { t.Errorf("a verb's own name: %+v", hits) } if hits := x.search("docker logs"); len(hits) == 0 || hits[0].Address != "anchor/docker.docker_logs" { t.Errorf("a tool's own name: %+v", hits) } }