package notify import ( "strings" "testing" ) // R-1287: a D47 set-aside reaches the hub as backup_run_failures, leg `unit-set-aside`, severity error, Failed 1 — // the payload controller 0.343.0 sends (cmd/controller/main.go SetUnitSetAsideNotify). Measured 2026-10-25 on Tester 1: // two mails „🔴 tester-1: 1 of 1 apps failed to back up (backup run)" while nothing failed. The consequence asserted: // the operator's mail says set aside / nothing failed, as a warning; a run with a real failure keeps its red subject. // RED-PROOF: drop the setAsideApps branch in renderBackupRunFailures and the severity override in processEvent → the // subject is the red „failed to back up" and the record reads error. const setAsidePayload = `{"run_id":"unit-set-aside-tandoor-20261025T173155Z","run_kind":"backup","failed":1,"attempted":1,` + `"apps":[{"app":"tandoor","leg":"unit-set-aside","reason":"the backup of an earlier install was set aside whole (D47)"}]}` func r1287Dispatch(t *testing.T, details string) (subjects []string, severity string) { t.Helper() st := opOnlyStore(t) d := opOnlyDispatcher(t, st, &sentTo{}) d.sendEmailFn = func(to, subject, body string, headers map[string]string) error { if to == "operator@felhom.eu" { subjects = append(subjects, subject) } return nil } d.ProcessEvent("c1", "backup_run_failures", "error", `App "tandoor": its backup belonged to an earlier install`, details, "controller") logs, err := st.GetRecentNotifications("c1", 10) if err != nil { t.Fatal(err) } for _, l := range logs { if l.EventType == "backup_run_failures" { severity = l.Severity } } return subjects, severity } func TestR1287_ASetAsideMailsAsSetAsideNotAsAFailure(t *testing.T) { subjects, sev := r1287Dispatch(t, setAsidePayload) if len(subjects) != 1 { t.Fatalf("the operator got %d mail(s), want 1: %v", len(subjects), subjects) } s := subjects[0] if strings.Contains(s, "failed to back up") || strings.Contains(s, "🔴") { t.Fatalf("a set-aside is dressed as a red backup failure: %q", s) } if !strings.Contains(s, "set aside — nothing failed") || !strings.Contains(s, "tandoor") || !strings.Contains(s, "⚠️") { t.Fatalf("the subject must say set aside / nothing failed and name the app: %q", s) } if sev != "warning" { t.Fatalf("the record reads %q, want warning", sev) } } func TestR1287_ARunWithARealFailureKeepsItsRedSubject(t *testing.T) { mixed := `{"run_kind":"backup","failed":2,"attempted":5,"apps":[` + `{"app":"tandoor","leg":"unit-set-aside","reason":"set aside (D47)"},` + `{"app":"immich","leg":"recovery-unit","reason":"disk full"}]}` subjects, sev := r1287Dispatch(t, mixed) if len(subjects) != 1 || !strings.Contains(subjects[0], "2 of 5 apps failed to back up") || sev != "error" { t.Fatalf("a run with a real failure must stay red and error: %v %q", subjects, sev) } }