Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/overview_test.go
T
jschoubben 91cd016f65 CAT-MS1, CAT-MS2: offered and closed carry the operator-channel definition's fields (id, asker, headline, channels; id, asker, how); the page reads them
The test of the five ends holds every key of a body to the seat definition's holder events, so the router and the contract cannot drift.

Issue 1968.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:35:35 +01:00

218 lines
9.9 KiB
Go

package main
import (
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"time"
)
// overviewWorld is a page model over the sources played here: what each verb answers, and what fails.
type overviewWorld struct {
o *Overview
now time.Time
answers map[string]string
fail map[string]bool
entries []*Entry
}
func newOverviewWorld(t *testing.T) *overviewWorld {
t.Helper()
w := &overviewWorld{now: time.Date(2026, 11, 4, 12, 0, 0, 0, time.UTC), fail: map[string]bool{}, answers: map[string]string{
TrackerMilestonesVerb: `{"milestones":[{"design":47,"id":"1","outcome":"A merge is never dropped","status":"open","issues":[973,525]},{"design":47,"id":"2","outcome":"Done thing","status":"done","issues":[1]}]}`,
TrackerListVerb: `[{"number":973,"title":"The window","status":"fixing"},{"number":525,"title":"The queue","status":"open"}]`,
DeliveryGroupsVerb: `[{"id":"fix/a","state":"held","why":"a member is held","members":["x@1"]}]`,
AsksVerb: `{"asks":[{"key":"mesh-knowledge.1","asker":"mesh-knowledge","headline":"Approve ADR 0301","state":"open","offered_on":["telegram"]},{"key":"ask-0","asker":"x","headline":"old","state":"answered"}]}`,
}}
w.o = NewOverview(func(key string, body any) (json.RawMessage, error) {
if w.fail[key] {
return nil, errors.New("no answer in time")
}
return json.RawMessage(w.answers[key]), nil
}, func() ([]*Entry, error) { return w.entries, nil }, func() time.Time { return w.now })
return w
}
func recordEntry(n int, status, date string, accepted time.Time, mut func(*Record)) *Entry {
r := &Record{Kind: KindDecision, Number: n, Title: "Record " + itoa(n), Status: status, Date: date, AcceptedAt: accepted}
if mut != nil {
mut(r)
}
return &Entry{Current: r, CurrentVersion: 1, Versions: 1}
}
// The page's model (ADR 0616 §1-3, AC P1): three blocks built from the ledger, the tracker's milestones and issues, the delivery
// groups and the operator channel's asks; events change the blocks and ask no verb; a record accepted eight days before the
// answer is not in the seven-day block, with no event between.
func TestThePageModelHasThreeBlocksKeptByEventsAndAsksNoVerbAgain(t *testing.T) {
w := newOverviewWorld(t)
w.entries = []*Entry{
recordEntry(301, StatusAccepted, "2026-11-02", w.now.Add(-2*24*time.Hour), nil),
recordEntry(302, StatusAccepted, "2026-10-20", w.now.Add(-6*24*time.Hour-23*time.Hour), nil),
recordEntry(303, StatusProposed, "2026-11-04", time.Time{}, func(r *Record) {
r.NeedsOperator = true
r.Ask = &OperatorAsk{ID: "a", Said: "waiting"}
}),
recordEntry(304, StatusProposed, "2026-11-04", time.Time{}, nil),
}
w.o.Start()
m, err := w.o.Model()
if err != nil {
t.Fatal(err)
}
if len(m.Designs) != 1 || m.Designs[0].ID != "1" || len(m.Designs[0].Issues) != 2 || m.Designs[0].Issues[0].Title != "The window" {
t.Fatalf("the design block: %+v", m.Designs)
}
if len(m.Accepted) != 2 || len(m.Waiting) != 2 || len(m.Operator.Records) != 1 || m.Operator.Records[0].Record != "ADR 0303" ||
len(m.Operator.Asks) != 1 || m.Operator.Asks[0].Key != "mesh-knowledge.1" || len(m.Operator.Groups) != 1 {
t.Fatalf("%+v", m)
}
asked := map[string]int{}
for _, v := range []string{TrackerMilestonesVerb, TrackerListVerb, DeliveryGroupsVerb, AsksVerb} {
asked[v] = w.o.Calls(v)
}
// Events change the blocks.
w.o.Heard(SourceDelivery, "mesh-delivery-gate.group", json.RawMessage(`{"group":"fix/b","state":"checking","members":["y@2"]}`))
w.o.Heard(SourceDelivery, "mesh-delivery-gate.transition", json.RawMessage(`{"group":"fix/a","to":"approved"}`))
w.o.Heard(SourceAsks, "messenger.offered", json.RawMessage(`{"id":"2","asker":"mesh-delivery","headline":"Approve the group","channels":["telegram"]}`))
w.o.Heard(SourceTracker, "mesh-issues.opened", json.RawMessage(`{"number":1999,"title":"New","status":"open"}`))
w.o.Heard(SourceTracker, "mesh-issues.milestone-moved", json.RawMessage(`{"design":47,"id":"1","to":"done","issues":[973,525,1999]}`))
m, _ = w.o.Model()
if len(m.Operator.Groups) != 2 || m.Operator.Groups[0].State != "approved" || len(m.Operator.Asks) != 2 || len(m.Designs) != 0 {
t.Fatalf("after the events: %+v", m)
}
w.o.Heard(SourceDelivery, "mesh-delivery-gate.merged", json.RawMessage(`{"group":"fix/a"}`))
w.o.Heard(SourceAsks, "messenger.closed", json.RawMessage(`{"id":"1","asker":"mesh-knowledge","how":"answered"}`))
m, _ = w.o.Model()
if len(m.Operator.Groups) != 1 || m.Operator.Groups[0].ID != "fix/b" || len(m.Operator.Asks) != 1 || m.Operator.Asks[0].Key != "mesh-delivery.2" {
t.Fatalf("after merged and closed: %+v", m.Operator)
}
for v, n := range asked {
if w.o.Calls(v) != n {
t.Errorf("%s was asked again by an event", v)
}
}
// The seven days are counted when the answer is given: no event between, the record falls out of the block.
w.now = w.now.Add(2 * 24 * time.Hour)
m, _ = w.o.Model()
if len(m.Accepted) != 1 || m.Accepted[0].Record != "ADR 0301" {
t.Fatalf("the seven-day block: %+v", m.Accepted)
}
}
// A source that does not answer says so, never as an empty list (ADR 0616 §5, §6; AC P3): from its verb, on a bounded backoff
// asked again alone, its first answer clearing the mark; from its subscription, asking nothing while it is silent and the verb once on its
// next event; the model as a tool equals the page's model; the re-read tool asks one source and records the hand act.
func TestASourceThatDoesNotAnswerIsSaidAndAskedAgainOnABackoff(t *testing.T) {
w := newOverviewWorld(t)
w.fail[DeliveryGroupsVerb] = true
w.o.Start()
m, _ := w.o.Model()
var delivery SourceState
for _, s := range m.Sources {
if s.Name == SourceDelivery {
delivery = s
}
}
if delivery.Answered || delivery.Mark == nil || delivery.Mark.Cause != MarkVerb || len(m.Operator.Groups) != 0 || !strings.Contains(delivery.Mark.Why, "no answer in time") {
t.Fatalf("%+v", delivery)
}
if len(m.Designs) != 1 {
t.Fatalf("another source's block was lost: %+v", m.Designs)
}
// Asked again on the backoff alone: 10 s, then doubling, at most 5 minutes; no other source is asked.
calls := w.o.Calls(DeliveryGroupsVerb)
if got := w.o.Due(); len(got) != 0 {
t.Fatalf("asked before the backoff ran out: %v", got)
}
var waits []time.Duration
for i := 0; i < 8; i++ {
before := w.now
w.now = w.o.state[SourceDelivery].Mark.Next
waits = append(waits, w.now.Sub(before))
if got := w.o.Due(); len(got) != 1 || got[0] != SourceDelivery {
t.Fatalf("%v", got)
}
}
if waits[0] != BackoffFrom || waits[1] != 2*BackoffFrom || waits[7] != BackoffMost || w.o.Calls(DeliveryGroupsVerb) != calls+8 || w.o.Calls(AsksVerb) != 1 {
t.Fatalf("waits %v, delivery asked %d, asks asked %d", waits, w.o.Calls(DeliveryGroupsVerb), w.o.Calls(AsksVerb))
}
// Its first answer rebuilds the block and clears the mark.
w.fail[DeliveryGroupsVerb] = false
w.now = w.o.state[SourceDelivery].Mark.Next
w.o.Due()
m, _ = w.o.Model()
if len(w.o.Unanswered()) != 0 || len(m.Operator.Groups) != 1 {
t.Fatalf("%+v", m)
}
// A subscription that goes silent marks the source from the subscription and asks nothing while it is silent.
asked := w.o.Calls(AsksVerb)
w.now = w.now.Add(HeartbeatWithin + time.Second)
if got := w.o.Silent(); len(got) != 3 {
t.Fatalf("%v", got)
}
if got := w.o.Due(); len(got) != 0 || w.o.Calls(AsksVerb) != asked {
t.Fatalf("a silent subscription was asked: %v", got)
}
if u := w.o.Unanswered(); len(u) != 3 || u[0].Mark.Cause != MarkSubscription {
t.Fatalf("%+v", u)
}
// Its next event (or heartbeat) makes the module ask the verb once, and that answer clears the mark.
w.o.Heard(SourceAsks, "messenger.offered", json.RawMessage(`{"id":"9","asker":"x","headline":"x"}`))
if w.o.Calls(AsksVerb) != asked+1 || w.o.state[SourceAsks].Mark != nil {
t.Fatalf("asked %d, mark %+v", w.o.Calls(AsksVerb), w.o.state[SourceAsks].Mark)
}
// An unanswered verb then turns the mark into one from the verb.
w.fail[DeliveryGroupsVerb] = true
w.o.Heard(SourceDelivery, "", nil)
if mk := w.o.state[SourceDelivery].Mark; mk == nil || mk.Cause != MarkVerb {
t.Fatalf("%+v", mk)
}
}
func TestThePagesModelIsAToolAndItsRereadIsAHandAct(t *testing.T) {
w := newOverviewWorld(t)
w.entries = []*Entry{recordEntry(301, StatusAccepted, "2026-11-02", w.now.Add(-24*time.Hour), nil)}
w.o.Start()
var acts []string
k := &KB{Overview: w.o, Seats: func(key string, body any) (json.RawMessage, error) {
acts = append(acts, key)
return json.RawMessage(`{}`), nil
}, Now: func() time.Time { return w.now }}
var tools []toolRun
for _, tool := range moduleTools(k, callerOf) {
tools = append(tools, toolRun{tool.Name, tool.Run})
}
got, err := callTool(t, tools, OverviewTool, map[string]any{})
want, _ := w.o.Model()
raw, _ := json.Marshal(want)
var wantMap map[string]any
_ = json.Unmarshal(raw, &wantMap)
if err != nil || !reflect.DeepEqual(got, wantMap) {
t.Fatalf("the tool's answer is not the page's model: %v", err)
}
// The re-read asks the one source and no other, and records itself.
before := map[string]int{}
for _, v := range []string{TrackerListVerb, DeliveryGroupsVerb, AsksVerb} {
before[v] = w.o.Calls(v)
}
w.fail[AsksVerb] = true
res, err := callTool(t, tools, RereadTool, map[string]any{"source": SourceAsks, "why": "the page showed it stale"})
if err != nil || res["answered"] != false || w.o.Calls(AsksVerb) != before[AsksVerb]+1 || w.o.Calls(DeliveryGroupsVerb) != before[DeliveryGroupsVerb] || w.o.Calls(TrackerListVerb) != before[TrackerListVerb] {
t.Fatalf("%v %v", err, res)
}
if len(acts) != 1 || acts[0] != "seat:mesh-hand-acts.hand-act" {
t.Fatalf("the hand act: %v", acts)
}
if _, err := callTool(t, tools, RereadTool, map[string]any{"source": "nobody"}); err == nil {
t.Fatal("a source the page lacks was re-read")
}
// The health carries the page: degraded while a source is unanswered, healthy.
h := k.pageHealth(HealthAnswer{State: "healthy", Healthy: true, Why: "ok"})
if !h.Healthy || !strings.Contains(h.Why, "overview page is degraded") || !strings.Contains(h.Why, SourceAsks) {
t.Fatalf("%+v", h)
}
}