504eae018b
gates / gates (push) Successful in 24s
Five red-proofs. 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
70 lines
3.1 KiB
Go
70 lines
3.1 KiB
Go
package stacks
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// R-681 (v0.270.0): an install a controller restart cut off is FINISHED through the failure path and
|
|
// REPORTED — never silent. The consequences asserted: `compose down` ran (never real Docker — a fake), the
|
|
// household is told (the deploy-done hook fires as a failure with the reason), the stale pin records are
|
|
// gone, and the page's flag survives a rescan.
|
|
//
|
|
// COMPANION RED-PROOF (REPORT.md): make RecoverInterruptedInstalls return without acting (v0.269.1: no
|
|
// install marker, nothing looked) → "an interrupted install was not reported".
|
|
func TestR681_InterruptedInstallIsFinishedAndReported(t *testing.T) {
|
|
m, dir := newPinManager(t, pinTplOld, pinTplOld, "deployed: false\nenv: {}\n")
|
|
m.stacks["nextcloud"].Deployed = false
|
|
mustWrite(t, filepath.Join(dir, installPendingFile), "2026-09-24T11:48:42Z\n")
|
|
mustWrite(t, AppliedComposePath(dir), pinTplOld) // stale, from an earlier install
|
|
var downs []string
|
|
m.composeDownFn = func(d string) error { downs = append(downs, d); return nil }
|
|
type call struct {
|
|
name string
|
|
ok bool
|
|
detail string
|
|
}
|
|
var hooks []call
|
|
m.SetDeployDoneHook(func(n string, ok bool, d string) { hooks = append(hooks, call{n, ok, d}) })
|
|
|
|
got := m.RecoverInterruptedInstalls()
|
|
if len(got) != 1 || len(hooks) != 1 || hooks[0].ok || hooks[0].detail != installInterruptedReason {
|
|
t.Fatalf("an interrupted install was not reported: resolved=%v hooks=%+v", got, hooks)
|
|
}
|
|
if len(downs) != 1 || downs[0] != dir {
|
|
t.Fatalf("what the interrupted install started was not removed: downs=%v", downs)
|
|
}
|
|
if _, err := os.Stat(AppliedComposePath(dir)); err == nil {
|
|
t.Fatal("the stale pin record of a version that never ran is still there")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, installPendingFile)); err == nil {
|
|
t.Fatal("the pending marker survived — the next start would report it again")
|
|
}
|
|
if err := m.ScanStacks(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st, _ := m.GetStack("nextcloud"); !st.InstallInterrupted || st.Deployed {
|
|
t.Fatalf("after a rescan: interrupted=%v deployed=%v — the page must still say it", st.InstallInterrupted, st.Deployed)
|
|
}
|
|
// A second start finds nothing to report.
|
|
if again := m.RecoverInterruptedInstalls(); len(again) != 0 || len(hooks) != 1 {
|
|
t.Fatalf("reported twice: %v", again)
|
|
}
|
|
}
|
|
|
|
// An install that FINISHED before the restart (its record says deployed) is left alone: no down, no event.
|
|
func TestR681_FinishedInstallIsLeftAlone(t *testing.T) {
|
|
m, dir := newPinManager(t, pinTplOld, pinTplOld, "deployed: true\nenv: {}\n")
|
|
mustWrite(t, filepath.Join(dir, installPendingFile), "x\n")
|
|
downs, hooks := 0, 0
|
|
m.composeDownFn = func(string) error { downs++; return nil }
|
|
m.SetDeployDoneHook(func(string, bool, string) { hooks++ })
|
|
if got := m.RecoverInterruptedInstalls(); len(got) != 0 || downs != 0 || hooks != 0 {
|
|
t.Fatalf("a finished install was treated as interrupted: resolved=%v downs=%d hooks=%d", got, downs, hooks)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, installPendingFile)); err == nil {
|
|
t.Fatal("the marker of a finished install was not cleared")
|
|
}
|
|
}
|