package web import ( "os" "path/filepath" "testing" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/backup" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // Slice 4 — the predicate moved from this package into backup.Tier2UnitRestorePoint. The page must // render IDENTICALLY: the row's four unit-restore fields are compared against the coverage computed // the old way (the inline expression that used to live in buildAppBackupRows). func TestSlice4_BackupRowUnitFieldsAreUnchangedByTheExtraction(t *testing.T) { s, sett, m := newOffboxWebServer(t) dest := t.TempDir() if err := sett.AddStoragePath(settings.StoragePath{Path: dest, Label: "flash"}); err != nil { t.Fatal(err) } if err := sett.SetCrossDriveConfig("app", &settings.CrossDriveBackup{ Enabled: true, Method: "rsync", DestinationPath: dest, LastRun: "2026-09-13T01:30:00Z", LastStatus: "ok", LastSuccess: "2026-09-13T01:30:00Z", SuccessTracked: true, }); err != nil { t.Fatal(err) } unit := filepath.Join(dest, "backups", "secondary", "app", "recovery-unit") if err := os.MkdirAll(unit, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(unit, "manifest.json"), []byte(`{"schema_version":2,"app_name":"app","created_at":"2026-09-12T02:15:29Z"}`), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dest, "backups", "secondary", "app", ".felhom-tier2-layout"), []byte("2"), 0o644); err != nil { t.Fatal(err) } cov, err := m.Tier2RestoreCoverage("app") if err != nil { t.Fatal(err) } wantDate, wantStale := cov.UnitRestoreDate() rows := s.buildAppBackupRows(&backup.FullBackupStatus{AppDataInfo: []backup.AppBackupInfo{{StackName: "app", DisplayName: "App"}}}) row := findRow(rows, "app") if row == nil { t.Fatal("no row") } if !cov.CanRestoreUnit() { t.Fatal("fixture: the copy must hold an openable unit") } if row.Tier2UnitRestorable != cov.CanRestoreUnit() || row.Tier2CopyDate != wantDate || row.Tier2CopyDateProven != (cov.CopyLastSuccess != "") || row.Tier2UnitConfirm != tier2UnitConfirmWithStaleness(wantDate, cov.CopyLastSuccess != "", wantStale) { t.Errorf("the row changed: restorable=%v date=%q proven=%v confirm=%q", row.Tier2UnitRestorable, row.Tier2CopyDate, row.Tier2CopyDateProven, row.Tier2UnitConfirm) } } // The drive-return gate starts apps UNATTENDED. A held app must be skipped. // // COMPANION RED-PROOF (REPORT.md): delete the appHeld check from restartStacks. The server has NO stack // manager here on purpose, so the unguarded StartStack call panics and this test fails. func TestSlice4_DriveReturnGateSkipsAHeldApp(t *testing.T) { s, _, m := newOffboxWebServer(t) if err := m.HoldAfterFailedUpdate("held", time.Now(), time.Now(), backup.UpdateTierLocal); err != nil { t.Fatal(err) } defer func() { if r := recover(); r != nil { t.Fatalf("the drive-return gate tried to START a held app: %v", r) } }() s.restartStacks([]string{"held"}) }