v0.270.0: no update for a current app (R-679); an interrupted install is reported (R-681); a restore brings back the pinned version's health check (R-669); R-674
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
This commit is contained in:
2026-09-24 16:49:43 +02:00
parent 7c6140d95b
commit 504eae018b
23 changed files with 1121 additions and 16 deletions
@@ -0,0 +1,69 @@
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")
}
}