fix(alarms): give up after three runs without delivery (FLAW-12)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m34s

New column alarm_failures counts ticker runs in which no channel
delivered; at three the alarm is marked as done and a warning is logged
instead of retrying every minute forever. Success, a new due date or
reopening the task starts over with three attempts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 12:22:13 +01:00
1 parent b5f926bcf7
commit d1982816c1
8 files changed
+80 -13

No files matched your search

+47
View File
@@ -138,3 +138,50 @@ func TestAlarmResetsOnNewDueDateOrReopen(t *testing.T) {
t.Errorf("reopening must re-arm the alarm")
}
}
func TestAlarmGivesUpAfterThreeFailedRuns(t *testing.T) {
store, err := NewStore(filepath.Join(t.TempDir(), "alarmfail.db"))
if err != nil {
t.Fatalf("NewStore: %v", err)
}
defer store.Close()
u, _ := store.CreateUser("anna@example.com", "hash", "Anna", "member")
task, _ := store.CreateThoughtWithDetails(u.ID, "Webhook kaputt", "normal", "manual", "2026-10-30", "", "todo", "")
_, _ = store.UpdateThoughtTriage(u.ID, task.ID, "keep", "task", "", "", "")
pending := func() bool {
alarms, _ := store.GetDueTasksForAlarm()
for _, a := range alarms {
if a.TaskID == task.ID {
return true
}
}
return false
}
for run := 1; run <= MaxAlarmFailures; run++ {
gaveUp, err := store.RecordTaskAlarmFailure(task.ID)
if err != nil {
t.Fatalf("run %d: %v", run, err)
}
if gaveUp != (run == MaxAlarmFailures) || pending() == (run == MaxAlarmFailures) {
t.Fatalf("run %d: gaveUp=%v pending=%v", run, gaveUp, pending())
}
}
// A new due date gets three fresh attempts
later := "2026-11-02"
_, _ = store.UpdateThoughtDetails(u.ID, task.ID, nil, nil, &later, nil, nil, nil, nil, nil, nil)
if !pending() {
t.Fatalf("new due date must re-arm the alarm")
}
if gaveUp, _ := store.RecordTaskAlarmFailure(task.ID); gaveUp {
t.Errorf("failure counter must start over after a new due date")
}
// Success clears the counter
_ = store.MarkTaskAlarmSent(task.ID)
var failures int
_ = store.db.QueryRow(`SELECT alarm_failures FROM thoughts WHERE id = ?`, task.ID).Scan(&failures)
if failures != 0 {
t.Errorf("successful alarm should reset failures, got %d", failures)
}
}