package backup import ( "errors" "io" "log" "path/filepath" "sync/atomic" "testing" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // r1342Drive registers the rig's app drive, as every drive is registered on a box, so the list and the delete see it. func r1342Drive(t *testing.T, m *Manager) { t.Helper() if m.settings == nil { sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0)) if err != nil { t.Fatal(err) } m.settings = sett } if err := m.settings.AddStoragePath(settings.StoragePath{Path: m.GetAppDrivePath("app"), Label: "rig"}); err != nil { t.Fatal(err) } } // R-1342 (found live on Tester 1, 2026-10-29 10:39 CET): (1) while the box still watches whether a restored app runs // stably, its restore copy is the way back — „Törlés most" must not take it (D62: never while an operation runs on the // app); (2) an app REMOVED during that watch is not „did not start" — no operator alarm, no „the copy is kept". // removableProvider is the stable-watch rig's provider that can answer StackRemoved. type removableProvider struct { *sampledProvider removed atomic.Bool } func (p *removableProvider) StackRemoved(string) bool { return p.removed.Load() } // RED-PROOF: drop the watching check from restoreCopyNeededFor / DeleteRestoreCopy → the copy is offered and deleted // mid-watch → FAILS. func TestR1342_ACopyUnderTheStableWatchIsNotDeletable(t *testing.T) { withStable(t, 400*time.Millisecond, 3*time.Second) m, rig, alarms := fixARig(t, func(int) bool { return true }, func(int) []ContainerSample { return []ContainerSample{healthy("c1", 0)} }) r1342Drive(t, m) m.BeginRestoreOp("restore", "app") if _, err := m.RestoreFromRecoveryUnit("app"); err != nil { t.Fatal(err) } m.EndRestoreOp(true, "ok") copies := m.RestoreCopies() if len(copies) != 1 || copies[0].Needed != RestoreCopyNeededChecking || !copies[0].DeleteOn.IsZero() { t.Fatalf("mid-watch the copy must read „checking\", got %+v", copies) } if _, err := m.DeleteRestoreCopy("app", copies[0].Stamp); !errors.Is(err, ErrRestoreCopyNeeded) { t.Fatalf("a delete mid-watch: %v, want ErrRestoreCopyNeeded", err) } if left := undoCopies(t, rig.undoRoot); len(left) != 1 { t.Fatalf("the copy must stay through the watch, found %v", left) } if left := waitNoCopies(t, rig.undoRoot); len(left) != 0 { t.Fatalf("after the stable window the watch releases it, found %v", left) } if len(*alarms) != 0 { t.Fatalf("no alarm: %v", *alarms) } if m.watchingCopy(filepath.Join(rig.undoRoot, copies[0].Stamp)) { t.Fatal("the watch mark must go when the watch ends") } } // RED-PROOF: drop the stableRemoved case → the removed app „did not run stably" → one alarm → FAILS. func TestR1342_AnAppRemovedDuringTheWatchRaisesNoAlarm(t *testing.T) { withStable(t, 2*time.Second, 400*time.Millisecond) var up atomic.Bool up.Store(true) m, rig, alarms := fixARig(t, func(int) bool { return up.Load() }, func(int) []ContainerSample { return []ContainerSample{healthy("c1", 0)} }) rp := &removableProvider{sampledProvider: m.stackProvider.(*sampledProvider)} m.stackProvider = rp r1342Drive(t, m) m.BeginRestoreOp("restore", "app") if _, err := m.RestoreFromRecoveryUnit("app"); err != nil { t.Fatal(err) } m.EndRestoreOp(true, "ok") // the household removes the app inside the watch up.Store(false) rp.removed.Store(true) time.Sleep(700 * time.Millisecond) if len(*alarms) != 0 { t.Fatalf("an app removed during the watch must not alarm „did not start\": %v", *alarms) } copies := m.RestoreCopies() if len(copies) != 1 || copies[0].Needed != "" { t.Fatalf("after the watch ended the copy is an unused one (sweep / Törlés most): %+v", copies) } _ = rig } // Control: an app that stays installed and never runs stably still alarms once (the R-1270 rule is unchanged). func TestR1342_AnInstalledUnstableAppStillAlarms(t *testing.T) { withStable(t, 2*time.Second, 400*time.Millisecond) var up atomic.Bool up.Store(true) m, _, alarms := fixARig(t, func(int) bool { return up.Load() }, func(int) []ContainerSample { return []ContainerSample{healthy("c1", 0)} }) m.stackProvider = &removableProvider{sampledProvider: m.stackProvider.(*sampledProvider)} m.BeginRestoreOp("restore", "app") if _, err := m.RestoreFromRecoveryUnit("app"); err != nil { t.Fatal(err) } m.EndRestoreOp(true, "ok") up.Store(false) time.Sleep(700 * time.Millisecond) if len(*alarms) != 1 { t.Fatalf("an installed app that never runs stably alarms once, got %v", *alarms) } }