use std::path::PathBuf;
use std::process::Command;
use serde_json::Value;
use tempfile::TempDir;
fn bin(home: &TempDir) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_orchestratectl"));
cmd.env("ORCHESTRATECTL_HOME", home.path());
cmd.env("HOME", home.path());
cmd
}
fn mk_home() -> TempDir {
tempfile::tempdir().expect("tempdir")
}
#[test]
fn skill_list_json_pins_catalog_shape() {
let home = mk_home();
let out = bin(&home)
.args(["skill", "list", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "exit: {:?}", out.status);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
assert_eq!(v["schema_version"], 1);
let skills = v["data"]["skills"].as_array().expect("skills array");
let mut names: Vec<&str> = skills.iter().map(|s| s["name"].as_str().unwrap()).collect();
names.sort_unstable();
assert_eq!(
names,
vec![
"fan-out",
"octl-run-overview",
"octl-spawn-spinoff",
"orchestratectl-overview",
"stint-handoff",
"stint-start",
"worktree",
"worktree-bug-analysis",
"worktree-merge",
"worktree-research",
"worktree-spinoff",
"worktree-status",
"worktree-technical-decision",
]
);
for s in skills {
assert!(
!s["description"].as_str().unwrap_or("").is_empty(),
"empty description for {}",
s["name"]
);
}
}
#[test]
fn skill_show_text_prints_skill_md_contents() {
let home = mk_home();
let out = bin(&home)
.args(["--output", "text", "skill", "show", "octl-run-overview"])
.output()
.expect("spawn");
assert!(out.status.success(), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf8");
assert!(
stdout.starts_with("---\nname: octl-run-overview"),
"show did not emit frontmatter: {stdout:?}"
);
}
#[test]
fn skill_show_json_wraps_content_under_data() {
let home = mk_home();
let out = bin(&home)
.args(["skill", "show", "octl-run-overview", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "exit: {:?}", out.status);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
assert_eq!(v["schema_version"], 1);
assert_eq!(v["data"]["name"], "octl-run-overview");
let content = v["data"]["content"].as_str().expect("content str");
assert!(content.starts_with("---\nname: octl-run-overview"));
}
#[test]
fn skill_install_refuses_overwrite_then_force_succeeds() {
let home = mk_home();
let dest = home.path().join("SKILL.md");
let out = bin(&home)
.args(["skill", "install", "octl-run-overview", "--dest"])
.arg(&dest)
.output()
.expect("spawn");
assert!(out.status.success(), "first install failed: {out:?}");
assert!(dest.exists(), "destination not created");
let out = bin(&home)
.args(["skill", "install", "octl-run-overview", "--dest"])
.arg(&dest)
.output()
.expect("spawn");
assert_eq!(out.status.code(), Some(2), "expected exit 2");
let err: Value = serde_json::from_slice(&out.stderr).expect("json err envelope");
assert_eq!(err["schema_version"], 1);
assert_eq!(err["error"]["code"], "refused_overwrite");
assert_eq!(err["error"]["invalid_value"], dest.display().to_string());
let out = bin(&home)
.args(["skill", "install", "octl-run-overview", "--dest"])
.arg(&dest)
.arg("--force")
.output()
.expect("spawn");
assert!(out.status.success(), "force install failed: {out:?}");
}
#[test]
fn skill_install_with_default_paths_writes_under_home() {
let home = mk_home();
let out = bin(&home)
.args(["skill", "install", "octl-run-overview", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "default install failed: {out:?}");
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let installed = v["data"]["installed"].as_array().expect("installed array");
let claude = installed
.iter()
.find(|f| f["agent"] == "claude")
.expect("claude entry");
let expected: PathBuf = home
.path()
.join(".claude/skills/octl-run-overview/SKILL.md");
assert_eq!(claude["path"], expected.display().to_string());
assert!(expected.exists(), "claude install not on disk");
}
#[test]
fn skill_install_force_prunes_orphan_companion_file() {
let home = mk_home();
let first = bin(&home)
.args(["skill", "install", "stint-start", "--output", "json"])
.output()
.expect("spawn");
assert!(first.status.success(), "first install failed: {first:?}");
let skill_dir = home.path().join(".claude/skills/stint-start");
let marker = skill_dir.join(".orchestratectl-managed");
let orphan = skill_dir.join("OLD-COMPANION.md");
std::fs::write(&orphan, "stale companion\n").expect("write orphan");
let mut marker_body = std::fs::read_to_string(&marker).expect("read marker");
marker_body.push_str("companion: OLD-COMPANION.md\n");
std::fs::write(&marker, marker_body).expect("append marker");
let real_companion = skill_dir.join("AGENTS-EXECUTION-DAG.md");
assert!(
real_companion.exists(),
"bundled companion missing after install"
);
let out = bin(&home)
.args([
"skill",
"install",
"stint-start",
"--force",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "force reinstall failed: {out:?}");
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let pruned: Vec<&str> = v["data"]["pruned_companions"]
.as_array()
.expect("pruned_companions array")
.iter()
.map(|e| e.as_str().unwrap())
.collect();
assert_eq!(pruned, vec!["stint-start/OLD-COMPANION.md"]);
assert!(!orphan.exists(), "orphan companion not removed by --force");
assert!(real_companion.exists(), "bundled companion wrongly removed");
let marker_after = std::fs::read_to_string(&marker).expect("read marker after");
assert!(
!marker_after.contains("OLD-COMPANION.md"),
"marker still records the pruned orphan: {marker_after:?}"
);
}
#[test]
fn codex_install_writes_provenance_marker() {
let home = mk_home();
let out = bin(&home)
.args([
"skill",
"install",
"stint-start",
"--agent",
"codex",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "codex install failed: {out:?}");
assert!(home.path().join(".codex/prompts/stint-start.md").exists());
assert!(home
.path()
.join(".codex/prompts/_shared/AGENTS-EXECUTION-DAG.md")
.exists());
let marker = home
.path()
.join(".codex/prompts/_shared/.orchestratectl-managed");
let body = std::fs::read_to_string(&marker).expect("codex marker not written");
assert!(
body.contains("managed-by: orchestratectl"),
"marker: {body}"
);
assert!(body.contains("prompt: stint-start"), "marker: {body}");
assert!(
body.contains("companion: AGENTS-EXECUTION-DAG.md"),
"marker: {body}"
);
}
#[test]
fn codex_force_prunes_orphan_prompt_and_companion() {
let home = mk_home();
let first = bin(&home)
.args(["skill", "install", "--agent", "codex", "--output", "json"])
.output()
.expect("spawn");
assert!(
first.status.success(),
"first codex install failed: {first:?}"
);
let prompts = home.path().join(".codex/prompts");
let shared = prompts.join("_shared");
let marker = shared.join(".orchestratectl-managed");
let orphan_prompt = prompts.join("gone-skill.md");
let orphan_companion = shared.join("OLD-SHARED.md");
std::fs::write(&orphan_prompt, "stale prompt\n").unwrap();
std::fs::write(&orphan_companion, "stale shared\n").unwrap();
let mut marker_body = std::fs::read_to_string(&marker).unwrap();
marker_body.push_str("prompt: gone-skill\n");
marker_body.push_str("companion: OLD-SHARED.md\n");
std::fs::write(&marker, marker_body).unwrap();
let real_companion = shared.join("AGENTS-EXECUTION-DAG.md");
assert!(real_companion.exists(), "bundled codex companion missing");
let out = bin(&home)
.args([
"skill", "install", "--agent", "codex", "--force", "--output", "json",
])
.output()
.expect("spawn");
assert!(
out.status.success(),
"force codex reinstall failed: {out:?}"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let pruned: Vec<&str> = v["data"]["pruned"]
.as_array()
.expect("pruned array")
.iter()
.map(|e| e.as_str().unwrap())
.collect();
assert!(
pruned.contains(&"gone-skill"),
"prompt not pruned: {pruned:?}"
);
let pruned_companions: Vec<&str> = v["data"]["pruned_companions"]
.as_array()
.expect("pruned_companions array")
.iter()
.map(|e| e.as_str().unwrap())
.collect();
assert!(
pruned_companions.contains(&"_shared/OLD-SHARED.md"),
"companion not pruned: {pruned_companions:?}"
);
assert!(!orphan_prompt.exists(), "orphan codex prompt not removed");
assert!(
!orphan_companion.exists(),
"orphan codex companion not removed"
);
assert!(
real_companion.exists(),
"bundled codex companion wrongly removed"
);
let marker_after = std::fs::read_to_string(&marker).unwrap();
assert!(
!marker_after.contains("gone-skill") && !marker_after.contains("OLD-SHARED.md"),
"marker still records a pruned orphan: {marker_after:?}"
);
}
#[test]
fn skill_install_agent_all_installs_to_both_default_paths() {
let home = mk_home();
let out = bin(&home)
.args([
"skill",
"install",
"octl-spawn-spinoff",
"--agent",
"all",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "agent=all install failed: {out:?}");
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let installed = v["data"]["installed"].as_array().expect("installed");
let agents: Vec<&str> = installed
.iter()
.map(|f| f["agent"].as_str().unwrap())
.collect();
assert!(agents.contains(&"claude"));
assert!(agents.contains(&"codex"));
assert!(home
.path()
.join(".claude/skills/octl-spawn-spinoff/SKILL.md")
.exists());
assert!(home
.path()
.join(".codex/prompts/octl-spawn-spinoff.md")
.exists());
}
#[test]
fn skill_install_no_name_installs_every_skill() {
let home = mk_home();
let out = bin(&home)
.args(["skill", "install", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install-all failed: {out:?}");
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let installed = v["data"]["installed"].as_array().expect("installed");
let names: Vec<&str> = installed
.iter()
.map(|f| f["name"].as_str().unwrap())
.collect();
assert!(names.contains(&"octl-run-overview"));
assert!(names.contains(&"octl-spawn-spinoff"));
}
#[test]
fn skill_install_agent_all_with_dest_is_rejected() {
let home = mk_home();
let dest = home.path().join("SKILL.md");
let out = bin(&home)
.args([
"skill",
"install",
"octl-run-overview",
"--agent",
"all",
"--dest",
])
.arg(&dest)
.output()
.expect("spawn");
assert_eq!(out.status.code(), Some(1), "expected user-error exit 1");
let err: Value = serde_json::from_slice(&out.stderr).expect("err json");
assert_eq!(err["error"]["code"], "invalid_arguments");
}
#[test]
fn skill_install_partial_failure_is_preflighted() {
let home = mk_home();
let codex = home.path().join(".codex/prompts");
std::fs::create_dir_all(&codex).unwrap();
std::fs::write(codex.join("octl-run-overview.md"), "pre-existing").unwrap();
let out = bin(&home)
.args(["skill", "install", "octl-run-overview", "--agent", "all"])
.output()
.expect("spawn");
assert_eq!(out.status.code(), Some(2));
let err: Value = serde_json::from_slice(&out.stderr).expect("err json");
assert_eq!(err["error"]["code"], "refused_overwrite");
assert!(!home
.path()
.join(".claude/skills/octl-run-overview/SKILL.md")
.exists());
}
#[test]
fn skill_install_accepts_bare_relative_dest() {
let home = mk_home();
let out = bin(&home)
.current_dir(home.path())
.args([
"skill",
"install",
"octl-run-overview",
"--dest",
"SKILL.md",
])
.output()
.expect("spawn");
assert!(out.status.success(), "bare-dest install failed: {out:?}");
assert!(home.path().join("SKILL.md").exists());
}
#[test]
fn skill_show_unknown_emits_skill_not_found() {
let home = mk_home();
let out = bin(&home)
.args(["skill", "show", "no-such-skill"])
.output()
.expect("spawn");
assert_eq!(out.status.code(), Some(1));
let err: Value = serde_json::from_slice(&out.stderr).expect("json err envelope");
assert_eq!(err["error"]["code"], "skill_not_found");
assert_eq!(err["error"]["invalid_value"], "no-such-skill");
}
#[test]
fn skill_print_default_streams_skill_md_byte_identically() {
let home = mk_home();
let print_out = bin(&home)
.args(["skill", "print", "orchestratectl-overview"])
.output()
.expect("spawn");
assert!(print_out.status.success(), "exit: {:?}", print_out.status);
let dest = home.path().join("printed.md");
let install_out = bin(&home)
.args(["skill", "install", "orchestratectl-overview", "--dest"])
.arg(&dest)
.output()
.expect("spawn");
assert!(install_out.status.success());
let on_disk = std::fs::read(&dest).expect("read installed");
assert_eq!(
print_out.stdout, on_disk,
"skill print stdout must equal skill install on-disk bytes"
);
}
#[test]
fn skill_print_json_payload_pins_schema() {
let home = mk_home();
let out = bin(&home)
.args([
"skill",
"print",
"orchestratectl-overview",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "exit: {:?}", out.status);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
assert_eq!(v["schema_version"], 1);
let data = &v["data"];
assert_eq!(data["name"], "orchestratectl-overview");
assert_eq!(data["schema_version"], 1);
assert_eq!(data["schema_version_skill"], 1);
assert_eq!(
data["cli_version"].as_str().unwrap(),
env!("CARGO_PKG_VERSION")
);
assert!(data["content"]
.as_str()
.unwrap()
.starts_with("---\nname: orchestratectl-overview"));
assert!(data["path_in_repo"]
.as_str()
.unwrap()
.contains("SKILL.template.md"));
}
#[test]
fn skill_print_unknown_emits_skill_not_found() {
let home = mk_home();
let out = bin(&home)
.args(["skill", "print", "no-such-skill"])
.output()
.expect("spawn");
assert_eq!(out.status.code(), Some(1));
let err: Value = serde_json::from_slice(&out.stderr).expect("err envelope");
assert_eq!(err["error"]["code"], "skill_not_found");
}
#[test]
fn skill_install_over_older_version_warns_and_succeeds_without_force() {
let home = mk_home();
let dest = home.path().join("SKILL.md");
std::fs::write(
&dest,
"---\nname: orchestratectl-overview\ndescription: old\ncli_version: \"0.0.0\"\nschema_version: 1\n---\n",
)
.unwrap();
let out = bin(&home)
.args(["skill", "install", "orchestratectl-overview", "--dest"])
.arg(&dest)
.args(["--output", "json"])
.output()
.expect("spawn");
assert!(
out.status.success(),
"install over older must succeed: {out:?}"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let warnings = v["warnings"].as_array().expect("warnings array");
assert!(
warnings
.iter()
.any(|w| w.as_str().unwrap().contains("skill_version_drift")
&& w.as_str().unwrap().contains("0.0.0")),
"expected skill_version_drift warning naming 0.0.0; got {warnings:?}"
);
let after = std::fs::read_to_string(&dest).unwrap();
assert!(after.contains(&format!("cli_version: \"{}\"", env!("CARGO_PKG_VERSION"))));
}
#[test]
fn skill_install_over_newer_version_refuses_with_skill_version_too_new() {
let home = mk_home();
let dest = home.path().join("SKILL.md");
std::fs::write(
&dest,
"---\nname: orchestratectl-overview\ndescription: future\ncli_version: \"99.0.0\"\nschema_version: 1\n---\n",
)
.unwrap();
let out = bin(&home)
.args(["skill", "install", "orchestratectl-overview", "--dest"])
.arg(&dest)
.output()
.expect("spawn");
assert_eq!(out.status.code(), Some(2), "expected exit 2");
let err: Value = serde_json::from_slice(&out.stderr).expect("err envelope");
assert_eq!(err["error"]["code"], "skill_version_too_new");
let out = bin(&home)
.args(["skill", "install", "orchestratectl-overview", "--dest"])
.arg(&dest)
.arg("--force")
.output()
.expect("spawn");
assert!(out.status.success(), "--force install must succeed");
}
const MARKER: &str = ".orchestratectl-managed";
fn write_marker(dir: &std::path::Path, skill_name: &str) {
std::fs::write(
dir.join(MARKER),
format!("managed-by: orchestratectl\ncli_version: 9.9.9\nskill_name: {skill_name}\n"),
)
.unwrap();
}
#[test]
fn skill_install_default_stamps_provenance_marker() {
let home = mk_home();
assert!(bin(&home)
.args(["skill", "install", "octl-run-overview"])
.output()
.expect("spawn")
.status
.success());
let marker = home
.path()
.join(".claude/skills/octl-run-overview")
.join(MARKER);
assert!(
marker.is_file(),
"provenance marker not written next to SKILL.md"
);
let body = std::fs::read_to_string(&marker).unwrap();
assert!(
body.contains("managed-by: orchestratectl"),
"marker: {body}"
);
assert!(
body.contains("skill_name: octl-run-overview"),
"marker: {body}"
);
}
#[test]
fn skill_install_default_dual_homes_into_pi() {
let home = mk_home();
let out = bin(&home)
.args(["skill", "install", "stint-start", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "default install failed: {out:?}");
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let installed = v["data"]["installed"].as_array().expect("installed array");
let agents: Vec<&str> = installed
.iter()
.map(|f| f["agent"].as_str().unwrap())
.collect();
assert!(
agents.contains(&"claude"),
"claude entry missing: {agents:?}"
);
assert!(agents.contains(&"pi"), "pi entry missing: {agents:?}");
let claude = home.path().join(".claude/skills/stint-start/SKILL.md");
let pi = home.path().join(".pi/agent/skills/stint-start/SKILL.md");
assert!(claude.exists(), "claude SKILL.md not on disk");
assert!(pi.exists(), "pi SKILL.md not on disk");
assert_eq!(
std::fs::read(&claude).unwrap(),
std::fs::read(&pi).unwrap(),
"pi mirror must be byte-identical to the claude SKILL.md"
);
let claude_companion = home
.path()
.join(".claude/skills/stint-start/AGENTS-EXECUTION-DAG.md");
let pi_companion = home
.path()
.join(".pi/agent/skills/stint-start/AGENTS-EXECUTION-DAG.md");
assert!(
claude_companion.exists(),
"companion missing from claude dir"
);
assert!(
pi_companion.exists(),
"companion must be mirrored into the pi dir"
);
assert_eq!(
std::fs::read(&claude_companion).unwrap(),
std::fs::read(&pi_companion).unwrap(),
"pi companion must be byte-identical to the claude companion"
);
assert!(
!home
.path()
.join(".pi/agent/skills/stint-start")
.join(MARKER)
.is_file(),
"pi mirror must not carry the claude provenance marker"
);
let record: Value = serde_json::from_slice(
&std::fs::read(env_orch_state_record(&home)).expect("provenance record"),
)
.expect("record json");
assert!(
record["skills"]["stint-start"]["files"]["AGENTS-EXECUTION-DAG.md"]["sha256"].is_string(),
"companion hash must be recorded under its owning skill: {record}"
);
assert_eq!(
record["skills"]["stint-start"]["files"]["AGENTS-EXECUTION-DAG.md"]["kind"], "companion",
"companion file recorded with kind=companion: {record}"
);
assert_eq!(
record["skills"]["stint-start"]["files"]["SKILL.md"]["kind"], "skill",
"body file recorded with kind=skill: {record}"
);
}
#[test]
fn skill_install_force_reconciles_dropped_pi_companion() {
let home = mk_home();
assert!(bin(&home)
.args(["skill", "install", "stint-start"])
.output()
.expect("spawn")
.status
.success());
let pi_dir = home.path().join(".pi/agent/skills/stint-start");
let record_path = env_orch_state_record(&home);
let real = pi_dir.join("AGENTS-EXECUTION-DAG.md");
let real_bytes = std::fs::read(&real).unwrap();
let orphan = pi_dir.join("OLD-COMPANION.md");
std::fs::write(&orphan, &real_bytes).unwrap();
let mut prov: Value = serde_json::from_slice(&std::fs::read(&record_path).unwrap()).unwrap();
let real_hash =
prov["skills"]["stint-start"]["files"]["AGENTS-EXECUTION-DAG.md"]["sha256"].clone();
prov["skills"]["stint-start"]["files"]["OLD-COMPANION.md"] =
serde_json::json!({ "sha256": real_hash, "kind": "companion" });
std::fs::write(&record_path, serde_json::to_string_pretty(&prov).unwrap()).unwrap();
let out = bin(&home)
.args([
"skill",
"install",
"stint-start",
"--force",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "force redeploy failed: {out:?}");
assert!(
!orphan.exists(),
"dropped pi companion must be removed on --force"
);
assert!(real.exists(), "the still-bundled companion is kept");
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let pruned_companions: Vec<&str> = v["data"]["pruned_companions"]
.as_array()
.expect("pruned_companions array")
.iter()
.map(|p| p.as_str().unwrap())
.collect();
assert!(
pruned_companions.contains(&"stint-start/OLD-COMPANION.md"),
"orphan pi companion must be reported: {pruned_companions:?}"
);
let after: Value = serde_json::from_slice(&std::fs::read(&record_path).unwrap()).unwrap();
assert!(
after["skills"]["stint-start"]["files"]
.get("OLD-COMPANION.md")
.is_none(),
"reconciled companion must be dropped from the record: {after}"
);
assert!(
after["skills"]["stint-start"]["files"]["AGENTS-EXECUTION-DAG.md"]["sha256"].is_string(),
"the bundled companion stays tracked"
);
}
#[test]
fn skill_install_force_relinquishes_diverged_dropped_pi_companion() {
let home = mk_home();
assert!(bin(&home)
.args(["skill", "install", "stint-start"])
.output()
.expect("spawn")
.status
.success());
let pi_dir = home.path().join(".pi/agent/skills/stint-start");
let record_path = env_orch_state_record(&home);
let orphan = pi_dir.join("OLD-COMPANION.md");
std::fs::write(&orphan, "user has since edited this\n").unwrap();
let mut prov: Value = serde_json::from_slice(&std::fs::read(&record_path).unwrap()).unwrap();
prov["skills"]["stint-start"]["files"]["OLD-COMPANION.md"] = serde_json::json!({
"sha256": "00000000000000000000000000000000000000000000000000000000deadbeef",
"kind": "companion"
});
std::fs::write(&record_path, serde_json::to_string_pretty(&prov).unwrap()).unwrap();
let out = bin(&home)
.args([
"skill",
"install",
"stint-start",
"--force",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "install failed: {out:?}");
assert!(
orphan.exists(),
"a companion whose bytes don't match the record is left on disk"
);
let after: Value = serde_json::from_slice(&std::fs::read(&record_path).unwrap()).unwrap();
assert!(
after["skills"]["stint-start"]["files"]
.get("OLD-COMPANION.md")
.is_none(),
"a diverged orphan is relinquished (dropped from tracking): {after}"
);
}
fn env_orch_state_record(home: &tempfile::TempDir) -> std::path::PathBuf {
home.path().join("state/pi-installed-skills.json")
}
#[test]
fn skill_install_repairs_missing_claude_when_pi_mirror_is_current() {
let home = mk_home();
assert!(bin(&home)
.args(["skill", "install", "stint-start"])
.output()
.expect("spawn")
.status
.success());
let claude = home.path().join(".claude/skills/stint-start/SKILL.md");
let pi = home.path().join(".pi/agent/skills/stint-start/SKILL.md");
assert!(claude.exists() && pi.exists());
let pi_bytes_before = std::fs::read(&pi).unwrap();
std::fs::remove_dir_all(home.path().join(".claude/skills/stint-start")).unwrap();
let out = bin(&home)
.args(["skill", "install", "stint-start", "--output", "json"])
.output()
.expect("spawn");
assert!(
out.status.success(),
"plain re-install must repair claude despite a current pi mirror: {out:?}"
);
assert!(claude.exists(), "claude skill was not repaired");
assert_eq!(
std::fs::read(&pi).unwrap(),
pi_bytes_before,
"pi mirror must be left untouched on a non-force run"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let agents: Vec<&str> = v["data"]["installed"]
.as_array()
.unwrap()
.iter()
.map(|f| f["agent"].as_str().unwrap())
.collect();
assert!(
agents.contains(&"claude"),
"claude not in installed: {agents:?}"
);
assert!(
!agents.contains(&"pi"),
"an untouched pi mirror must not be reported as installed: {agents:?}"
);
}
#[test]
fn skill_install_self_repairs_missing_pi_mirror_without_force() {
let home = mk_home();
assert!(bin(&home)
.args(["skill", "install", "stint-start"])
.output()
.expect("spawn")
.status
.success());
let pi = home.path().join(".pi/agent/skills/stint-start/SKILL.md");
std::fs::remove_dir_all(home.path().join(".pi/agent/skills/stint-start")).unwrap();
assert!(!pi.exists());
assert!(bin(&home)
.args(["skill", "install", "stint-start", "--force"])
.output()
.expect("spawn")
.status
.success());
assert!(pi.exists(), "pi mirror was not recreated by --force");
}
#[test]
fn skill_install_force_refreshes_stale_pi_mirror() {
let home = mk_home();
assert!(bin(&home)
.args(["skill", "install", "stint-start"])
.output()
.expect("spawn")
.status
.success());
let pi = home.path().join(".pi/agent/skills/stint-start/SKILL.md");
std::fs::write(
&pi,
"---\nname: stint-start\ncli_version: \"0.0.0\"\n---\nstale\n",
)
.unwrap();
let out = bin(&home)
.args([
"skill",
"install",
"stint-start",
"--force",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "force redeploy failed: {out:?}");
let after = std::fs::read_to_string(&pi).unwrap();
assert!(
after.contains(&format!("cli_version: \"{}\"", env!("CARGO_PKG_VERSION"))),
"pi mirror was not refreshed to the binary version under --force"
);
}
#[test]
fn skill_install_does_not_clobber_divergent_pi_mirror_without_force() {
let home = mk_home();
let pi_dir = home.path().join(".pi/agent/skills/stint-start");
std::fs::create_dir_all(&pi_dir).unwrap();
let pi = pi_dir.join("SKILL.md");
let user_content = "---\nname: stint-start\ncli_version: \"0.0.0\"\n---\nMINE — do not touch\n";
std::fs::write(&pi, user_content).unwrap();
let out = bin(&home)
.args(["skill", "install", "stint-start", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install must succeed: {out:?}");
assert_eq!(
std::fs::read_to_string(&pi).unwrap(),
user_content,
"divergent pi file must not be clobbered without --force"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let warnings = v["warnings"].as_array().expect("warnings");
assert!(
warnings
.iter()
.any(|w| w.as_str().unwrap().contains("pi_mirror_skipped")),
"expected a pi_mirror_skipped warning; got {warnings:?}"
);
}
#[test]
fn skill_install_agent_all_also_dual_homes_into_pi() {
let home = mk_home();
let out = bin(&home)
.args([
"skill",
"install",
"octl-spawn-spinoff",
"--agent",
"all",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "agent=all install failed: {out:?}");
assert!(home
.path()
.join(".pi/agent/skills/octl-spawn-spinoff/SKILL.md")
.exists());
}
#[test]
fn skill_install_agent_codex_does_not_dual_home_into_pi() {
let home = mk_home();
let out = bin(&home)
.args([
"skill",
"install",
"octl-run-overview",
"--agent",
"codex",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "codex install failed: {out:?}");
assert!(
!home.path().join(".pi/agent/skills").exists(),
"codex-only install must not create the pi skill dir"
);
}
#[test]
fn skill_install_dest_does_not_dual_home_into_pi() {
let home = mk_home();
let dest = home.path().join("custom/SKILL.md");
let out = bin(&home)
.args(["skill", "install", "octl-run-overview", "--dest"])
.arg(&dest)
.args(["--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "dest install failed: {out:?}");
assert!(dest.exists(), "dest SKILL.md not on disk");
assert!(
!home.path().join(".pi/agent/skills").exists(),
"--dest install must not create the pi skill dir"
);
}
#[test]
fn skill_install_all_prunes_managed_orphan() {
let home = mk_home();
let orphan = home.path().join(".claude/skills/gone-skill");
std::fs::create_dir_all(&orphan).unwrap();
std::fs::write(orphan.join("SKILL.md"), "---\nname: gone-skill\n---\n").unwrap();
write_marker(&orphan, "gone-skill");
let out = bin(&home)
.args(["skill", "install", "--force", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install-all failed: {out:?}");
assert!(!orphan.exists(), "managed orphan was not pruned");
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let pruned: Vec<&str> = v["data"]["pruned"]
.as_array()
.expect("pruned array")
.iter()
.map(|p| p.as_str().unwrap())
.collect();
assert!(pruned.contains(&"gone-skill"), "pruned list: {pruned:?}");
let warnings = v["warnings"].as_array().expect("warnings");
assert!(
warnings
.iter()
.any(|w| w.as_str().unwrap().contains("skill_pruned")
&& w.as_str().unwrap().contains("gone-skill")),
"expected skill_pruned warning; got {warnings:?}"
);
}
#[test]
fn skill_install_all_spares_unmanaged_same_name_dir() {
let home = mk_home();
let user_skill = home.path().join(".claude/skills/my-own-skill");
std::fs::create_dir_all(&user_skill).unwrap();
std::fs::write(
user_skill.join("SKILL.md"),
"---\nname: my-own-skill\n---\nmine\n",
)
.unwrap();
let out = bin(&home)
.args(["skill", "install", "--force", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install-all failed: {out:?}");
assert!(
user_skill.join("SKILL.md").exists(),
"unmanaged user skill was deleted — provenance guard failed"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let pruned = v["data"]["pruned"].as_array().expect("pruned array");
assert!(
pruned.is_empty(),
"unmanaged dir must not appear in pruned: {pruned:?}"
);
}
#[test]
fn skill_install_all_keeps_registered_skills() {
let home = mk_home();
assert!(bin(&home)
.args(["skill", "install", "--force"])
.output()
.expect("spawn")
.status
.success());
let registered = home.path().join(".claude/skills/octl-run-overview");
assert!(registered.join("SKILL.md").exists());
assert!(registered.join(MARKER).is_file());
let out = bin(&home)
.args(["skill", "install", "--force", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "second install failed: {out:?}");
assert!(
registered.join("SKILL.md").exists(),
"still-registered skill was pruned"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
assert!(v["data"]["pruned"].as_array().expect("pruned").is_empty());
}
#[test]
fn skill_install_named_does_not_prune() {
let home = mk_home();
let orphan = home.path().join(".claude/skills/gone-skill");
std::fs::create_dir_all(&orphan).unwrap();
std::fs::write(orphan.join("SKILL.md"), "---\nname: gone-skill\n---\n").unwrap();
write_marker(&orphan, "gone-skill");
let out = bin(&home)
.args([
"skill",
"install",
"octl-run-overview",
"--force",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "named install failed: {out:?}");
assert!(
orphan.exists(),
"targeted install pruned an orphan — must be scoped to install-all"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
assert!(v["data"]["pruned"].as_array().expect("pruned").is_empty());
}
#[test]
fn skill_install_all_without_force_does_not_prune() {
let home = mk_home();
let orphan = home.path().join(".claude/skills/gone-skill");
std::fs::create_dir_all(&orphan).unwrap();
std::fs::write(orphan.join("SKILL.md"), "---\nname: gone-skill\n---\n").unwrap();
write_marker(&orphan, "gone-skill");
let out = bin(&home)
.args(["skill", "install", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install-all failed: {out:?}");
assert!(orphan.exists(), "prune ran without --force");
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
assert!(v["data"]["pruned"].as_array().expect("pruned").is_empty());
}
#[test]
fn skill_install_all_spares_copied_and_renamed_managed_skill() {
let home = mk_home();
let copy = home.path().join(".claude/skills/my-worktree");
std::fs::create_dir_all(©).unwrap();
std::fs::write(copy.join("SKILL.md"), "---\nname: my-worktree\n---\nmine\n").unwrap();
write_marker(©, "worktree");
let out = bin(&home)
.args(["skill", "install", "--force", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install-all failed: {out:?}");
assert!(
copy.join("SKILL.md").exists(),
"a copied-and-renamed managed skill was deleted — name-binding guard failed"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
assert!(v["data"]["pruned"].as_array().expect("pruned").is_empty());
}
#[cfg(unix)]
#[test]
fn skill_install_all_does_not_follow_symlinked_orphan() {
let home = mk_home();
let outside = home.path().join("precious");
std::fs::create_dir_all(&outside).unwrap();
std::fs::write(outside.join("SKILL.md"), "important user data\n").unwrap();
write_marker(&outside, "evil");
let skills = home.path().join(".claude/skills");
std::fs::create_dir_all(&skills).unwrap();
std::os::unix::fs::symlink(&outside, skills.join("evil")).unwrap();
let out = bin(&home)
.args(["skill", "install", "--force", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install-all failed: {out:?}");
assert!(
outside.join("SKILL.md").exists(),
"remove_dir_all followed a symlink and deleted an outside directory"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
assert!(v["data"]["pruned"].as_array().expect("pruned").is_empty());
}
#[test]
fn skill_install_stint_start_writes_companion_resource_for_claude() {
let home = mk_home();
let out = bin(&home)
.args(["skill", "install", "stint-start", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install failed: {out:?}");
let skill_md = home.path().join(".claude/skills/stint-start/SKILL.md");
let companion = home
.path()
.join(".claude/skills/stint-start/AGENTS-EXECUTION-DAG.md");
assert!(skill_md.exists(), "SKILL.md not installed");
assert!(
companion.exists(),
"companion AGENTS-EXECUTION-DAG.md not installed alongside SKILL.md"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let installed = v["data"]["installed"].as_array().expect("installed array");
let paths: Vec<&str> = installed
.iter()
.map(|f| f["path"].as_str().unwrap())
.collect();
assert!(
paths.iter().any(|p| p.ends_with("AGENTS-EXECUTION-DAG.md")),
"companion not reported in install payload: {paths:?}"
);
}
#[test]
fn skill_install_stint_start_codex_writes_companion_to_shared_and_rewrites_link() {
let home = mk_home();
let out = bin(&home)
.args([
"skill",
"install",
"stint-start",
"--agent",
"codex",
"--output",
"json",
])
.output()
.expect("spawn");
assert!(out.status.success(), "codex install failed: {out:?}");
let prompt = home.path().join(".codex/prompts/stint-start.md");
assert!(prompt.exists(), "flat codex prompt not installed");
assert!(
home.path()
.join(".codex/prompts/_shared/AGENTS-EXECUTION-DAG.md")
.exists(),
"companion not installed into the codex _shared/ subdir"
);
assert!(
!home
.path()
.join(".codex/prompts/AGENTS-EXECUTION-DAG.md")
.exists(),
"companion must not leak into the flat codex prompts dir as a bogus prompt"
);
let body = std::fs::read_to_string(&prompt).unwrap();
assert!(
body.contains("](_shared/AGENTS-EXECUTION-DAG.md)"),
"codex body link was not rewritten to the _shared/ target"
);
assert!(
!body.contains("](AGENTS-EXECUTION-DAG.md)"),
"codex body still carries the un-rewritten claude sibling link"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let installed = v["data"]["installed"].as_array().expect("installed array");
let paths: Vec<&str> = installed
.iter()
.map(|f| f["path"].as_str().unwrap())
.collect();
assert!(
paths
.iter()
.any(|p| p.ends_with("_shared/AGENTS-EXECUTION-DAG.md")),
"companion not reported in codex install payload: {paths:?}"
);
}
#[test]
fn skill_install_stint_handoff_codex_rewrites_cross_skill_link() {
let home = mk_home();
let out = bin(&home)
.args(["skill", "install", "stint-handoff", "--agent", "codex"])
.output()
.expect("spawn");
assert!(out.status.success(), "codex install failed: {out:?}");
let body =
std::fs::read_to_string(home.path().join(".codex/prompts/stint-handoff.md")).unwrap();
assert!(
body.contains("](_shared/AGENTS-EXECUTION-DAG.md)"),
"cross-skill codex link was not rewritten to the _shared/ target"
);
assert!(
!body.contains("](../stint-start/AGENTS-EXECUTION-DAG.md)"),
"codex body still carries the un-rewritten cross-skill link"
);
}
#[test]
fn skill_install_all_codex_writes_shared_companion_once() {
let home = mk_home();
let out = bin(&home)
.args(["skill", "install", "--agent", "all"])
.output()
.expect("spawn");
assert!(out.status.success(), "install --agent all failed: {out:?}");
assert!(
home.path()
.join(".codex/prompts/_shared/AGENTS-EXECUTION-DAG.md")
.exists(),
"codex shared companion missing after --agent all"
);
let claude_body =
std::fs::read_to_string(home.path().join(".claude/skills/stint-start/SKILL.md")).unwrap();
assert!(
claude_body.contains("](AGENTS-EXECUTION-DAG.md)"),
"claude body sibling link must be preserved verbatim"
);
assert!(
home.path()
.join(".claude/skills/stint-start/AGENTS-EXECUTION-DAG.md")
.exists(),
"claude sibling companion missing after --agent all"
);
}
#[test]
fn skill_install_over_older_companion_upgrades_without_force() {
let home = mk_home();
assert!(bin(&home)
.args(["skill", "install", "stint-start"])
.output()
.expect("spawn")
.status
.success());
let skill_md = home.path().join(".claude/skills/stint-start/SKILL.md");
let companion = home
.path()
.join(".claude/skills/stint-start/AGENTS-EXECUTION-DAG.md");
std::fs::write(
&skill_md,
"---\nname: stint-start\ndescription: old\ncli_version: \"0.0.0\"\nschema_version: 1\n---\n",
)
.unwrap();
std::fs::write(
&companion,
"---\ncli_version: \"0.0.0\"\nschema_version: 1\n---\nstale\n",
)
.unwrap();
let out = bin(&home)
.args(["skill", "install", "stint-start"])
.output()
.expect("spawn");
assert!(
out.status.success(),
"redeploy over an older companion must not require --force: {out:?}"
);
let after = std::fs::read_to_string(&companion).unwrap();
assert!(
after.contains(&format!("cli_version: \"{}\"", env!("CARGO_PKG_VERSION"))),
"companion was not upgraded to the binary version"
);
}
fn pi_provenance_path(home: &TempDir) -> PathBuf {
home.path().join("state").join("pi-installed-skills.json")
}
fn read_provenance(home: &TempDir) -> Value {
let body = std::fs::read_to_string(pi_provenance_path(home)).expect("provenance record");
serde_json::from_str(&body).expect("provenance json")
}
#[test]
fn skill_install_writes_pi_provenance_record() {
let home = mk_home();
assert!(bin(&home)
.args(["skill", "install", "stint-start"])
.output()
.expect("spawn")
.status
.success());
let prov = read_provenance(&home);
assert_eq!(prov["schema_version"], 3);
let rec = &prov["skills"]["stint-start"];
assert!(rec.is_object(), "stint-start not recorded: {prov}");
let sha = rec["files"]["SKILL.md"]["sha256"].as_str().expect("sha256");
assert_eq!(sha.len(), 64, "sha256 must be 32-byte hex");
assert!(sha.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(rec["files"]["SKILL.md"]["kind"], "skill");
assert_eq!(
rec["cli_version"].as_str().unwrap(),
env!("CARGO_PKG_VERSION")
);
}
fn seed_deregistered_pi_mirror(home: &TempDir, fake: &str, diverge: bool) -> PathBuf {
assert!(bin(home)
.args(["skill", "install", "stint-start"])
.output()
.expect("spawn")
.status
.success());
let real_mirror = home.path().join(".pi/agent/skills/stint-start/SKILL.md");
let real_bytes = std::fs::read(&real_mirror).unwrap();
let mut prov = read_provenance(home);
let recorded_hash = prov["skills"]["stint-start"]["files"]["SKILL.md"]["sha256"]
.as_str()
.unwrap()
.to_string();
let fake_dir = home.path().join(".pi/agent/skills").join(fake);
std::fs::create_dir_all(&fake_dir).unwrap();
let fake_mirror = fake_dir.join("SKILL.md");
if diverge {
std::fs::write(&fake_mirror, b"user has taken this over\n").unwrap();
} else {
std::fs::write(&fake_mirror, &real_bytes).unwrap();
}
prov["skills"][fake] = serde_json::json!({
"cli_version": "0.0.1",
"files": { "SKILL.md": { "sha256": recorded_hash, "kind": "skill" } },
});
std::fs::write(
pi_provenance_path(home),
serde_json::to_string_pretty(&prov).unwrap(),
)
.unwrap();
fake_mirror
}
#[test]
fn skill_install_force_prunes_deregistered_pi_mirror() {
let home = mk_home();
let fake_mirror = seed_deregistered_pi_mirror(&home, "gone-skill", false);
assert!(fake_mirror.exists());
let out = bin(&home)
.args(["skill", "install", "--force", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install-all --force failed: {out:?}");
assert!(!fake_mirror.exists(), "de-registered pi mirror not pruned");
assert!(
!fake_mirror.parent().unwrap().exists(),
"emptied per-skill dir should be cleaned up"
);
let prov = read_provenance(&home);
assert!(
prov["skills"].get("gone-skill").is_none(),
"gone-skill still tracked: {prov}"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let pruned: Vec<&str> = v["data"]["pruned"]
.as_array()
.expect("pruned")
.iter()
.map(|p| p.as_str().unwrap())
.collect();
assert!(pruned.contains(&"gone-skill"), "pruned: {pruned:?}");
let warnings = v["warnings"].as_array().expect("warnings");
assert!(
warnings
.iter()
.any(|w| w.as_str().unwrap().contains("pi_mirror_pruned")
&& w.as_str().unwrap().contains("gone-skill")),
"expected pi_mirror_pruned warning; got {warnings:?}"
);
}
#[test]
fn skill_install_force_preserves_diverged_pi_mirror() {
let home = mk_home();
let fake_mirror = seed_deregistered_pi_mirror(&home, "gone-skill", true);
let out = bin(&home)
.args(["skill", "install", "--force", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install-all --force failed: {out:?}");
assert!(
fake_mirror.exists(),
"a user-edited (diverged) pi mirror must NOT be deleted"
);
let prov = read_provenance(&home);
assert!(
prov["skills"].get("gone-skill").is_none(),
"diverged mirror should be dropped from tracking"
);
let v: Value = serde_json::from_slice(&out.stdout).expect("json");
let warnings = v["warnings"].as_array().expect("warnings");
assert!(
warnings
.iter()
.any(|w| w.as_str().unwrap().contains("pi_mirror_diverged")),
"expected pi_mirror_diverged warning; got {warnings:?}"
);
let pruned = v["data"]["pruned"].as_array().expect("pruned");
assert!(!pruned.iter().any(|p| p == "gone-skill"));
}
#[test]
fn skill_install_fails_closed_on_corrupt_pi_provenance() {
let home = mk_home();
std::fs::create_dir_all(home.path().join("state")).unwrap();
std::fs::write(pi_provenance_path(&home), "{ this is not json").unwrap();
let claude = home.path().join(".claude/skills/stint-start/SKILL.md");
let out = bin(&home)
.args(["skill", "install", "stint-start", "--output", "json"])
.output()
.expect("spawn");
assert!(
!out.status.success(),
"corrupt record must fail the install"
);
let v: Value = serde_json::from_slice(&out.stderr).expect("json");
assert_eq!(v["error"]["code"], "pi_provenance_corrupt");
assert!(
!claude.exists(),
"no file should be written when the record is rejected pre-write"
);
}
#[test]
fn skill_install_without_force_does_not_prune_pi_mirror() {
let home = mk_home();
let fake_dir = home.path().join(".pi/agent/skills/gone-skill");
std::fs::create_dir_all(&fake_dir).unwrap();
let fake_mirror = fake_dir.join("SKILL.md");
std::fs::write(&fake_mirror, b"stale de-registered body\n").unwrap();
std::fs::create_dir_all(home.path().join("state")).unwrap();
std::fs::write(
pi_provenance_path(&home),
serde_json::to_string_pretty(&serde_json::json!({
"schema_version": 1,
"skills": { "gone-skill": { "sha256": "deadbeef", "cli_version": "0.0.1" } },
}))
.unwrap(),
)
.unwrap();
let out = bin(&home)
.args(["skill", "install", "--output", "json"])
.output()
.expect("spawn");
assert!(out.status.success(), "install-all failed: {out:?}");
assert!(fake_mirror.exists(), "pi prune ran without --force");
let prov = read_provenance(&home);
assert!(prov["skills"].get("gone-skill").is_some());
}