mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.62s)
mesh/delivery-group group feat/a-terminal-line-takes-standard-input rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
A secret given at the controller's terminal through mesh-cli (secret accept … --from -) never reached the line: every line ran with no standard input. A line that runs as the terminal now reads what mesh-cli carried (at most 64 KiB); an ordinary call carrying any is refused and nothing runs; the calls record keeps only that some was given, the journal and the answer nothing of it.
297 lines
12 KiB
Go
297 lines
12 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-controller/internal/testbus"
|
|
)
|
|
|
|
// The node-engine's request and the answer it hands mesh-cli hold these field names; the engine's side holds the
|
|
// same list (mesh-host internal/meshcli, TestTheRequestToTheControllerKeepsItsFieldNames).
|
|
func TestTheMeshCLIRequestAndAnswerKeepTheirFieldNames(t *testing.T) {
|
|
body, _ := json.Marshal(CLIAsked{Line: []string{"status"}, Account: "a", UID: 1, Session: "session-3.scope"})
|
|
if got := keysIn(t, body); got != "account line session uid" {
|
|
t.Fatalf("the request's fields are %q", got)
|
|
}
|
|
body, _ = json.Marshal(CLIAsked{Line: []string{"status"}, Account: "a", UID: 1})
|
|
if got := keysIn(t, body); got != "account line uid" {
|
|
t.Fatalf("the request's fields are %q", got)
|
|
}
|
|
body, _ = json.Marshal(CLIAnswer{Stdout: []byte("o"), Stderr: []byte("e"), Exit: 1, Terminal: true, Why: "w",
|
|
Refused: "r", Cut: true})
|
|
if got := keysIn(t, body); got != "cut exit refused stderr stdout terminal why" {
|
|
t.Fatalf("the answer's fields are %q", got)
|
|
}
|
|
body, _ = json.Marshal(CLIAsked{Line: []string{"secret"}, Account: "a", UID: 1, Stdin: []byte("x")})
|
|
if got := keysIn(t, body); got != "account line stdin uid" {
|
|
t.Fatalf("a request with standard input has the fields %q", got)
|
|
}
|
|
body, _ = json.Marshal(CLIAsked{Follow: "call-1", Account: "a", UID: 1})
|
|
if got := keysIn(t, body); got != "account follow uid" {
|
|
t.Fatalf("a follow's fields are %q", got)
|
|
}
|
|
// What a line still running answers, in the envelope every call's answer is in: `result`, then these.
|
|
var env struct {
|
|
Result json.RawMessage `json:"result"`
|
|
}
|
|
_ = json.Unmarshal(running(&Call{ID: "call-1", Seat: CLISeat, Verb: "a"}, AnswerWithin, false, ""), &env)
|
|
if got := keysIn(t, env.Result); got != "call output running started" {
|
|
t.Fatalf("a running answer's fields are %q", got)
|
|
}
|
|
}
|
|
|
|
func keysIn(t *testing.T, body []byte) string {
|
|
t.Helper()
|
|
var m map[string]any
|
|
if err := json.Unmarshal(body, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var keys []string
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return strings.Join(keys, " ")
|
|
}
|
|
|
|
// A node's mesh-cli subject names the node, and no other subject does.
|
|
func TestTheMeshCLISubjectNamesItsNode(t *testing.T) {
|
|
if node, ok := CLINode(CLISubject("laptop")); !ok || node != "laptop" {
|
|
t.Fatalf("CLINode(%q) = %q %v", CLISubject("laptop"), node, ok)
|
|
}
|
|
for _, s := range []string{"mesh.control.laptop.report", "mesh.control..cli", "mesh.control.a.b.cli", "mesh.node.a.cli"} {
|
|
if _, ok := CLINode(s); ok {
|
|
t.Fatalf("%s was read as a mesh-cli subject", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// An answer larger than one bus message is cut to fit, and the cut is said.
|
|
func TestALargeMeshCLIAnswerIsCutAndSaysSo(t *testing.T) {
|
|
a := CLIAnswer{Stdout: []byte(strings.Repeat("o", 200000)), Stderr: []byte(strings.Repeat("e", 1000)), Why: "the terminal"}
|
|
body := FitCLIAnswer(a, 64<<10)
|
|
if len(body) > 64<<10 {
|
|
t.Fatalf("the answer is %d bytes, over the bound", len(body))
|
|
}
|
|
var got CLIAnswer
|
|
if err := json.Unmarshal(body, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !got.Cut || got.Why != "the terminal" || len(got.Stdout) == 0 || string(got.Stderr) != strings.Repeat("e", 1000) {
|
|
t.Fatalf("the cut answer is %+v", got)
|
|
}
|
|
if small := FitCLIAnswer(CLIAnswer{Stdout: []byte("ok")}, 64<<10); strings.Contains(string(small), `"cut"`) {
|
|
t.Fatal("an answer that fits was said to be cut")
|
|
}
|
|
}
|
|
|
|
// cliBus serves mesh-cli on a bus of the test's own with a call log of its own, and returns a connection to ask on.
|
|
func cliBus(t *testing.T, l *CallLog, atOnce int, handle CLIHandler) *nats.Conn {
|
|
t.Helper()
|
|
conn, err := nats.Connect(testbus.URL(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(conn.Close)
|
|
stop, err := OverNATS{Conn: conn}.serveCLI(l, atOnce, handle, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(stop)
|
|
return conn
|
|
}
|
|
|
|
// askCLI asks one request on a node's subject and reads the envelope.
|
|
func askCLI(t *testing.T, conn *nats.Conn, node string, asked CLIAsked) (CLIAnswer, map[string]any, string) {
|
|
t.Helper()
|
|
body, _ := json.Marshal(asked)
|
|
msg, err := conn.Request(CLISubject(node), body, 5*time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var env struct {
|
|
Result json.RawMessage `json:"result"`
|
|
Error string `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(msg.Data, &env); err != nil {
|
|
t.Fatalf("not an envelope: %s", msg.Data)
|
|
}
|
|
var a CLIAnswer
|
|
var raw map[string]any
|
|
_ = json.Unmarshal(env.Result, &a)
|
|
_ = json.Unmarshal(env.Result, &raw)
|
|
return a, raw, env.Error
|
|
}
|
|
|
|
// **A line that outlasts the bus's window for an answer is followed to its answer, never lost** (review of ADR
|
|
// 0272): the first answer says it is running and names its call, and following the call by the same account on
|
|
// the same node gives what the line printed once it ends. Another account, or another node, is told no such call.
|
|
func TestALongMeshCLILineIsFollowedToItsAnswer(t *testing.T) {
|
|
was := AnswerWithin
|
|
AnswerWithin = 50 * time.Millisecond
|
|
t.Cleanup(func() { AnswerWithin = was })
|
|
release := make(chan struct{})
|
|
conn := cliBus(t, NewCallLog(), 4, func(ctx context.Context, node string, asked CLIAsked) CLIAnswer {
|
|
<-release
|
|
return CLIAnswer{Stdout: []byte("done on " + node), Terminal: true}
|
|
})
|
|
_, first, failed := askCLI(t, conn, "laptop", CLIAsked{Line: []string{"push", "laptop"}, Account: "op", UID: 1000})
|
|
call, _ := first["call"].(string)
|
|
if failed != "" || first["running"] != true || call == "" {
|
|
t.Fatalf("a long line was not answered as running with its call: %v %q", first, failed)
|
|
}
|
|
_, still, _ := askCLI(t, conn, "laptop", CLIAsked{Follow: call, Account: "op", UID: 1000})
|
|
if still["running"] != true {
|
|
t.Fatalf("following a running line answered %v", still)
|
|
}
|
|
close(release)
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
a, raw, failed := askCLI(t, conn, "laptop", CLIAsked{Follow: call, Account: "op", UID: 1000})
|
|
if failed != "" {
|
|
t.Fatalf("following answered %q", failed)
|
|
}
|
|
if raw["running"] != true {
|
|
if string(a.Stdout) != "done on laptop" || !a.Terminal {
|
|
t.Fatalf("followed to %+v", a)
|
|
}
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("the line never finished for its follower")
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
if _, _, failed := askCLI(t, conn, "laptop", CLIAsked{Follow: call, Account: "agent", UID: 1001}); failed == "" {
|
|
t.Fatal("another account followed the operator's line")
|
|
}
|
|
if _, _, failed := askCLI(t, conn, "desktop", CLIAsked{Follow: call, Account: "op", UID: 1000}); failed == "" {
|
|
t.Fatal("another node followed the line")
|
|
}
|
|
}
|
|
|
|
// Lines running at once are bounded: one over the bound is answered busy at once, and nothing ran.
|
|
func TestMeshCLILinesAtOnceAreBounded(t *testing.T) {
|
|
was := AnswerWithin
|
|
AnswerWithin = 50 * time.Millisecond
|
|
t.Cleanup(func() { AnswerWithin = was })
|
|
release := make(chan struct{})
|
|
t.Cleanup(func() { close(release) })
|
|
ran := make(chan struct{}, 4)
|
|
conn := cliBus(t, NewCallLog(), 1, func(context.Context, string, CLIAsked) CLIAnswer {
|
|
ran <- struct{}{}
|
|
<-release
|
|
return CLIAnswer{}
|
|
})
|
|
if _, first, _ := askCLI(t, conn, "laptop", CLIAsked{Line: []string{"status"}, Account: "op"}); first["running"] != true {
|
|
t.Fatalf("the first line answered %v", first)
|
|
}
|
|
a, _, _ := askCLI(t, conn, "laptop", CLIAsked{Line: []string{"status"}, Account: "op"})
|
|
if !strings.Contains(a.Refused, "busy") || a.Exit == 0 {
|
|
t.Fatalf("a line over the bound answered %+v", a)
|
|
}
|
|
if len(ran) != 1 {
|
|
t.Fatalf("%d lines ran, one was bound", len(ran))
|
|
}
|
|
}
|
|
|
|
// A mesh-cli line's record keeps its first word, never the rest of its line, and its answer is never kept on the
|
|
// bus, where `calls` answers anyone who may call the seat.
|
|
func TestAMeshCLIRecordKeepsNeitherItsLineNorItsAnswerOnTheBus(t *testing.T) {
|
|
l, a := NewCallLog(), newAnswers(t)
|
|
writes := make(chan Call, 4)
|
|
l.writes = writes
|
|
asked, _ := json.Marshal(CLIAsked{Line: []string{"settings", "set", "x", `{"password":"s3cret"}`}, Account: "op"})
|
|
l.serveCall(CLISeat, "laptop", asked, "_INBOX.node.laptop.abcdefghijklmnopqrstuv",
|
|
func(context.Context, json.RawMessage) (any, error) {
|
|
return CLIAnswer{Stdout: []byte("s3cret-join")}, nil
|
|
},
|
|
a.respond, nil)
|
|
_ = a.only()
|
|
close(writes)
|
|
for c := range writes {
|
|
if carries(c.Args, "s3cret") || carries(c.Answer, "s3cret-join") {
|
|
t.Fatalf("the bus was sent %s / %s", c.Args, c.Answer)
|
|
}
|
|
if !strings.Contains(string(c.Args), "settings") || !strings.Contains(string(c.Args), `"op"`) {
|
|
t.Fatalf("the record does not say what was asked and by whom: %s", c.Args)
|
|
}
|
|
}
|
|
}
|
|
|
|
// `calls` answers anyone who may call the seat, agents among them: a mesh-cli line's answer is never shown there,
|
|
// even from the memory of the controller that ran it; every other call's is (review of ADR 0272).
|
|
func TestCallsNeverShowsAMeshCLILinesAnswer(t *testing.T) {
|
|
l, a := NewCallLog(), newAnswers(t)
|
|
asked, _ := json.Marshal(CLIAsked{Line: []string{"token", "issue", "x"}, Account: "operator"})
|
|
l.serveCall(CLISeat, "control", asked, "_INBOX.node.control.abcdefghijklmnopqrstuv",
|
|
func(context.Context, json.RawMessage) (any, error) {
|
|
return CLIAnswer{Stdout: []byte("s3cret-join")}, nil
|
|
},
|
|
a.respond, nil)
|
|
_ = a.only()
|
|
recent, _ := l.Recent()
|
|
shown, found, err := l.Shown(recent[0].ID)
|
|
if err != nil || !found {
|
|
t.Fatalf("the line is not shown at all: %v %v", found, err)
|
|
}
|
|
if carries(shown.Answer, "s3cret-join") {
|
|
t.Fatalf("calls shows a mesh-cli line's answer: %s", shown.Answer)
|
|
}
|
|
b := newAnswers(t)
|
|
l.serveCall("mesh-controller", "status", nil, "_INBOX.x.abcdefghijklmnopqrstuv",
|
|
func(context.Context, json.RawMessage) (any, error) { return "all well", nil }, b.respond, nil)
|
|
_ = b.only()
|
|
recent, _ = l.Recent()
|
|
if shown, _, _ := l.Shown(recent[0].ID); !strings.Contains(string(shown.Answer), "all well") {
|
|
t.Fatalf("another call's answer is withheld: %s", shown.Answer)
|
|
}
|
|
}
|
|
|
|
// carries says whether a record holds a secret as text or as the base64 JSON writes bytes in: a line's output is
|
|
// bytes, so a search for its text alone finds nothing whatever the record keeps (review of ADR 0272).
|
|
func carries(body []byte, secret string) bool {
|
|
return strings.Contains(string(body), secret) ||
|
|
strings.Contains(string(body), base64.StdEncoding.EncodeToString([]byte(secret)))
|
|
}
|
|
|
|
// The two tests above hold what they claim: each fails when the protection it names is taken away.
|
|
func TestTheWithholdingTestsHoldSomething(t *testing.T) {
|
|
// The answer as a mesh-cli line's record would carry it, were it kept: the search finds it.
|
|
body, _ := json.Marshal(map[string]any{"result": CLIAnswer{Stdout: []byte("s3cret-join")}})
|
|
if !carries(body, "s3cret-join") {
|
|
t.Fatalf("a record carrying the answer is not found carrying it: %s", body)
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0259 §10: mesh-cli's standard input — a secret given at the terminal — is never in the calls record,
|
|
// in any form, whichever way the request reaches it.
|
|
func TestAMeshCLIRecordNeverKeepsItsStandardInput(t *testing.T) {
|
|
secret := "123456789:AAEhBOweik6ad9r_QxGivenAtTheTerminal"
|
|
raw, _ := json.Marshal(CLIAsked{Line: []string{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "-"},
|
|
Account: "operator", UID: 1000, Stdin: []byte(secret)})
|
|
for name, got := range map[string]json.RawMessage{"as answerCLI records it": kept(cliRecorded(raw)),
|
|
"as the record alone would keep it": kept(raw)} {
|
|
if carries(got, secret) || strings.Contains(string(got), base64.StdEncoding.EncodeToString([]byte(secret))[:20]) {
|
|
t.Fatalf("%s, the record keeps %s", name, got)
|
|
}
|
|
if !strings.Contains(string(got), "secret") || !strings.Contains(string(got), "given, not kept") &&
|
|
!strings.Contains(string(got), "stdin-given") {
|
|
t.Fatalf("%s, the record does not say a line was asked with standard input: %s", name, got)
|
|
}
|
|
}
|
|
// The record still reads as the request, so the asker can follow its call.
|
|
var asked CLIAsked
|
|
if err := json.Unmarshal(kept(cliRecorded(raw)), &asked); err != nil || asked.Account != "operator" || len(asked.Stdin) > 0 {
|
|
t.Fatalf("the recorded request reads as %+v (%v)", asked, err)
|
|
}
|
|
}
|