gates / gates (push) Successful in 1m58s
app_held / app_hold_cleared allow-listed and operator-only; handleEvent mirrors them in held_apps; HeldAppsChecker sends ONE app_held_reminder (warning, operator-only, per-app cooldown) after the grace per reason (6 h / 1 d / 3 d), then weekly; recorded before sending; stale rows not reminded; the hub never clears a hold. Test-only alerting.held_app_grace_override. TestD53_* in api/monitor/notify/web, red-proved by six mutations. Commit only.
44 lines
2.0 KiB
Go
44 lines
2.0 KiB
Go
package notify
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// D53 (operator ruling 2026-10-26: operator-only reminders first): the held-app reminder reaches the OPERATOR — one mail
|
|
// whose subject names the app — and never the household, even with every event switched on (the R-97c configuration).
|
|
// The box's own app_held / app_hold_cleared are info: they mail nobody on either leg.
|
|
func TestD53_TheHeldAppReminderMailsTheOperatorOnly(t *testing.T) {
|
|
st := opOnlyStore(t)
|
|
if err := st.SaveNotificationPrefs("c1", "customer@example.com", []string{"app_held_reminder", "app_held", "app_hold_cleared", "backup_failed"}, 6); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var to, subjects []string
|
|
d := opOnlyDispatcher(t, st, &sentTo{})
|
|
d.sendEmailFn = func(addr, subject, body string, headers map[string]string) error {
|
|
to = append(to, addr)
|
|
subjects = append(subjects, subject)
|
|
return nil
|
|
}
|
|
d.ProcessBoxEvent("c1", "app_held", "info", "App kimai is held: …", "", `{"stack_name":"kimai","reason":"unhealthy_stop"}`, "controller")
|
|
d.ProcessBoxEvent("c1", "app_hold_cleared", "info", "App kimai is no longer held", "", `{"stack_name":"kimai"}`, "controller")
|
|
if len(to) != 0 {
|
|
t.Fatalf("the box's held-app events mailed %v — they must be recorded, never mailed", to)
|
|
}
|
|
d.ProcessEvent("c1", "app_held_reminder", "warning", "Customer c1: the app kimai has been held for 3 days …",
|
|
`{"stack_name":"kimai","reason":"unhealthy_stop"}`, "hub")
|
|
d.ProcessEvent("c1", "app_held_reminder", "warning", "Customer c1: the app paperless has been held for 3 days …",
|
|
`{"stack_name":"paperless","reason":"update_failed"}`, "hub")
|
|
for _, a := range to {
|
|
if a == "customer@example.com" {
|
|
t.Fatal("the household was mailed an operator-only held-app reminder")
|
|
}
|
|
}
|
|
if len(to) != 2 {
|
|
t.Fatalf("want 2 operator mails (two apps, per-app cooldown), got %d: %v", len(to), to)
|
|
}
|
|
if !strings.Contains(subjects[0], "kimai is still held") || !strings.Contains(subjects[1], "paperless is still held") {
|
|
t.Fatalf("each subject must name its app: %q", subjects)
|
|
}
|
|
}
|