package main import ( "bytes" "errors" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // A hand-over's line is judged before anything is asked (novox/hq issue 356): a node's name and the directory's // absolute path exactly as the engine states it — no `..`, no doubled or trailing separator, nothing relative. func TestAHandOverLineIsJudgedBeforeItIsAsked(t *testing.T) { for _, args := range [][]string{ {}, {"laptop"}, {"laptop", "/srv/notes", "extra"}, {"", "/srv/notes"}, {"--node", "/srv/notes"}, {"laptop", "srv/notes"}, {"laptop", "/srv/../etc"}, {"laptop", "/srv//notes"}, {"laptop", "/srv/notes/"}, {"laptop", "/srv/notes/."}, } { if _, _, err := handOverLine(args); err == nil { t.Errorf("%q was taken", args) } } node, path, err := handOverLine([]string{"laptop", "/srv/notes"}) if err != nil || node != "laptop" || path != "/srv/notes" { t.Fatalf("read as %q %q %v", node, path, err) } } // Who hands over is the line's caller in the words every verb's caller is recorded in — the operator through // mesh-cli — or the controller's terminal when nobody is named. func TestAHandOverNamesWhoAsked(t *testing.T) { t.Setenv(link.CallerVar, " jo through mesh-cli on anchor ") if by := handOverBy(); by != "jo through mesh-cli on anchor" { t.Fatalf("by %q", by) } t.Setenv(link.CallerVar, "") if by := handOverBy(); by != "the controller's terminal" { t.Fatalf("by %q", by) } } // **A hand-over is the controller's terminal's alone** (novox/hq issue 356, ADR 0266): through any verb, and // through mesh-cli outside the terminal, `node hand-over` is refused and nothing runs — at the next apply root // gives the directory to the account the module declares, and whoever may call a verb includes agents. func TestAHandOverIsRefusedThroughEveryVerb(t *testing.T) { line := []string{"node", "hand-over", "laptop", "/srv/notes"} if err := terminalOnly(line); err == nil { t.Fatal("node hand-over passed as a verb's line") } if _, err := argvFor("command", map[string]any{"command": "node hand-over laptop /srv/notes"}); err == nil { t.Fatal("the command verb composed node hand-over") } if _, err := ordinaryLine(line); err == nil { t.Fatal("node hand-over composed as an ordinary mesh-cli line") } if _, err := argvFor("node", map[string]any{"node": "laptop", "hand-over": "/srv/notes"}); err == nil { t.Fatal("the node verb composed a hand-over") } } // The condition's words are plain and name no machine's binary; the operator's line, with the node and the path // (novox/hq ADR 0272), is in the summary the module's health gives (moduleHealthWord) and in the evidence. func TestTheUsedAsFoundConditionNamesTheOperatorsLine(t *testing.T) { rs := []inventory.ResourceHealth{foundDirectory("notes", time.Now().Add(-24*time.Hour))} o := usedAsFoundObservation("notes", "laptop", "notes on laptop uses notes.data as found", rs) if strings.Contains(o.Needs, "mesh-host") || strings.Contains(o.Explanation, "mesh-host") || !strings.Contains(o.Needs, "control-node") { t.Fatalf("the condition's words: %q %q", o.Needs, o.Explanation) } if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation, Needs: o.Needs, Resolved: o.Resolved}, "laptop"); !ok { t.Fatalf("not plain: %s", why) } f := gateFacts{now: time.Now(), health: map[string]inventory.NodeHealth{"laptop": {Node: "laptop", HeardAt: time.Now(), Resources: rs}}} h, why := moduleHealthWord("notes", "laptop", time.Now().Add(-time.Hour), f) if h != healthPerson || !strings.Contains(why, "`nox node hand-over laptop ` on the control-node") || strings.Contains(why, "mesh-host") || strings.Contains(why, "/srv/") { t.Fatalf("the module's health reads %v %q; want the operator's line", h, why) } } // **Nothing is asked of a node the mesh does not know, and the engine's refusal is the command's failure** // (review of issue 356): a refused hand-over never exits as a success. func TestAHandOverAsksOnlyAKnownNodeAndFailsOnARefusal(t *testing.T) { t.Setenv(link.CallerVar, "jo through mesh-cli on anchor") asked := 0 ask := func(answer link.HandOverAnswer) func(node, path, by string) (link.HandOverAnswer, error) { return func(node, path, by string) (link.HandOverAnswer, error) { asked++ if node != "laptop" || path != "/srv/notes" || by != "jo through mesh-cli on anchor" { t.Fatalf("asked %q %q %q", node, path, by) } return answer, nil } } unknown := func(string) error { return errors.New("no node called laptop") } known := func(string) error { return nil } var out bytes.Buffer err := handOverAsked([]string{"laptop", "/srv/notes"}, unknown, ask(link.HandOverAnswer{Said: "x"}), &out) if err == nil || asked != 0 || !strings.Contains(err.Error(), "nothing was asked") { t.Fatalf("an unknown node: %v, asked %d", err, asked) } err = handOverAsked([]string{"laptop", "/srv/../etc"}, known, ask(link.HandOverAnswer{Said: "x"}), &out) if err == nil || asked != 0 { t.Fatalf("a refused line was asked: %v, asked %d", err, asked) } err = handOverAsked([]string{"laptop", "/srv/notes"}, known, ask(link.HandOverAnswer{Refused: "/srv/notes is not used as found; nothing was handed over"}), &out) if err == nil || !strings.Contains(err.Error(), "laptop refused: /srv/notes is not used as found") || out.Len() != 0 { t.Fatalf("a refusal: %v, printed %q", err, out.String()) } err = handOverAsked([]string{"laptop", "/srv/notes"}, known, ask(link.HandOverAnswer{Said: "handed over"}), &out) if err != nil || !strings.HasPrefix(out.String(), "handed over\n") || !strings.Contains(out.String(), "`nox push laptop`") { t.Fatalf("a record: %v, printed %q", err, out.String()) } failing := func(string, string, string) (link.HandOverAnswer, error) { return link.HandOverAnswer{}, errors.New("no engine") } if err := handOverAsked([]string{"laptop", "/srv/notes"}, known, failing, &out); err == nil { t.Fatal("an ask that failed was a success") } } // The setuid search's line asks only a known node, one name and nothing else, and fails on a refusal // (novox/hq issue 361). func TestASetuidSearchAsksOnlyAKnownNodeAndFailsOnARefusal(t *testing.T) { t.Setenv(link.CallerVar, "jo through mesh-cli on anchor") asked := 0 ask := func(answer link.HandOverAnswer) func(node, by string) (link.HandOverAnswer, error) { return func(node, by string) (link.HandOverAnswer, error) { asked++ if node != "novox" || by != "jo through mesh-cli on anchor" { t.Fatalf("asked %q %q", node, by) } return answer, nil } } known := func(string) error { return nil } var out bytes.Buffer for _, args := range [][]string{nil, {"novox", "extra"}, {"-x"}} { if err := setuidSearchAsked(args, known, ask(link.HandOverAnswer{Said: "x"}), &out); err == nil || asked != 0 { t.Fatalf("%v was asked: %v", args, err) } } if err := setuidSearchAsked([]string{"novox"}, func(string) error { return errors.New("no node called novox") }, ask(link.HandOverAnswer{Said: "x"}), &out); err == nil || asked != 0 { t.Fatalf("an unknown node: %v", err) } if err := setuidSearchAsked([]string{"novox"}, known, ask(link.HandOverAnswer{Refused: "no; no search was started"}), &out); err == nil || !strings.Contains(err.Error(), "novox refused") || out.Len() != 0 { t.Fatalf("a refusal: %v, printed %q", err, out.String()) } if err := setuidSearchAsked([]string{"novox"}, known, ask(link.HandOverAnswer{Said: "a new search starts"}), &out); err != nil || !strings.HasPrefix(out.String(), "a new search starts\n") { t.Fatalf("a start: %v, printed %q", err, out.String()) } } // The directory judgement's line asks only a known node, one name and nothing else, fails on a refusal, and says it // changed nothing (novox/hq issue 358). func TestADirectoryJudgementAsksOnlyAKnownNodeAndSaysItIsReadOnly(t *testing.T) { t.Setenv(link.CallerVar, "jo through mesh-cli on anchor") asked := 0 ask := func(answer link.HandOverAnswer) func(node, by string) (link.HandOverAnswer, error) { return func(node, by string) (link.HandOverAnswer, error) { asked++ if node != "novox" || by != "jo through mesh-cli on anchor" { t.Fatalf("asked %q %q", node, by) } return answer, nil } } known := func(string) error { return nil } var out bytes.Buffer for _, args := range [][]string{nil, {"novox", "extra"}, {"-x"}} { if err := judgeDirectoriesAsked(args, known, ask(link.HandOverAnswer{Said: "x"}), &out); err == nil || asked != 0 { t.Fatalf("%v was asked: %v", args, err) } } if err := judgeDirectoriesAsked([]string{"novox"}, func(string) error { return errors.New("no node called novox") }, ask(link.HandOverAnswer{Said: "x"}), &out); err == nil || asked != 0 { t.Fatalf("an unknown node: %v", err) } if err := judgeDirectoriesAsked([]string{"novox"}, known, ask(link.HandOverAnswer{Refused: "no; nothing was judged"}), &out); err == nil || !strings.Contains(err.Error(), "novox refused") || out.Len() != 0 { t.Fatalf("a refusal: %v, printed %q", err, out.String()) } said := "2 directories declared: 1 placed, 1 would be refused\n/var/lib/a: placed\n/var/lib/b: would be refused: …" if err := judgeDirectoriesAsked([]string{"novox"}, known, ask(link.HandOverAnswer{Said: said}), &out); err != nil || out.String() != "novox (read-only: nothing on novox was changed)\n"+said+"\n" { t.Fatalf("a judgement: %v, printed %q", err, out.String()) } } // The judgement is a seat verb an agent may call — it reads, and changes nothing on the node — and runs exactly // `node judge-directories ` (novox/hq issue 358). func TestTheDirectoryJudgementIsAVerbThatOnlyReads(t *testing.T) { argv, err := argvFor("judge-directories", map[string]any{"node": "novox"}) if err != nil || strings.Join(argv, " ") != "node judge-directories novox" { t.Fatalf("the verb runs %v, %v", argv, err) } if _, err := argvFor("judge-directories", map[string]any{}); err == nil { t.Fatal("the verb ran with no node") } if !readingVerbs["judge-directories"] { t.Fatal("the judgement is not counted a reading verb") } }