6be6c53e29
gates / gates (push) Successful in 25s
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
64 lines
2.4 KiB
Go
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)
|
|
}
|
|
}
|