hub v0.75.0: R-81 — "no signal" is not "bad signal" (anchor the backup deadline check)

Third instance of one class (hub v0.12.0, v0.73.0, this), fixed as a class.
On 2026-07-26 03:00 UTC expected_backup_missed fired on demo-felhom, demo-hp
and drill-r50 at once; the demo-felhom one reached the CUSTOMER channel
claiming "newest backup is 176h0m0s old". Nothing was wrong — three vzdump
archives were on disk. Cause: the agent backup store is in-memory, so the
R-50 fleet restart emptied `backups` until the next run, and the hub read
empty as "no backup exists".

- assessBackupFreshness returns OK/UNKNOWN/MISSED instead of `missed bool`;
  absence is UNKNOWN until it outlives an anchored window. Still pure.
- store.GetHostReportsSince + monitor.newestBackupEvidence read the hubs own
  retained history (bounded 7-day lookback, early-exit on fresh evidence) —
  "when did I last SEE evidence of a backup?" The anchor was free: the hub
  already retains 90 days. No agent change, no new persisted state.
- store.GetFirstHostReportAt anchors absence at first contact, reusing the
  existing 26h threshold as the grace (no new knob, the v0.73.0 shape).
- Deferrals logged + counted; reason strings kept distinct.
- backupStaleAfter untouched; landmine recorded (a weekly PBS snapshot would
  alarm six days in seven) and owned by R-82.

Tests 493->508. Red-proofs A/B/C observed and restored; A reproduces the live
message verbatim. Replayed the real 03:00 reports (600/417/77 rows): all
three now silent.

Source: documentation/audits/DIAG-backup-missed-2026-07-26.md
This commit is contained in:
Claude Code
2026-07-26 11:44:15 +02:00
parent add5b9bbbb
commit f5a5e2b911
9 changed files with 843 additions and 28 deletions
+9 -3
View File
@@ -171,6 +171,12 @@ func TestCheckBackupDeadlines_DbDumpHalfPreserved(t *testing.T) {
}
// TestAssessBackupFreshness exercises the pure freshness policy directly.
//
// R-81: every case here passes a ZERO backupEvidence — no hub-history evidence and no
// first-contact anchor. That is deliberate: it pins the latest-report-only behaviour
// unchanged, and the "no snapshots and no backups" row exercises the UNANCHORED absence
// branch (zero anchor → fail toward visibility, the v0.73.0 legacy-shape precedent).
// The anchored branches have their own named tests below.
func TestAssessBackupFreshness(t *testing.T) {
now := time.Date(2026, 6, 16, 3, 0, 0, 0, time.UTC)
at := func(d time.Duration) string { return now.Add(d).Format(time.RFC3339) }
@@ -192,9 +198,9 @@ func TestAssessBackupFreshness(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := assessBackupFreshness(c.report, now)
if got.missed != c.wantMissed {
t.Fatalf("missed=%v want=%v (reason=%q)", got.missed, c.wantMissed, got.reason)
got := assessBackupFreshness(c.report, backupEvidence{}, now)
if got.missed() != c.wantMissed {
t.Fatalf("missed=%v want=%v (reason=%q)", got.missed(), c.wantMissed, got.reason)
}
})
}