package notify import ( "io" "log" "testing" ) // R-521 F6 (S32, `09` §3 decision 227). MEASURED 2026-09-14 (BIGNIGHT): a drive was unplugged, plugged // back (`storage_reconnected`), and unplugged again 40 minutes later — and the second loss mailed // NOBODY: the per-key cooldown armed by the first `storage_disconnected` outlived the recovery. // A re-plug ends the incident, so it ends that incident's cooldown, on both channels. // // Also pins the S32 rule: the household gets the lost-drive mail ONLY when its own list carries // `storage_disconnected`; a household that did not switch it on gets none. // RED-PROOF: without the reset in processEvent the second unplug gives 1 operator / 1 customer mail. func TestDriveLostAgainAfterReplugMailsAgain(t *testing.T) { st := newDispStore(t) if err := st.SaveNotificationPrefs("on", "house-on@example.com", []string{"storage_disconnected"}, 6); err != nil { t.Fatal(err) } if err := st.SaveNotificationPrefs("off", "house-off@example.com", []string{"backup_failed"}, 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) for _, c := range []string{"on", "off"} { d.ProcessEvent(c, "storage_disconnected", "error", "drive lost", `{}`, "controller") d.ProcessEvent(c, "storage_reconnected", "info", "drive back", `{}`, "controller") d.ProcessEvent(c, "storage_disconnected", "error", "drive lost again", `{}`, "controller") } if got := len(mailsFor(*sent, "house-on@example.com")); got != 2 { t.Fatalf("household that switched it on: %d lost-drive mails, want 2 (one per loss)", got) } if got := len(mailsFor(*sent, "house-off@example.com")); got != 0 { t.Fatalf("household that did not switch it on got %d mails, want 0", got) } if got := len(mailsFor(*sent, "op@felhom.eu")); got != 4 { t.Fatalf("operator: %d mails, want 4 (two losses × two customers)", got) } }