//! `pic apply --prune` journey: a resource dropped from the manifest //! survives a plain (additive) apply but is deleted with `--prune`. use std::fs; use tempfile::TempDir; use crate::common; use crate::common::cleanup::AppGuard; fn scripts_ls(env: &common::TestEnv, slug: &str) -> String { String::from_utf8( common::pic_as(env) .args(["scripts", "ls", "--app", slug]) .output() .unwrap() .stdout, ) .unwrap() } #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn prune_deletes_stale_resources() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let slug = common::unique_slug("prune"); common::pic_as(&env) .args(["apps", "create", &slug]) .assert() .success(); let _guard = AppGuard::new(&env.url, &env.token, &slug); let dir = TempDir::new().unwrap(); fs::create_dir_all(dir.path().join("scripts")).unwrap(); fs::write(dir.path().join("scripts/keep.rhai"), "let x = 1; x").unwrap(); fs::write(dir.path().join("scripts/drop.rhai"), "let y = 2; y").unwrap(); let manifest_path = dir.path().join("picloud.toml"); // v1: two scripts + a route on `drop`. let v1 = format!( "[app]\nslug = \"{slug}\"\nname = \"Prune Test\"\n\n\ [[scripts]]\nname = \"keep\"\nfile = \"scripts/keep.rhai\"\n\n\ [[scripts]]\nname = \"drop\"\nfile = \"scripts/drop.rhai\"\n\n\ [[routes]]\nscript = \"drop\"\nmethod = \"GET\"\n\ host_kind = \"any\"\npath_kind = \"exact\"\npath = \"/drop\"\n" ); fs::write(&manifest_path, &v1).unwrap(); common::pic_as(&env) .args(["apply", "--file"]) .arg(&manifest_path) .assert() .success(); // v2: drop `drop` and its route. let v2 = format!( "[app]\nslug = \"{slug}\"\nname = \"Prune Test\"\n\n\ [[scripts]]\nname = \"keep\"\nfile = \"scripts/keep.rhai\"\n" ); fs::write(&manifest_path, &v2).unwrap(); // Plain apply is additive — `drop` survives. common::pic_as(&env) .args(["apply", "--file"]) .arg(&manifest_path) .assert() .success(); assert!( scripts_ls(&env, &slug).contains("drop"), "additive apply must not delete" ); // Prune apply removes `drop` and its route. `--yes` skips the // confirmation prompt (this test runs non-interactively). let out = common::pic_as(&env) .args(["apply", "--file"]) .arg(&manifest_path) .args(["--prune", "--yes"]) .output() .expect("apply --prune"); assert!( out.status.success(), "prune failed: {}", String::from_utf8_lossy(&out.stderr) ); let report = String::from_utf8(out.stdout).unwrap(); assert!( report.contains("-1"), "expected deletions in report:\n{report}" ); let s = scripts_ls(&env, &slug); assert!(!s.contains("drop"), "prune should delete `drop`:\n{s}"); assert!(s.contains("keep"), "prune must keep `keep`:\n{s}"); // Plan is clean after prune. let p = String::from_utf8( common::pic_as(&env) .args(["plan", "--file"]) .arg(&manifest_path) .output() .unwrap() .stdout, ) .unwrap(); assert!( !p.contains("delete"), "plan should be clean after prune:\n{p}" ); } /// The prune confirmation gate: `--prune` without `--yes`, run /// non-interactively (no TTY, as in CI), must refuse and delete nothing. #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn prune_without_yes_refuses_noninteractively() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let slug = common::unique_slug("prune-gate"); common::pic_as(&env) .args(["apps", "create", &slug]) .assert() .success(); let _guard = AppGuard::new(&env.url, &env.token, &slug); let dir = TempDir::new().unwrap(); fs::create_dir_all(dir.path().join("scripts")).unwrap(); fs::write(dir.path().join("scripts/keep.rhai"), "let x = 1; x").unwrap(); fs::write(dir.path().join("scripts/drop.rhai"), "let y = 2; y").unwrap(); let manifest_path = dir.path().join("picloud.toml"); // Deploy two scripts, then drop one from the manifest. let v1 = format!( "[app]\nslug = \"{slug}\"\nname = \"Gate Test\"\n\n\ [[scripts]]\nname = \"keep\"\nfile = \"scripts/keep.rhai\"\n\n\ [[scripts]]\nname = \"drop\"\nfile = \"scripts/drop.rhai\"\n" ); fs::write(&manifest_path, &v1).unwrap(); common::pic_as(&env) .args(["apply", "--file"]) .arg(&manifest_path) .assert() .success(); let v2 = format!( "[app]\nslug = \"{slug}\"\nname = \"Gate Test\"\n\n\ [[scripts]]\nname = \"keep\"\nfile = \"scripts/keep.rhai\"\n" ); fs::write(&manifest_path, &v2).unwrap(); // `--prune` with no `--yes` and no TTY → refuse, non-zero exit. let out = common::pic_as(&env) .args(["apply", "--file"]) .arg(&manifest_path) .arg("--prune") .output() .expect("apply --prune"); assert!( !out.status.success(), "prune without --yes must refuse non-interactively" ); let err = String::from_utf8_lossy(&out.stderr); assert!( err.contains("--yes"), "refusal should mention --yes:\n{err}" ); // The dropped script must still be there — the gate blocked the delete. let s = scripts_ls(&env, &slug); assert!( s.contains("drop"), "refused prune must not delete anything:\n{s}" ); }