use std::path::{Path, PathBuf};
use std::process::Command;
fn checkout() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.unwrap()
}
fn run(host: &str, root: &Path) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_shepherd"))
.args(["doctor", "--host", host, "--plugin-root"])
.arg(root)
.arg("--json")
.output()
.unwrap()
}
#[test]
fn actual_source_carriers_are_diagnosable_without_runtime() {
let root = checkout();
for (host, path) in [
("claude", root.clone()),
("codex", root.join("plugins/shepherd")),
("claude", root.join("packages/harness-claude")),
("codex", root.join("packages/harness-codex")),
("pi", root.join("packages/harness-pi")),
] {
let output = run(host, &path);
assert!(
output.status.success(),
"{host}: {}",
String::from_utf8_lossy(&output.stdout)
);
let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["ok"], true, "{host}: {report}");
assert_eq!(report["helper_version"], env!("CARGO_PKG_VERSION"));
assert_eq!(report["plugin_version"], env!("CARGO_PKG_VERSION"));
if host == "pi" {
assert!(
report["warnings"]
.as_array()
.unwrap()
.iter()
.any(|warning| {
warning
.as_str()
.unwrap()
.contains("separately verified dispatch extension")
})
);
assert!(!report["findings"].to_string().contains("pi.subagents"));
}
}
}
#[test]
fn corrupt_manifest_and_escaping_declared_path_are_rejected() {
let temp = tempfile::tempdir().unwrap();
let claude = temp.path().join("claude");
std::fs::create_dir_all(claude.join(".claude-plugin")).unwrap();
std::fs::write(claude.join(".claude-plugin/plugin.json"), "{").unwrap();
let corrupt = run("claude", &claude);
assert!(!corrupt.status.success());
assert!(String::from_utf8_lossy(&corrupt.stdout).contains("not valid JSON"));
let codex = temp.path().join("codex");
std::fs::create_dir_all(codex.join(".codex-plugin")).unwrap();
std::fs::write(codex.join(".codex-plugin/plugin.json"),
serde_json::json!({"name":"shepherd","version":env!("CARGO_PKG_VERSION"),"skills":"../outside","hooks":"../outside/hooks.json"}).to_string()).unwrap();
let escaped = run("codex", &codex);
assert!(!escaped.status.success());
assert!(String::from_utf8_lossy(&escaped.stdout).contains("must stay inside plugin root"));
}
#[test]
fn corrupt_installed_skill_is_not_silently_skipped() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(
temp.path().join("package.json"),
serde_json::json!({"name":"@pzzld/claude-shepherd","version":env!("CARGO_PKG_VERSION")})
.to_string(),
)
.unwrap();
let skill = temp.path().join("skills/shepherd/SKILL.md");
std::fs::create_dir_all(skill.parent().unwrap()).unwrap();
std::fs::write(&skill, [0xff, 0xfe]).unwrap();
let result = run("claude", temp.path());
assert!(!result.status.success());
assert!(String::from_utf8_lossy(&result.stdout).contains("installed skill is not UTF-8"));
}
#[test]
#[cfg(unix)]
fn installed_hook_must_be_registered_and_executable() {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(temp.path().join(".claude-plugin")).unwrap();
std::fs::create_dir_all(temp.path().join("hooks/scripts")).unwrap();
std::fs::write(
temp.path().join(".claude-plugin/plugin.json"),
serde_json::json!({"name":"shepherd","version":env!("CARGO_PKG_VERSION")}).to_string(),
)
.unwrap();
std::fs::write(
temp.path().join("hooks/hooks.json"),
r#"{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"bash hooks/scripts/seed_preflight_check.sh","timeout":5}]}]}}"#,
)
.unwrap();
let script = temp.path().join("hooks/scripts/seed_preflight_check.sh");
std::fs::write(&script, "#!/bin/sh\nexit 0\n").unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o644)).unwrap();
let result = run("claude", temp.path());
assert!(!result.status.success());
let text = String::from_utf8_lossy(&result.stdout);
assert!(text.contains("does not register the bounded Write seed check"));
assert!(text.contains("installed hook is not executable"));
}
#[test]
fn extra_event_duplicate_hook_and_echo_only_command_are_not_healthy() {
let temp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(temp.path().join(".claude-plugin")).unwrap();
std::fs::create_dir_all(temp.path().join("hooks/scripts")).unwrap();
std::fs::write(
temp.path().join(".claude-plugin/plugin.json"),
serde_json::json!({"name":"shepherd","version":env!("CARGO_PKG_VERSION")}).to_string(),
)
.unwrap();
std::fs::write(
temp.path().join("hooks/scripts/seed_preflight_check.sh"),
"#!/bin/sh\nexit 0\n",
)
.unwrap();
let expected = serde_json::json!({
"description": "focused seed check",
"hooks": {"PreToolUse": [{"matcher": "Write", "hooks": [{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scripts/seed_preflight_check.sh\"",
"timeout": 5
}]}]}
});
let mut extra_event = expected.clone();
extra_event["hooks"]["SessionStart"] = serde_json::json!([]);
let mut duplicate = expected.clone();
duplicate["hooks"]["PreToolUse"][0]["hooks"]
.as_array_mut()
.unwrap()
.push(expected["hooks"]["PreToolUse"][0]["hooks"][0].clone());
let mut echo_only = expected;
echo_only["hooks"]["PreToolUse"][0]["hooks"][0]["command"] =
serde_json::json!("echo hooks/scripts/seed_preflight_check.sh");
for candidate in [extra_event, duplicate, echo_only] {
std::fs::write(
temp.path().join("hooks/hooks.json"),
serde_json::to_vec(&candidate).unwrap(),
)
.unwrap();
let output = run("claude", temp.path());
let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
report["findings"]
.as_array()
.unwrap()
.iter()
.any(|finding| {
finding
.as_str()
.unwrap()
.contains("does not register the bounded Write seed check")
}),
"hook false positive for {candidate}"
);
}
}
#[test]
fn root_only_roles_are_not_registered_as_children() {
let temp = tempfile::tempdir().unwrap();
let claude = temp.path().join("claude");
std::fs::create_dir_all(claude.join(".claude-plugin")).unwrap();
std::fs::create_dir_all(claude.join("agents")).unwrap();
std::fs::write(
claude.join(".claude-plugin/plugin.json"),
serde_json::json!({"name":"shepherd","version":env!("CARGO_PKG_VERSION")}).to_string(),
)
.unwrap();
std::fs::write(claude.join("agents/shepherd.md"), "root only").unwrap();
let claude_output = run("claude", &claude);
assert!(
String::from_utf8_lossy(&claude_output.stdout)
.contains("unexpected child agent: shepherd.md")
);
let pi = temp.path().join("pi");
std::fs::create_dir_all(pi.join("agents")).unwrap();
std::fs::write(pi.join("package.json"), serde_json::json!({"name":"@pzzld/pi-shepherd","version":env!("CARGO_PKG_VERSION"),"pi":{"skills":["./skills"],"prompts":["./prompts"]}}).to_string()).unwrap();
std::fs::write(pi.join("agents/planter.md"), "root-local only").unwrap();
let pi_output = run("pi", &pi);
assert!(
String::from_utf8_lossy(&pi_output.stdout).contains("unexpected child agent: planter.md")
);
}
#[test]
fn missing_optional_jq_is_warning_not_blanket_failure() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(temp.path().join("jq"), "not executable").unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_shepherd"))
.args(["doctor", "--host", "claude", "--plugin-root"])
.arg(checkout())
.arg("--json")
.env("PATH", temp.path())
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stdout)
);
let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["ok"], true);
assert!(
report["warnings"]
.as_array()
.unwrap()
.iter()
.any(|warning| { warning.as_str().unwrap().contains("jq is absent") })
);
}
#[test]
fn wrong_version_is_actionable_and_read_only() {
let temp = tempfile::tempdir().unwrap();
let manifest = temp.path().join(".claude-plugin/plugin.json");
std::fs::create_dir_all(manifest.parent().unwrap()).unwrap();
std::fs::write(&manifest, r#"{"name":"shepherd","version":"6.7.3"}"#).unwrap();
let before = std::fs::read(&manifest).unwrap();
let output = run("claude", temp.path());
assert!(!output.status.success());
let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
report["findings"]
.as_array()
.unwrap()
.iter()
.any(|finding| {
finding.as_str().unwrap().contains("6.7.3")
&& finding
.as_str()
.unwrap()
.contains(env!("CARGO_PKG_VERSION"))
})
);
assert_eq!(std::fs::read(&manifest).unwrap(), before);
}
#[test]
fn absent_plugin_and_relative_root_are_failures() {
let temp = tempfile::tempdir().unwrap();
let missing = run("pi", temp.path());
assert!(!missing.status.success());
let report: serde_json::Value = serde_json::from_slice(&missing.stdout).unwrap();
assert_eq!(report["ok"], false);
assert!(!report["findings"].as_array().unwrap().is_empty());
let relative = run("claude", Path::new("relative-plugin"));
assert!(!relative.status.success());
let report: serde_json::Value = serde_json::from_slice(&relative.stdout).unwrap();
assert!(
report["findings"]
.as_array()
.unwrap()
.iter()
.any(|finding| { finding.as_str().unwrap().contains("absolute") })
);
}