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.
95 lines
3.9 KiB
Go
95 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// **Every verb that repairs by hand takes a required why** (novox/hq to-be 45 §7): refused before
|
|
// anything is done, through the seat, through `command`, and at a shell where nothing automated runs
|
|
// the verb.
|
|
func TestARepairByHandWithoutAReasonIsRefused(t *testing.T) {
|
|
for _, c := range []struct {
|
|
verb string
|
|
args map[string]any
|
|
}{
|
|
{"push", map[string]any{"node": "anchor"}},
|
|
{"push", map[string]any{}},
|
|
{"plans", map[string]any{"close": "plan-1"}},
|
|
{"plans", map[string]any{"stop": "plan-1"}},
|
|
{"hand-act", map[string]any{"what": "restarted the proxy", "cause": "proxy-stuck"}},
|
|
{"command", map[string]any{"command": "push anchor"}},
|
|
{"command", map[string]any{"command": "plans close plan-1"}},
|
|
{"command", map[string]any{"command": "broker consumer-reset EVENTS controller"}},
|
|
{"command", map[string]any{"command": "hand-act record restarted --cause x"}},
|
|
} {
|
|
argv, err := argvFor(c.verb, c.args)
|
|
if c.verb == "plans" && err == nil {
|
|
// The seat composes the command line; the command refuses it, before opening anything.
|
|
err = plansCommand(context.Background(), argv[1:])
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "why") {
|
|
t.Errorf("%s %v was not refused for want of why: %v %v", c.verb, c.args, argv, err)
|
|
}
|
|
}
|
|
for _, args := range [][]string{{"EVENTS", "controller"}} {
|
|
if err := consumerReset(context.Background(), args); err == nil || !strings.Contains(err.Error(), "--why") {
|
|
t.Errorf("consumer-reset without why: %v", err)
|
|
}
|
|
}
|
|
if err := handActCommand(context.Background(), []string{"record", "restarted the proxy", "--cause", "x"}); err == nil ||
|
|
!strings.Contains(err.Error(), "--why") {
|
|
t.Errorf("hand-act record without why: %v", err)
|
|
}
|
|
if err := handActCommand(context.Background(), []string{"record", "restarted the proxy", "--why", "it hung"}); err == nil ||
|
|
!strings.Contains(err.Error(), "--cause") {
|
|
t.Errorf("hand-act record without a cause: %v", err)
|
|
}
|
|
}
|
|
|
|
// With a reason, the seat passes it to the command, and a verb that only reads is not held to one.
|
|
func TestARepairByHandCarriesItsReason(t *testing.T) {
|
|
for _, c := range []struct {
|
|
verb string
|
|
args map[string]any
|
|
want string
|
|
}{
|
|
{"push", map[string]any{"node": "anchor", "why": "stuck", "cause": "sent-not-reported"},
|
|
"push anchor --wait 0 --why stuck --cause sent-not-reported"},
|
|
{"plans", map[string]any{"close": "plan-1", "why": "the report will not come"},
|
|
"plans close plan-1 --why the report will not come"},
|
|
{"plans", map[string]any{"retry": "plan-1"}, "plans retry plan-1"},
|
|
{"hand-act", map[string]any{"what": "restarted", "why": "hung", "cause": "proxy", "condition": "machine.a.silent"},
|
|
"hand-act record restarted --why hung --cause proxy --condition machine.a.silent"},
|
|
{"command", map[string]any{"command": "push anchor --why stuck"}, "push anchor --why stuck"},
|
|
{"command", map[string]any{"command": "plans plan-1"}, "plans plan-1"},
|
|
} {
|
|
argv, err := argvFor(c.verb, c.args)
|
|
if err != nil || strings.Join(argv, " ") != c.want {
|
|
t.Errorf("%s %v: %v %v, want %q", c.verb, c.args, argv, err, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The summary of durations says, per kind and subject, what a bound would be set from.
|
|
func TestDurationsAreSummarisedPerSubject(t *testing.T) {
|
|
var ds []inventory.Duration
|
|
for i := 1; i <= 10; i++ {
|
|
ds = append(ds, inventory.Duration{Kind: inventory.DurationApply, Subject: "anchor",
|
|
Took: time.Duration(i) * time.Second})
|
|
}
|
|
ds = append(ds, inventory.Duration{Kind: inventory.DurationHeartbeatGap, Subject: "anchor", Took: time.Minute})
|
|
got := summarise(ds)
|
|
if len(got) != 2 || got[0].Kind != inventory.DurationApply || got[0].Count != 10 ||
|
|
got[0].Max != "10s" || got[0].Median != "5s" || got[0].P90 != "9s" {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
if !strings.Contains(got[1].Suggests, "3m0s") {
|
|
t.Fatalf("a minute between words suggests %q", got[1].Suggests)
|
|
}
|
|
}
|