//! Bound-plan staleness: `pic plan` records a state token under `.picloud/`, //! and a later `pic apply` refuses (without `--force`) if the app changed //! out-of-band since the plan was reviewed. use std::fs; use tempfile::TempDir; use crate::common; use crate::common::cleanup::AppGuard; #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn apply_refuses_when_state_moved_since_plan() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let slug = common::unique_slug("stale"); 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/hello.rhai"), "let x = 1; x").unwrap(); let manifest_path = dir.path().join("picloud.toml"); let manifest = format!( "[app]\nslug = \"{slug}\"\nname = \"Stale\"\n\n\ [[scripts]]\nname = \"hello\"\nfile = \"scripts/hello.rhai\"\n" ); fs::write(&manifest_path, &manifest).unwrap(); // Establish hello, then plan — the plan records the current state token. apply(&env, &manifest_path).assert().success(); common::pic_as(&env) .args(["plan", "--file"]) .arg(&manifest_path) .assert() .success(); assert!( dir.path().join(".picloud/plan.json").exists(), "plan must record the bound-plan token under .picloud/" ); // Out-of-band change: deploy an extra script the manifest doesn't mention. fs::write(dir.path().join("scripts/sneaky.rhai"), "let y = 2; y").unwrap(); common::pic_as(&env) .args(["scripts", "deploy"]) .arg(dir.path().join("scripts/sneaky.rhai")) .args(["--app", &slug]) .assert() .success(); // Apply must now refuse — the live state no longer matches the plan. let refused = apply(&env, &manifest_path).output().expect("apply"); assert!( !refused.status.success(), "apply must refuse after out-of-band change" ); let err = String::from_utf8_lossy(&refused.stderr); assert!( err.contains("changed since") || err.contains("pic plan"), "refusal should explain the staleness:\n{err}" ); // `--force` bypasses the check and applies anyway. common::pic_as(&env) .args(["apply", "--file"]) .arg(&manifest_path) .arg("--force") .assert() .success(); } fn apply(env: &common::TestEnv, manifest_path: &std::path::Path) -> assert_cmd::Command { let mut cmd = common::pic_as(env); cmd.args(["apply", "--file"]).arg(manifest_path); cmd }