6da3a7d866
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
87 lines
3.4 KiB
Go
87 lines
3.4 KiB
Go
package monitor
|
|
|
|
import (
|
|
"database/sql"
|
|
"io"
|
|
"log"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
|
)
|
|
|
|
// R-872 (v0.134.0) — a box DOWN at the 05:00 deadline is judged on longer lines instead of being skipped.
|
|
// THE MEASURED SHAPE (Tester 2, 2026-10-05 05:00 Budapest): "Deadline check: … 0 backup missed … 1 skipped (down)" —
|
|
// a laptop off at every deadline, no dump ever, no alarm ever.
|
|
// COMPANION RED-PROOF: restore `if st == "down" || st == StateDisabled { skipped++; continue }` → the Tester 2
|
|
// shape raises nothing.
|
|
|
|
func downBox(t *testing.T, firstReportAge time.Duration) (*store.Store, string, *StalenessChecker) {
|
|
t.Helper()
|
|
st, path := seedStalenessCustomer(t, "ok", 3*time.Hour) // the controller report is 3 h old → down
|
|
if err := st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.SaveHostReport("h1", "c1", []byte(`{"pbs_snapshots":[],"backups":[]}`), store.HostReportDenorm{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r872Backdate(t, path, "UPDATE host_reports SET received_at = ?", time.Now().UTC().Add(-firstReportAge))
|
|
sc, _ := newChecker(t, st)
|
|
sc.Check()
|
|
if sc.GetState("c1") != "down" {
|
|
t.Fatalf("setup: state %q, want down", sc.GetState("c1"))
|
|
}
|
|
return st, path, sc
|
|
}
|
|
|
|
func r872Backdate(t *testing.T, path, q string, at time.Time) {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
if _, err := db.Exec(q, at.Format("2006-01-02 15:04:05")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func deadlineEvents(st *store.Store, sc *StalenessChecker) map[string]int {
|
|
got := map[string]int{}
|
|
CheckBackupDeadlines(st, sc, func(cid, et, sev, msg, det, src string) { got[et]++ }, log.New(io.Discard, "", 0))
|
|
return got
|
|
}
|
|
|
|
// Tester 2's shape: bound 5 days ago, down at every deadline, no dump, no backup → both alarms.
|
|
func TestR872_DownEveryNightRaisesTheMissedAlarms(t *testing.T) {
|
|
st, _, sc := downBox(t, 5*24*time.Hour)
|
|
got := deadlineEvents(st, sc)
|
|
if got["expected_dbdump_missed"] != 1 || got["expected_backup_missed"] != 1 {
|
|
t.Fatalf("events %v — a box off at every deadline must raise the missed-backup alarms, down or not", got)
|
|
}
|
|
}
|
|
|
|
// A box that went down last night but made its dump yesterday evening (the catch-up) → no alarm (staleness owns it).
|
|
func TestR872_DownWithARecentDumpIsQuiet(t *testing.T) {
|
|
st, path, sc := downBox(t, 5*24*time.Hour)
|
|
if _, err := st.SaveEvent("c1", "db_dump_completed", "info", "ok", "{}", "controller"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r872Backdate(t, path, "UPDATE events SET created_at = ? WHERE event_type = 'db_dump_completed'", time.Now().UTC().Add(-20*time.Hour))
|
|
ts := time.Now().UTC().Add(-30 * time.Hour).Format(time.RFC3339)
|
|
if err := st.SaveHostReport("h1", "c1", []byte(`{"pbs_snapshots":[{"backup_time":"`+ts+`","verify_state":"ok"}],"backups":[]}`), store.HostReportDenorm{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := deadlineEvents(st, sc); got["expected_dbdump_missed"] != 0 || got["expected_backup_missed"] != 0 {
|
|
t.Fatalf("events %v — a dump 20 h ago and a backup 30 h ago are inside the down box's lines", got)
|
|
}
|
|
}
|
|
|
|
// A new box (first report a day ago) that is down → nothing expected yet.
|
|
func TestR872_NewDownBoxIsQuiet(t *testing.T) {
|
|
st, _, sc := downBox(t, 24*time.Hour)
|
|
if got := deadlineEvents(st, sc); len(got) != 0 {
|
|
t.Fatalf("events %v for a box bound a day ago", got)
|
|
}
|
|
}
|