use std::path::{Path, PathBuf};
use std::process::Command;
fn checkout() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.unwrap()
}
fn context(temp: &Path, next: &str) -> PathBuf {
let path = temp.join(format!("context-{next}.json"));
let value = serde_json::json!({
"task": "fixture task", "objective": "deliver one reviewed artifact",
"artifact_root": "/tmp/artifact-root", "status": "completed",
"changed_paths_or_commit": "report.csv", "verification": "schema check: exit 0",
"review": "Auditor: pass", "evidence_location": "review.md",
"remaining_work": "none", "next_action": next,
});
std::fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap();
path
}
fn handoff(host: &str, root: &Path, context: &Path, output: Option<&Path>) -> std::process::Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_shepherd"));
command
.args(["handoff", "--host", host, "--plugin-root"])
.arg(root)
.arg("--context")
.arg(context);
if let Some(output) = output {
command.arg("--output").arg(output);
}
command.output().unwrap()
}
#[test]
fn installed_templates_render_identical_handoff_without_writes() {
let root = checkout();
let temp = tempfile::tempdir().unwrap();
let input = context(temp.path(), "return reviewed result");
let before = std::fs::read(&input).unwrap();
let mut outputs = Vec::new();
for (host, plugin) 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 result = handoff(host, &plugin, &input, None);
assert!(
result.status.success(),
"{host}: {}",
String::from_utf8_lossy(&result.stderr)
);
let body = String::from_utf8(result.stdout).unwrap();
assert!(body.contains("fixture task") && body.contains("report.csv"));
assert!(!body.contains("{{") && !body.contains("<completed"));
outputs.push(body);
}
assert!(outputs.windows(2).all(|pair| pair[0] == pair[1]));
assert_eq!(std::fs::read(&input).unwrap(), before);
assert_eq!(std::fs::read_dir(temp.path()).unwrap().count(), 1);
}
#[test]
fn continuation_instructions_follow_supplied_git_and_review_state() {
let root = checkout();
let temp = tempfile::tempdir().unwrap();
let input = context(temp.path(), "return report");
let mut value: serde_json::Value = serde_json::from_slice(&std::fs::read(&input).unwrap()).unwrap();
for supplied in [None, Some(false), Some(true)] {
if supplied == Some(true) {
value["custody"] = serde_json::json!("verified Git tuple");
value["review_episode"] = serde_json::json!("episode 7, one round remaining");
} else if supplied == Some(false) {
value["custody"] = serde_json::Value::Null;
value["review_episode"] = serde_json::Value::Null;
for key in ["branch", "base", "candidate", "date", "rollback", "activation", "reconciliation"] {
value[key] = serde_json::Value::Null;
}
}
std::fs::write(&input, serde_json::to_vec(&value).unwrap()).unwrap();
let result = handoff("claude", &root, &input, None);
assert!(result.status.success());
let body = String::from_utf8(result.stdout).unwrap();
assert_eq!(body.contains("live worktree/common directory"), supplied == Some(true));
assert_eq!(body.contains("Carry the same review episode"), supplied == Some(true));
assert!(!body.contains(": None"));
assert!(body.contains("artifact root"));
if supplied == Some(true) {
assert!(body.contains("verified Git tuple") && body.contains("episode 7, one round remaining"));
}
}
}
#[test]
#[cfg(unix)]
fn installed_template_cannot_escape_plugin_root() {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().unwrap();
let plugin = temp.path().join("plugin");
std::fs::create_dir_all(plugin.join("templates")).unwrap();
std::fs::write(
plugin.join("package.json"),
serde_json::json!({"name":"@pzzld/pi-shepherd","version":env!("CARGO_PKG_VERSION")})
.to_string(),
)
.unwrap();
let outside = temp.path().join("outside.md");
std::fs::write(&outside, "{{ task }}").unwrap();
symlink(&outside, plugin.join("templates/handoff.md")).unwrap();
let input = context(temp.path(), "safe action");
let output = temp.path().join("result.md");
let result = handoff("pi", &plugin, &input, Some(&output));
assert!(!result.status.success());
assert!(String::from_utf8_lossy(&result.stderr).contains("escapes plugin root"));
assert!(!output.exists());
}
#[test]
fn malformed_or_missing_context_fails_before_output() {
let root = checkout();
let temp = tempfile::tempdir().unwrap();
let input = temp.path().join("context.json");
let output = temp.path().join("handoff.md");
std::fs::write(&input, "not json").unwrap();
assert!(
!handoff("claude", &root, &input, Some(&output))
.status
.success()
);
assert!(!output.exists());
std::fs::write(&input, r#"{"task":"only a task"}"#).unwrap();
assert!(
!handoff("claude", &root, &input, Some(&output))
.status
.success()
);
assert!(!output.exists());
}
#[test]
fn explicit_output_is_no_clobber_and_identical_retry_is_noop() {
let root = checkout();
let temp = tempfile::tempdir().unwrap();
let first = context(temp.path(), "first action");
let second = context(temp.path(), "second action");
let output = temp.path().join("handoff.md");
assert!(
handoff("claude", &root, &first, Some(&output))
.status
.success()
);
let bytes = std::fs::read(&output).unwrap();
let modified = std::fs::metadata(&output).unwrap().modified().unwrap();
assert!(
handoff("claude", &root, &first, Some(&output))
.status
.success()
);
assert_eq!(
std::fs::metadata(&output).unwrap().modified().unwrap(),
modified
);
assert!(
!handoff("claude", &root, &second, Some(&output))
.status
.success()
);
assert_eq!(std::fs::read(&output).unwrap(), bytes);
}
#[test]
fn racing_different_outputs_leave_one_complete_artifact() {
let root = checkout();
let temp = tempfile::tempdir().unwrap();
let first = context(temp.path(), "first action");
let second = context(temp.path(), "second action");
let output = temp.path().join("handoff.md");
let expected_first = handoff("pi", &root.join("packages/harness-pi"), &first, None).stdout;
let expected_second = handoff("pi", &root.join("packages/harness-pi"), &second, None).stdout;
let mut a = Command::new(env!("CARGO_BIN_EXE_shepherd"));
a.args(["handoff", "--host", "pi", "--plugin-root"])
.arg(root.join("packages/harness-pi"))
.arg("--context")
.arg(&first)
.arg("--output")
.arg(&output);
let mut b = Command::new(env!("CARGO_BIN_EXE_shepherd"));
b.args(["handoff", "--host", "pi", "--plugin-root"])
.arg(root.join("packages/harness-pi"))
.arg("--context")
.arg(&second)
.arg("--output")
.arg(&output);
let mut child_a = a.spawn().unwrap();
let mut child_b = b.spawn().unwrap();
let status_a = child_a.wait().unwrap();
let status_b = child_b.wait().unwrap();
assert_ne!(status_a.success(), status_b.success());
let actual = std::fs::read(&output).unwrap();
assert!(actual == expected_first || actual == expected_second);
}