Files
felhom.eu/hub/internal/monitor/offsite_r431_test.go
T
admin 30681764cb
gates / gates (push) Successful in 17s
hub v0.111.0: notice a deletion within a day (R-431); correct R-429; re-scope R-95
THE RECORD WAS TELLING A WORSE STORY THAN THE TRUTH FOR TWO MONTHS, and my own probe is why.

R-429 CORRECTED. Yesterday's spike searched for a directory called `.snapshots`. The vendor documents
the path as /.zfs/snapshot. The probe's CONTROLS were sound and its SUBJECT was wrong, so "not found"
was true and meant nothing. Re-probed at the documented path on both boxes, with controls:

  - /.zfs lists (shares, snapshot) from inside the jail;
  - a write into /.zfs/snapshot is REFUSED - dest open ...: Failure - while the identical write to
    the account home SUCCEEDS and was cleaned up.

That is the append-only property PROVEN rather than cited, and it is the sentence the whole re-scope
rests on. Seven daily snapshots are confirmed in the panel. The mitigation works. What remains true,
and was always the actual finding: the row claiming it had no R-number, its "confirm tomorrow" went
36 days unanswered, and the DUE-CHECKS block built for that class was empty. THE FINDING WAS NEVER THE
SNAPSHOTS - IT WAS THAT NOBODY COULD TELL.

R-95 RE-SCOPED: the box can delete its LIVE repository but cannot write to the daily snapshots of it,
so a deletion costs at most one day plus a per-file recovery - not open-ended loss. The ranking is
Viktor's; it has been #1 since July on the old story.

R-432 FILED: a sub-account sees /.zfs/snapshot EMPTY while the same box holds seven snapshots, so
per-file recovery is operator-only today. One panel read settles whether a NAMED snapshot can still be
entered, which would make it product-reachable.

R-431 SHIPPED. Third signal in OffsiteChecker. On the hub deliberately: a detector on the box is one
the deletion can silence. Threshold REASONED, not invented - over 12 898 reports every decrease lands
on ZERO and predates stats_known, and in the stats_known window there are none, so observed churn gave
nothing to calibrate against. Retention cannot halve a total; a mass deletion goes to ~0. Hence: more
than half, and at least 5. Guarded by StatsKnown (R-331), the declared State (R-204) and run success
(R-100 - whose lesson lives in this very file).

ACCEPTANCE: 9 009 real report points replayed through the detector produced ZERO alarms.

Three red-proofs run. The escalation one only became real after the first version was found HOLLOW -
it re-swept the same report, so the baseline had already moved and the latch was never consulted.

07 row 10's status is NOT moved: the write-refusal is measured, but the recovery ROUTE has never been
walked, which is what PARTIAL means.
2026-09-01 14:25:24 +02:00

279 lines
10 KiB
Go

