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) } } }