A controller restart lost every call's outcome, `status` composed the mesh while its caller waited (18.6s live on 2026-10-06, past the 10s window), a repair by hand left no trace, and the core's bounds had nothing measured to be set from. - calls: kept in the controller's bucket mesh-controller_calls (last 1000 or 14 days, answers bounded to 64 KiB), read by id across a restart; a controller starting marks a stopped one's running calls abandoned; each call names its caller from the inbox its answer goes to. - status: the serving controller composes it at start, after news from a machine, a build or an acting verb, and every minute; the verb answers the last composition at once with when and how long it took. Composing resolves each machine once instead of twice. - hand-act log in mesh-controller_hand-acts: push (required through the seat), plans stop/close, broker consumer-reset and the new hand-act record take --why/--cause/--condition; `hand-acts` lists them and repeated causes; status counts the week's. - durations (migration 0066): apply (send to first report), heartbeat gap, plan tier and build, recorded as heard; `durations` summarises them. - the controller's seat row takes this binary's definition of its own verbs, so the console no longer judges calls against an older build's schema. - the controller is granted its two buckets' subjects.
82 lines
3.0 KiB
Go
82 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// consoleWaits is how long the console waits for an answer (mesh-tools node-tools/internal/bus
|
|
// RequestTimeout) — the shortest wait of a caller the mesh ships.
|
|
const consoleWaits = 30 * time.Second
|
|
|
|
// **A call's one answer is never later than its caller or the bus allow** (novox/hq issue 265): a
|
|
// holder answers within AnswerWithin, which must be inside both the console's wait and the window the
|
|
// bus gives an answer. Before, the console waited 30s, the bus 60s, and a push ran as long as it ran.
|
|
func TestAVerbAnswersInsideEveryWaitOnIt(t *testing.T) {
|
|
if link.AnswerWithin >= consoleWaits/2 {
|
|
t.Errorf("a call answers within %s: not well inside the console's %s", link.AnswerWithin, consoleWaits)
|
|
}
|
|
if link.AnswerWithin >= broker.ResponseTTL {
|
|
t.Errorf("a call answers within %s, after the bus stops permitting an answer at %s", link.AnswerWithin, broker.ResponseTTL)
|
|
}
|
|
}
|
|
|
|
// A push — named or through command — answers before it runs: it sends the machine holding the bus
|
|
// first, and the broker reloading its user list forgets the answer it was about to permit.
|
|
func TestAPushAnswersBeforeItSends(t *testing.T) {
|
|
for _, c := range []struct {
|
|
verb string
|
|
args map[string]any
|
|
want bool
|
|
}{
|
|
{"push", map[string]any{"node": "anchor", "why": "w"}, true},
|
|
{"push", map[string]any{"why": "w"}, true},
|
|
{"command", map[string]any{"command": "push anchor --why w"}, true},
|
|
{"command", map[string]any{"command": "push --behind --why=w"}, true},
|
|
{"command", map[string]any{"command": "builds"}, false},
|
|
{"status", map[string]any{}, false},
|
|
{"assign", map[string]any{"node": "anchor", "module": "m"}, false},
|
|
} {
|
|
argv, err := argvFor(c.verb, c.args)
|
|
if err != nil {
|
|
t.Fatalf("%s %v: %v", c.verb, c.args, err)
|
|
}
|
|
if got := answersFirst(argv); got != c.want {
|
|
t.Errorf("%s %v answers first: %v, want %v", c.verb, c.args, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// `calls` is served, takes a call's id and nothing else, and says plainly when it holds no such call.
|
|
func TestCallsIsServedAndSaysWhatItKeeps(t *testing.T) {
|
|
handlers, behind, err := seatToolHandlers()
|
|
if err != nil || len(behind) != 0 {
|
|
t.Fatalf("%v %v", behind, err)
|
|
}
|
|
calls, ok := handlers["calls"]
|
|
if !ok {
|
|
t.Fatal("calls is not served")
|
|
}
|
|
if _, err := calls(context.Background(), json.RawMessage(`{"node":"anchor"}`)); err == nil ||
|
|
!strings.Contains(err.Error(), `"node"`) {
|
|
t.Errorf("calls took an argument it does not declare: %v", err)
|
|
}
|
|
if _, err := calls(context.Background(), json.RawMessage(`{"call":"call-0-0"}`)); err == nil ||
|
|
!strings.Contains(err.Error(), "not across a restart") {
|
|
t.Errorf("an unknown call was not said plainly: %v", err)
|
|
}
|
|
got, err := calls(context.Background(), json.RawMessage(`{}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, listed := got.(map[string]any)["calls"]; !listed {
|
|
t.Errorf("calls answered %v", got)
|
|
}
|
|
}
|