use std::fmt::Write as _;
use std::path::PathBuf;
use std::process::Command;
use serde_json::Value;
use sha2::{Digest, Sha256};
use tempfile::TempDir;
fn sha256_hex(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
}
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:?}");
}
#[cfg(unix)]
#[test]
fn skill_install_force_replaces_dangling_symlink() {
let home = mk_home();
let dest = home.path().join("SKILL.md");
std::os::unix::fs::symlink(home.path().join("missing-SKILL.md"), &dest)
.expect("create dangling symlink");
assert!(
std::fs::symlink_metadata(&dest)
.expect("link metadata")
.file_type()
.is_symlink(),
"fixture must be a symlink"
);
assert!(!dest.exists(), "fixture must be dangling");
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:?}");
assert!(
std::fs::symlink_metadata(&dest)
.expect("installed file metadata")
.file_type()
.is_file(),
"dangling symlink was not replaced"
);
assert!(
std::fs::read_to_string(&dest)
.expect("installed body")
.contains("name: octl-run-overview"),
"installed body missing"
);
}
#[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 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");
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 forced_full_install_prunes_retired_dag_companion_from_all_mirrors() {
let home = mk_home();
assert!(bin(&home)
.args(["skill", "install", "--agent", "all"])
.output()
.unwrap()
.status
.success());
let retired_name = "AGENTS-EXECUTION-DAG.md";
let retired_bytes = b"retired managed companion\n";
let claude_dir = home.path().join(".claude/skills/stint-start");
std::fs::write(claude_dir.join(retired_name), retired_bytes).unwrap();
let claude_marker = claude_dir.join(".orchestratectl-managed");
let mut marker = std::fs::read_to_string(&claude_marker).unwrap();
writeln!(marker, "companion: {retired_name}").expect("writing to a String cannot fail");
std::fs::write(&claude_marker, marker).unwrap();
let pi_dir = home.path().join(".pi/agent/skills/stint-start");
std::fs::write(pi_dir.join(retired_name), retired_bytes).unwrap();
let pi_record = env_orch_state_record(&home);
let mut record: Value = serde_json::from_slice(&std::fs::read(&pi_record).unwrap()).unwrap();
record["skills"]["stint-start"]["files"][retired_name] = serde_json::json!({
"sha256": sha256_hex(retired_bytes),
"kind": "companion"
});
std::fs::write(&pi_record, serde_json::to_vec_pretty(&record).unwrap()).unwrap();
let codex_shared = home.path().join(".codex/prompts/_shared");
std::fs::write(codex_shared.join(retired_name), retired_bytes).unwrap();
let codex_marker = codex_shared.join(".orchestratectl-managed");
let mut marker = std::fs::read_to_string(&codex_marker).unwrap();
writeln!(marker, "companion: {retired_name}").expect("writing to a String cannot fail");
std::fs::write(&codex_marker, marker).unwrap();
let doctor = bin(&home)
.args(["doctor", "--output", "json"])
.output()
.unwrap();
let doctor: Value = serde_json::from_slice(&doctor.stdout).unwrap();
let checks = doctor["data"]["checks"].as_array().unwrap();
for id in [
"skill.orphan.stint-start.AGENTS-EXECUTION-DAG.md",
"skill.orphan.stint-start.pi.AGENTS-EXECUTION-DAG.md",
"skill.orphan.codex._shared.AGENTS-EXECUTION-DAG.md",
] {
let check = checks
.iter()
.find(|c| c["id"] == id)
.unwrap_or_else(|| panic!("doctor did not report retired companion {id}: {checks:?}"));
assert_eq!(check["status"], "warn", "{id}: {check:?}");
assert!(
check["fix_suggestion"]
.as_str()
.is_some_and(|s| s.contains("--force")),
"{id}: {check:?}"
);
}
assert!(bin(&home)
.args(["skill", "install", "--force"])
.output()
.unwrap()
.status
.success());
assert!(bin(&home)
.args(["skill", "install", "--agent", "codex", "--force"])
.output()
.unwrap()
.status
.success());
for retired in [
claude_dir.join(retired_name),
pi_dir.join(retired_name),
codex_shared.join(retired_name),
] {
assert!(
!retired.exists(),
"retired companion survived at {}",
retired.display()
);
}
assert!(!std::fs::read_to_string(claude_marker)
.unwrap()
.contains(retired_name));
assert!(!std::fs::read_to_string(codex_marker)
.unwrap()
.contains(retired_name));
let record: Value = serde_json::from_slice(&std::fs::read(pi_record).unwrap()).unwrap();
assert!(record["skills"]["stint-start"]["files"]
.get(retired_name)
.is_none());
let doctor = bin(&home)
.args(["doctor", "--output", "json"])
.output()
.unwrap();
let doctor: Value = serde_json::from_slice(&doctor.stdout).unwrap();
assert!(!doctor["data"]["checks"]
.as_array()
.unwrap()
.iter()
.any(|c| c["id"].as_str().is_some_and(|id| id.contains(retired_name))));
}
#[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(),
"retired DAG companion must not be installed for codex"
);
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("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 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"
);
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"
);
for retired in [
home.path()
.join(".claude/skills/stint-start/AGENTS-EXECUTION-DAG.md"),
home.path()
.join(".pi/agent/skills/stint-start/AGENTS-EXECUTION-DAG.md"),
] {
assert!(
!retired.exists(),
"retired DAG companion must not be installed: {}",
retired.display()
);
}
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_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 orphan = pi_dir.join("OLD-COMPANION.md");
let orphan_bytes = b"former bundled companion\n";
std::fs::write(&orphan, orphan_bytes).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": sha256_hex(orphan_bytes),
"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"
);
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}"
);
}
#[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());
}
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());
}