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)
gates / gates (push) Successful in 25s
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
This commit is contained in:
@@ -744,9 +744,21 @@ func within(p, start, span int) bool {
|
||||
// no caller could learn that a customer's app had not come back — and the classifier downstream
|
||||
// therefore had nothing to key on. A restart failure is the single most important fact this loop
|
||||
// produces; it must leave the function.
|
||||
// householdStopped is the optional question a Stacks implementation answers (R-721): did the household
|
||||
// press Stop on this app while we had it down? The stacks manager implements it; test fakes may not.
|
||||
type householdStopped interface {
|
||||
WantsStopped(name string) bool
|
||||
}
|
||||
|
||||
func (l *Loop) restartAll(stacks []string) []string {
|
||||
var failed []string
|
||||
hs, _ := l.stacks.(householdStopped)
|
||||
for _, s := range stacks {
|
||||
if hs != nil && hs.WantsStopped(s) {
|
||||
// R-721: a Stop pressed while the backup had the app down holds — the backup does not undo it.
|
||||
l.logger.Printf("[INFO] [quiesce] %s NOT restarted: the household stopped it during the backup", s)
|
||||
continue
|
||||
}
|
||||
if err := l.stacks.StartStack(s); err != nil {
|
||||
l.logger.Printf("[ERROR] [quiesce] restart %s: %v", s, err)
|
||||
failed = append(failed, s)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user