package notify import ( "io" "log" "strings" "testing" ) // R-1007 (v0.150.0, controller v0.311.0, `09` §3 decision 236) — app_repaired_after_crash reaches the OPERATOR and // never the household, even a household that enabled it by name; warning mails, info is recorded only; the mail // carries the counts as one line. // RED-PROOF: drop it from operatorOnlyEvents → the customer gets a mail → fails. func TestAppRepairedAfterCrash_OperatorOnlyEvenWhenTheHouseholdEnabledIt(t *testing.T) { st := newDispStore(t) if err := st.SaveNotificationPrefs("c1", "cust@example.com", []string{"app_repaired_after_crash", "node_down"}, 6); err != nil { t.Fatal(err) } d := NewDispatcher(st, "test-key", "from@felhom.eu", "op@felhom.eu", true, log.New(io.Discard, "", 0)) sent := captureSeam(d) details := `{"app":"Immich","stack_name":"immich","boot_at":"2026-10-14T12:07:26Z","found":81,"repaired":81,"still_broken":0,"lost":1,"lost_checked":true,"duration_s":4}` d.ProcessEvent("c1", "app_repaired_after_crash", "warning", "Immich: ...", details, "controller") if got := mailsFor(*sent, "cust@example.com"); len(got) != 0 { t.Fatalf("the household got %d mail(s) — the counts are the operator's; its side is the app page's note", len(got)) } if got := len(mailsFor(*sent, "op@felhom.eu")); got != 1 { t.Fatalf("operator mails = %d, want 1", got) } // info (everything repaired): recorded, not mailed. *sent = nil d.ProcessEvent("c1", "app_repaired_after_crash", "info", "Immich: ...", strings.Replace(details, `"lost":1`, `"lost":0`, 1), "controller") if got := len(*sent); got != 0 { t.Fatalf("an all-repaired (info) run mailed %d time(s)", got) } } // RED-PROOF: drop the renderAppRepair call → the body lacks the counts line → fails. func TestAppRepairedAfterCrash_OperatorMailHasTheCountsLine(t *testing.T) { _, body := FormatOperatorEmail("c1", "app_repaired_after_crash", "warning", "msg", `{"app":"Immich","boot_at":"2026-10-14T12:07:26Z","found":81,"repaired":80,"still_broken":1,"lost":1,"lost_checked":true,"duration_s":4}`) want := "Repair: Immich — after the unclean boot at 2026-10-14T12:07:26Z: 81 empty file(s) found, 80 repaired, 1 still broken; originals lost: 1; 4 s" if !strings.Contains(body, want) { t.Fatalf("body lacks %q:\n%s", want, body) } _, skipped := FormatOperatorEmail("c1", "app_repaired_after_crash", "warning", "msg", `{"app":"Immich","boot_at":"b","skipped":"not proven"}`) if !strings.Contains(skipped, "SKIPPED after the unclean boot at b: not proven") { t.Fatalf("skipped body:\n%s", skipped) } }