package notify import ( "io" "log" "testing" ) // TestR1049_DriveBackMailsBothEdges pins the CONSEQUENCE of R-1049: a drive that is pulled and comes back mails the // operator BOTH edges, twice in a row inside the hour (the 2026-10-14 night: pulls at 22:55 and 23:21), and the household // hears the return only if it heard the loss. Measured before the fix: the hub received `storage_reconnected` and // `health_recovered` and logged no notification at all (audits/media-drive-events-2026-10-14/K8-hub-mails.txt). // // COMPANION RED-PROOF (2026-10-15): remove the two R-1049 entries from recoveredPairedDownTypes → the operator gets 4 // mails, not 8, and the household 1, not 2; this test fails. Restored. func TestR1049_DriveBackMailsBothEdges(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) // One pull per customer for the household leg: the customer pairing compares SECOND-granular send times and a tie // is „no mail" by design (flap-safe), so a second pull in the same test second would be skipped for a reason that // has nothing to do with R-1049. The operator leg has no pairing: two pulls inside the hour, both edges each time. for _, c := range []string{"on", "off"} { d.ProcessEvent(c, "storage_disconnected", "error", "Meghajtó váratlanul leválasztva: Media B", `{"label":"Media B"}`, "controller") d.ProcessEvent(c, "storage_reconnected", "info", "Meghajtó újra csatlakoztatva: Media B", `{"label":"Media B"}`, "controller") } if got := len(mailsFor(*sent, "house-on@example.com")); got != 2 { t.Fatalf("household that hears losses: %d mails, want 2 (the loss and the return)", got) } for _, c := range []string{"on", "off"} { d.ProcessEvent(c, "storage_disconnected", "error", "Meghajtó váratlanul leválasztva: Media C", `{"label":"Media C"}`, "controller") d.ProcessEvent(c, "storage_reconnected", "info", "Meghajtó újra csatlakoztatva: Media C", `{"label":"Media C"}`, "controller") } if got := len(mailsFor(*sent, "op@felhom.eu")); got != 8 { t.Fatalf("operator: %d mails, want 8 (two customers × two pulls × loss + return)", got) } if got := len(mailsFor(*sent, "house-off@example.com")); got != 0 { t.Fatalf("household that hears no losses: %d mails, want 0 — a return it was never told about is noise", got) } // health_recovered pairs with health_degraded for the operator the same way. d.ProcessEvent("on", "health_degraded", "warning", "Rendszer állapot romlott (volt: ok)", `{}`, "controller") before := len(mailsFor(*sent, "op@felhom.eu")) d.ProcessEvent("on", "health_recovered", "info", "Rendszer állapot helyreállt: ok (volt: warn)", `{}`, "controller") if got := len(mailsFor(*sent, "op@felhom.eu")); got != before+1 { t.Fatalf("health_recovered: the operator got %d new mail(s), want 1", got-before) } }