Files
mesh-catalog/modules/messenger/cmd/messenger/restart_test.go
T
jochen 45361b013a
mesh/merge-gate pass: builds desk-channel, messenger → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
Review: keep the desk's notification ids so lost answers are said again in place (issue 384)
dunst.started says the notifier's bundle started again: the notifications stay on screen and only the
answers they waited for are lost. Forgetting their ids made the router's message a second popup beside a
dead one. The desk now keeps the ids and says answers-lost-at; the router's message replaces each
notification in place.

Whether a message is said again is now its own (last said before the answers were lost), so a router
that started after the loss still says it and a repeated standing says nothing. It is sent to the desk
directly, not through the outbox, whose failure path moves a warning to Telegram; a failed send is tried
again at the next standing.
2026-10-10 16:13:57 +02:00

204 lines
9.2 KiB
Go

package main
import (
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// Issue 384: the desk's notifier started again and the desk showed, from its own memory, nine old popups —
// resolved, silenced and cancelled ones among them. Now the desk says in its standing when the answers were
// lost, and this router says again only what is still open and unanswered: with the desk showing one open
// message with an answer, one silenced, one open asking nothing and one resolved, one is said again under its
// handle (in place of its message), with its answers; one is edited to the silenced words, with no answers
// and no urgency; nothing is said for the others. A standing that repeats the time says nothing more; its
// ticket is one-time still, and the channel that reaches the operator away hears nothing of it.
func TestLostAnswersGetOnlyWhatIsOpenAndUnansweredSaidAgain(t *testing.T) {
h, tg, dt, c, _, w := newRouter(t)
h.Start()
c.pass(2 * time.Minute)
// Open, with a question the desk carries.
open := cond("machine.shanks.units", Warning, "2 failed services on shanks")
open.Raised = c.now()
h.Condition(EventRaised, open)
settle(h, c)
if len(dt.sends) != 1 {
t.Fatalf("desk %d", len(dt.sends))
}
ask := ackAsk(c, "silence-units")
ask.About = open.Key
submit(t, h, ConditionOwner, ask)
if e := dt.edits["g14=1"]; ticketFor(e, "Silence for a week") == "" {
t.Fatalf("the question was not put on the open message: %+v", e)
}
// Silenced, with a question of its own, and its message says it is silenced.
runFor(h, c, 20*time.Minute)
sil := cond("machine.ace.agent-home", Warning, "Agent-home shrank")
sil.Raised = c.now()
h.Condition(EventRaised, sil)
settle(h, c)
if len(dt.sends) != 2 {
t.Fatalf("desk %d", len(dt.sends))
}
stand(h, c) // the channels' word, fresh again after the minutes passed
silAsk := ackAsk(c, "silence-home")
silAsk.About = sil.Key
submit(t, h, ConditionOwner, silAsk)
if e := dt.edits["g14=2"]; ticketFor(e, "Silence for a week") == "" {
t.Fatalf("the question was not put on the message to be silenced: %+v", e)
}
sil.SilencedTill, sil.SilencedWhy, sil.At = c.now().Add(7*24*time.Hour), "known", c.now()
h.Condition(EventChanged, sil)
if e := dt.edits["g14=2"]; !strings.HasPrefix(e.Title, "Silenced until") {
t.Fatalf("the silence was not said: %+v", e)
}
// Open, told, and asking nothing: not unanswered, so not said again.
runFor(h, c, 20*time.Minute)
info := cond("seat.mesh-controller.assign-by-hand", Warning, "Assign keeps being fixed by hand")
info.Raised = c.now()
h.Condition(EventRaised, info)
settle(h, c)
if len(dt.sends) != 3 {
t.Fatalf("desk %d", len(dt.sends))
}
// Resolved, and its clearing said.
runFor(h, c, 20*time.Minute)
res := cond("seat.operator-channel.asks-undelivered", Warning, "Questions for you not delivered")
res.Raised = c.now()
h.Condition(EventRaised, res)
settle(h, c)
if len(dt.sends) != 4 {
t.Fatalf("desk %d", len(dt.sends))
}
h.Condition(EventCleared, Condition{Key: res.Key, At: c.now()})
runFor(h, c, 3*time.Minute)
if e := dt.edits["g14=4"]; !strings.HasPrefix(e.Title, "Resolved: ") {
t.Fatalf("the clearing was not said: %+v", e)
}
silencedBefore := dt.edits["g14=2"]
delete(dt.edits, "g14=2") // so the edit said again is seen as such
sends, edits, replaced, tgSaid := len(dt.sends), len(dt.edits), len(dt.replaced), saidOn(tg)
// The desk's notifier started again: its standing says when the answers were lost.
c.pass(time.Minute)
newSince := c.now()
h.Stand("desktop", Standing{Ready: true, At: c.now(), AnswersLostAt: newSince})
runFor(h, c, 2*time.Minute)
if len(dt.sends) != sends {
t.Fatalf("a new message was sent instead of one under its handle: %+v", dt.sends[sends:])
}
if len(dt.replaced) != replaced+1 || dt.replaced[replaced].id != "g14=1" {
t.Fatalf("the open question was not said again under its handle, once and alone: %+v", dt.replaced[replaced:])
}
again := dt.replaced[replaced].m
if ticketFor(again, "Silence for a week") == "" || !strings.Contains(again.Title, headlineOf(open.Key)) {
t.Fatalf("said again without its answer or its words: %+v", again)
}
if e, said := dt.edits["g14=2"]; !said || !strings.HasPrefix(e.Title, "Silenced until") || len(e.Actions) != 0 || e.Urgent ||
!e.Quiet || e.Title != silencedBefore.Title {
t.Fatalf("the silenced one was not edited to the silenced words again: %+v (said %v)", e, said)
}
if len(dt.edits) != edits+1 {
t.Fatalf("an edit reached a message that is not silenced: %+v", dt.edits)
}
if e := dt.edits["g14=4"]; !strings.HasPrefix(e.Title, "Resolved: ") {
t.Fatalf("the resolved one was changed: %+v", e)
}
if saidOn(tg) != tgSaid {
t.Fatalf("the channel that reaches the operator away heard of the desk's screen: %+v %+v", tg.sends, tg.edits)
}
// The standing said again with the same time, and one with an older time: nothing more.
h.Stand("desktop", Standing{Ready: true, At: c.now(), AnswersLostAt: newSince})
h.Stand("desktop", Standing{Ready: true, At: c.now(), AnswersLostAt: newSince.Add(-time.Hour)})
runFor(h, c, 2*time.Minute)
if len(dt.replaced) != replaced+1 || len(dt.sends) != sends {
t.Fatalf("said again more than once: replaced %d, sends %d", len(dt.replaced)-replaced, len(dt.sends)-sends)
}
// Its ticket is one-time still: one warrant, however often it is tapped, and refused from another machine.
tk := ticketFor(again, "Silence for a week")
ch := choiceOf(tk, "g14", c.now())
ch.Sender.Authenticated = false
if got, err := h.Chosen("desktop", ch); got != "chosen" || err != nil {
t.Fatalf("the answer on the message said again: %s, %v", got, err)
}
ch.ID = "c-second"
_, _ = h.Chosen("desktop", ch) // the same tap again: said as recorded (issue 372), nothing more done
other := choiceOf(tk, "shanks", c.now())
other.Sender.Authenticated = false
if got, _ := h.Chosen("desktop", other); got != "refused" {
t.Fatalf("the ticket taken from another machine after it was used: %s", got)
}
chosen := 0
for _, wt := range w.of(ConditionOwner) {
if wt.Outcome == asks.OutcomeChosen {
chosen++
}
}
if chosen != 1 {
t.Fatalf("warrants for one ticket: %d", chosen)
}
}
// Whether a message is said again is its own: last said before the answers were lost. A loss before it was
// last said says nothing; a router that started again (it remembers no standing it acted on) says it again
// once, and then nothing more. A loss while the desk cannot send is said once it can — never pushed to the
// channel that reaches the operator away. Telegram, whose answers are never lost, says no time and gets nothing.
func TestLostAnswersAreJudgedPerMessageAndWaitForTheDesk(t *testing.T) {
h, tg, dt, c, _, _ := newRouter(t)
h.Start()
c.pass(2 * time.Minute)
open := cond("machine.shanks.units", Warning, "2 failed services on shanks")
open.Raised = c.now()
h.Condition(EventRaised, open)
settle(h, c)
ask := ackAsk(c, "silence-units")
ask.About = open.Key
submit(t, h, ConditionOwner, ask)
runFor(h, c, time.Minute) // the question reaches Telegram too, as its own message
replaced, tgSaid := len(dt.replaced), saidOn(tg)
// Lost before the message was last said: its answers are those of the notifier that runs now.
h.Stand("desktop", Standing{Ready: true, At: c.now(), AnswersLostAt: open.Raised.Add(-time.Minute)})
h.Stand("telegram", Standing{Ready: true, At: c.now()})
runFor(h, c, time.Minute)
if len(dt.replaced) != replaced || saidOn(tg) != tgSaid {
t.Fatalf("said again for answers that were not lost: desk %+v, telegram %+v", dt.replaced[replaced:], tg.sends)
}
// Lost while the desk cannot send: waits, and goes to the desk once it can — never to Telegram.
c.pass(time.Minute)
lost := c.now()
dt.notReady = errorf("notifications paused")
h.Stand("desktop", Standing{Ready: false, Why: "notifications paused", At: c.now(), AnswersLostAt: lost})
runFor(h, c, 2*time.Minute)
if len(dt.replaced) != replaced || saidOn(tg) != tgSaid {
t.Fatalf("said while the desk cannot send: desk %+v, telegram %+v", dt.replaced[replaced:], tg.sends)
}
dt.notReady = nil
h.Stand("desktop", Standing{Ready: true, At: c.now(), AnswersLostAt: lost})
runFor(h, c, time.Minute)
if len(dt.replaced) != replaced+1 || saidOn(tg) != tgSaid {
t.Fatalf("once the desk can send: desk %+v, telegram %+v", dt.replaced[replaced:], tg.sends)
}
// The router started again, remembering no standing it acted on: the message was said since, so nothing.
h.shows = map[string]time.Time{}
h.Stand("desktop", Standing{Ready: true, At: c.now(), AnswersLostAt: lost})
runFor(h, c, time.Minute)
if len(dt.replaced) != replaced+1 {
t.Fatalf("said again twice for one loss: %+v", dt.replaced[replaced:])
}
// A send that fails is tried again at the next standing, on the desk alone.
c.pass(time.Minute)
lost = c.now()
dt.fail = errorf("no session answers")
h.Stand("desktop", Standing{Ready: true, At: c.now(), AnswersLostAt: lost})
dt.fail = nil
runFor(h, c, time.Minute)
if len(dt.replaced) != replaced+1 || saidOn(tg) != tgSaid {
t.Fatalf("after a failed send: desk %+v, telegram %+v", dt.replaced[replaced:], tg.sends)
}
h.Stand("desktop", Standing{Ready: true, At: c.now(), AnswersLostAt: lost})
if len(dt.replaced) != replaced+2 || saidOn(tg) != tgSaid {
t.Fatalf("the failed send was not tried again on the desk: desk %+v, telegram %+v", dt.replaced[replaced:], tg.sends)
}
}