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 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"
);
}