package stacks import ( "os" "path/filepath" "runtime" "strings" "testing" ) // R-649 (v0.266.0, operator ruling 2026-09-23) — a failed install removes the containers it started. // The stub compose binary FAILS `up` and records every call, so the test sees what the deploy ran // across the real process boundary. // // COMPANION RED-PROOF (REPORT.md): delete the `down` call in runComposeDeploy's failure branch — the // recorded calls are only [up -d] and this fails. func TestR649_AFailedInstallRemovesWhatItStarted(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("the stub compose binary is a shell script") } m, dir := newInstalledManager(t, "services:\n web:\n image: nginx:1.27\n", "deployed: true\nenv: {}\n") bin := t.TempDir() calls := filepath.Join(t.TempDir(), "calls") script := "#!/bin/sh\necho \"$*\" >> " + calls + "\ncase \"$*\" in *up*) exit 1;; esac\nexit 0\n" if err := os.WriteFile(filepath.Join(bin, "docker-compose"), []byte(script), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin) // ONLY the stub: no real docker can ever be reached from here (R-650) m.composeCmd = "docker-compose" m.execFn = func(string, ...string) (string, error) { return "", nil } m.runComposeDeploy("bookstack", dir, map[string]string{}, &AppConfig{Deployed: true}) b, _ := os.ReadFile(calls) got := strings.Split(strings.TrimSpace(string(b)), "\n") if len(got) != 2 || !strings.Contains(got[0], "up -d") || !strings.HasSuffix(got[1], "down") { t.Fatalf("a failed install must run `down` after its failed `up`; calls=%q", got) } if strings.Contains(got[1], "-v") || strings.Contains(got[1], "--volumes") { t.Fatalf("the cleanup must KEEP named volumes (a reinstall finds its data); got %q", got[1]) } if cfg := LoadAppConfig(dir); cfg != nil && cfg.Deployed { t.Fatal("the record must read not deployed") } } // A successful install never runs `down` (control: the cleanup is on the failure branch only). func TestR649_ASuccessfulInstallIsNotTakenDown(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("the stub compose binary is a shell script") } m, dir := newInstalledManager(t, "services:\n web:\n image: nginx:1.27\n", "deployed: true\nenv: {}\n") bin := t.TempDir() calls := filepath.Join(t.TempDir(), "calls") script := "#!/bin/sh\necho \"$*\" >> " + calls + "\nexit 0\n" if err := os.WriteFile(filepath.Join(bin, "docker-compose"), []byte(script), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin) m.composeCmd = "docker-compose" m.execFn = func(string, ...string) (string, error) { return "", nil } m.runComposeDeploy("bookstack", dir, map[string]string{}, &AppConfig{Deployed: true}) b, _ := os.ReadFile(calls) if strings.Contains(string(b), "down") { t.Fatalf("a successful install must not be taken down; calls=%q", string(b)) } }