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