use super::*;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn write_agent(dir: &Path, name: &str, content: &str) {
fs::write(dir.join(format!("{name}.md")), content).expect("write agent");
}
#[test]
fn compose_base_only() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"base-agent",
"---\nname: base-agent\nrole: base\n---\n\n# Base\n\nFoundation content.\n",
);
let composed = compose_agent("base-agent", tmp.path()).unwrap();
assert!(composed.starts_with("---\n"));
assert!(composed.contains("name: base-agent"));
assert!(composed.contains("role: base"));
assert!(composed.contains("Foundation content."));
assert!(!composed.contains("extends:"));
}
#[test]
fn compose_engineer_chain() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"base-agent",
"---\nname: base-agent\nrole: base\n---\n\n# Base\n\nBASE BODY\n",
);
write_agent(
tmp.path(),
"base-engineer",
"---\nname: base-engineer\nrole: base-engineer\nextends: base-agent\n---\n\n# Base Engineer\n\nENGINEER BASE BODY\n",
);
write_agent(
tmp.path(),
"engineer",
"---\nname: engineer\nrole: engineer\nextends: base-engineer\nmodel: sonnet\n---\n\n# Engineer\n\nLEAF BODY\n",
);
let composed = compose_agent("engineer", tmp.path()).unwrap();
assert!(composed.contains("name: engineer"));
assert!(composed.contains("role: engineer"));
assert!(composed.contains("model: sonnet"));
let base = composed.find("BASE BODY").expect("base body present");
let mid = composed
.find("ENGINEER BASE BODY")
.expect("base-engineer body present");
let leaf = composed.find("LEAF BODY").expect("leaf body present");
assert!(base < mid, "base body must precede base-engineer body");
assert!(mid < leaf, "base-engineer body must precede leaf body");
}
#[test]
fn cycle_detection() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"agent-a",
"---\nname: agent-a\nextends: agent-b\n---\n\nA body\n",
);
write_agent(
tmp.path(),
"agent-b",
"---\nname: agent-b\nextends: agent-a\n---\n\nB body\n",
);
let err = compose_agent("agent-a", tmp.path()).unwrap_err();
match err {
AgentBuildError::Cycle(chain) => {
assert!(chain.contains(&"agent-a".to_string()));
assert!(chain.contains(&"agent-b".to_string()));
}
other => panic!("expected Cycle, got {other:?}"),
}
}
#[test]
fn depth_exceeded() {
let tmp = TempDir::new().unwrap();
write_agent(tmp.path(), "level0", "---\nname: level0\n---\n\nroot\n");
for i in 1..=10 {
write_agent(
tmp.path(),
&format!("level{i}"),
&format!(
"---\nname: level{i}\nextends: level{}\n---\n\nbody{i}\n",
i - 1
),
);
}
let err = compose_agent("level10", tmp.path()).unwrap_err();
assert!(
matches!(err, AgentBuildError::DepthExceeded(MAX_DEPTH)),
"expected DepthExceeded, got {err:?}"
);
}
#[test]
fn compose_missing_agent() {
let tmp = TempDir::new().unwrap();
let err = compose_agent("ghost", tmp.path()).unwrap_err();
assert!(matches!(err, AgentBuildError::NotFound(name) if name == "ghost"));
}
#[test]
fn missing_parent_is_not_found() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"child",
"---\nname: child\nextends: nowhere\n---\n\nbody\n",
);
let err = compose_agent("child", tmp.path()).unwrap_err();
assert!(matches!(err, AgentBuildError::NotFound(name) if name == "nowhere"));
}
#[test]
fn unterminated_frontmatter_errors() {
let tmp = TempDir::new().unwrap();
write_agent(tmp.path(), "broken", "---\nname: broken\n\n# No close\n");
let err = compose_agent("broken", tmp.path()).unwrap_err();
assert!(matches!(err, AgentBuildError::FrontmatterParse(_)));
}
#[test]
fn source_chain_engineer() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"base-agent",
"---\nname: base-agent\n---\n\nb\n",
);
write_agent(
tmp.path(),
"base-engineer",
"---\nname: base-engineer\nextends: base-agent\n---\n\nbe\n",
);
write_agent(
tmp.path(),
"engineer",
"---\nname: engineer\nextends: base-engineer\n---\n\ne\n",
);
let chain = source_chain("engineer", tmp.path()).unwrap();
assert_eq!(chain, vec!["base-agent", "base-engineer", "engineer"]);
}
#[test]
fn source_chain_base_only() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"base-agent",
"---\nname: base-agent\n---\n\nb\n",
);
let chain = source_chain("base-agent", tmp.path()).unwrap();
assert_eq!(chain, vec!["base-agent"]);
}
#[test]
fn url_value_round_trips() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"web-agent",
"---\nname: web-agent\nrole: web\nmodel: https://example.com/model-api\n---\n\n# Web\n",
);
let composed = compose_agent("web-agent", tmp.path()).unwrap();
assert!(
composed.contains("model: https://example.com/model-api"),
"URL value must be preserved verbatim; got:\n{composed}"
);
}
#[test]
fn timestamp_value_round_trips() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"ts-agent",
"---\nname: ts-agent\nrole: worker\ndescription: Created at 2026-06-05T14:31:34\n---\n\n# TS\n",
);
let composed = compose_agent("ts-agent", tmp.path()).unwrap();
assert!(
composed.contains("2026-06-05T14:31:34"),
"timestamp in description must survive; got:\n{composed}"
);
}
#[test]
fn bedrock_model_id_round_trips() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"ai-agent",
"---\nname: ai-agent\nrole: ai\nmodel: bedrock/us.anthropic.claude-sonnet-4-6\n---\n\n# AI\n",
);
let composed = compose_agent("ai-agent", tmp.path()).unwrap();
assert!(
composed.contains("model: bedrock/us.anthropic.claude-sonnet-4-6"),
"model id must be preserved; got:\n{composed}"
);
}
#[test]
fn tier_model_mapping() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"heavy",
"---\nname: heavy\nrole: engineer\nresource_tier: intensive\n---\n\n# Heavy\n",
);
let composed = compose_agent("heavy", tmp.path()).unwrap();
assert!(
composed.contains("model: opus"),
"intensive tier must map to opus; got:\n{composed}"
);
assert!(composed.contains("resource_tier: intensive"));
}
#[test]
fn tier_lightweight_maps_to_haiku() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"light",
"---\nname: light\nrole: ops\nresource_tier: lightweight\n---\n\n# Light\n",
);
let composed = compose_agent("light", tmp.path()).unwrap();
assert!(
composed.contains("model: haiku"),
"lightweight tier must map to haiku; got:\n{composed}"
);
}
#[test]
fn tier_high_and_standard_map_to_sonnet() {
let tmp = TempDir::new().unwrap();
for (name, tier) in [("hi", "high"), ("std", "standard")] {
write_agent(
tmp.path(),
name,
&format!("---\nname: {name}\nrole: engineer\nresource_tier: {tier}\n---\n\n# {name}\n"),
);
let composed = compose_agent(name, tmp.path()).unwrap();
assert!(
composed.contains("model: sonnet"),
"{tier} tier must map to sonnet; got:\n{composed}"
);
}
}
#[test]
fn tier_unknown_falls_back_to_sonnet() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"weird",
"---\nname: weird\nrole: engineer\nresource_tier: galaxy-brain\n---\n\n# Weird\n",
);
let composed = compose_agent("weird", tmp.path()).unwrap();
assert!(
composed.contains("model: sonnet"),
"unknown tier must fall back to sonnet; got:\n{composed}"
);
}
#[test]
fn explicit_model_wins_over_tier() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"pinned",
"---\nname: pinned\nrole: engineer\nresource_tier: intensive\nmodel: haiku\n---\n\n# Pinned\n",
);
let composed = compose_agent("pinned", tmp.path()).unwrap();
assert!(
composed.contains("model: haiku"),
"explicit model must win over tier-derived opus; got:\n{composed}"
);
assert!(
!composed.contains("model: opus"),
"tier-derived model must not appear when explicit model is set"
);
}
#[test]
fn no_tier_no_model_emits_no_model() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"bare",
"---\nname: bare\nrole: base\n---\n\n# Bare\n",
);
let composed = compose_agent("bare", tmp.path()).unwrap();
assert!(
!composed.contains("model:"),
"no model should be emitted without tier or explicit model; got:\n{composed}"
);
}
#[test]
fn initial_prompt_injected_by_role() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"eng",
"---\nname: eng\nrole: engineer\nmodel: sonnet\n---\n\n# Eng\n",
);
let composed = compose_agent("eng", tmp.path()).unwrap();
assert!(
composed.contains(r#"initialPrompt: "Begin implementation."#),
"engineer role must get the implementation initialPrompt; got:\n{composed}"
);
}
#[test]
fn initial_prompt_role_qa_and_ops() {
let tmp = TempDir::new().unwrap();
write_agent(tmp.path(), "q", "---\nname: q\nrole: qa\n---\n\n# Q\n");
write_agent(tmp.path(), "o", "---\nname: o\nrole: ops\n---\n\n# O\n");
let q = compose_agent("q", tmp.path()).unwrap();
let o = compose_agent("o", tmp.path()).unwrap();
assert!(
q.contains(r#"initialPrompt: "Begin verification."#),
"qa prompt:\n{q}"
);
assert!(
o.contains(r#"initialPrompt: "Begin operations."#),
"ops prompt:\n{o}"
);
}
#[test]
fn initial_prompt_excluded_role() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"mgr",
"---\nname: mgr\nrole: base\nmodel: sonnet\n---\n\n# Mgr\n",
);
let composed = compose_agent("mgr", tmp.path()).unwrap();
assert!(
!composed.contains("initialPrompt:"),
"excluded role must not get an initialPrompt; got:\n{composed}"
);
}
#[test]
fn initial_prompt_explicit_wins() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"custom",
"---\nname: custom\nrole: engineer\ninitialPrompt: Do the special thing first.\n---\n\n# Custom\n",
);
let composed = compose_agent("custom", tmp.path()).unwrap();
assert!(
composed.contains(r#"initialPrompt: "Do the special thing first.""#),
"explicit initialPrompt must win; got:\n{composed}"
);
assert!(
!composed.contains("Begin implementation."),
"role default must not appear when an explicit prompt is set"
);
}
#[test]
fn enrichment_survives_inheritance_chain() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"base-agent",
"---\nname: base-agent\nrole: base\n---\n\n# Base\n\nBASE GUARANTEE\n",
);
write_agent(
tmp.path(),
"base-engineer",
"---\nname: base-engineer\nrole: base-engineer\nextends: base-agent\n---\n\n# Base Eng\n\nENG GUARANTEE\n",
);
write_agent(
tmp.path(),
"leaf",
"---\nname: leaf\nrole: engineer\nextends: base-engineer\nresource_tier: intensive\n---\n\n# Leaf\n\nLEAF GUARANTEE\n",
);
let composed = compose_agent("leaf", tmp.path()).unwrap();
assert!(
composed.contains("model: opus"),
"tier→model on chain:\n{composed}"
);
assert!(
composed.contains(r#"initialPrompt: "Begin implementation."#),
"role-derived prompt on chain:\n{composed}"
);
assert!(composed.contains("BASE GUARANTEE"));
assert!(composed.contains("ENG GUARANTEE"));
assert!(composed.contains("LEAF GUARANTEE"));
}
#[test]
fn initial_prompt_with_embedded_quote_round_trips() {
let tmp = TempDir::new().unwrap();
let original = r#"Run the "fast path" then stop."#;
write_agent(
tmp.path(),
"quoted",
&format!("---\nname: quoted\nrole: engineer\ninitialPrompt: {original}\n---\n\n# Quoted\n"),
);
let composed = compose_agent("quoted", tmp.path()).unwrap();
assert!(
composed.contains(r#"initialPrompt: "Run the \"fast path\" then stop.""#),
"embedded quote must be escaped in emitted frontmatter; got:\n{composed}"
);
write_agent(tmp.path(), "quoted2", &composed);
let (fm, _) = split_frontmatter(&composed).expect("composed frontmatter must re-parse");
assert_eq!(
fm.initial_prompt.as_deref(),
Some(original),
"initialPrompt must survive the compose→re-parse cycle verbatim"
);
let recomposed = compose_agent("quoted2", tmp.path()).unwrap();
assert!(
recomposed.contains(r#"initialPrompt: "Run the \"fast path\" then stop.""#),
"re-composed frontmatter must be stable; got:\n{recomposed}"
);
}
#[test]
fn initial_prompt_with_backslash_round_trips() {
let tmp = TempDir::new().unwrap();
let original = r#"Use the C:\path and the \d+ pattern."#;
write_agent(
tmp.path(),
"slash",
&format!("---\nname: slash\nrole: engineer\ninitialPrompt: {original}\n---\n\n# Slash\n"),
);
let composed = compose_agent("slash", tmp.path()).unwrap();
let (fm, _) = split_frontmatter(&composed).expect("composed frontmatter must re-parse");
assert_eq!(
fm.initial_prompt.as_deref(),
Some(original),
"backslash-bearing initialPrompt must round-trip verbatim; got:\n{composed}"
);
}
#[test]
fn initial_prompt_role_default_round_trips() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"eng",
"---\nname: eng\nrole: engineer\n---\n\n# Eng\n",
);
let composed = compose_agent("eng", tmp.path()).unwrap();
let (fm, _) = split_frontmatter(&composed).expect("composed frontmatter must re-parse");
assert_eq!(
fm.initial_prompt.as_deref(),
Some("Begin implementation. Read the task context and start coding immediately."),
"role default initialPrompt must re-parse unchanged; got:\n{composed}"
);
write_agent(tmp.path(), "eng2", &composed);
let recomposed = compose_agent("eng2", tmp.path()).unwrap();
let first_line = "initialPrompt: \"Begin implementation.";
assert!(
composed.contains(first_line) && recomposed.contains(first_line),
"initialPrompt line must be stable across the cycle"
);
}
#[test]
fn case_insensitive_resolve_via_map() {
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join("BASE-QA.md"),
"---\nname: base-qa\nrole: base-qa\n---\n\n# Base QA\n\nBASE QA BODY\n",
)
.expect("write BASE-QA.md");
write_agent(
tmp.path(),
"qa",
"---\nname: qa\nrole: qa\nextends: base-qa\n---\n\n# QA Agent\n\nQA BODY\n",
);
let map = build_source_map(tmp.path());
assert!(
map.contains_key("base-qa"),
"SourceMap must contain 'base-qa' key (from BASE-QA.md); map keys: {:?}",
map.keys().collect::<Vec<_>>()
);
assert!(
map["base-qa"].ends_with("BASE-QA.md"),
"SourceMap['base-qa'] must point to BASE-QA.md, got {:?}",
map["base-qa"]
);
let composed = compose_agent("qa", tmp.path())
.expect("compose_agent must succeed: base-qa (lowercase) -> BASE-QA.md (uppercase)");
let base_pos = composed.find("BASE QA BODY").expect("base body present");
let qa_pos = composed.find("QA BODY").expect("qa body present");
assert!(
base_pos < qa_pos,
"base body must precede concrete agent body"
);
assert!(
composed.contains("name: qa"),
"merged frontmatter has child name"
);
assert!(
!composed.contains("extends:"),
"extends must not appear in output"
);
}