hub v0.151.0: a drive that comes back mails too — storage_reconnected / health_recovered paired with their downs (R-1049); not deployed
gates / gates (push) Successful in 6m45s
gates / gates (push) Successful in 6m45s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1,3 +1,17 @@
|
||||
## v0.151.0 — a drive that comes back mails too (R-1049) (2026-10-15)
|
||||
|
||||
- **`storage_reconnected` → `storage_disconnected`** and **`health_recovered` → `health_degraded`** join
|
||||
`recoveredPairedDownTypes`: the operator hears both edges, the household hears the return only if it heard the loss.
|
||||
Measured 2026-10-14 night on Tester 1: two pulled drives mailed „disconnected" twice; both returns and both health
|
||||
recoveries were received (severity info) and mailed to nobody — the severity gate dropped them
|
||||
(`documentation/audits/media-drive-events-2026-10-14/K8-hub-mails.txt`).
|
||||
- `cooldownEndedBy`: a new `storage_disconnected` ends the previous `storage_reconnected` cooldown (and
|
||||
`health_degraded` the `health_recovered` one), so the next return inside the hour mails again (the night's two pulls
|
||||
were 26 minutes apart).
|
||||
- Tests: `TestR1049_DriveBackMailsBothEdges` (red-proved: without the two pairs the household gets 1 mail, not 2);
|
||||
`TestDriveLostAgainAfterReplugMailsAgain` now counts the return mail too (3 household / 6 operator).
|
||||
- **Not deployed** — the operator's „yes" first (`09` §3 decision 162).
|
||||
|
||||
## v0.150.0 — an app's repair after a power cut reaches the operator, counts only (R-1007) (2026-10-14)
|
||||
|
||||
Needs **controller ≥ 0.311.0** to see anything (it sends the event). Older boxes never send it — nothing changes for them.
|
||||
|
||||
@@ -157,6 +157,13 @@ var recoveredPairedDownTypes = map[string][]string{
|
||||
// no customer hears either, which is correct — neither store belongs to a customer.
|
||||
"pbsdr_box_recovered": {"pbsdr_box_unreachable"},
|
||||
"offsite_box_recovered": {"offsite_box_unreachable"},
|
||||
// R-1049 (2026-10-15). MEASURED 2026-10-14 night on Tester 1: two pulled drives mailed „unexpectedly
|
||||
// disconnected" (error) twice, and the hub RECEIVED both `storage_reconnected` and both `health_recovered` (severity
|
||||
// info) and mailed none — the severity gate dropped them, so the operator could not tell a returned drive from one
|
||||
// still gone. Same reasoning and same necessity as the entries above; the customer hears the return iff it heard the
|
||||
// loss (the pairing gate). Pinned by TestR1049_DriveBackMailsBothEdges.
|
||||
"storage_reconnected": {"storage_disconnected"},
|
||||
"health_recovered": {"health_degraded"},
|
||||
}
|
||||
|
||||
// severityNotifies reports whether a severity triggers email notifications. warning / error / critical
|
||||
@@ -1017,6 +1024,10 @@ func (d *Dispatcher) replyToFor(to string) string {
|
||||
// Pinned by TestDriveLostAgainAfterReplugMailsAgain.
|
||||
var cooldownEndedBy = map[string][]string{
|
||||
"storage_reconnected": {"storage_disconnected"},
|
||||
// R-1049: a new loss ends the previous return's cooldown, so the NEXT return mails again (both pulls of the
|
||||
// 2026-10-14 night were 26 minutes apart — inside the hour).
|
||||
"storage_disconnected": {"storage_reconnected"},
|
||||
"health_degraded": {"health_recovered"},
|
||||
}
|
||||
|
||||
// forgetIncidentCooldowns drops both channels' cooldowns for customerID and each down type, including
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -30,13 +30,14 @@ func TestDriveLostAgainAfterReplugMailsAgain(t *testing.T) {
|
||||
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)
|
||||
// R-1049 (2026-10-15): the return now mails too — 2 losses + 1 return.
|
||||
if got := len(mailsFor(*sent, "house-on@example.com")); got != 3 {
|
||||
t.Fatalf("household that switched it on: %d mails, want 3 (one per loss + the return, R-1049)", 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)
|
||||
if got := len(mailsFor(*sent, "op@felhom.eu")); got != 6 {
|
||||
t.Fatalf("operator: %d mails, want 6 (two losses + one return, × two customers; R-1049)", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user