CAT-N6: the keeper's health tool nats_keeper_health, which the process keeper's tool check asks

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
This commit is contained in:
2026-11-04 12:34:59 +01:00
parent 68dd69f4ef
commit 4b2d5aeaab
4 changed files with 122 additions and 1 deletions
+30
View File
@@ -4,8 +4,11 @@ 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
@@ -63,3 +66,30 @@ func line(module, digest string, revision uint64, entries []catalogue.BusStateEn
}
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)
}
})
}
}