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
218 lines
9.9 KiB
Go
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)
|
|
}
|
|
}
|