use std::{fs, path::PathBuf};
use shepherd_compiler::content::{ContentError, embedded_compile_input, load_compile_input};
const CANONICAL_ROLE_STARTUP_SKILLS: [(&str, &str); 9] = [
("auditor", "reviewing"),
("coder", "implementing"),
("conductor", "lane-execution"),
("critic", "reviewing"),
("discovery", "researching"),
("engineer", "planning"),
("planter", "planting"),
("shepherd", "shepherd"),
("worker", "artifact-work"),
];
#[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 authored_planning_flock_contract_is_rooted_bounded_and_lane_immutable() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../content");
let planning =
fs::read_to_string(root.join("skills/planning/SKILL.md")).expect("planning skill source");
let plan_contract =
fs::read_to_string(root.join("skills/planning/references/plan-contract.md"))
.expect("plan contract source");
let lane_execution = fs::read_to_string(root.join("skills/lane-execution/SKILL.md"))
.expect("lane-execution skill source");
let engineer =
fs::read_to_string(root.join("roles/engineer.md")).expect("Engineer role source");
let conductor =
fs::read_to_string(root.join("roles/conductor.md")).expect("Conductor role source");
let critic = fs::read_to_string(root.join("roles/critic.md")).expect("Critic role source");
let normalize = |text: &str| text.split_whitespace().collect::<Vec<_>>().join(" ");
let planning_contract = normalize(&format!("{planning} {plan_contract} {engineer}"));
let lane_contract = normalize(&format!("{lane_execution} {conductor}"));
for marker in [
"Only Shepherd and the root-local Planter are roots.",
"exactly two child lead role types: Engineer and Conductor",
"exactly one active Engineer",
"without routine operator questions",
"scope-changing contradiction",
"2-6 Conductor lanes are the 99% envelope",
"min(host, project spawn.max_parallel, plan, parent/role cap, run budget)",
"new run binding",
"prior child authority and review counts cleared",
] {
assert!(
planning_contract.contains(marker),
"planning contract omitted `{marker}`"
);
}
for marker in [
"immutable verified lane slice",
"primarily as dispatcher",
"does not plan, rescope the graph or lane, or implement",
"Rejections one through three",
"fourth rejection",
"malignant",
"revoked",
"quarantined",
"forbids resume",
"lineage-bound replacement custody to root",
] {
assert!(
lane_contract.contains(marker),
"lane contract omitted `{marker}`"
);
}
assert!(normalize(&engineer).contains("without routine operator questions"));
assert!(normalize(&conductor).contains("immutable verified lane slice"));
assert!(normalize(&critic).contains("fourth rejection"));
}
#[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]\nskill: thinking\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 every_canonical_role_requires_non_empty_startup_skill_frontmatter() {
for (role, _) in CANONICAL_ROLE_STARTUP_SKILLS {
for (case, skill_field, expected) in [
("missing", "", "invalid role frontmatter"),
("empty", "skill: \"\"\n", "startup skill must not be empty"),
] {
let root = std::env::temp_dir().join(format!(
"shepherd-compiler-startup-skill-{role}-{case}-{}",
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/example")).expect("skills directory");
fs::write(
root.join(format!("roles/{role}.md")),
format!(
"---\nrole: {role}\ndescription: A test role.\nsource: test\nmodel_hint: standard\nwrite_eligible: true\ndispatchable: false\ncapabilities: [read]\n{skill_field}write_scope: test\n---\n\nBody\n"
),
)
.expect("role source");
fs::write(
root.join("skills/example/SKILL.md"),
"---\nname: example\ndescription: A test skill.\nsource: test\nportability: cross-harness\n---\n\nSkill body\n",
)
.expect("skill source");
let error = match load_compile_input(&root) {
Ok(_) => panic!("{role} {case} unexpectedly loaded"),
Err(error) => error,
};
assert!(
error.to_string().contains(expected),
"{role} {case}: {error}"
);
let _ = fs::remove_dir_all(root);
}
}
}
#[test]
fn planter_role_contract_is_parsed_and_exact() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../content");
let input = load_compile_input(&root).expect("authored content");
let planter = input
.roles
.iter()
.find(|role| role.role == "planter")
.expect("Planter role");
assert_eq!(
planter
.capabilities
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec![
"read",
"search",
"shell",
"write",
"skill-load",
"tool-discovery",
"ask-operator",
]
);
assert!(planter.write_eligible);
assert!(!planter.dispatchable);
assert_eq!(planter.startup_skill, "planting");
assert_eq!(
planter.write_scope,
"the bound run's mesh.md and seed.md only; native verification and seed-pointer commands"
);
}
#[test]
fn plant_command_wraps_one_canonical_planting_behavior_bundle() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../content");
let input = load_compile_input(&root).expect("authored content");
let command = input
.skills
.iter()
.find(|skill| skill.name == "plant")
.expect("plant command entry");
let behavior = input
.skills
.iter()
.find(|skill| skill.name == "planting")
.expect("planting behavior bundle");
assert!(
command.resources.is_empty(),
"plant command owns no behavior resources"
);
assert!(
command.body.contains("`planting`"),
"plant command must load the planting behavior"
);
assert!(
!command.body.contains("Mesh first:"),
"plant command must not duplicate planting behavior"
);
assert!(behavior.body.contains("Mesh first:"));
assert_eq!(behavior.resources.len(), 1);
assert_eq!(
behavior.resources[0].relative_path,
"references/seed-contract.md"
);
assert!(String::from_utf8_lossy(&behavior.resources[0].content).contains("# Seed contract"));
}
#[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\nskill: thinking\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);
}
fn resource_fixture(name: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!(
"shepherd-compiler-resources-{name}-{}",
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\nskill: thinking\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");
root
}
#[test]
fn filesystem_loader_collects_sorted_bounded_skill_resources() {
let root = resource_fixture("valid");
fs::create_dir_all(root.join("skills/thinking/assets")).expect("assets");
fs::create_dir_all(root.join("skills/thinking/references")).expect("references");
fs::create_dir_all(root.join("skills/thinking/scripts")).expect("scripts");
fs::write(root.join("skills/thinking/assets/example.txt"), "asset\n").expect("asset");
fs::write(
root.join("skills/thinking/references/contract.md"),
"contract\n",
)
.expect("reference");
fs::write(
root.join("skills/thinking/scripts/check.sh"),
"#!/bin/sh\nexit 0\n",
)
.expect("script");
let input = load_compile_input(&root).expect("resource fixture");
assert_eq!(input.roles[0].startup_skill, "thinking");
assert_eq!(
input.skills[0]
.resources
.iter()
.map(|resource| (resource.relative_path.as_str(), resource.executable))
.collect::<Vec<_>>(),
[
("assets/example.txt", false),
("references/contract.md", false),
("scripts/check.sh", true),
]
);
let _ = fs::remove_dir_all(root);
}
#[test]
fn filesystem_loader_rejects_invalid_resource_shapes_and_bounds() {
for (name, expected) in [
("unexpected", "unexpected skill entry"),
("nested", "one file below"),
("utf8", "UTF-8"),
("oversized", "65536 bytes"),
] {
let root = resource_fixture(name);
match name {
"unexpected" => {
fs::write(root.join("skills/thinking/notes.md"), "no category\n").expect("entry");
}
"nested" => {
fs::create_dir_all(root.join("skills/thinking/references/nested"))
.expect("nested directory");
fs::write(
root.join("skills/thinking/references/nested/contract.md"),
"nested\n",
)
.expect("nested entry");
}
"utf8" => {
fs::create_dir_all(root.join("skills/thinking/assets")).expect("assets");
fs::write(root.join("skills/thinking/assets/data.txt"), [0xff]).expect("binary");
}
"oversized" => {
fs::create_dir_all(root.join("skills/thinking/assets")).expect("assets");
fs::write(
root.join("skills/thinking/assets/data.txt"),
vec![b'x'; 65_537],
)
.expect("large resource");
}
_ => unreachable!(),
}
let error = load_compile_input(&root).expect_err(name);
assert!(error.to_string().contains(expected), "{name}: {error}");
let _ = fs::remove_dir_all(root);
}
}
#[test]
fn filesystem_loader_enforces_combined_resource_bound() {
let root = resource_fixture("combined");
fs::create_dir_all(root.join("skills/thinking/assets")).expect("assets");
for index in 0..4 {
fs::write(
root.join(format!("skills/thinking/assets/{index}.txt")),
vec![b'x'; 65_536],
)
.expect("boundary resource");
}
load_compile_input(&root).expect("256 KiB boundary");
fs::write(root.join("skills/thinking/assets/overflow.txt"), b"x").expect("overflow");
let error = load_compile_input(&root).expect_err("combined overflow");
assert!(error.to_string().contains("262144 bytes"), "{error}");
let _ = fs::remove_dir_all(root);
}
#[cfg(unix)]
#[test]
fn filesystem_loader_rejects_symlink_resources() {
use std::os::unix::fs::symlink;
let root = resource_fixture("symlink");
fs::create_dir_all(root.join("skills/thinking/references")).expect("references");
fs::write(root.join("target.md"), "target\n").expect("target");
symlink(
root.join("target.md"),
root.join("skills/thinking/references/contract.md"),
)
.expect("symlink");
let error = load_compile_input(&root).expect_err("symlink resource");
assert!(error.to_string().contains("symlink"), "{error}");
let _ = fs::remove_dir_all(root);
}