From c1d299336b8893333d092ba4882487aa3e5912da Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 11:03:35 +0200 Subject: [PATCH] messenger: send an ask's message alone even when only one answer can be given there The lab's proof showed an ask whose approval was held back (an agent could become root where the channel runs) folded into a burst's digest with a condition, and its one remaining answer, Decline, lost with it. A message carrying an ask's answer now always goes alone. --- .../messenger/cmd/messenger/answers_test.go | 35 +++++++++++++++++++ modules/messenger/cmd/messenger/outbox.go | 6 ++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/modules/messenger/cmd/messenger/answers_test.go b/modules/messenger/cmd/messenger/answers_test.go index 8d90d4ac..ce194024 100644 --- a/modules/messenger/cmd/messenger/answers_test.go +++ b/modules/messenger/cmd/messenger/answers_test.go @@ -839,3 +839,38 @@ func codeOf(t *testing.T, m Message) string { t.Fatalf("no code in %q", m.Body) return "" } + +// An ask's message goes alone even when only one of its answers can be given on a channel: folded into a burst's +// digest, its one button was lost (found by the lab's proof, mesh-lab asks/, with approval held by an open +// agent-root condition and that condition's message in the same burst). +func TestAnAsksMessageWithOneAnswerIsNotFoldedIntoADigest(t *testing.T) { + h, tg, _, c, store, _ := newRouter(t) + link(h, store, c, "42", c.now().Add(-2*time.Hour)) + settle(h, c) + // Something was just said on the channel, so what follows waits for the burst. + submit(t, h, "mesh-delivery", heldAsk(c, "d-0")) + settle(h, c) + h.mu.Lock() + h.open["machine.anchor.agent-root"] = &Record{Key: "machine.anchor.agent-root", Kind: KindAgentRoot, Severity: Urgent, + Origin: "condition", Raised: c.now()} + h.mu.Unlock() + k := cond("machine.anchor.disk", Urgent, "the disk on anchor is full") + k.Raised = c.now() + h.Condition(EventRaised, k) + before := len(tg.sends) + submit(t, h, "mesh-delivery", heldAsk(c, "d-1")) + settle(h, c) + var asked *Message + for i := range tg.sends[before:] { + m := tg.sends[before+i] + if ticketFor(m, "Silence for a week") != "" { + asked = &m + } + } + if asked == nil { + t.Fatalf("the ask's one answer was not offered on Telegram: %+v", tg.sends[before:]) + } + if ticketFor(*asked, "Release") != "" { + t.Errorf("an approval was offered while an agent can become root there: %q", labelsOf(*asked)) + } +} diff --git a/modules/messenger/cmd/messenger/outbox.go b/modules/messenger/cmd/messenger/outbox.go index 4ae846e1..d3c473a3 100644 --- a/modules/messenger/cmd/messenger/outbox.go +++ b/modules/messenger/cmd/messenger/outbox.go @@ -287,10 +287,12 @@ func (h *Holder) flushChannel(ch Channel, now time.Time, force bool) { return } if len(entries) > 1 { - // A message that asks the operator something goes alone, so its answers come with it. + // A message that asks the operator something goes alone, so its answers come with it: one with several + // answers, and one carrying an ask's answer even when only one can be given here (the lab's proof found + // an ask whose approval was not offered folded into a digest, and its one answer lost with it). var alone, rest []entry for _, e := range entries { - if len(e.msg.Actions) > 1 { + if len(e.msg.Actions) > 1 || (len(e.msg.Actions) > 0 && e.r != nil && e.r.Ask != "") { alone = append(alone, e) } else { rest = append(rest, e)