nox calls the seats that own each job, not the unassigned controller
The controller nox forwarded every line to is unassigned (issue 1126), so nox answered nothing but enrol open. Each line now calls the seat verb or module tool that owns its job through this machine's console (mesh_call); a line no seat owns says so and names what to use, never failing silently. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -3,153 +3,282 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/cli"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The tests' own standard input is the runner's, which may be a pipe nobody closes: read as a terminal unless a test
|
||||
// says otherwise.
|
||||
// The tests' own standard input is the runner's, which may be a pipe nobody closes: read as a terminal.
|
||||
func init() { standardInput = func() (io.Reader, bool) { return nil, true } }
|
||||
|
||||
// lastStdin is the standard input the engine was last given.
|
||||
var lastStdin []byte
|
||||
// asked is one call the stand-in console took: the console's tool, and for mesh_call the address and its arguments.
|
||||
type asked struct {
|
||||
Tool string
|
||||
Address string
|
||||
Args string
|
||||
}
|
||||
|
||||
// engine answers one connection on a socket of the test's own as the node-engine would, and says what it was asked.
|
||||
func engine(t *testing.T, answer cli.Answer) <-chan []string {
|
||||
// console is a stand-in for this machine's console: it answers MCP tools/call over HTTP as node-tools does, with the
|
||||
// answer the test gives per address (an error when it starts with "!"), and records what it was asked.
|
||||
func console(t *testing.T, answers map[string]string) *[]asked {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "cli.sock")
|
||||
l, err := net.Listen("unix", path)
|
||||
if err != nil {
|
||||
t.Skipf("no unix socket here: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { l.Close() })
|
||||
before := socket
|
||||
socket = path
|
||||
t.Cleanup(func() { socket = before })
|
||||
asked := make(chan []string, 1)
|
||||
lastStdin = nil
|
||||
go func() {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
var got []asked
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/mcp" || r.Method != http.MethodPost {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
req, err := cli.ReadRequest(conn)
|
||||
if err != nil {
|
||||
return
|
||||
var req struct {
|
||||
Method string `json:"method"`
|
||||
Params struct {
|
||||
Name string `json:"name"`
|
||||
Arguments struct {
|
||||
Address string `json:"address"`
|
||||
Arguments map[string]any `json:"arguments"`
|
||||
Query string `json:"query"`
|
||||
} `json:"arguments"`
|
||||
} `json:"params"`
|
||||
}
|
||||
lastStdin = req.Stdin
|
||||
asked <- req.Line
|
||||
_ = cli.WriteAnswer(conn, answer)
|
||||
}()
|
||||
return asked
|
||||
}
|
||||
|
||||
// The line is asked as given, what the controller printed is printed, and its exit is mesh-cli's.
|
||||
func TestTheLineIsAskedAndTheAnswerIsPrinted(t *testing.T) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Method != "tools/call" {
|
||||
t.Errorf("the console was asked %v %v", req.Method, err)
|
||||
}
|
||||
a := req.Params.Arguments
|
||||
args, _ := json.Marshal(a.Arguments)
|
||||
got = append(got, asked{req.Params.Name, a.Address, string(args)})
|
||||
key := a.Address
|
||||
if req.Params.Name != "mesh_call" {
|
||||
key = req.Params.Name
|
||||
}
|
||||
text, ok := answers[key]
|
||||
if !ok {
|
||||
text = `{"ok":true}`
|
||||
}
|
||||
isError := strings.HasPrefix(text, "!")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": 1, "result": map[string]any{
|
||||
"content": []any{map[string]any{"type": "text", "text": strings.TrimPrefix(text, "!")}}, "isError": isError}})
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
t.Setenv("MESH_CONSOLE", srv.URL+"/mcp")
|
||||
t.Setenv("MESH_VERB", "")
|
||||
asked := engine(t, cli.Answer{Stdout: []byte("out\n"), Stderr: []byte("err\n"), Exit: 3, Terminal: true, Why: "the terminal"})
|
||||
return &got
|
||||
}
|
||||
|
||||
func runLine(args ...string) (int, string, string) {
|
||||
var out, errOut bytes.Buffer
|
||||
code := run(context.Background(), []string{"settings", "set", "zsh", `{"execute":"withhold"}`}, &out, &errOut)
|
||||
if line := <-asked; strings.Join(line, "|") != `settings|set|zsh|{"execute":"withhold"}` {
|
||||
t.Fatalf("asked %q", line)
|
||||
}
|
||||
if code != 3 || out.String() != "out\n" || errOut.String() != "err\n" {
|
||||
t.Fatalf("exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
|
||||
code := run(context.Background(), args, &out, &errOut)
|
||||
return code, out.String(), errOut.String()
|
||||
}
|
||||
|
||||
// Every owned line calls the seat verb or module tool that owns its job, with the arguments the owner reads
|
||||
// (novox/hq issue 1126; hq .work/1126/nox-map.md), and prints its answer.
|
||||
func TestEachLineCallsItsOwner(t *testing.T) {
|
||||
now = func() time.Time { return time.Date(2026, 11, 2, 10, 0, 0, 0, time.UTC) }
|
||||
t.Cleanup(func() { now = time.Now })
|
||||
for _, c := range []struct {
|
||||
line string
|
||||
address string
|
||||
args string
|
||||
}{
|
||||
{"node show ace", "mesh-placement.placement", `{"node":"ace"}`},
|
||||
{"node public-domain ace", "mesh-placement.placement", `{"node":"ace"}`},
|
||||
{"node public-domain ace example.org --why moved", "mesh-placement.propose", `{"changes":[{"field":"public-domain","node":"ace","op":"decide","value":"example.org"}],"why":"moved"}`},
|
||||
{"plan ace", "mesh-delivery-view.assigner_wants", `{"node":"ace"}`},
|
||||
{"haves", "mesh-delivery-view.node_haves", `{}`},
|
||||
{"composition ace plex", "ace/node-engine.composition", `{"module":"plex"}`},
|
||||
{"seats", "mesh-placement.placement", `{"seats":"true"}`},
|
||||
{"assign ace plex sonarr --why=media", "mesh-placement.propose", `{"changes":[{"module":"plex","node":"ace","op":"place"},{"module":"sonarr","node":"ace","op":"place"}],"why":"media"}`},
|
||||
{"unassign ace plex --why gone", "mesh-placement.propose", `{"changes":[{"module":"plex","node":"ace","op":"take-off"}],"why":"gone"}`},
|
||||
{"pin ace postgres-database novox postgres --why one", "mesh-placement.propose", `{"changes":[{"node":"ace","op":"pin","provider":{"module":"postgres","node":"novox"},"provision":"postgres-database"}],"why":"one"}`},
|
||||
{"unpin ace postgres-database --why back", "mesh-placement.propose", `{"changes":[{"node":"ace","op":"unpin","provision":"postgres-database"}],"why":"back"}`},
|
||||
{"deliveries --state merged --json", "mesh-delivery.deliveries", `{"state":"merged"}`},
|
||||
{"delivery novox/mesh-sdk@dff0ce2abb21", "mesh-delivery.show", `{"id":"novox/mesh-sdk@dff0ce2abb21"}`},
|
||||
{"checks novox/mesh-catalog --number 591", "mesh-delivery.checks", `{"number":"591","repository":"novox/mesh-catalog"}`},
|
||||
{"try novox/mesh-catalog abc123 --level unit", "mesh-delivery-checker.try", `{"head":"abc123","level":"unit","repository":"novox/mesh-catalog"}`},
|
||||
{"builds", "mesh-delivery-builder.builder_builds_since", `{"since":"2026-11-01T10:00:00Z"}`},
|
||||
{"upgrade nats --policy record --why bus", "mesh-wants.upgrade", `{"module":"nats","policy":"record","why":"bus"}`},
|
||||
{"bus", "mesh-wants.bus", `{}`},
|
||||
{"conditions --severity urgent", "mesh-conditions.conditions", `{"severity":"urgent"}`},
|
||||
{"conditions show machine.g14.root-not-free", "mesh-conditions.conditions", `{"key":"machine.g14.root-not-free"}`},
|
||||
{"conditions silence k1 --for 4h --why known", "mesh-conditions.silence", `{"for":"4h","key":"k1","why":"known"}`},
|
||||
{"conditions history --days 3", "mesh-conditions.conditions", `{"days":"3","history":"true"}`},
|
||||
{"conditions history --key k1", "mesh-conditions.conditions_history", `{"key":"k1"}`},
|
||||
{"doctor", "mesh-self-check.doctor", `{}`},
|
||||
{"doctor run", "mesh-self-check.doctor", `{"run":true}`},
|
||||
{"doctor probe D6", "mesh-self-check.doctor", `{"probe":"D6"}`},
|
||||
{"healers --days 2", "mesh-healers.healers", `{"days":2}`},
|
||||
{"hand-act record restarted the view --why stuck --cause view", "mesh-hand-acts.hand-act", `{"cause":"view","what":"restarted the view","why":"stuck"}`},
|
||||
{"hand-acts --days 3", "mesh-hand-acts.hand-acts", `{"days":3}`},
|
||||
{"settings show zsh --node g14", "mesh-config.config", `{"action":"list","module":"zsh","node":"g14"}`},
|
||||
{"settings set zsh execute withhold --node g14 --why test", "mesh-config.config", `{"action":"set","key":"execute","module":"zsh","node":"g14","value":"withhold","why":"test"}`},
|
||||
{"settings clear zsh execute --node g14 --why test", "mesh-config.config", `{"action":"clear","key":"execute","module":"zsh","node":"g14","why":"test"}`},
|
||||
{"config history --key k --limit 5", "mesh-config.config", `{"action":"history","key":"k","limit":5}`},
|
||||
{"secret export", "mesh-vault.secret_vault_export", `{}`},
|
||||
{"operator key show", "mesh-vault.vault_status", `{}`},
|
||||
{"rotate --node ace --module plex --name token --why leak", "mesh-vault.rotate", `{"module":"plex","name":"token","node":"ace","why":"leak"}`},
|
||||
{"retire approve c1 c2 --why done", "mesh-vault.provisioner_retire_approve", `{"consumers":["c1","c2"],"why":"done"}`},
|
||||
{"collect --confirm --why space", "mesh-artifact-sweep.collect", `{"confirm":"true","why":"space"}`},
|
||||
{"images ace", "mesh-artifact-sweep.images", `{"node":"ace"}`},
|
||||
{"licence use g14 work", "anthropic-licence-manager.switch", `{"consumer":"g14","licence":"work"}`},
|
||||
{"module show plex", "mesh-delivery-view.indexer_module", `{"module":"plex"}`},
|
||||
{"issues apostrophe bug --status open --limit 5", "issue-tracker.list", `{"limit":5,"search":"apostrophe bug","status":"open"}`},
|
||||
{"issue 1126 --brief", "issue-tracker.get", `{"brief":true,"number":1126}`},
|
||||
{"kb ask what holds about the merge window", "knowledge-base.ask", `{"question":"what holds about the merge window"}`},
|
||||
{"kb get 587 --reasoning", "knowledge-base.get", `{"number":587,"reasoning":true}`},
|
||||
{"progress 47", "knowledge-base.progress", `{"of":"design 47"}`},
|
||||
{"enrol issue new1 --why laptop --expires 2h", "mesh-enrolment.issue", `{"expires":"2h","node":"new1","why":"laptop"}`},
|
||||
{"token issue --new new1 --why laptop", "mesh-enrolment.issue", `{"node":"new1","why":"laptop"}`},
|
||||
{"enrol tokens", "mesh-enrolment.tokens", `{}`},
|
||||
{"call ace/node-service-manager.status {\"unit\":\"plex\"}", "ace/node-service-manager.status", `{"unit":"plex"}`},
|
||||
{"ask mesh-catalog catalog_modules", "mesh-catalog.catalog_modules", `{}`},
|
||||
} {
|
||||
got := console(t, nil)
|
||||
args := strings.Fields(c.line)
|
||||
if strings.HasPrefix(c.line, "call ") {
|
||||
args = []string{"call", "ace/node-service-manager.status", `{"unit":"plex"}`}
|
||||
}
|
||||
code, out, errOut := runLine(args...)
|
||||
if code != 0 || len(*got) != 1 || (*got)[0].Tool != "mesh_call" || (*got)[0].Address != c.address || (*got)[0].Args != c.args {
|
||||
t.Errorf("nox %s: exit %d, asked %+v, stderr %q; want %s %s", c.line, code, *got, errOut, c.address, c.args)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(out, `"ok":true`) {
|
||||
t.Errorf("nox %s printed %q, not the owner's answer", c.line, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not the terminal, refused, or cut: each is said, and a refusal never exits 0.
|
||||
func TestWhyARefusalAndACutAreSaid(t *testing.T) {
|
||||
t.Setenv("MESH_VERB", "")
|
||||
engine(t, cli.Answer{Why: "not the controller's terminal: agents on laptop may run as operator",
|
||||
Refused: "set at the controller's terminal only", Cut: true})
|
||||
var out, errOut bytes.Buffer
|
||||
code := run(context.Background(), []string{"settings", "set", "x", "{}"}, &out, &errOut)
|
||||
said := errOut.String()
|
||||
if code == 0 || !strings.Contains(said, "not the controller's terminal") || !strings.Contains(said, "terminal only") ||
|
||||
!strings.Contains(said, "cut") {
|
||||
t.Fatalf("exit %d, said %q", code, said)
|
||||
// A line the controller ran that no seat owns says so and what to use, exits 1 and calls nothing; a line nox cannot
|
||||
// read says how it is written, exits 2 and calls nothing.
|
||||
func TestALineWithNoOwnerOrNotReadCallsNothingAndSaysWhy(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
line string
|
||||
code int
|
||||
err string
|
||||
}{
|
||||
{"push ace", 1, "mesh-delivery-assigner.assigner_put"},
|
||||
{"migrate", 1, "no seat or module does this now"},
|
||||
{"secret accept ace plex token", 1, "mesh-vault.secret-ask"},
|
||||
{"node add new1", 1, "nox enrol issue"},
|
||||
{"pause", 1, "node-build-go.pause"},
|
||||
{"operator key make", 1, "operator key show"},
|
||||
{"assign ace plex", 2, "needs --why"},
|
||||
{"issue twelve", 2, "not an issue's number"},
|
||||
{"conditions --bogus x", 2, "--bogus is not a flag"},
|
||||
{"settings set zsh {\"execute\":\"withhold\"} --node g14 --why x", 2, "set each key"},
|
||||
{"healers --days soon", 2, "takes a number"},
|
||||
{"frobnicate", 2, "is not a command"},
|
||||
} {
|
||||
got := console(t, nil)
|
||||
args := strings.Fields(c.line)
|
||||
code, _, errOut := runLine(args...)
|
||||
if code != c.code || len(*got) != 0 || !strings.Contains(errOut, c.err) {
|
||||
t.Errorf("nox %s: exit %d, asked %+v, stderr %q; want exit %d saying %q", c.line, code, *got, errOut, c.code, c.err)
|
||||
}
|
||||
if c.code == 1 && !strings.Contains(errOut, "issue 1126") {
|
||||
t.Errorf("nox %s does not say the controller is unassigned: %q", c.line, errOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A verb's own process is never the operator: refused before anything is asked.
|
||||
func TestAVerbsProcessIsRefusedBeforeAsking(t *testing.T) {
|
||||
t.Setenv("MESH_VERB", "command")
|
||||
before := socket
|
||||
socket = filepath.Join(t.TempDir(), "nothing-listens.sock")
|
||||
t.Cleanup(func() { socket = before })
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"status"}, &out, &errOut); code != 1 ||
|
||||
!strings.Contains(errOut.String(), "never the operator") {
|
||||
t.Fatalf("exit %d, said %q", code, errOut.String())
|
||||
// What the console says is printed as said: an answer it marks an error on standard error with exit 1, a console that
|
||||
// does not answer as nothing called, a verb's process refused before anything is called; nodes are one line each.
|
||||
func TestAnswersRefusalsAndSilenceAreSaid(t *testing.T) {
|
||||
got := console(t, map[string]string{"mesh-enrolment.tokens": "!nothing serves seat:mesh-enrolment.tokens"})
|
||||
if code, out, errOut := runLine("enrol", "tokens"); code != 1 || out != "" || !strings.Contains(errOut, "nothing serves") {
|
||||
t.Errorf("an error answer: exit %d, stdout %q, stderr %q", code, out, errOut)
|
||||
}
|
||||
placement := `{"keys":{"novox":{"placement":{"decided":{"address":"10.10.0.1","site":"hub"},"modules":[{},{}]}},` +
|
||||
`"ace":{"placement":{"decided":{"address":"10.10.0.2"},"modules":[{}]}}}}`
|
||||
console(t, map[string]string{"mesh-placement.placement": placement})
|
||||
if code, out, _ := runLine("nodes"); code != 0 || !strings.HasPrefix(out, "ace ") ||
|
||||
!strings.Contains(out, "10.10.0.1") || !strings.Contains(out, "2 modules") {
|
||||
t.Errorf("nodes: exit %d, %q", code, out)
|
||||
}
|
||||
got = console(t, map[string]string{"mesh_search": `{"matches":[]}`})
|
||||
if code, out, _ := runLine("search", "restart", "a", "unit"); code != 0 || (*got)[0].Tool != "mesh_search" || !strings.Contains(out, "matches") {
|
||||
t.Errorf("search: exit %d, asked %+v, %q", code, *got, out)
|
||||
}
|
||||
t.Setenv("MESH_CONSOLE", "http://127.0.0.1:1/mcp")
|
||||
if code, _, errOut := runLine("conditions"); code != 1 || !strings.Contains(errOut, "nothing was called") {
|
||||
t.Errorf("no console: exit %d, %q", code, errOut)
|
||||
}
|
||||
t.Setenv("MESH_VERB", "some.verb")
|
||||
if code, _, errOut := runLine("conditions"); code != 1 || !strings.Contains(errOut, "a verb is never the operator") {
|
||||
t.Errorf("a verb's process: exit %d, %q", code, errOut)
|
||||
}
|
||||
}
|
||||
|
||||
// An engine that does not answer is said, with where it was asked.
|
||||
func TestAnEngineThatDoesNotAnswerIsSaid(t *testing.T) {
|
||||
t.Setenv("MESH_VERB", "")
|
||||
before := socket
|
||||
socket = filepath.Join(t.TempDir(), "nothing-listens.sock")
|
||||
t.Cleanup(func() { socket = before })
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"status"}, &out, &errOut); code != 1 ||
|
||||
!strings.Contains(errOut.String(), "does not answer") {
|
||||
t.Fatalf("exit %d, said %q", code, errOut.String())
|
||||
// The status is the delivery view's. When the view does not answer, that is said first, the conditions and the
|
||||
// deliveries are shown under their own addresses, and the exit is 1: they are never passed off as the status.
|
||||
func TestStatusIsTheViewsAndItsSilenceIsSaid(t *testing.T) {
|
||||
got := console(t, map[string]string{"mesh-delivery-view.view_status": `{"well":true}`})
|
||||
if code, out, _ := runLine("status"); code != 0 || len(*got) != 1 || !strings.Contains(out, `"well":true`) {
|
||||
t.Errorf("status: exit %d, asked %+v, %q", code, *got, out)
|
||||
}
|
||||
got = console(t, map[string]string{"mesh-delivery-view.view_status": "!mesh-delivery-view.view_status did not answer within 30s",
|
||||
"mesh-conditions.conditions": `{"conditions":[]}`, "mesh-delivery.deliveries": `{"deliveries":[]}`})
|
||||
code, out, errOut := runLine("status")
|
||||
if code != 1 || len(*got) != 3 || !strings.Contains(errOut, "did not answer") || !strings.Contains(errOut, "not the status") ||
|
||||
!strings.Contains(out, "== mesh-conditions.conditions") || !strings.Contains(out, "== mesh-delivery.deliveries") {
|
||||
t.Errorf("status, the view silent: exit %d, asked %+v, stdout %q, stderr %q", code, *got, out, errOut)
|
||||
}
|
||||
}
|
||||
|
||||
// An outcome not known is said as such — the line may have run — and never as nothing having run.
|
||||
func TestAnOutcomeNotKnownIsSaid(t *testing.T) {
|
||||
t.Setenv("MESH_VERB", "")
|
||||
engine(t, cli.NotKnown("call-4", "no answer within 6m0s"))
|
||||
var out, errOut bytes.Buffer
|
||||
code := run(context.Background(), []string{"push", "laptop"}, &out, &errOut)
|
||||
if code == 0 || !strings.Contains(errOut.String(), "may have run") || !strings.Contains(errOut.String(), "call-4") {
|
||||
t.Fatalf("exit %d, said %q", code, errOut.String())
|
||||
// The console's address comes from the file the module writes from its mcp-endpoint; MESH_CONSOLE overrides it; with
|
||||
// neither, the port node-tools declares. An answer given as a server-sent event is read like one given as JSON.
|
||||
func TestTheConsoleIsFoundAndItsAnswerRead(t *testing.T) {
|
||||
t.Setenv("MESH_CONSOLE", "")
|
||||
dir := t.TempDir()
|
||||
before := consoleFile
|
||||
consoleFile = filepath.Join(dir, "console")
|
||||
t.Cleanup(func() { consoleFile = before })
|
||||
if url, _ := consoleAddress(); url != defaultConsole {
|
||||
t.Errorf("no file: %s", url)
|
||||
}
|
||||
if err := os.WriteFile(consoleFile, []byte("http://127.0.0.1:4999/mcp\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if url, from := consoleAddress(); url != "http://127.0.0.1:4999/mcp" || from != consoleFile {
|
||||
t.Errorf("the file: %s from %s", url, from)
|
||||
}
|
||||
t.Setenv("MESH_CONSOLE", "http://127.0.0.1:5000/mcp")
|
||||
if url, _ := consoleAddress(); url != "http://127.0.0.1:5000/mcp" {
|
||||
t.Errorf("the variable: %s", url)
|
||||
}
|
||||
a, err := decodeAnswer([]byte("event: message\ndata: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"hi\"}],\"isError\":true}}\n\n"))
|
||||
if err != nil || a.Text != "hi" || !a.IsError {
|
||||
t.Errorf("an event: %+v %v", a, err)
|
||||
}
|
||||
if _, err := decodeAnswer([]byte("<html>")); err == nil {
|
||||
t.Error("a page that is no answer was read as one")
|
||||
}
|
||||
}
|
||||
|
||||
// A pipe's or a file's standard input goes with the line, whole; a terminal's is never read; more than the bound is
|
||||
// refused and nothing is asked (novox/hq ADR 0259 §10).
|
||||
func TestStandardInputGoesWithTheLineAndATerminalIsNeverRead(t *testing.T) {
|
||||
t.Setenv("MESH_VERB", "")
|
||||
was := standardInput
|
||||
t.Cleanup(func() { standardInput = was })
|
||||
secret := "123456789:the-bot-token\n"
|
||||
|
||||
standardInput = func() (io.Reader, bool) { return strings.NewReader(secret), false }
|
||||
asked := engine(t, cli.Answer{Terminal: true})
|
||||
var out, errOut bytes.Buffer
|
||||
if code := run(context.Background(), []string{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "-"},
|
||||
&out, &errOut); code != 0 {
|
||||
t.Fatalf("exit %d: %s", code, errOut.String())
|
||||
}
|
||||
<-asked
|
||||
if string(lastStdin) != secret {
|
||||
t.Fatalf("the engine was given %q", lastStdin)
|
||||
}
|
||||
if strings.Contains(out.String()+errOut.String(), "bot-token") {
|
||||
t.Fatal("mesh-cli printed what it carried")
|
||||
}
|
||||
|
||||
standardInput = func() (io.Reader, bool) { return strings.NewReader("typed"), true }
|
||||
asked = engine(t, cli.Answer{})
|
||||
_ = run(context.Background(), []string{"status"}, &out, &errOut)
|
||||
<-asked
|
||||
if len(lastStdin) != 0 {
|
||||
t.Fatalf("a terminal's input was read: %q", lastStdin)
|
||||
}
|
||||
|
||||
standardInput = func() (io.Reader, bool) { return bytes.NewReader(make([]byte, cli.MaxStdin+1)), false }
|
||||
errOut.Reset()
|
||||
if code := run(context.Background(), []string{"secret", "accept", "a", "b", "c", "--from", "-"}, &out, &errOut); code == 0 ||
|
||||
!strings.Contains(errOut.String(), "nothing was asked") {
|
||||
t.Fatalf("more than the bound was asked: %d %s", code, errOut.String())
|
||||
// Every command's line is in the help, and none is defined twice.
|
||||
func TestEveryLineIsInTheHelpOnce(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
help := usage()
|
||||
for _, c := range commands {
|
||||
w := strings.Join(c.words, " ")
|
||||
if seen[w] {
|
||||
t.Errorf("nox %s is defined twice", w)
|
||||
}
|
||||
seen[w] = true
|
||||
if c.build != nil && !strings.Contains(help, "nox "+w+" ") {
|
||||
t.Errorf("nox %s is not in the help", w)
|
||||
}
|
||||
if c.build == nil && c.instead == "" {
|
||||
t.Errorf("nox %s has neither an owner nor an instead", w)
|
||||
}
|
||||
if _, dup := unowned[c.words[0]]; dup && len(c.words) == 1 {
|
||||
t.Errorf("nox %s is both owned and unowned", w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user