package monitor
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// R-431 — the snapshot-drop signal, proven in BOTH directions.
//
// The acceptance test is TestR431_RealHistoryProducesZeroAlarms: a detector that fires on healthy
// boxes is the mistake this project caught twice in one week, and it is the one that would get this
// signal switched off within a fortnight.
// dropJSON builds a trustworthy offsite object (stats_known, no declared state, last run ok).
func dropJSON(count int, statsKnown bool, state, lastStatus string) string {
ts := time.Now().UTC().Add(-1 * time.Hour).Format(time.RFC3339)
success := ts
if lastStatus != "ok" {
success = ""
}
return fmt.Sprintf(
`{"enabled":true,"escrow_state":"escrowed","last_run":%q,"last_status":%q,"last_success":%q,`+
`"snapshot_count":%d,"repo_size_bytes":1073741824,"quota_gb":0,"stats_known":%v,"state":%q}`,
ts, lastStatus, success, count, statsKnown, state)
}
// TestR431_FiresOnAMassDeletion — direction 1. A drop past the threshold alarms EXACTLY once.
//
// RED-PROOF (run 2026-09-01, recorded in REPORT.md): setting snapshotDropFraction to 0.99 makes this
// fail — 69 → 4 is a 94% fall and would no longer qualify, which is what an over-loose threshold
// looks like in production.
func TestR431_FiresOnAMassDeletion(t *testing.T) {
st := newDiskStore(t)
var got []struct{ et, sev, msg string }
saveOffsiteReport(t, st, "victim", dropJSON(69, true, "", "ok"))
oc := NewOffsiteChecker(st, 48*time.Hour, func(_, et, sev, msg, _, _ string) {
got = append(got, struct{ et, sev, msg string }{et, sev, msg})
}, quietLog())
// the constructor seeded the baseline at 69; now the store is emptied
saveOffsiteReport(t, st, "victim", dropJSON(4, true, "", "ok"))
oc.Check()
var drops []struct{ et, sev, msg string }
for _, g := range got {
if g.et == "offsite_snapshots_dropped" {
drops = append(drops, g)
}
}
if len(drops) != 1 {
t.Fatalf("want exactly 1 offsite_snapshots_dropped, got %d (%v)", len(drops), got)
}
// The severity MUST be in the hub's exact vocabulary — anything else is coerced to info and
// mailed to nobody (08 §6.1, shipped twice).
switch drops[0].sev {
case "info", "warning", "error", "critical":
default:
t.Fatalf("severity %q is outside the hub vocabulary — it would be coerced to info and reach nobody", drops[0].sev)
}
for _, frag := range []string{"69", "4", "read-only", "NOT confirmed data loss"} {
if !strings.Contains(drops[0].msg, frag) {
t.Fatalf("message must contain %q; got: %s", frag, drops[0].msg)
}
}
if strings.Contains(drops[0].msg, "data is lost") || strings.Contains(drops[0].msg, "data lost") {
t.Fatalf("the message must NOT claim data loss — the snapshots usually still hold it: %s", drops[0].msg)
}
}
// TestR431_EscalationOnlyLatch — a CONTINUING deletion must not page on every report cycle.
//
// WRITTEN AFTER A HOLLOW FIRST ATTEMPT, and the failure is recorded because it is instructive: the
// original assertion re-swept the SAME report and called that "escalation-only". It proved nothing —
// the baseline had already moved to the new count, so the second sweep saw a drop of zero and the
// latch was never consulted. Its red-proof (removing the latch) PASSED, which is how it was caught.
//
// This drives a count that keeps FALLING, which is the only shape where the latch is load-bearing.
//
// RED-PROOF (run 2026-09-01): replacing `dropped && oc.dropStates[...] != "dropped"` with `dropped`
// makes this fail with 2 alarms — one per report cycle, which is the noise that trains an operator
// to ignore the alarm.
func TestR431_EscalationOnlyLatch(t *testing.T) {
st := newDiskStore(t)
var n int
saveOffsiteReport(t, st, "sliding", dropJSON(69, true, "", "ok"))
oc := NewOffsiteChecker(st, 48*time.Hour, func(_, et, _, _, _, _ string) {
if et == "offsite_snapshots_dropped" {
n++
}
}, quietLog())
saveOffsiteReport(t, st, "sliding", dropJSON(30, true, "", "ok")) // 69 -> 30: alarm
oc.Check()
if n != 1 {
t.Fatalf("the first large drop must alarm exactly once; got %d", n)
}
saveOffsiteReport(t, st, "sliding", dropJSON(2, true, "", "ok")) // 30 -> 2: still falling
oc.Check()
if n != 1 {
t.Fatalf("a CONTINUING deletion must not re-page while the latch is set; got %d alarms", n)
}
if oc.GetDropState("sliding") != "dropped" {
t.Fatalf("the latch must be held, got %q", oc.GetDropState("sliding"))
}
// RECOVERY RE-ARMS: a clean sweep clears the latch, so a LATER deletion is caught again.
saveOffsiteReport(t, st, "sliding", dropJSON(40, true, "", "ok")) // rebuilt, no drop
oc.Check()
if oc.GetDropState("sliding") != "ok" {
t.Fatalf("a clean sweep must re-arm the latch, got %q", oc.GetDropState("sliding"))
}
saveOffsiteReport(t, st, "sliding", dropJSON(1, true, "", "ok")) // deleted again
oc.Check()
if n != 2 {
t.Fatalf("after re-arming, a NEW deletion must alarm again; got %d", n)
}
}
// TestR431_SilentWhenNotTrustworthy — direction 2, the three pre-conditions, each with its scar.
func TestR431_SilentWhenNotTrustworthy(t *testing.T) {
cases := []struct {
name, first, second string
}{
{"stats_known absent (R-331: a zero that means UNMEASURED)",
dropJSON(69, true, "", "ok"), dropJSON(0, false, "", "ok")},
{"a DECLARED state (R-204: the box says what happened)",
dropJSON(69, true, "", "ok"), dropJSON(0, true, "needs_credential", "ok")},
{"the run FAILED (R-100: presence is not success)",
dropJSON(69, true, "", "ok"), dropJSON(0, true, "", "error")},
{"the run was INCOMPLETE (R-203: a partial run counts less)",
dropJSON(69, true, "", "ok"), dropJSON(0, true, "", "incomplete")},
}
for i, c := range cases {
t.Run(c.name, func(t *testing.T) {
st := newDiskStore(t)
cid := fmt.Sprintf("c%d", i)
var n int
saveOffsiteReport(t, st, cid, c.first)
oc := NewOffsiteChecker(st, 48*time.Hour, func(_, et, _, _, _, _ string) {
if et == "offsite_snapshots_dropped" {
n++
}
}, quietLog())
saveOffsiteReport(t, st, cid, c.second)
oc.Check()
if n != 0 {
t.Fatalf("%s: must NOT alarm; got %d", c.name, n)
}
// AND the baseline must be untouched, so the RECOVERY does not read as a rise-then-drop.
oc.mu.Lock()
base := oc.lastCounts[cid]
oc.mu.Unlock()
if base != 69 {
t.Fatalf("%s: an untrustworthy report must not overwrite the baseline; got %d", c.name, base)
}
})
}
}
// TestR431_OrdinaryRetentionIsSilent — a fall that retention CAN explain must not alarm.
func TestR431_OrdinaryRetentionIsSilent(t *testing.T) {
for _, c := range []struct {
name string
from, to int
wantAlarms int
}{
{"69 -> 60 (9 gone, under half)", 69, 60, 0},
{"10 -> 7 (3 gone, under the floor of 5)", 10, 7, 0},
{"10 -> 5 (5 gone, exactly half — NOT more than half)", 10, 5, 0},
{"69 -> 34 (35 gone, more than half)", 69, 34, 1},
} {
t.Run(c.name, func(t *testing.T) {
st := newDiskStore(t)
cid := fmt.Sprintf("r%d%d", c.from, c.to)
var n int
saveOffsiteReport(t, st, cid, dropJSON(c.from, true, "", "ok"))
oc := NewOffsiteChecker(st, 48*time.Hour, func(_, et, _, _, _, _ string) {
if et == "offsite_snapshots_dropped" {
n++
}
}, quietLog())
saveOffsiteReport(t, st, cid, dropJSON(c.to, true, "", "ok"))
oc.Check()
if n != c.wantAlarms {
t.Fatalf("%s: want %d alarm(s), got %d", c.name, c.wantAlarms, n)
}
})
}
}
// TestR431_RealHistoryProducesZeroAlarms — THE ACCEPTANCE STEP.
//
// Replays the ACTUAL snapshot-count history of both live boxes, exported from the hub's own reports
// table on 2026-09-01, through the real detector. It must produce ZERO alarms. A warning that fires
// on healthy boxes is worse than no warning at all.
//
// The fixture is committed beside this test so the assertion does not depend on a live database.
// If it is absent the test FAILS rather than skipping — a silent skip is how a green tick comes to
// mean nothing.
func TestR431_RealHistoryProducesZeroAlarms(t *testing.T) {
path := filepath.Join("testdata", "r431_real_history.json")
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("the real-history fixture is missing (%v) — this is a FAILURE, never a skip: "+
"without it the acceptance step asserts nothing", err)
}
var hist map[string][]struct {
At string `json:"at"`
Count int `json:"count"`
StatsKnown bool `json:"stats_known"`
State string `json:"state"`
LastStatus string `json:"last_status"`
}
if err := json.Unmarshal(raw, &hist); err != nil {
t.Fatal(err)
}
if len(hist) == 0 {
t.Fatal("the fixture is empty — it would pass vacuously")
}
total := 0
for cust, points := range hist {
if len(points) < 2 {
t.Fatalf("%s: fewer than 2 points — nothing to compare", cust)
}
total += len(points)
st := newDiskStore(t)
var alarms int
var first = points[0]
saveOffsiteReport(t, st, cust, dropJSON(first.Count, first.StatsKnown, first.State, first.LastStatus))
oc := NewOffsiteChecker(st, 48*time.Hour, func(_, et, _, msg, _, _ string) {
if et == "offsite_snapshots_dropped" {
alarms++
t.Errorf("%s: FIRED ON REAL HISTORY: %s", cust, msg)
}
}, quietLog())
// Feed the remaining points straight through the detector. Driving Check() per point would
// need a 1.1s sleep each time (received_at is second-resolution) — hours for 5 000 points —
// so the sweep's own decision path is exercised directly instead, with the same guards.
for _, p := range points[1:] {
off := &offsiteReport{
SnapshotCount: p.Count, StatsKnown: p.StatsKnown, State: p.State,
LastStatus: p.LastStatus, LastSuccess: "2026-09-01T00:00:00Z",
}
if p.LastStatus != "ok" {
off.LastSuccess = ""
}
oc.mu.Lock()
if oc.countIsTrustworthy(off) {
dropped, prev, cur := oc.snapshotDropped(cust, off)
if dropped && oc.dropStates[cust] != "dropped" {
alarms++
t.Errorf("%s at %s: FIRED ON REAL HISTORY %d -> %d", cust, p.At, prev, cur)
oc.dropStates[cust] = "dropped"
} else if !dropped {
oc.dropStates[cust] = "ok"
}
oc.lastCounts[cust] = cur
}
oc.mu.Unlock()
}
if alarms != 0 {
t.Fatalf("%s: %d alarm(s) on real history — the threshold is wrong", cust, alarms)
}
}
// POSITIVE CONTROL: the replay must actually have looked at something. Without this the test
// passes when the fixture is a list of empty lists.
if total < 100 {
t.Fatalf("only %d points replayed — too few for this to mean anything", total)
}
t.Logf("replayed %d real report points across %d customers: ZERO alarms", total, len(hist))
}