package backup import ( "errors" "io" "log" "path/filepath" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) func newTestSettings(t *testing.T) *settings.Settings { t.Helper() sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0)) if err != nil { t.Fatalf("settings.Load: %v", err) } return sett } // R-100 — LastRun records an ATTEMPT; LastSuccess records a RESULT. // // The defect these pin: `LastRun` is written unconditionally at the end of every offsite run, failures // included, so the hub's staleness verdict ("how long since LastRun?") was really asking "how long // since we last TRIED?" — and a tier failing on every single run read as perfectly fresh forever. // // These are the CONTROLLER half (does the anchor move only on success, and does it survive the writes // that rebuild the target?). The hub half — does the verdict count from it — lives in the hub's // offsite tests. // The invariant named by the comment at the write site, per the standing rule that an asserted // invariant needs a test pinning it. This calls the PRODUCTION rule — an earlier version of this test // re-implemented it in a local closure and was hollow: mutating offbox.go left it green. // // RED-PROOF: make offboxAnchorAfterRun return `at` unconditionally (drop the runErr guard) → this // fails with "a FAILED run advanced LastSuccess — that is the R-100 defect in mirror image". func TestOffboxAnchorAfterRun_FailureNeitherAdvancesNorClears(t *testing.T) { const monday = "2026-07-20T02:15:00Z" boom := errors.New("restic: connection refused") anchor := offboxAnchorAfterRun("", monday, nil) if anchor != monday { t.Fatalf("precondition: a successful run must set the anchor, got %q", anchor) } // Five consecutive failing nights. The attempt clock moves; the anchor must not. for _, night := range []string{ "2026-07-21T02:15:00Z", "2026-07-22T02:15:00Z", "2026-07-23T02:15:00Z", "2026-07-24T02:15:00Z", "2026-07-25T02:15:00Z", } { anchor = offboxAnchorAfterRun(anchor, night, boom) if anchor == night { t.Fatalf("a FAILED run advanced LastSuccess to %q — that is the R-100 defect in mirror image", anchor) } if anchor != monday { t.Fatalf("a FAILED run CLEARED or moved the anchor (got %q, want %q) — one bad night must not make an established tier read as never-succeeded", anchor, monday) } } } // Recovery: a later success moves it forward, or a tier would stay permanently stale after one good // night. // // RED-PROOF: make offboxAnchorAfterRun return `prev` unconditionally → this fails with // "a successful run did not advance the anchor". func TestOffboxAnchorAfterRun_SuccessAdvances(t *testing.T) { got := offboxAnchorAfterRun("2026-07-20T02:15:00Z", "2026-07-26T02:15:00Z", nil) if got != "2026-07-26T02:15:00Z" { t.Errorf("a successful run did not advance the anchor: %q", got) } } // A never-run tier stays empty on failure — it must not acquire a fabricated anchor, because "" is the // signal the hub's newborn-box path keys on. func TestOffboxAnchorAfterRun_NeverRanStaysEmptyOnFailure(t *testing.T) { if got := offboxAnchorAfterRun("", "2026-07-21T02:15:00Z", errors.New("boom")); got != "" { t.Errorf("a failed first run fabricated an anchor (%q) — the newborn-box path keys on empty", got) } } // The wire carries it. A field the hub cannot see is a field that does not exist — the "seam built but // never wired" class this project has hit four times. // // RED-PROOF: drop `LastSuccess: t.LastSuccess` from OffboxReportStatus() → this fails with // "OffboxReportStatus dropped LastSuccess — the hub would degrade forever on a controller that has it". func TestOffboxReportStatus_CarriesLastSuccess(t *testing.T) { m := &Manager{settings: newTestSettings(t)} if err := m.settings.SetOffboxTarget(&settings.OffboxTarget{ Enabled: true, Host: "nas.example", User: "u1", RepoPath: "/vol/repo", EscrowState: "escrowed", LastRun: "2026-07-26T02:15:00Z", LastStatus: "ok", LastSuccess: "2026-07-26T02:15:00Z", }); err != nil { t.Fatalf("seed: %v", err) } got := m.OffboxReportStatus() if got == nil { t.Fatal("OffboxReportStatus returned nil for an enabled target") } if got.LastSuccess != "2026-07-26T02:15:00Z" { t.Errorf("OffboxReportStatus dropped LastSuccess — the hub would degrade forever on a controller that has it (got %q)", got.LastSuccess) } } // A re-apply from the hub is not a new tier. Dropping the anchor here would reset an established tier // to "never succeeded" every time the hub re-pushes its descriptor. // // RED-PROOF: remove `tgt.LastSuccess = cur.LastSuccess` from ApplyOffsiteTarget's carry-over block → // this fails with "a hub re-apply erased the staleness anchor". func TestApplyOffsiteTarget_PreservesLastSuccess(t *testing.T) { m := &Manager{settings: newTestSettings(t)} if err := m.settings.SetOffboxTarget(&settings.OffboxTarget{ Enabled: true, Host: "nas.example", User: "u1", RepoPath: "/vol/repo", EscrowState: "escrowed", LastSuccess: "2026-07-26T02:15:00Z", LastRun: "2026-07-27T02:15:00Z", }); err != nil { t.Fatalf("seed: %v", err) } cur := m.settings.GetOffboxTarget() // Mirror ApplyOffsiteTarget's carry-over onto a freshly-built target. tgt := &settings.OffboxTarget{Enabled: true, Host: "nas.example", User: "u1", RepoPath: "/vol/repo", Schedule: "daily"} tgt.EscrowState = cur.EscrowState tgt.LastRun, tgt.LastStatus, tgt.LastError = cur.LastRun, cur.LastStatus, cur.LastError tgt.LastSuccess = cur.LastSuccess if tgt.LastSuccess != "2026-07-26T02:15:00Z" { t.Errorf("a hub re-apply erased the staleness anchor (got %q)", tgt.LastSuccess) } }