R-1342: a restore copy under the stable-start watch is not deletable (D62 gap); an app removed during the watch raises no alarm — tests red first; commit only
gates / gates (push) Successful in 31s
gates / gates (push) Successful in 31s
This commit is contained in:
1 parent
6c72e3ff34
commit
652a690e5d
10 files changed
+231
-1
No files matched your search
@@ -0,0 +1,122 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user