package stacks import ( "os" "path/filepath" "testing" ) // R-682 — the consequence: a remove the process died in is FINISHED at the next start (the app no // longer reads installed, its record is gone), and the drive data the household asked to delete is // KEPT (no unattended deletion at boot); the marker is gone afterwards. func TestR682_InterruptedRemoveIsFinishedAtBootKeepingData(t *testing.T) { drive := t.TempDir() m, dir, composeRan := newR442Manager(t, "app", driveCompose, driveAppYAML(drive), drive) plantFile(t, filepath.Join(drive, "appdata", "app", "keep.txt")) // a path the compose binds: removable // The state a kill mid-remove leaves: journal written (asking for drive data too), app.yaml present. if err := markRemovePending(dir, true, 2); err != nil { t.Fatal(err) } got := m.RecoverInterruptedRemoves() if len(got) != 1 || got[0] != "app" { t.Fatalf("RecoverInterruptedRemoves = %v, want [app]", got) } if _, err := os.Stat(composeRan); err != nil { t.Error("the finishing remove must run compose down") } if _, err := os.Stat(filepath.Join(dir, "app.yaml")); !os.IsNotExist(err) { t.Errorf("app.yaml survived: the app still reads installed (err=%v)", err) } if st, ok := m.GetStack("app"); ok && st.Deployed { t.Error("the app still reads deployed after the recovery") } if _, err := os.Stat(filepath.Join(drive, "appdata", "app", "keep.txt")); err != nil { t.Errorf("drive data was deleted unattended at boot: %v", err) } if _, err := os.Stat(filepath.Join(dir, removePendingFile)); !os.IsNotExist(err) { t.Error("the remove marker survived the recovery") } } // RemoveStack journals before compose down and clears on return; a clean start with no marker // touches nothing. func TestR682_RemoveJournalsAndClears(t *testing.T) { drive := t.TempDir() m, dir, _ := newR442Manager(t, "app", ssdCompose, driveAppYAML(drive), drive) // The stub compose records whether the journal existed when `down` ran. seen := filepath.Join(t.TempDir(), "journal-seen") stub := t.TempDir() script := "#!/bin/sh\ntest -f " + filepath.Join(dir, removePendingFile) + " && touch " + seen + "\nexit 0\n" if err := os.WriteFile(filepath.Join(stub, "docker-compose"), []byte(script), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", stub+string(os.PathListSeparator)+os.Getenv("PATH")) if got := m.RecoverInterruptedRemoves(); len(got) != 0 { t.Fatalf("no marker: nothing to recover, got %v", got) } if _, err := os.Stat(filepath.Join(dir, "app.yaml")); err != nil { t.Fatal("a start with no marker removed the app") } if _, err := m.RemoveStack("app", false, nil); err != nil { t.Fatalf("RemoveStack: %v", err) } if _, err := os.Stat(seen); err != nil { t.Error("the remove journal did not exist while compose down ran") } if _, err := os.Stat(filepath.Join(dir, removePendingFile)); !os.IsNotExist(err) { t.Error("the journal survived a completed remove") } }