It reads the keeper's status from its own state asked, what the keeper holds, and how many accepted verdicts wait on its consumer <node>_nats_keeper on EVENTS from the monitoring endpoint, and answers HealthOf's judgement: unhealthy, naming why, past the three-minute bound; the entries refused, waiting and past the raise mark named beside. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
96 lines
4.0 KiB
Go
96 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
|
|
"nats-tools/internal/keeper"
|
|
)
|
|
|
|
// The keeper's inputs (design 25 § 12.2; CAT-N2): a manifest-accepted read with the indexer's revision when it carries
|
|
// one (0 when not yet), its entries whole; the first view of indexer_bus_state read bare or as the tool runner wraps it,
|
|
// an error answer an error; anything else refused, naming why.
|
|
func TestTheKeepersInputsAreRead(t *testing.T) {
|
|
for _, row := range []struct {
|
|
name, body, want, refused string
|
|
view bool
|
|
}{
|
|
{name: "accepted with its revision", body: `{"v":1,"module":"m","digest":"sha256:a","judge":"j","revision":7,"bus-state":[{"name":"drafts","storage":"key-value","template":"state"}]}`,
|
|
want: "m sha256:a 7 [drafts/key-value/state]"},
|
|
{name: "accepted before the event carries a revision", body: `{"v":1,"module":"m","digest":"sha256:b","judge":"j","bus-state":[]}`,
|
|
want: "m sha256:b 0 []"},
|
|
{name: "accepted naming no module", body: `{"v":1,"digest":"x"}`, refused: "names no module"},
|
|
{name: "first view as the tool runner answers it", view: true,
|
|
body: `{"result":{"revision":12,"modules":[{"module":"m","digest":"d","revision":11,"bus-state":[{"name":"log","storage":"stream","template":"log"}]}]}}`,
|
|
want: "12: m d 11 [log/stream/log]"},
|
|
{name: "first view bare", view: true, body: `{"revision":3,"modules":[]}`, want: "3:"},
|
|
{name: "first view answering an error", view: true, body: `{"error":"not granted"}`, refused: "not granted"},
|
|
{name: "first view that is none", view: true, body: `[1,2]`, refused: "is not a first view"},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
var got string
|
|
var err error
|
|
if row.view {
|
|
v, e := FirstViewOf([]byte(row.body))
|
|
err = e
|
|
got = fmt.Sprintf("%d:", v.Revision)
|
|
for _, m := range v.Modules {
|
|
got += " " + line(m.Module, m.Digest, m.Revision, m.BusState)
|
|
}
|
|
} else {
|
|
a, e := AcceptedOf([]byte(row.body))
|
|
err = e
|
|
got = line(a.Module, a.Digest, a.Revision, a.BusState)
|
|
}
|
|
if row.refused != "" {
|
|
if err == nil || !strings.Contains(err.Error(), row.refused) {
|
|
t.Fatalf("read %s (%v): refused, naming %q", got, err, row.refused)
|
|
}
|
|
return
|
|
}
|
|
if err != nil || got != row.want {
|
|
t.Errorf("read %q (%v), want %q", got, err, row.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func line(module, digest string, revision uint64, entries []catalogue.BusStateEntry) string {
|
|
es := []string{}
|
|
for _, e := range entries {
|
|
es = append(es, e.Name+"/"+e.Storage+"/"+e.Template)
|
|
}
|
|
return fmt.Sprintf("%s %s %d [%s]", module, digest, revision, strings.Join(es, " "))
|
|
}
|
|
|
|
// The keeper's health tool (CAT-N6): healthy naming its pass; unhealthy, naming why, when its pass is old or verdicts
|
|
// wait on its consumer `<node>_nats_keeper` past the bound.
|
|
func TestTheKeepersHealthToolReadsItsStatusAndItsConsumer(t *testing.T) {
|
|
at := time.Date(2026, 11, 4, 12, 0, 0, 0, time.UTC)
|
|
jsz := `{"account_details":[{"stream_detail":[{"name":"EVENTS","consumer_detail":[{"name":"anchor_nats_keeper","num_pending":3},{"name":"anchor_nats","num_pending":9}]}]}]}`
|
|
status := keeper.Status{Started: at.Add(-time.Hour), Pass: at.Add(-time.Minute), Heard: at.Add(-time.Minute),
|
|
FirstView: keeper.FirstViewStatus{Owner: keeper.FirstViewOwner, Verb: keeper.FirstViewVerb, Answered: at.Add(-time.Hour)}}
|
|
none := func() (map[string]keeper.Asked, error) { return nil, nil }
|
|
for _, row := range []struct {
|
|
name string
|
|
now time.Time
|
|
state string
|
|
why string
|
|
}{
|
|
{"its pass is fresh", at, "healthy", "last pass 2026-11-04T11:59:00Z"},
|
|
{"its pass and its verdicts are old", at.Add(5 * time.Minute), "unhealthy", "3 accepted verdict(s) wait on its consumer"},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
a := keeperHealth(func() (keeper.Status, error) { return status, nil }, none,
|
|
func() ([]byte, error) { return []byte(jsz), nil }, func() time.Time { return row.now })
|
|
if a.State != row.state || !strings.Contains(a.Why, row.why) {
|
|
t.Errorf("answered %+v: %s, naming %q", a, row.state, row.why)
|
|
}
|
|
})
|
|
}
|
|
}
|