shepherd-cli 6.8.0

Small installed-plugin diagnostics for the Shepherd workflow
use std::path::Path;
use std::process::{Command, Output};

fn shepherd(root: &Path, args: &[&str]) -> Output {
    Command::new(env!("CARGO_BIN_EXE_shepherd"))
        .args(args)
        .arg("--root")
        .arg(root)
        .output()
        .unwrap()
}

#[test]
fn init_and_lane_create_only_the_focused_workspace() {
    let project = tempfile::tempdir().unwrap();
    let initialized = shepherd(project.path(), &["init"]);
    assert!(
        initialized.status.success(),
        "{}",
        String::from_utf8_lossy(&initialized.stderr)
    );
    assert!(project.path().join(".shepherd/.gitignore").is_file());
    assert!(project.path().join(".shepherd/runs").is_dir());

    let lane = shepherd(
        project.path(),
        &["lane", "--run", "release-1", "--name", "cli"],
    );
    assert!(
        lane.status.success(),
        "{}",
        String::from_utf8_lossy(&lane.stderr)
    );
    let lane_root = project.path().join(".shepherd/runs/release-1/lanes/cli");
    assert!(lane_root.join("plan.md").is_file());
    assert!(lane_root.join("ledger.md").is_file());
    assert!(lane_root.join("evidence").is_dir());
    assert!(
        std::fs::read_to_string(lane_root.join("ledger.md"))
            .unwrap()
            .contains("Status: pending")
    );

    assert!(shepherd(project.path(), &["init"]).status.success());
    assert!(
        shepherd(
            project.path(),
            &["lane", "--run", "release-1", "--name", "cli"]
        )
        .status
        .success()
    );
}

#[test]
fn lane_requires_init_and_safe_identifiers() {
    let project = tempfile::tempdir().unwrap();
    let missing = shepherd(project.path(), &["lane", "--run", "r1", "--name", "docs"]);
    assert!(!missing.status.success());
    assert!(String::from_utf8_lossy(&missing.stderr).contains("is not initialized"));

    assert!(shepherd(project.path(), &["init"]).status.success());
    for args in [
        ["lane", "--run", "../escape", "--name", "docs"],
        ["lane", "--run", "r1", "--name", "a/b"],
    ] {
        let invalid = shepherd(project.path(), &args);
        assert!(!invalid.status.success());
        assert!(String::from_utf8_lossy(&invalid.stderr).contains("must be 1-80 ASCII"));
    }
    assert!(!project.path().join("escape").exists());
}

#[test]
fn generated_files_are_never_overwritten() {
    let project = tempfile::tempdir().unwrap();
    assert!(shepherd(project.path(), &["init"]).status.success());
    let ignore = project.path().join(".shepherd/.gitignore");
    std::fs::write(&ignore, "custom\n").unwrap();
    let conflict = shepherd(project.path(), &["init"]);
    assert!(!conflict.status.success());
    assert_eq!(std::fs::read_to_string(ignore).unwrap(), "custom\n");
}

#[test]
#[cfg(unix)]
fn workspace_paths_cannot_follow_symlinks() {
    use std::os::unix::fs::symlink;

    let project = tempfile::tempdir().unwrap();
    let outside = tempfile::tempdir().unwrap();
    symlink(outside.path(), project.path().join(".shepherd")).unwrap();
    let result = shepherd(project.path(), &["init"]);
    assert!(!result.status.success());
    assert!(String::from_utf8_lossy(&result.stderr).contains("linked"));
    assert_eq!(std::fs::read_dir(outside.path()).unwrap().count(), 0);
}

#[test]
fn edited_lane_records_and_evidence_survive_reinitialization() {
    let project = tempfile::tempdir().unwrap();
    assert!(shepherd(project.path(), &["init"]).status.success());
    let args = ["lane", "--run", "r1", "--name", "508-custody.12"];
    assert!(shepherd(project.path(), &args).status.success());
    let lane = project.path().join(".shepherd/runs/r1/lanes/508-custody.12");
    let ledger = std::fs::read_to_string(lane.join("ledger.md")).unwrap();
    for field in ["Git common directory:", "Full branch ref:", "Base ref and SHA:",
                  "Integration target:", "Tested candidate SHA:", "Accepted plan revision:"] {
        assert!(ledger.contains(field), "missing custody field {field}");
    }
    std::fs::write(lane.join("plan.md"), "accepted edited plan\n").unwrap();
    std::fs::write(lane.join("ledger.md"), "live owner and candidate\n").unwrap();
    std::fs::write(lane.join("evidence/preserved.txt"), "precious evidence\n").unwrap();
    assert!(!shepherd(project.path(), &args).status.success());
    assert_eq!(std::fs::read_to_string(lane.join("plan.md")).unwrap(), "accepted edited plan\n");
    assert_eq!(std::fs::read_to_string(lane.join("ledger.md")).unwrap(), "live owner and candidate\n");
    assert_eq!(std::fs::read_to_string(lane.join("evidence/preserved.txt")).unwrap(), "precious evidence\n");
}