Files
mesh-tools/node-tools/internal/console/search_test.go
T
jochen 49cb06f6c5 Find a tool by the shell command it replaces, best first (hq ADR 0245)
The agent searched with journalctl, logs and docker ps and found nothing, so it went over ssh. Search
now reads what each verb and tool replaces from the controller's records, matches a command line
against it, ranks the matches, and says the command it matched.
2026-10-07 19:21:13 +02:00

152 lines
7.3 KiB
Go

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)
}
}