From 91cd016f65ff48fe11ae2ef5424c59e94e8dc961 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:35:35 +0100 Subject: [PATCH] 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 --- .../mesh-knowledge/cmd/mesh-knowledge/overview.go | 5 +++-- .../cmd/mesh-knowledge/overview_test.go | 12 ++++++------ modules/messenger/cmd/messenger/asks.go | 4 ++-- modules/messenger/cmd/messenger/asks_events_test.go | 7 +++++-- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/overview.go b/modules/mesh-knowledge/cmd/mesh-knowledge/overview.go index c1ffea5e..5b53fbee 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/overview.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/overview.go @@ -398,9 +398,10 @@ func (o *Overview) Heard(source, event string, body json.RawMessage) { case SourceAsks: switch name { case "offered": - o.asks[f.Key] = askRow{Key: f.Key, Asker: f.Asker, Headline: f.Headline, Offered: firstStrings(f.Offered, f.Channels)} + k := firstNonEmpty(f.Key, f.Asker+"."+f.ID) + o.asks[k] = askRow{Key: k, Asker: f.Asker, Headline: f.Headline, Offered: firstStrings(f.Offered, f.Channels)} case "closed": - delete(o.asks, f.Key) + delete(o.asks, firstNonEmpty(f.Key, f.Asker+"."+f.ID)) } } o.rebuilt = o.now() diff --git a/modules/mesh-knowledge/cmd/mesh-knowledge/overview_test.go b/modules/mesh-knowledge/cmd/mesh-knowledge/overview_test.go index b595505c..379cb4bf 100644 --- a/modules/mesh-knowledge/cmd/mesh-knowledge/overview_test.go +++ b/modules/mesh-knowledge/cmd/mesh-knowledge/overview_test.go @@ -24,7 +24,7 @@ func newOverviewWorld(t *testing.T) *overviewWorld { 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":"ask-1","asker":"mesh-knowledge","headline":"Approve ADR 0301","state":"open","offered_on":["telegram"]},{"key":"ask-0","asker":"x","headline":"old","state":"answered"}]}`, + 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] { @@ -66,7 +66,7 @@ func TestThePageModelHasThreeBlocksKeptByEventsAndAsksNoVerbAgain(t *testing.T) 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 != "ask-1" || len(m.Operator.Groups) != 1 { + 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{} @@ -76,7 +76,7 @@ func TestThePageModelHasThreeBlocksKeptByEventsAndAsksNoVerbAgain(t *testing.T) // 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(`{"key":"ask-2","asker":"mesh-delivery","headline":"Approve the group","channels":["telegram"]}`)) + 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() @@ -84,9 +84,9 @@ func TestThePageModelHasThreeBlocksKeptByEventsAndAsksNoVerbAgain(t *testing.T) 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(`{"key":"ask-1","how":"answered"}`)) + 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 != "ask-2" { + 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 { @@ -160,7 +160,7 @@ func TestASourceThatDoesNotAnswerIsSaidAndAskedAgainOnABackoff(t *testing.T) { 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(`{"key":"ask-9","headline":"x"}`)) + 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) } diff --git a/modules/messenger/cmd/messenger/asks.go b/modules/messenger/cmd/messenger/asks.go index 70116c23..2e9e6180 100644 --- a/modules/messenger/cmd/messenger/asks.go +++ b/modules/messenger/cmd/messenger/asks.go @@ -1445,7 +1445,7 @@ func (h *Holder) sayOffered(key string, rec AskRecord) { on = append(on, k) } sort.Strings(on) - if err := h.Emit("offered", map[string]any{"key": key, "asker": rec.Asker, "headline": rec.Ask.Headline, "offered_on": on, "at": h.Now()}); err != nil { + if err := h.Emit("offered", map[string]any{"id": rec.Ask.ID, "asker": rec.Asker, "headline": rec.Ask.Headline, "channels": on}); err != nil { h.Logf("[messenger] that the ask %s was offered could not be said: %v", key, err) } } @@ -1466,7 +1466,7 @@ func (h *Holder) sayClosed(key string, rec AskRecord, outcome asks.Outcome, word h.mu.Lock() delete(h.offeredSaid, key) h.mu.Unlock() - if err := h.Emit("closed", map[string]any{"key": key, "asker": rec.Asker, "headline": rec.Ask.Headline, "how": how, "at": h.Now()}); err != nil { + if err := h.Emit("closed", map[string]any{"id": rec.Ask.ID, "asker": rec.Asker, "how": how}); err != nil { h.Logf("[messenger] that the ask %s ended could not be said: %v", key, err) } } diff --git a/modules/messenger/cmd/messenger/asks_events_test.go b/modules/messenger/cmd/messenger/asks_events_test.go index c3f9ca37..1f0f96a7 100644 --- a/modules/messenger/cmd/messenger/asks_events_test.go +++ b/modules/messenger/cmd/messenger/asks_events_test.go @@ -30,7 +30,7 @@ func TestTheRouterSaysWhenAnAskIsOfferedAndWhenItEnds(t *testing.T) { ends := func() (out []string) { for _, s := range got { if s.event == "closed" { - out = append(out, s.body["key"].(string)+"="+s.body["how"].(string)) + out = append(out, s.body["asker"].(string)+"."+s.body["id"].(string)+"="+s.body["how"].(string)) } } return @@ -38,7 +38,7 @@ func TestTheRouterSaysWhenAnAskIsOfferedAndWhenItEnds(t *testing.T) { // Answered. submit(t, h, "mesh-delivery", heldAsk(c, "d-1")) settle(h, c) - if len(got) != 1 || got[0].event != "offered" || got[0].body["key"] != "mesh-delivery.d-1" || got[0].body["asker"] != "mesh-delivery" { + if len(got) != 1 || got[0].event != "offered" || got[0].body["id"] != "d-1" || got[0].body["asker"] != "mesh-delivery" { t.Fatalf("%+v", got) } release := ticketFor(tg.sends[0], "Release") @@ -82,6 +82,9 @@ func TestTheRouterSaysWhenAnAskIsOfferedAndWhenItEnds(t *testing.T) { offered++ } for k := range s.body { + if k != "id" && k != "asker" && k != "headline" && k != "channels" && k != "how" { + t.Errorf("%s carries %s, which the seat's contract does not have", s.event, k) + } if k == "options" || k == "tickets" || k == "token" || k == "proofs" || k == "warrant" { t.Errorf("%s carries %s", s.event, k) }