hub(D53): held apps reach the operator — mirror, Held apps list (host + Hosts page), reminder after the grace
gates / gates (push) Successful in 1m58s
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.
This commit is contained in:
1 parent
ecd82abab8
commit
7e57e824a2
27 files changed
+935
-2
No files matched your search
@@ -0,0 +1,43 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user