Files
mesh-catalog/modules/messenger/cmd/messenger/held_test.go
T

195 lines
8.0 KiB
Go

package main
import (
"encoding/json"
"reflect"
"strings"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/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)
}
}
// Issue 595: operator-channel.asks answers one ask, the open or the ended, those opened from a time, and no more bytes than a
// bound, said when it cut the answer short; with no argument it answers the open asks only, within a default bound, and says so (issue 1974).
func TestTheAsksVerbNarrowsItsAnswer(t *testing.T) {
view := func(key, state, opened string) AskView {
return AskView{Key: key, Asker: "mesh-delivery", Headline: "Approve " + key, State: state, Opened: opened}
}
all := []AskView{view("a.3", StateOpen, "2026-11-04T10:00:00Z"), view("a.2", StateHeld, "2026-11-03T10:00:00Z"),
view("a.1", "ended", "2026-11-02T10:00:00Z"), view("a.0", "ended", "2026-11-01T10:00:00Z")}
one, _ := json.Marshal(all[0])
for _, tc := range []struct {
name string
args map[string]any
want []string
matched int
cut bool
err bool
}{
{"no argument is the open asks, said", map[string]any{}, []string{"a.3", "a.2"}, 2, false, false},
{"state all is every ask", map[string]any{"state": "all"}, []string{"a.3", "a.2", "a.1", "a.0"}, 4, false, false},
{"a key finds an ended ask whatever its state", map[string]any{"key": "a.0"}, []string{"a.0"}, 1, false, false},
{"key", map[string]any{"key": "a.1"}, []string{"a.1"}, 1, false, false},
{"state open takes open and held", map[string]any{"state": "open"}, []string{"a.3", "a.2"}, 2, false, false},
{"state ended", map[string]any{"state": "ended"}, []string{"a.1", "a.0"}, 2, false, false},
{"since", map[string]any{"since": "2026-11-02T12:00:00Z"}, []string{"a.3", "a.2"}, 2, false, false},
{"bound cuts and says so", map[string]any{"bound": float64(2*len(one) + 10), "state": "open"}, []string{"a.3", "a.2"}, 2, false, false},
{"a bound under the asks cuts", map[string]any{"bound": float64(len(one) + 5)}, []string{"a.3"}, 2, true, false},
{"a state that is neither", map[string]any{"state": "done"}, nil, 0, false, true},
{"a time that is not one", map[string]any{"since": "yesterday"}, nil, 0, false, true},
} {
q, err := AsksQueryOf(tc.args)
if (err != nil) != tc.err {
t.Errorf("%s: err = %v", tc.name, err)
continue
}
if tc.err {
continue
}
got := q.Narrow(all)
if (got.Default != "") != (tc.args["key"] == nil && tc.args["state"] == nil && tc.args["since"] == nil) || q.Bound == 0 {
t.Errorf("%s: default %q, bound %d: only no argument is defaulted, and every query is bounded", tc.name, got.Default, q.Bound)
}
var keys []string
for _, v := range got.Asks {
keys = append(keys, v.Key)
}
if !reflect.DeepEqual(keys, tc.want) || got.Matched != tc.matched || (got.Cut != "") != tc.cut {
t.Errorf("%s: asks %v matched %d cut %q, want %v, %d, cut %v", tc.name, keys, got.Matched, got.Cut, tc.want, tc.matched, tc.cut)
}
}
}