shepherd-cli 6.4.5

The canonical shepherd command-line interface over the per-project registry, run artifacts, and sprint pipeline.
use std::{
    fs,
    path::PathBuf,
    time::{SystemTime, UNIX_EPOCH},
};

use shepherd_cli::{
    content_compiler::{embedded_compile_input, load_compile_input},
    shepherd::compiler::{BudgetClass, BudgetLimits, HarnessProfile, compile, measure_text},
};

#[test]
fn embedded_content_is_the_exact_canonical_authored_corpus() {
    let filesystem = load_compile_input(&content_dir()).expect("load live content");
    let embedded = embedded_compile_input().expect("load embedded content");
    assert_eq!(embedded, filesystem);
}

fn content_dir() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../content")
}

fn fixture(label: &str) -> PathBuf {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock")
        .as_nanos();
    let root = std::env::temp_dir().join(format!("shepherd-content-{label}-{nonce:x}"));
    fs::create_dir_all(root.join("content/roles")).expect("roles");
    fs::create_dir_all(root.join("content/skills/example")).expect("skills");
    root
}

#[test]
fn live_content_matches_the_frozen_target_final_oracle() {
    let input = load_compile_input(&content_dir()).expect("load live content");
    assert_eq!(input.roles.len(), 9);
    assert_eq!(input.skills.len(), 7);
    let oracle: serde_json::Value = serde_json::from_str(include_str!(
        "../../../conformance/content-target-final.json"
    ))
    .expect("target-final oracle is valid JSON");
    assert_eq!(oracle["schema"], "shepherd.content-target-final/1");

    for profile in HarnessProfile::canonical() {
        let tree = compile(&input, &profile).expect("compile live content");
        let target = profile.target.as_str();
        let expected = &oracle["targets"][target];
        let expected_files = expected["files"].as_object().expect("oracle files");
        assert_eq!(tree.roles.len(), 9);
        assert_eq!(tree.files.len(), expected_files.len());
        assert_eq!(
            tree.digest,
            expected["tree_digest"].as_str().unwrap(),
            "{target}"
        );
        for file in &tree.files {
            let frozen = expected_files
                .get(&file.path)
                .unwrap_or_else(|| panic!("{target}: missing frozen path {}", file.path))
                .as_array()
                .expect("[bytes, sha256]");
            assert_eq!(
                file.content.len(),
                usize::try_from(frozen[0].as_u64().expect("byte count"))
                    .expect("byte count fits usize"),
                "{target}: {} byte count",
                file.path
            );
            assert_eq!(
                file.content_sha256,
                frozen[1].as_str().expect("content digest"),
                "{target}: {} digest",
                file.path
            );
        }

        for (role_name, expected_role) in oracle["roles"].as_object().expect("oracle roles") {
            let role = tree
                .roles
                .iter()
                .find(|role| &role.role == role_name)
                .unwrap_or_else(|| panic!("{target}: missing role {role_name}"));
            match target {
                "claude" => assert_eq!(
                    role.model.as_deref(),
                    expected_role["claude_model"].as_str(),
                    "Claude model for {role_name}"
                ),
                "codex" => {
                    assert_eq!(
                        role.profile.as_deref(),
                        expected_role["codex_profile"].as_str(),
                        "Codex profile for {role_name}"
                    );
                    assert_eq!(
                        role.reasoning_effort.as_deref(),
                        expected_role["codex_effort"].as_str(),
                        "Codex effort for {role_name}"
                    );
                }
                "pi" => {
                    assert_eq!(
                        role.model.as_deref(),
                        expected_role["pi_model"].as_str(),
                        "Pi model for {role_name}"
                    );
                    assert_eq!(
                        serde_json::to_value(&role.tools).expect("Pi tools JSON"),
                        expected_role["pi_tools"],
                        "Pi tools for {role_name}"
                    );
                    assert_eq!(
                        serde_json::to_value(&role.unsupported_capabilities)
                            .expect("Pi unsupported JSON"),
                        expected_role["pi_unsupported"],
                        "Pi unsupported capabilities for {role_name}"
                    );
                }
                _ => unreachable!("canonical target"),
            }
        }
    }
}

