655f5f66f8
gates / gates (push) Successful in 6m0s
- S4 / R-242: GoldenVouchChecker - a published golden unvouched for 2 h raises golden_not_vouched (operator only) - S6 / R-244: the delete cascade de-references app_log_issues; one-time sweep flag -sweep-app-log-residue=count|apply - S10 / R-266: read_failed storage entry shows "read failed", not 0 % - S29 / R-250: host-key scan IPv4 first, ~2 min ladder, "safe to press Save again" error - S30 / R-246: stale_at retired (reader, setter, badge) - S32 / R-521: storage_reconnected ends the storage_disconnected cooldown on both channels - S33 / R-402: host page "Last full check (off-site)" line; two wire-gate allowlist entries removed Each with a test seen failing first. Not built or deployed yet (operator-attended deploy). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
43 lines
1.9 KiB
Go
43 lines
1.9 KiB
Go
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)
|
||
}
|
||
}
|