use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};
fn binary() -> &'static str {
env!("CARGO_BIN_EXE_shepherd")
}
fn repository(label: &str) -> PathBuf {
let suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_nanos();
let root = std::env::temp_dir().join(format!("shepherd-dispatch-cli-{label}-{suffix:x}"));
std::fs::create_dir_all(root.join(".shepherd/runs/v645")).expect("namespace");
assert!(
Command::new("git")
.args(["init", "--quiet"])
.current_dir(&root)
.status()
.expect("git init")
.success()
);
std::fs::write(
root.join(".shepherd/project.json"),
br#"{"id":"018f47ce-72d7-7f64-9eb1-2f651d521c2a"}"#,
)
.expect("project");
std::fs::write(
root.join(".shepherd/runs/v645/run.json"),
br#"{"run":"v645","status":"executing"}"#,
)
.expect("run");
root
}
fn run(root: &Path, args: &[&str], input: &[u8]) -> Output {
let mut child = Command::new(binary())
.args(args)
.current_dir(root)
.env("SHEPHERD_HOME", root.join("home"))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn");
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(input);
}
child.wait_with_output().expect("wait")
}
#[test]
fn cli_keeps_root_bootstrap_and_identity_resolution() {
let root = repository("root");
let bind = run(
&root,
&["dispatch", "bind-root"],
br#"{"schema":"shepherd.dispatch-request/1","run":"v645","harness":"pi","session_id":"root-session","role_carrier":"shepherd:shepherd","mode":"execution","lease_ms":60000}"#,
);
assert!(
bind.status.success(),
"{}",
String::from_utf8_lossy(&bind.stderr)
);
std::fs::write(
root.join(".shepherd/runs/v645/run.json"),
br#"{"run":"v645","status":"planned"}"#,
)
.expect("deactivate bound run");
std::fs::create_dir_all(root.join(".shepherd/runs/v646")).expect("second run");
std::fs::write(
root.join(".shepherd/runs/v646/run.json"),
br#"{"run":"v646","status":"executing"}"#,
)
.expect("activate another run");
let rebound = run(
&root,
&["dispatch", "bind-root"],
br#"{"schema":"shepherd.dispatch-request/1","run":"v645","harness":"pi","session_id":"root-session","role_carrier":"shepherd:shepherd","mode":"execution","lease_ms":60000}"#,
);
assert!(
!rebound.status.success(),
"an execution binding must not survive the selected run moving to planned"
);
assert!(
String::from_utf8_lossy(&rebound.stderr).contains("root mode `execution`"),
"mode mismatch denial must be explicit: {}",
String::from_utf8_lossy(&rebound.stderr)
);
std::fs::write(
root.join(".shepherd/runs/v646/run.json"),
br#"{"run":"v646","status":"planned"}"#,
)
.expect("deactivate second run");
std::fs::write(
root.join(".shepherd/runs/v645/run.json"),
br#"{"run":"v645","status":"executing"}"#,
)
.expect("restore bound active run");
let resolve = run(
&root,
&["dispatch", "resolve"],
br#"{"schema":"shepherd.dispatch-request/1","run":"v645","harness":"pi","session_id":"root-session"}"#,
);
assert!(
resolve.status.success(),
"{}",
String::from_utf8_lossy(&resolve.stderr)
);
let response: serde_json::Value = serde_json::from_slice(&resolve.stdout).expect("response");
assert_eq!(response["role"], "shepherd");
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn cli_has_no_child_start_or_resume_command() {
let root = repository("removed-child-entry");
let start = run(&root, &["dispatch", "start"], b"{}");
assert!(!start.status.success());
let resume = run(&root, &["dispatch", "resume"], b"{}");
assert!(!resume.status.success());
let output = format!(
"{}{}",
String::from_utf8_lossy(&start.stderr),
String::from_utf8_lossy(&resume.stderr)
);
assert!(!output.contains("token"));
assert!(!output.contains("protected descriptor"));
std::fs::remove_dir_all(root).expect("cleanup");
}