Files
felhom-controller/controller/internal/backup/r1342_watch_test.go
T

123 lines
4.5 KiB
Go

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)
}
}