Files
felhom-controller/controller/internal/stacks/r651_r658_remove_test.go
T

86 lines
3.3 KiB
Go

package stacks
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
// R-651 (v0.268.0) — a removed app left `applied-compose.yml` and `applied-meta/` in its stack dir.
// The CONSEQUENCE asserted: after remove + reinstall, the probe the undo would judge the old version
// by (savePreUpdateMeta) is the REINSTALL's .felhom.yml — not the removed install's record. (A
// reinstall whose pin step fails writes no record of its own, which is when the stale one was read.)
//
// COMPANION RED-PROOF (REPORT.md): delete the R-651 block in RemoveStack. This test then fails at
// "the undo would probe with the REMOVED install's .felhom.yml".
func TestR651_RemoveDeletesTheAppliedRecord(t *testing.T) {
drive := t.TempDir()
m, dir, _ := newR442Manager(t, "app", ssdCompose, driveAppYAML(drive), drive)
mustWrite(t, AppliedComposePath(dir), ssdCompose)
if err := storeAppliedMeta(dir, []byte("# the REMOVED install's probe\n")); err != nil {
t.Fatal(err)
}
if _, err := m.RemoveStack("app", false, nil); err != nil {
t.Fatalf("RemoveStack: %v", err)
}
for _, p := range []string{AppliedComposePath(dir), filepath.Join(dir, appliedMetaDir)} {
if _, err := os.Stat(p); err == nil {
t.Errorf("%s survived the remove (R-651)", p)
}
}
// The catalog mirror stays: it is the template, not the install.
if _, err := os.Stat(filepath.Join(dir, "docker-compose.yml")); err != nil {
t.Fatalf("the catalog mirror's compose file must stay: %v", err)
}
// Reinstall (its pin step wrote no record) → the undo's probe source.
mustWrite(t, filepath.Join(dir, ".felhom.yml"), "# the REINSTALL's probe\n")
md, _ := savePreUpdateMeta(dir)
b, _ := os.ReadFile(filepath.Join(md, ".felhom.yml"))
if !strings.Contains(string(b), "REINSTALL") {
t.Fatalf("the undo would probe with the REMOVED install's .felhom.yml: %q", b)
}
}
// R-658 — the removal's report counts a declared volume that carries no label (a restored app).
// Measured at the 2026-09-23 night's teardown: `volumes_removed: []` over volumes that did go.
//
// COMPANION RED-PROOF (REPORT.md): make appVolumeSet return projectVolumes alone — the report reads
// `[]` again and this test fails.
func TestR658_RemoveReportsUnlabelledVolumes(t *testing.T) {
drive := t.TempDir()
m, _, _ := newR442Manager(t, "app", ssdCompose, driveAppYAML(drive), drive)
downDone := false
m.execFn = func(name string, args ...string) (string, error) {
a := strings.Join(args, " ")
switch {
case strings.Contains(a, "label=com.docker.compose.project="):
return "", nil // restored: no label
case a == "volume ls -q":
if downDone {
return "other_vol\n", nil
}
return "app_app_cfg\nother_vol\n", nil
}
return "", nil
}
// the stub compose (newR442Manager) is the `down`; flip the listing after the first before-read.
calls := 0
inner := m.execFn
m.execFn = func(name string, args ...string) (string, error) {
if strings.Join(args, " ") == "volume ls -q" {
calls++
downDone = calls > 1
}
return inner(name, args...)
}
resp, err := m.RemoveStack("app", false, nil)
if err != nil {
t.Fatalf("RemoveStack: %v", err)
}
if !reflect.DeepEqual(resp.VolumesRemoved, []string{"app_app_cfg"}) {
t.Fatalf("volumes_removed = %v, want [app_app_cfg] — an unlabelled volume the remove did delete", resp.VolumesRemoved)
}
}