Files
felhom-controller/controller/internal/quiesce/r721_stop_holds_test.go
T
admin 6be6c53e29
gates / gates (push) Successful in 25s
v0.283.0: apps go off-site by themselves (decision 50) with a size warning; a Stop holds during a backup (R-721); page slips (R-724/R-725)
Red-proofs RP31-RP38. MinAgent 0.131.0 (unchanged).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-30 10:31:55 +02:00

64 lines
2.4 KiB
Go

package quiesce
import (
"context"
"reflect"
"sync"
"testing"
)
// householdStacks is fakeStacks plus the household's recorded intent (the stacks manager's WantsStopped).
type householdStacks struct {
fakeStacks
mu2 sync.Mutex
stopped map[string]bool
}
func (h *householdStacks) WantsStopped(name string) bool {
h.mu2.Lock()
defer h.mu2.Unlock()
return h.stopped[name]
}
// pressStopDuringBackup is a backend that, on its first status poll (the backup is running, the apps are
// down for it), lets the household press Stop on one app — the 2026-09-29 19:37:16 shape.
type pressStopDuringBackup struct {
*fakeBackend
st *householdStacks
app string
once sync.Once
}
func (b *pressStopDuringBackup) BackupStatus(ctx context.Context) (string, error) {
b.once.Do(func() { b.st.mu2.Lock(); b.st.stopped[b.app] = true; b.st.mu2.Unlock() })
return b.fakeBackend.BackupStatus(ctx)
}
// R-721 (v0.283.0). The CONSEQUENCE asserted: after the backup's resume, the app the household stopped during
// the backup was NOT started again; the other one was.
// COMPANION RED-PROOF: remove the WantsStopped skip in restartAll → this test fails with actualbudget in the
// started list (the measured 2026-09-29 defect).
func TestR721_AStopPressedDuringTheBackupHolds(t *testing.T) {
st := &householdStacks{fakeStacks: fakeStacks{running: []string{"actualbudget", "bookstack"}}, stopped: map[string]bool{}}
be := &pressStopDuringBackup{fakeBackend: &fakeBackend{due: true, phases: []string{"running", "snapshotted", "done"}}, st: st, app: "actualbudget"}
l := testLoop(t, be, st)
if err := l.runOnce(context.Background()); err != nil {
t.Fatalf("runOnce: %v", err)
}
if got := st.startedNames(); !reflect.DeepEqual(got, []string{"bookstack"}) {
t.Fatalf("the backup's resume started %v — the household's Stop on actualbudget must hold (R-721)", got)
}
}
// Control: with no Stop pressed, both come back (the skip must not swallow a normal resume).
func TestR721_ControlBothResumeWithoutAStop(t *testing.T) {
st := &householdStacks{fakeStacks: fakeStacks{running: []string{"actualbudget", "bookstack"}}, stopped: map[string]bool{}}
l := testLoop(t, &fakeBackend{due: true, phases: []string{"running", "done"}}, st)
if err := l.runOnce(context.Background()); err != nil {
t.Fatal(err)
}
if got := st.startedNames(); !reflect.DeepEqual(got, []string{"actualbudget", "bookstack"}) {
t.Fatalf("got %v", got)
}
}