From 26d0af7a68708d2f2c7a3b2c1280886dfc532941 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 03:51:04 +0100 Subject: [PATCH] test(alerts): cover checkpoint recovery mirror write failures SQLite remains authoritative when the JSON recovery mirror cannot be renamed. Exercise firing and resolved snapshots across restart so a failed mirror cannot silently lose or resurrect an incident. Close the event store before shutdown to prevent a second checkpoint from masking failure; also verify error reporting and temporary-file cleanup. Change-source: pulse-maintainer --- .../alerts/active_checkpoint_failure_test.go | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 internal/alerts/active_checkpoint_failure_test.go diff --git a/internal/alerts/active_checkpoint_failure_test.go b/internal/alerts/active_checkpoint_failure_test.go new file mode 100644 index 000000000..7bd1a459f --- /dev/null +++ b/internal/alerts/active_checkpoint_failure_test.go @@ -0,0 +1,92 @@ +package alerts + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// A recovery-file failure must be reported without discarding a successful +// SQLite checkpoint, including an empty checkpoint after resolution. +func TestSQLiteCheckpointSurvivesRecoveryMirrorRenameFailure(t *testing.T) { + for _, resolved := range []bool{false, true} { + name := "fired" + if resolved { + name = "resolved" + } + t.Run(name, func(t *testing.T) { + dataDir := t.TempDir() + alert := durableRestoreAlert(time.Now().Add(-72 * time.Hour).UTC()) + initial := []*Alert{} + if resolved { + initial = append(initial, alert) + } + writeActiveRecoveryFixture(t, dataDir, initial) + m := NewManagerWithDataDir(dataDir, WithDurableAlertStore()) + t.Cleanup(m.Stop) + if !m.activeStateAuthoritative.Load() { + t.Fatal("durable constructor did not establish SQLite authority") + } + + // A non-empty directory forces rename failure even when tests run as + // root; chmod-based fault injection would not reliably do so. + mirror := filepath.Join(dataDir, "alerts", "active-alerts.json") + if err := os.Remove(mirror); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(mirror, alertsDirPerm); err != nil { + t.Fatal(err) + } + sentinel := filepath.Join(mirror, "preserve") + if err := os.WriteFile(sentinel, []byte("unchanged"), alertsFilePerm); err != nil { + t.Fatal(err) + } + // Change only memory: SaveActiveAlerts, not a lifecycle append, + // must be responsible for the durable state checked after restart. + m.mu.Lock() + if resolved { + m.removeActiveAlertNoLock(alert.ID) + } else { + m.setActiveAlertNoLock(alert.ID, alert) + } + m.mu.Unlock() + err := m.SaveActiveAlerts() + if err == nil || !strings.Contains(err.Error(), "SQLite active alert checkpoint succeeded but recovery persistence failed") || !strings.Contains(err.Error(), "failed to rename") { + t.Fatalf("checkpoint error = %v; want reported mirror rename failure after SQLite success", err) + } + // Close SQLite before shutdown so its final save cannot repair a + // missing checkpoint and conceal a failure of the explicit save. + m.SetEventLog(nil) + m.Stop() + remaining, err := filepath.Glob(filepath.Join(dataDir, "alerts", "active-alerts-*.json.tmp")) + if err != nil || len(remaining) != 0 { + t.Fatalf("temporary mirrors left after failed writes = %v, error = %v", remaining, err) + } + if got, err := os.ReadFile(sentinel); err != nil || string(got) != "unchanged" { + t.Fatalf("rename failure damaged destination: %q, %v", got, err) + } + if err := os.Remove(sentinel); err != nil { + t.Fatal(err) + } + if err := os.Remove(mirror); err != nil { + t.Fatal(err) + } + + restarted := NewManagerWithDataDir(dataDir, WithDurableAlertStore()) + t.Cleanup(restarted.Stop) + if !restarted.activeStateAuthoritative.Load() { + t.Fatal("restart did not establish SQLite authority") + } + alerts := restarted.snapshotActiveAlerts() + if resolved { + if len(alerts) != 0 { + t.Fatalf("resolved alert resurrected: %+v", alerts) + } + } else if len(alerts) != 1 || alerts[0].ID != alert.ID || !alerts[0].StartTime.Equal(alert.StartTime) || !alerts[0].Acknowledged || alerts[0].AckUser != alert.AckUser { + t.Fatalf("checkpoint lost incident identity or acknowledgement: %+v", alerts) + } + }) + } +}