package main import ( "encoding/json" "errors" "reflect" "strings" "testing" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) // 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) } // The runtime's word on the module's consumer (stdio.OnHeartbeat): a missed heartbeat marks every subscribed source // at once, before the bound passes, with the runtime's why; the next beat asks each verb once and clears the marks. w.fail[DeliveryGroupsVerb] = false w.now = w.o.state[SourceDelivery].Mark.Next w.o.Due() asked = w.o.Calls(AsksVerb) w.o.Beat(stdio.Heartbeat{At: w.now, Missed: true, Why: "the bus's heartbeat stopped arriving"}) if u := w.o.Unanswered(); len(u) != 3 || u[0].Mark.Cause != MarkSubscription || !strings.Contains(u[0].Mark.Why, "stopped arriving") { t.Fatalf("a missed heartbeat marked %+v", u) } w.o.Beat(stdio.Heartbeat{At: w.now}) if len(w.o.Unanswered()) != 0 || w.o.Calls(AsksVerb) != asked+1 { t.Fatalf("a beat left %+v, asks asked %d", w.o.Unanswered(), w.o.Calls(AsksVerb)-asked) } // A beat keeps a quiet source from being marked silent: it is heard, not silent. w.now = w.now.Add(HeartbeatWithin - time.Second) w.o.Beat(stdio.Heartbeat{At: w.now}) w.now = w.now.Add(2 * time.Second) if got := w.o.Silent(); len(got) != 0 { t.Fatalf("a source heard by its beat was marked silent: %v", got) } } 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) } }