At 17:44:53 the controller said 30 calls (tools, nodes, modules) were "kept in memory only: the bus is not taking calls' records". No write had failed (no "could not be kept" line): a burst of about 200 read calls in one second queued a begin and a finish each into a channel of 256 while the one writer put them one by one, and the channel overflowed. The queue now holds the latest state of each waiting call (a finish replaces its own waiting begin, as only the last state is kept anyway), in first-queued order, bounded at 8192 calls; the writer takes the whole queue at each wake. A burst now costs one write per call and drops nothing. Quick fix: a larger channel. Thorough fix: coalesce by call, so the writes a burst asks are halved and the bound is spent on calls, not states. Chose thorough; both are small. Research 072 piece: §2's "read calls said" row: the calls bucket is the controller's own state on the bus, kept from events (each call's change), never re-read.
213 lines
9.1 KiB
Go
213 lines
9.1 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// 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.
|
|
|
|
// 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.
|
|
|
|
// Lines running at once are bounded: one over the bound is answered busy at once, and nothing 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)
|
|
l.woken = make(chan struct{}, 1) // queued for the keeper, and nothing takes them: read below
|
|
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()
|
|
for _, c := range l.queued.take() {
|
|
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)
|
|
}
|
|
}
|