shepherd-compiler 6.4.5

Pure, deterministic Shepherd content compiler and prompt-budget engine.
use std::fs;

use shepherd_compiler::content::{ContentError, embedded_compile_input, load_compile_input};

#[test]
fn embedded_content_is_the_canonical_compile_input() {
    let input = embedded_compile_input().expect("canonical content");
    assert!(!input.roles.is_empty());
    assert!(!input.skills.is_empty());
    assert!(
        input
            .roles
            .iter()
            .any(|role| role.source_path == "content/roles/coder.md")
    );
    assert!(
        input
            .skills
            .iter()
            .any(|skill| skill.source_path == "content/skills/thinking/SKILL.md")
    );
    assert!(
        input
            .roles
            .iter()
            .all(|role| !role.source_content.is_empty())
    );
    assert!(
        input
            .skills
            .iter()
            .all(|skill| !skill.source_content.is_empty())
    );
}

#[test]
fn filesystem_loader_uses_the_same_typed_contract() {
    let root =
        std::env::temp_dir().join(format!("shepherd-compiler-content-{}", std::process::id()));
    let _ = fs::remove_dir_all(&root);
    fs::create_dir_all(root.join("roles")).expect("roles directory");
    fs::create_dir_all(root.join("skills/thinking")).expect("skills directory");
    fs::write(
        root.join("roles/coder.md"),
        "---\nrole: coder\ndescription: A test role.\nsource: test\nmodel_hint: standard\nwrite_eligible: true\ndispatchable: true\ncapabilities: [read]\nwrite_scope: test\n---\n\nBody\n",
    )
    .expect("role source");
    fs::write(
        root.join("skills/thinking/SKILL.md"),
        "---\nname: thinking\ndescription: A test skill.\nsource: test\nportability: cross-harness\n---\n\nSkill body\n",
    )
    .expect("skill source");

    let input = load_compile_input(&root).expect("fixture content");
    assert_eq!(input.roles[0].role, "coder");
    assert_eq!(input.skills[0].name, "thinking");
    assert!(input.roles[0].source_path.ends_with("roles/coder.md"));
    assert!(
        input.skills[0]
            .source_path
            .ends_with("skills/thinking/SKILL.md")
    );
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn malformed_canonical_content_fails_closed() {
    let root = std::env::temp_dir().join(format!(
        "shepherd-compiler-content-invalid-{}",
        std::process::id()
    ));
    let _ = fs::remove_dir_all(&root);
    fs::create_dir_all(root.join("roles")).expect("roles directory");
    fs::create_dir_all(root.join("skills/thinking")).expect("skills directory");
    fs::write(root.join("roles/coder.md"), "not frontmatter").expect("role source");
    fs::write(
        root.join("skills/thinking/SKILL.md"),
        "---\nname: thinking\ndescription: A test skill.\nsource: test\nportability: cross-harness\n---\n\nSkill body\n",
    )
    .expect("skill source");

    let error = load_compile_input(&root).expect_err("invalid role must fail");
    assert!(matches!(error, ContentError::InvalidFrontmatter { .. }));
    let _ = fs::remove_dir_all(&root);
}

#[test]
fn filesystem_loader_accepts_crlf_authored_content() {
    let root = std::env::temp_dir().join(format!(
        "shepherd-compiler-content-crlf-{}",
        std::process::id()
    ));
    let _ = fs::remove_dir_all(&root);
    fs::create_dir_all(root.join("roles")).expect("roles directory");
    fs::create_dir_all(root.join("skills/thinking")).expect("skills directory");
    fs::write(
        root.join("roles/coder.md"),
        "---\r\nrole: coder\r\ndescription: A test role.\r\nsource: test\r\nmodel_hint: standard\r\nwrite_eligible: true\r\ndispatchable: true\r\ncapabilities: [read]\r\nwrite_scope: test\r\n---\r\n\r\nBody\r\n",
    )
    .expect("role source");
    fs::write(
        root.join("skills/thinking/SKILL.md"),
        "---\r\nname: thinking\r\ndescription: A test skill.\r\nsource: test\r\nportability: cross-harness\r\n---\r\n\r\nSkill body\r\n",
    )
    .expect("skill source");

    let input = load_compile_input(&root).expect("CRLF fixture content");
    assert!(input.roles[0].source_path.ends_with("roles/coder.md"));
    assert!(
        input.skills[0]
            .source_path
            .ends_with("skills/thinking/SKILL.md")
    );
    let _ = fs::remove_dir_all(&root);
}