shepherd-cli 6.6.1

The canonical shepherd command-line interface over the per-project registry, run artifacts, and sprint pipeline.
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");
    // Best effort, and deliberately not unwrapped. A command that rejects its
    // arguments exits before it ever reads stdin, so this write races the
    // child's exit: once the read end is gone the write returns EPIPE on Linux
    // while the same call still succeeds into the pipe buffer elsewhere. That
    // is why `cli_has_no_child_start_or_resume_command` failed only on
    // ubuntu/default and passed on ubuntu/full and both Windows matrices --
    // the other legs won the race rather than exercising a different contract.
    // Every assertion here is about the child's exit status and stderr, never
    // about the write landing, and dropping the handle still closes stdin so a
    // command that DOES read is not left blocking.
    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");
}