//! M2 declarative tree SHAPE via `pic apply --dir`: //! * a group manifest for a group that does NOT exist yet is CREATED under //! its declared parent, with its content, in one atomic apply, //! * re-applying the same tree is a no-op (the group now exists), //! * changing the declared parent REPARENTS the group on the next apply. use std::fs; use tempfile::TempDir; use crate::common; use crate::common::cleanup::{GroupGuard, ScriptGuard}; /// Write a single-group project dir declaring group `slug` under `parent` with /// one endpoint script. fn group_dir(slug: &str, parent: &str) -> TempDir { let dir = TempDir::new().expect("tempdir"); fs::create_dir_all(dir.path().join("scripts")).unwrap(); fs::write(dir.path().join("scripts/hello.rhai"), r#""hi""#).unwrap(); fs::write( dir.path().join("picloud.toml"), format!( "[group]\nslug = \"{slug}\"\nname = \"Child Group\"\nparent = \"{parent}\"\n\n\ [[scripts]]\nname = \"hello\"\nfile = \"scripts/hello.rhai\"\n" ), ) .unwrap(); dir } #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn tree_apply_creates_then_reparents_a_group() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let parent = common::unique_slug("ts-par"); let child = common::unique_slug("ts-child"); // Guards drop in reverse order: parent (last) ← child (mid) ← script (first). let _gp = GroupGuard::new(&env.url, &env.token, &parent); let _gc = GroupGuard::new(&env.url, &env.token, &child); common::pic_as(&env) .args(["groups", "create", &parent]) .assert() .success(); // --- Create: the child group does NOT exist; apply --dir creates it. --- let dir = group_dir(&child, &parent); let out = common::pic_as(&env) .args(["apply", "--dir"]) .arg(dir.path()) .output() .expect("apply --dir"); assert!( out.status.success(), "tree apply (create) failed: {}", String::from_utf8_lossy(&out.stderr) ); // The group now exists with its script. let groups = ls(&env, &["groups", "ls"]); assert!(groups.contains(&child), "created group missing:\n{groups}"); let scripts = ls(&env, &["scripts", "ls", "--group", &child]); let script_id = scripts .lines() .map(common::cells) .find(|c| c.get(2) == Some(&"hello")) .and_then(|c| c.first().map(|s| (*s).to_string())) .unwrap_or_else(|| panic!("group script `hello` should exist:\n{scripts}")); let _gs = ScriptGuard::new(&env.url, &env.token, &script_id); // Re-applying the same tree is a no-op (group + content unchanged). let plan_out = common::pic_as(&env) .args(["plan", "--dir"]) .arg(dir.path()) .output() .expect("plan --dir"); let plan_txt = String::from_utf8_lossy(&plan_out.stdout).to_lowercase(); assert!( !plan_txt.contains("create") && !plan_txt.contains("delete"), "re-plan of an applied tree must be a no-op:\n{plan_txt}" ); // --- Reparent: move the child under the instance root. Rewrite the SAME // repo's manifest (M3: a stable `.picloud/` project key per repo — a fresh // dir would be a different project and conflict on the owned group). --- fs::write( dir.path().join("picloud.toml"), format!( "[group]\nslug = \"{child}\"\nname = \"Child Group\"\nparent = \"root\"\n\n\ [[scripts]]\nname = \"hello\"\nfile = \"scripts/hello.rhai\"\n" ), ) .unwrap(); let dir2 = &dir; let out = common::pic_as(&env) .args(["apply", "--dir"]) .arg(dir2.path()) .output() .expect("apply --dir reparent"); assert!( out.status.success(), "tree apply (reparent) failed: {}", String::from_utf8_lossy(&out.stderr) ); // Re-plan is a no-op now that the parent matches. let plan_out = common::pic_as(&env) .args(["plan", "--dir"]) .arg(dir2.path()) .output() .expect("plan --dir after reparent"); let plan_txt = String::from_utf8_lossy(&plan_out.stdout).to_lowercase(); assert!( !plan_txt.contains("create") && !plan_txt.contains("delete"), "re-plan after reparent must be a no-op:\n{plan_txt}" ); } fn ls(env: &common::TestEnv, args: &[&str]) -> String { String::from_utf8(common::pic_as(env).args(args).output().expect("ls").stdout).unwrap() }