Files
mesh-catalog/modules/messenger/cmd/messenger/held_test.go
T
jschoubben 9d83987815 Join item 1: the catalogue's modules take the router's ask from operator-channel's contract and client
mesh-sdk holds neither asks nor asker, nor the vault's answer types, and mesh-delivery's
sdk/views is mesh-sdk's go/views (a1-progress.md, the rewrite map for the join): thirteen
modules switch to the operatorchannel contract and client, mesh-sdk's channel package and
meshvault, with go.mod requiring each at the group pin (nats: meshbroker too). The gate
holds the checks mesh-sdk dropped (an ask's id is its key's asking, at least one option),
with a row that fails without them; claude-code refuses a router record whose times do
not parse, as its JSON decode did, with a row.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:33:27 +01:00

143 lines
5.2 KiB
Go

package main
import (
"encoding/json"
"strings"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
)
// 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 != occ.OutcomeExpired || store.asks["mesh-delivery.d-1"].State != string(occ.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 != occ.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)
}
}