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