controller v0.240.0: seven defects from the any-tier proof and the first nightly rotation
gates / gates (push) Successful in 13s
gates / gates (push) Successful in 13s
R-486 (P1): removing an app with its backups KEPT keeps its Tier-2 record, so the second-drive restore is no longer refused over an intact mirror. R-484: postgis/pgvector/timescaledb images are Postgres (logical dumps). R-485: the backup card sizes the recovery unit and the mirror(s). R-480: a held update's sentence leaves the card once the hold is lifted. R-477: the update's off-site lookup is one snapshots call, no stats. R-478: a copy older than this install's deploy does not count. R-474: "delete backups" deletes the unit, the mirror(s) and the prefs. Tests and red-proofs per row; evidence in felhom.eu documentation/audits/v0240-2026-09-13/ and nightly-2026-09-13-adventurelog/.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// R-477 — the update's Tier-3 lookup is ONE snapshots call. The control proves the fake runner does
|
||||
// see stats calls when something makes them (the inventory page), so a zero is a measurement.
|
||||
//
|
||||
// COMPANION RED-PROOF (REPORT.md): make OffsiteSnapshotTimes read OffsiteInventoryList — this fails.
|
||||
func TestR477_TheUpdateOffsiteLookupRunsNoStats(t *testing.T) {
|
||||
m, _ := newOffboxManager(t)
|
||||
var calls []string
|
||||
m.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) {
|
||||
calls = append(calls, strings.Join(args, " "))
|
||||
if contains(args, "snapshots") {
|
||||
return []byte(`[{"short_id":"a1","time":"2026-09-13T01:00:00Z","tags":["gokapi"]},{"short_id":"b2","time":"2026-09-13T02:00:00Z","tags":["kimai"]}]`), nil
|
||||
}
|
||||
return []byte(`{"total_size":1024,"total_file_count":1}`), nil
|
||||
})
|
||||
m.updateTier2PointFn, m.updateTier1PointsFn = noTier2, noTier1
|
||||
count := func(word string) int {
|
||||
n := 0
|
||||
for _, c := range calls {
|
||||
if strings.Contains(" "+c+" ", " "+word+" ") {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
p, ok, _ := m.UpdateRestorePoints(context.Background(), "gokapi", nil)
|
||||
if !ok || p.Tier != UpdateTierOffsite || !p.At.Equal(time.Date(2026, 9, 13, 1, 0, 0, 0, time.UTC)) {
|
||||
t.Fatalf("want gokapi's off-site snapshot; got %+v ok=%v", p, ok)
|
||||
}
|
||||
if count("snapshots") != 1 || count("stats") != 0 {
|
||||
t.Errorf("the update lookup must be one snapshots call and no stats; calls=%v", calls)
|
||||
}
|
||||
calls = nil
|
||||
if _, err := m.OffsiteInventoryList(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count("stats") == 0 {
|
||||
t.Fatalf("control: the inventory DOES run stats, so the fake must see them; calls=%v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// R-474 — only the app's own Tier-2 mirror is found and removed.
|
||||
func TestR474_OnlyTheAppsOwnMirrorIsRemoved(t *testing.T) {
|
||||
m, sett := newOffboxManager(t)
|
||||
drive := t.TempDir()
|
||||
if err := sett.AddStoragePath(settings.StoragePath{Path: drive, Label: "HDD", Schedulable: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := NamespaceRootFor(drive, m.systemDataPath)
|
||||
sec := filepath.Join(root, "backups", "secondary")
|
||||
mine, other, shares := filepath.Join(sec, "gokapi"), filepath.Join(sec, "kimai"), filepath.Join(sec, SharesPseudoStack)
|
||||
for _, d := range []string{mine, other, shares} {
|
||||
if err := os.MkdirAll(filepath.Join(d, "recovery-unit"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(d, "recovery-unit", "manifest.json"), []byte("{}"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
dirs := m.Tier2MirrorDirsForApp("gokapi")
|
||||
if len(dirs) != 1 || dirs[0] != mine {
|
||||
t.Fatalf("mirror dirs for gokapi = %v, want [%s]", dirs, mine)
|
||||
}
|
||||
removed := m.RemoveTier2Mirrors("gokapi", append(dirs, other, shares, filepath.Join(sec, "gokapi", "..", "kimai")))
|
||||
if len(removed) != 1 {
|
||||
t.Errorf("exactly the app's own mirror must be removed, got %v", removed)
|
||||
}
|
||||
if _, err := os.Stat(mine); !os.IsNotExist(err) {
|
||||
t.Error("gokapi's mirror must be gone")
|
||||
}
|
||||
for _, keep := range []string{other, shares} {
|
||||
if _, err := os.Stat(keep); err != nil {
|
||||
t.Errorf("%s must survive: %v", keep, err)
|
||||
}
|
||||
}
|
||||
if m.Tier2MirrorDirsForApp("../kimai") != nil || m.RemoveTier2Mirrors(SharesPseudoStack, []string{shares}) != nil {
|
||||
t.Error("an unsafe or shares name must find and remove nothing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestR474_DeleteAppBackupPrefsForgetsOnlyThatApp(t *testing.T) {
|
||||
_, sett := newOffboxManager(t)
|
||||
_ = sett.SetAppOffbox("gokapi", true)
|
||||
_ = sett.SetCrossDriveConfig("gokapi", &settings.CrossDriveBackup{DestinationPath: "/mnt/x"})
|
||||
_ = sett.SetAppOffbox("kimai", true)
|
||||
if err := sett.DeleteAppBackupPrefs("gokapi"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sett.IsAppOffbox("gokapi") || sett.GetCrossDriveConfig("gokapi") != nil {
|
||||
t.Error("gokapi's prefs must be forgotten")
|
||||
}
|
||||
if !sett.IsAppOffbox("kimai") {
|
||||
t.Error("another app's prefs must stay")
|
||||
}
|
||||
if err := sett.DeleteAppBackupPrefs("never-there"); err != nil {
|
||||
t.Errorf("deleting absent prefs is a no-op, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user