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") } }