Files
mesh-catalog/modules/messenger/cmd/messenger/held_test.go
T
jschoubben c211762813 Hold an ask no channel can carry now and offer it when one can; show held and why (issue 926, ADR 0397)
The router refused such an ask with 'no channel', so the asker waited for an Approve nobody saw. It is now held as an open ask with the per-kind reason, judged again when the kinds or a channel's standing change and every minute, and the knowledge base says held and why.
2026-10-16 17:51:32 +02:00

143 lines
5.2 KiB
Go

package main
import (
"encoding/json"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// A held ask (novox/hq ADR 0397, issue 926): an ask that breaks no rule but that no ready channel can carry now
// is kept, not refused, and offered the moment a channel can carry it.
// withoutVerifiedSender is the controller's word on the kinds when Telegram's claim lacks verified-sender, as
// on 2026-10-16 (issue 930).
func withoutVerifiedSender(h *Holder) {
h.Bench.SetKinds([]KindInfo{
{Kind: "telegram", Module: "telegram", Node: "anchor", Capabilities: []string{"deliver", "reaches-away", "choice", "edit", "exact-render"}},
{Kind: "desktop", Module: "desk-channel", Node: "anchor", Capabilities: []string{"deliver", "choice", "edit", "exact-render", "private"}},
})
}
func withVerifiedSender(h *Holder) {
h.Bench.SetKinds([]KindInfo{
{Kind: "telegram", Module: "telegram", Node: "anchor", Capabilities: []string{"deliver", "reaches-away", "choice", "edit", "verified-sender", "exact-render"}},
{Kind: "desktop", Module: "desk-channel", Node: "anchor", Capabilities: []string{"deliver", "choice", "edit", "exact-render", "private"}},
})
}
func recordJSON(t *testing.T, store *memAsks, key string) string {
t.Helper()
raw, _ := json.Marshal(store.asks[key])
return string(raw)
}
func TestAnApprovalAskNoChannelCanCarryIsHeldAndItsAskerToldWhy(t *testing.T) {
h, tg, dt, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
withoutVerifiedSender(h)
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
settle(h, c)
for _, g := range w.of("mesh-delivery") {
t.Errorf("the asker was told it ended: %+v", g)
}
rec, kept := store.asks["mesh-delivery.d-1"]
if !kept || rec.State != StateHeld || len(rec.Tickets) != 0 {
t.Fatalf("not held: %+v", rec)
}
for _, want := range []string{"Telegram cannot prove who answered", "held"} {
if !strings.Contains(recordJSON(t, store, "mesh-delivery.d-1"), want) {
t.Errorf("the record does not say %q: %s", want, recordJSON(t, store, "mesh-delivery.d-1"))
}
}
if strings.Contains(rec.HeldWhy, "no channel") && !strings.Contains(rec.HeldWhy, "Telegram") {
t.Errorf("only no channel: %q", rec.HeldWhy)
}
if len(tg.sends) != 0 || len(dt.sends) != 0 {
t.Errorf("a held ask was shown: %d %d", len(tg.sends), len(dt.sends))
}
}
func TestAHeldAskIsOfferedWhenTheKindsChange(t *testing.T) {
h, tg, _, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
withoutVerifiedSender(h)
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
c.pass(time.Minute)
stand(h, c)
h.Tick() // still held: the reason is unchanged
if rec := store.asks["mesh-delivery.d-1"]; rec.State != StateHeld || len(tg.sends) != 0 {
t.Fatalf("offered too soon: %+v", rec)
}
withVerifiedSender(h)
h.Tick()
settle(h, c)
rec := store.asks["mesh-delivery.d-1"]
if rec.State != StateOpen || len(rec.Tickets) != 3 || rec.HeldWhy != "" {
t.Fatalf("not offered: %+v", rec)
}
if len(tg.sends) != 1 || labelsOf(tg.sends[0]) != "Release, Stop, Silence for a week" {
t.Fatalf("telegram was sent %+v", tg.sends)
}
if got := w.of("mesh-delivery"); len(got) != 0 {
t.Errorf("an offer is not an end: %+v", got)
}
// Answered as any ask is.
out, err := h.Chosen("telegram", choiceOf(ticketFor(tg.sends[0], "Release"), "42", c.now()))
if err != nil || out != "chosen" {
t.Fatalf("%s %v", out, err)
}
}
func TestAHeldAskExpiresAsAnyOpenAskDoes(t *testing.T) {
h, _, _, c, store, w := newRouter(t)
withoutVerifiedSender(h)
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
c.pass(3 * time.Hour)
stand(h, c)
h.Tick()
got := w.of("mesh-delivery")
if len(got) != 1 || got[0].Outcome != asks.OutcomeExpired || store.asks["mesh-delivery.d-1"].State != string(asks.OutcomeExpired) {
t.Fatalf("%+v %+v", got, store.asks["mesh-delivery.d-1"])
}
}
func TestAHeldAskCountsTowardTheOpenAsksOfItsAsker(t *testing.T) {
h, _, _, c, _, w := newRouter(t)
withoutVerifiedSender(h)
for i := 1; i <= 4; i++ {
a := heldAsk(c, "d-"+string(rune('0'+i)))
a.About = "about-" + string(rune('0'+i))
submit(t, h, "mesh-delivery", a)
}
got := w.of("mesh-delivery")
if len(got) != 1 || got[0].Outcome != asks.OutcomeRefused || !strings.Contains(got[0].Words, "already has 3 open asks") {
t.Fatalf("%+v", got)
}
}
func TestTheAsksVerbShowsAHeldAskWithSinceAndWhy(t *testing.T) {
h, _, _, c, _, _ := newRouter(t)
withoutVerifiedSender(h)
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
raw, _ := json.Marshal(h.AsksNow())
for _, want := range []string{`"state":"held"`, `"held_since"`, `"held_why":"`, "Telegram cannot prove who answered"} {
if !strings.Contains(string(raw), want) {
t.Errorf("the asks verb lacks %q: %s", want, raw)
}
}
}
func TestAHeldAskNamesEveryKindAndWhyWhenNoChannelIsReady(t *testing.T) {
h, _, _, c, store, _ := newRouter(t)
h.Bench.Stand("telegram", Standing{Ready: false, Why: "no bot token", At: c.now()})
h.Bench.Stand("desktop", Standing{Ready: false, Why: "no session", At: c.now()})
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
why := store.asks["mesh-delivery.d-1"].HeldWhy
if store.asks["mesh-delivery.d-1"].State != StateHeld || !strings.Contains(why, "Telegram: no bot token") || !strings.Contains(why, "the desk: no session") {
t.Fatalf("%q", why)
}
}