package stacks import ( "sort" "strings" "sync" "testing" ) // R-1117: a template that drops a volume leaves the old one on an installed box, still carrying the app's compose project // label (and a restore re-creates it with that label). `compose down --volumes` removes only the volumes the CURRENT // definition declares, so before this fix the removal listed it and kept it — measured on scratch 9202, 2026-10-18: // Nextcloud removed „with data", `nextcloud_nextcloud_redis_data` still there. The consequence pinned: after the // removal no volume with the app's project label is left, and a volume of another project is never touched. // RED-PROOF: delete the leftover loop in RemoveStack → "left behind: [app_app_redis_data]". func TestR1117_RemoveDeletesALabelledVolumeTheDefinitionNoLongerDeclares(t *testing.T) { drive := t.TempDir() m, _, _ := newR442Manager(t, "app", ssdCompose, driveAppYAML(drive), drive) var mu sync.Mutex // project label per volume: the app's own (declared or not), and another app's. vols := map[string]string{"app_app_redis_data": "app", "other_app_data": "other"} var removedByRm []string m.execFn = func(name string, args ...string) (string, error) { mu.Lock() defer mu.Unlock() a := strings.Join(args, " ") switch { case strings.HasPrefix(a, "volume ls --filter label=com.docker.compose.project="): p := strings.TrimPrefix(strings.Fields(a)[3], "label=com.docker.compose.project=") var out []string for v, proj := range vols { if proj == p { out = append(out, v) } } sort.Strings(out) return strings.Join(out, "\n"), nil case a == "volume ls -q": var out []string for v := range vols { out = append(out, v) } sort.Strings(out) return strings.Join(out, "\n"), nil case strings.HasPrefix(a, "volume rm "): v := strings.Fields(a)[len(strings.Fields(a))-1] delete(vols, v) removedByRm = append(removedByRm, v) return v, nil } return "", nil } resp, err := m.RemoveStack("app", false, nil) if err != nil { t.Fatalf("RemoveStack: %v", err) } mu.Lock() defer mu.Unlock() var left []string for v, proj := range vols { if proj == "app" { left = append(left, v) } } if len(left) > 0 { t.Fatalf("left behind: %v — a volume with the app's project label outlived its removal", left) } if _, ok := vols["other_app_data"]; !ok { t.Fatalf("another app's volume was removed (rm calls: %v)", removedByRm) } found := false for _, v := range resp.VolumesRemoved { found = found || v == "app_app_redis_data" } if !found { t.Errorf("volumes_removed = %v, want it to name app_app_redis_data", resp.VolumesRemoved) } }