Files
mesh-catalog/modules/messenger/cmd/messenger/holder_test.go
T
jochen 0ae7933d54 Tell the operator what the mesh finds wrong, and watch the watcher (hq to-be 45 phase 1)
The mesh noticed 48 core failures in six days and told nobody (ADR 0227).
messenger holds the operator-channel seat: it consumes the controller's
condition events and sends them to Telegram and the desktop notifier,
deduplicated by key, reminded once, edited on clear, capped at 20 an hour
with the rest folded, and refusing anything carrying an address, a path or
a secret. mesh-watcher, on a machine other than the control node, sends to
Telegram directly when the self-check heartbeat or the bus goes silent.
2026-10-06 09:35:55 +02:00

401 lines
13 KiB
Go

package main
import (
"errors"
"fmt"
"strings"
"testing"
"time"
)
type fakeChannel struct {
name string
notReady error
fail error
sends []Message
edits map[string]Message
n int
}
func (f *fakeChannel) Name() string { return f.name }
func (f *fakeChannel) Ready() error { return f.notReady }
func (f *fakeChannel) CanEdit() bool { return true }
func (f *fakeChannel) Send(m Message) (string, error) {
if f.fail != nil {
return "", f.fail
}
f.n++
f.sends = append(f.sends, m)
return fmt.Sprint(f.n), nil
}
func (f *fakeChannel) Edit(id string, m Message) error {
if f.fail != nil {
return f.fail
}
if f.edits == nil {
f.edits = map[string]Message{}
}
f.edits[id] = m
return nil
}
type memStore struct {
recs map[string]Record
recent []Sent
}
func (m *memStore) Put(r Record) error {
if m.recs == nil {
m.recs = map[string]Record{}
}
m.recs[r.Key] = r
return nil
}
func (m *memStore) Delete(k string) error { delete(m.recs, k); return nil }
func (m *memStore) All() ([]Record, error) {
var out []Record
for _, r := range m.recs {
out = append(out, r)
}
return out, nil
}
func (m *memStore) PutRecent(s []Sent) error { m.recent = s; return nil }
func (m *memStore) Recent() ([]Sent, error) { return m.recent, nil }
type clock struct{ t time.Time }
func (c *clock) now() time.Time { return c.t }
func (c *clock) pass(d time.Duration) { c.t = c.t.Add(d) }
func start() *clock { return &clock{t: time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)} }
func cond(key, sev, summary string) Condition {
parts := strings.Split(key, ".")
return Condition{Key: key, Scope: parts[0], ID: parts[1], Kind: parts[len(parts)-1], Severity: sev, Summary: summary}
}
func newHolder(t *testing.T) (*Holder, *fakeChannel, *fakeChannel, *clock, *memStore) {
t.Helper()
c := start()
tg, dt := &fakeChannel{name: "telegram"}, &fakeChannel{name: "desktop"}
st := &memStore{}
var emitted []string
h := &Holder{Telegram: tg, Desktop: dt, Store: st, Now: c.now, Logf: t.Logf,
Emit: func(e string, _ any) error { emitted = append(emitted, e); return nil }}
h.init()
return h, tg, dt, c, st
}
func TestARaisedConditionIsSentOnceByItsKey(t *testing.T) {
h, tg, dt, c, _ := newHolder(t)
k := cond("machine.ace.silent", Urgent, "the home server has not been heard for 15 min")
k.Raised = c.now()
h.Condition(EventRaised, k)
h.Condition(EventRaised, k) // a redelivery
c.pass(time.Minute)
h.Condition(EventRaised, k) // said again by the controller
if len(tg.sends) != 1 || len(dt.sends) != 1 {
t.Fatalf("urgent: telegram %d, desktop %d; want one each", len(tg.sends), len(dt.sends))
}
if !strings.Contains(tg.sends[0].Text(), "machine.ace.silent") || !strings.HasPrefix(tg.sends[0].Title, "URGENT: ") {
t.Fatalf("message: %q", tg.sends[0].Text())
}
if open := h.Open(); len(open) != 1 || open[0].Count != 1 {
t.Fatalf("open: %+v", open)
}
}
func TestAWarningGoesToTheDesktopWhenASessionAnswersElseTelegram(t *testing.T) {
h, tg, dt, _, _ := newHolder(t)
h.Condition(EventRaised, cond("plan.41.stalled", Warning, "plan 41 waits on a build"))
if len(dt.sends) != 1 || len(tg.sends) != 0 {
t.Fatalf("desktop answered: desktop %d telegram %d", len(dt.sends), len(tg.sends))
}
dt.fail = errors.New("the account is not logged in")
h.Condition(EventRaised, cond("plan.42.stalled", Warning, "plan 42 waits on a build"))
if len(tg.sends) != 1 {
t.Fatalf("no session: telegram %d", len(tg.sends))
}
dt.fail, dt.notReady = nil, errors.New("not configured")
h.Condition(EventRaised, cond("plan.43.stalled", Warning, "plan 43 waits on a build"))
if len(tg.sends) != 2 {
t.Fatalf("no desktop configured: telegram %d", len(tg.sends))
}
}
func TestTheCapHoldsTheRestAndFoldsThemIntoOneMessage(t *testing.T) {
h, tg, _, c, _ := newHolder(t)
h.Desktop = nil
for i := 0; i < 25; i++ {
h.Condition(EventRaised, cond(fmt.Sprintf("machine.m%d.silent", i), Urgent, "a machine is silent"))
}
if len(tg.sends) != CapPerHour {
t.Fatalf("sent %d, cap %d", len(tg.sends), CapPerHour)
}
st := h.Status("listening")
if st.Channels[0].Held != 5 || st.Channels[0].SentLastHour != CapPerHour {
t.Fatalf("status: %+v", st.Channels[0])
}
// Said, not silent: the fold goes out at the first tick, and again only after ten minutes.
h.Tick()
if len(tg.sends) != CapPerHour+1 {
t.Fatalf("no fold: %d sends", len(tg.sends))
}
fold := tg.sends[len(tg.sends)-1]
if !strings.Contains(fold.Title, "HELD BACK: 5") {
t.Fatalf("fold: %q", fold.Text())
}
for i := 20; i < 25; i++ {
if !strings.Contains(fold.Body, fmt.Sprintf("machine.m%d.silent", i)) {
t.Fatalf("fold does not name m%d: %q", i, fold.Body)
}
}
h.Condition(EventRaised, cond("machine.late.silent", Urgent, "a machine is silent"))
c.pass(time.Minute)
h.Tick()
if len(tg.sends) != CapPerHour+1 {
t.Fatalf("a second fold inside ten minutes")
}
c.pass(FoldEvery)
h.Tick()
if last := tg.sends[len(tg.sends)-1]; !strings.Contains(last.Body, "machine.late.silent") {
t.Fatalf("the second fold: %q", last.Text())
}
// An hour on, the window is free again.
c.pass(time.Hour)
h.Condition(EventRaised, cond("machine.next.silent", Urgent, "a machine is silent"))
if last := tg.sends[len(tg.sends)-1]; !strings.Contains(last.Text(), "machine.next.silent") {
t.Fatalf("not sent after the window: %q", last.Text())
}
}
func TestAMessageCarryingAnAddressIsRefusedAndSaidWithItsWordsWithheld(t *testing.T) {
h, tg, _, _, _ := newHolder(t)
var emitted []string
h.Emit = func(e string, _ any) error { emitted = append(emitted, e); return nil }
h.Condition(EventRaised, cond("provider.keycloak.ace.failing", Urgent, "cannot reach 192.168.1.20:8443 with token=abc"))
if len(tg.sends) != 1 {
t.Fatalf("sends: %d", len(tg.sends))
}
text := tg.sends[0].Text()
if strings.Contains(text, "192.168") || strings.Contains(text, "token=") {
t.Fatalf("the address left: %q", text)
}
if !strings.Contains(text, "channel-refused") || !strings.Contains(text, "provider.keycloak.ace.failing") {
t.Fatalf("not said as refused, by key: %q", text)
}
hist := h.History(10)["refusals"].([]RefusalNote)
if len(hist) != 1 || hist[0].Class != "address" {
t.Fatalf("refusals: %+v", hist)
}
if len(emitted) != 1 || emitted[0] != "refused" {
t.Fatalf("emitted: %v", emitted)
}
// Its clearing keeps the words withheld.
h.Condition(EventCleared, Condition{Key: "provider.keycloak.ace.failing"})
for _, m := range tg.edits {
if strings.Contains(m.Text(), "192.168") {
t.Fatalf("the clearing carried it: %q", m.Text())
}
}
if len(tg.edits) != 1 {
t.Fatalf("edits: %d", len(tg.edits))
}
}
func TestAKeyThatCarriesAnAddressIsWithheldToo(t *testing.T) {
h, tg, _, _, _ := newHolder(t)
h.Desktop = nil
h.Condition(EventRaised, cond("machine.10.0.0.7.silent", Urgent, "a machine is silent"))
if len(tg.sends) != 1 || strings.Contains(tg.sends[0].Text(), "10.0.0.7") {
t.Fatalf("sends: %+v", tg.sends)
}
}
func TestStillOpenPastItsBoundItIsSaidOnceMore(t *testing.T) {
h, tg, _, c, _ := newHolder(t)
h.Desktop = nil
u := cond("bus.controller.slow-consumer", Urgent, "the controller's consumer is far behind")
u.Raised = c.now()
w := cond("plan.41.stalled", Warning, "plan 41 waits")
w.Raised = c.now()
h.Condition(EventRaised, u)
h.Condition(EventRaised, w)
c.pass(59 * time.Minute)
h.Tick()
if len(tg.sends) != 2 {
t.Fatalf("reminded before the hour: %d", len(tg.sends))
}
c.pass(2 * time.Minute)
h.Tick()
if len(tg.sends) != 3 || !strings.HasPrefix(tg.sends[2].Title, "STILL OPEN after 61 min") {
t.Fatalf("urgent reminder: %d %q", len(tg.sends), tg.sends[len(tg.sends)-1].Title)
}
c.pass(3 * time.Hour)
h.Tick()
if len(tg.sends) != 3 {
t.Fatalf("reminded twice")
}
c.pass(9 * time.Hour) // the warning is now 13 h old
h.Tick()
if len(tg.sends) != 4 || !strings.Contains(tg.sends[3].Text(), "plan.41.stalled") {
t.Fatalf("warning reminder: %d", len(tg.sends))
}
}
func TestClearedEditsTheFirstMessageAndReopenedWithinTenMinutesIsNotNew(t *testing.T) {
h, tg, dt, c, st := newHolder(t)
k := cond("machine.ace.silent", Urgent, "the home server is silent")
k.Raised = c.now()
h.Condition(EventRaised, k)
c.pass(14 * time.Minute)
h.Condition(EventCleared, Condition{Key: k.Key})
if len(tg.sends) != 1 || len(tg.edits) != 1 || len(dt.edits) != 1 {
t.Fatalf("telegram sends %d edits %d, desktop edits %d", len(tg.sends), len(tg.edits), len(dt.edits))
}
if m := tg.edits["1"]; !strings.HasPrefix(m.Title, "CLEARED after 14 min") {
t.Fatalf("edit: %q", m.Title)
}
if len(h.Open()) != 0 {
t.Fatalf("still open")
}
c.pass(5 * time.Minute)
h.Condition(EventRaised, k)
if len(tg.sends) != 1 || !strings.Contains(tg.edits["1"].Title, "open again, 2 times") {
t.Fatalf("reopened as new: sends %d, edit %q", len(tg.sends), tg.edits["1"].Title)
}
h.Condition(EventCleared, Condition{Key: k.Key})
c.pass(11 * time.Minute)
h.Tick()
if _, kept := st.recs[k.Key]; kept {
t.Fatalf("a cleared message kept past the reopen window")
}
h.Condition(EventRaised, k)
if len(tg.sends) != 2 {
t.Fatalf("a raise after the window is a new message: %d", len(tg.sends))
}
}
func TestASilencedConditionSendsNothing(t *testing.T) {
h, tg, dt, c, _ := newHolder(t)
k := cond("machine.ace.silent", Urgent, "silent")
k.SilencedTill = c.now().Add(2 * time.Hour)
h.Condition(EventRaised, k)
c.pass(90 * time.Minute)
h.Tick()
h.Condition(EventCleared, Condition{Key: k.Key})
if len(tg.sends)+len(dt.sends)+len(tg.edits)+len(dt.edits) != 0 {
t.Fatalf("a silenced condition said something")
}
// Silenced after it was sent: no reminder.
k2 := cond("machine.shanks.silent", Urgent, "silent")
k2.Raised = c.now()
h.Condition(EventRaised, k2)
k2.SilencedTill = c.now().Add(3 * time.Hour)
h.Condition(EventChanged, k2)
c.pass(2 * time.Hour)
h.Tick()
if len(tg.sends) != 1 {
t.Fatalf("reminded while silenced: %d", len(tg.sends))
}
}
func TestEscalationIsSaidOnce(t *testing.T) {
h, tg, dt, _, _ := newHolder(t)
k := cond("machine.novox.silent", Warning, "the anchor is silent")
h.Condition(EventRaised, k)
k.Severity = Urgent
h.Condition(EventChanged, k)
h.Condition(EventChanged, k)
if len(dt.sends) != 2 || len(tg.sends) != 1 || !strings.HasPrefix(tg.sends[0].Title, "NOW URGENT") {
t.Fatalf("desktop %d telegram %d", len(dt.sends), len(tg.sends))
}
}
func TestAChannelThatCannotSendSaysSoAndIsTriedAgain(t *testing.T) {
h, tg, _, _, _ := newHolder(t)
h.Desktop = nil
tg.fail = errors.New("telegram sendMessage: cannot connect (dial)")
h.Condition(EventRaised, cond("machine.ace.silent", Urgent, "silent"))
st := h.Status("listening")
if !strings.HasPrefix(st.Verdict, "CANNOT SEND") || len(st.Unsent) != 1 || st.Channels[0].LastError == "" {
t.Fatalf("status: %+v", st)
}
tg.fail = nil
h.Tick()
if len(tg.sends) != 1 || h.Status("listening").Verdict != "ok" {
t.Fatalf("not retried: %d, %s", len(tg.sends), h.Status("listening").Verdict)
}
}
func TestNothingConfiguredIsSaidInTheStatus(t *testing.T) {
h, tg, dt, _, _ := newHolder(t)
tg.notReady = errors.New("no bot token")
dt.notReady = errors.New("no machine")
st := h.Status("listening")
if !strings.HasPrefix(st.Verdict, "CANNOT SEND") || st.Channels[0].NotReady != "no bot token" {
t.Fatalf("status: %+v", st)
}
}
func TestARestartForgetsNothing(t *testing.T) {
h, tg, _, c, st := newHolder(t)
h.Desktop = nil
k := cond("machine.ace.silent", Urgent, "silent")
k.Raised = c.now()
h.Condition(EventRaised, k)
again := &Holder{Telegram: tg, Store: st, Now: c.now, Logf: t.Logf}
if err := again.Load(); err != nil {
t.Fatal(err)
}
again.Condition(EventRaised, k)
if len(tg.sends) != 1 {
t.Fatalf("a restart sent it again")
}
again.Condition(EventCleared, Condition{Key: k.Key})
if len(tg.edits) != 1 {
t.Fatalf("a restart lost the message to edit")
}
}
func TestAnUnreadableEventIsToldOnceAnHour(t *testing.T) {
h, tg, _, c, _ := newHolder(t)
h.Desktop = nil
h.Unreadable("mesh-controller.condition-raised", errors.New("no severity"))
h.Unreadable("mesh-controller.condition-raised", errors.New("no severity"))
if len(tg.sends) != 1 || h.Status("listening").Unreadable != 2 {
t.Fatalf("sends %d", len(tg.sends))
}
c.pass(61 * time.Minute)
h.Unreadable("mesh-controller.condition-raised", errors.New("no severity"))
if len(tg.sends) != 2 {
t.Fatalf("not said again after an hour")
}
}
func TestNotifyIsKeptApartFromConditions(t *testing.T) {
h, tg, _, _, _ := newHolder(t)
h.Desktop = nil
out, err := h.Notify("backup.ace.failed", Warning, "last night's backup of the home server failed", "backup", false)
if err != nil || out["key"] != "notify.backup.ace.failed" || len(tg.sends) != 1 {
t.Fatalf("%v %v %d", out, err, len(tg.sends))
}
if _, err := h.Notify("x", "", "s", "", false); err == nil {
t.Fatalf("no severity was not refused")
}
if _, err := h.Notify("x", Urgent, "see /var/lib/x", "", false); err != nil {
t.Fatal(err)
}
if strings.Contains(tg.sends[len(tg.sends)-1].Text(), "/var/lib") {
t.Fatalf("a path left")
}
}
func TestATestMessageRespectsTheRule(t *testing.T) {
h, tg, dt, _, _ := newHolder(t)
if out := h.Test("telegram", "see https://example.org"); out["refused"] == "" || len(tg.sends) != 0 {
t.Fatalf("%v", out)
}
if out := h.Test("", ""); out["telegram"] != "sent" || out["desktop"] != "sent" || len(dt.sends) != 1 {
t.Fatalf("%v", out)
}
}