#[test]
fn codex_target_final_config_bytes_are_frozen() {
    const EXPECTED: &str = "# Generated by the canonical Rust shepherd compiler. Source: content/roles/*.md.\n\
# Do not hand-edit; regenerate via `shepherd compile --target codex --out <directory>`.\n\n\
max_concurrent_children = 3\n\n\
[agent_types]\n\
auditor = \"explorer\"\n\
coder = \"worker\"\n\
conductor = \"worker\"\n\
critic = \"explorer\"\n\
discovery = \"explorer\"\n\
engineer = \"worker\"\n\
planter = \"worker\"\n\
worker = \"worker\"\n\n\
[models]\n\
auditor = \"standard\"\n\
coder = \"standard\"\n\
conductor = \"standard\"\n\
critic = \"standard\"\n\
discovery = \"standard\"\n\
engineer = \"reasoning-high\"\n\
planter = \"reasoning-high\"\n\
worker = \"standard\"\n\n\
[profiles.\"reasoning-high\"]\n\
reasoning_effort = \"high\"\n\n\
[profiles.\"standard\"]\n\
reasoning_effort = \"medium\"\n";

    let input = load_compile_input(&content_dir()).expect("load live content");
    let tree = compile(&input, &HarnessProfile::codex()).expect("compile Codex");
    let config = tree
        .files
        .iter()
        .find(|file| file.path == "shepherd.codex.toml")
        .expect("Codex config");
    assert_eq!(config.content, EXPECTED);
}

#[test]
fn live_authored_entrypoints_respect_every_file_budget() {
    let input = load_compile_input(&content_dir()).expect("load live content");
    let mut violations = Vec::new();
    for (name, class, source) in input
        .roles
        .iter()
        .map(|role| {
            (
                role.source_path.as_str(),
                BudgetClass::Role,
                role.source_content.as_str(),
            )
        })
        .chain(input.skills.iter().map(|skill| {
            (
                skill.source_path.as_str(),
                BudgetClass::Skill,
                skill.source_content.as_str(),
            )
        }))
    {
        let measured = measure_text(source);
        let (lines, words, bytes) = BudgetLimits::for_class(class);
        eprintln!(
            "{name}: {} lines, {} words, {} bytes, {} prompt tokens",
            measured.lines, measured.words, measured.utf8_bytes, measured.prompt_tokens
        );
        if measured.lines > lines || measured.words > words || measured.utf8_bytes > bytes {
            violations.push(format!(
                "{name}: {}/{lines} lines, {}/{words} words, {}/{bytes} bytes",
                measured.lines, measured.words, measured.utf8_bytes
            ));
        }
    }
    assert!(violations.is_empty(), "{}", violations.join("\n"));
}

#[test]
fn malformed_metadata_and_symlinks_fail_closed_without_echoing_content() {
    let root = fixture("invalid");
    let content = root.join("content");
    fs::write(
        content.join("roles/coder.md"),
        "---\nrole: coder\ndescription: [secret]\nsource: agents/coder.md\nmodel_hint: standard\nwrite_eligible: true\ndispatchable: true\ncapabilities: [read]\nwrite_scope: scope\n---\nbody\n",
    )
    .expect("role");
    fs::write(
        content.join("skills/example/SKILL.md"),
        "---\nname: example\ndescription: ok\nsource: x\nportability: cross-harness\n---\nbody\n",
    )
    .expect("skill");
    let error = load_compile_input(&content).expect_err("typed description");
    let message = error.message_text().expect("message");
    assert!(message.contains("invalid role frontmatter"));
    assert!(!message.contains("secret"));

    fs::remove_file(content.join("roles/coder.md")).expect("remove invalid role");
    #[cfg(unix)]
    std::os::unix::fs::symlink(
        content.join("skills/example/SKILL.md"),
        content.join("roles/coder.md"),
    )
    .expect("symlink");
    let error = load_compile_input(&content).expect_err("symlink");
    assert!(error.message_text().expect("message").contains("symlink"));

    fs::remove_dir_all(root).expect("cleanup");
}