use super::*;
use std::fs;
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 scan_finds_agents() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"engineer",
"---\nname: engineer\nrole: engineer\nextends: base-engineer\n\
description: Implements features and fixes bugs.\nmodel: sonnet\n---\n\n# Engineer\n",
);
write_agent(
tmp.path(),
"BASE-AGENT",
"---\nname: base-agent\nrole: base\n---\n\n# Base\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(agents.len(), 1, "only the engineer is deployable");
let engineer = &agents[0];
assert_eq!(engineer.name, "engineer");
assert_eq!(engineer.role, "engineer");
assert_eq!(
engineer.description.as_deref(),
Some("Implements features and fixes bugs.")
);
assert_eq!(engineer.model.as_deref(), Some("sonnet"));
assert_eq!(
engineer.extends_chain,
vec!["base-engineer".to_string(), "engineer".to_string()]
);
}
#[test]
fn scan_excludes_base_agents() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"BASE-AGENT",
"---\nname: base-agent\nrole: base\n---\n\nfoundation\n",
);
write_agent(
tmp.path(),
"base-engineer",
"---\nname: base-engineer\nrole: base-engineer\n---\n\nfoundation\n",
);
write_agent(
tmp.path(),
"qa",
"---\nname: qa\nrole: qa\ndescription: Tests things.\n---\n\n# QA\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].name, "qa");
}
#[test]
fn agent_with_a_base_role_but_no_base_filename_stays_in_the_roster() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"memory-manager",
"---\nname: memory-manager\nrole: base\ndescription: Manages memory.\n---\n\n# MM\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(
agents.iter().map(|a| a.name.as_str()).collect::<Vec<_>>(),
vec!["memory-manager"],
"a non-`BASE-*` file must never be classified as a foundation \
template by its `role:` value alone (#4589)"
);
}
#[test]
fn near_miss_base_names_are_not_treated_as_foundation_files() {
let tmp = TempDir::new().unwrap();
let kept = [
"baseline-analyzer",
"base64-decoder",
"basecamp-sync",
"based-reviewer",
"database-migrator",
"engineer",
];
let excluded = [
"BASE-AGENT",
"BASE-ENGINEER",
"BASE-OPS",
"BASE-QA",
"BASE-RESEARCH",
"base-custom",
"Base-Custom-Two",
];
for name in kept.iter().chain(excluded.iter()) {
write_agent(
tmp.path(),
name,
&format!("---\nname: {name}\nrole: {name}\n---\n\n# {name}\n"),
);
}
let mut scanned: Vec<String> = scan_agents(tmp.path())
.into_iter()
.map(|a| a.name)
.collect();
scanned.sort();
let mut want: Vec<String> = kept.iter().map(|s| s.to_string()).collect();
want.sort();
assert_eq!(
scanned, want,
"only `base-`-prefixed files are foundation templates; a bare \
`base` prefix silently deletes real agents (#4589 review)"
);
}
#[test]
fn scan_empty_dir() {
let tmp = TempDir::new().unwrap();
let agents = scan_agents(tmp.path());
assert!(agents.is_empty());
}
#[test]
fn scan_missing_dir_is_empty() {
let agents = scan_agents(Path::new("/no/such/agents/dir/xyz"));
assert!(agents.is_empty());
}
#[test]
fn scan_ignores_manifest_and_non_md() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join("manifest.json"), "{}").unwrap();
fs::write(tmp.path().join("notes.txt"), "hello").unwrap();
write_agent(
tmp.path(),
"writer",
"---\nname: writer\nrole: writer\n---\n\n# Writer\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].name, "writer");
}
#[test]
fn scan_handles_no_frontmatter() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"plain",
"# Plain agent\n\nNo frontmatter here.\n",
);
let agents = scan_agents(tmp.path());
assert!(
agents.is_empty(),
"a file with no frontmatter has no `name:`, so it is not dispatchable \
and must not enter the roster (#4711)"
);
}
#[test]
fn bare_base_md_is_excluded_from_the_roster() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"base",
"---\nname: base\nrole: base\n---\n\n# Base QA Instructions\n\n\
Appended to all QA agents.\n",
);
write_agent(
tmp.path(),
"Base",
"---\nname: Base\n---\n\n# Mixed-case spelling of the same convention\n",
);
write_agent(
tmp.path(),
"engineer",
"---\nname: engineer\nrole: engineer\n---\n\n# Engineer\n",
);
let names: Vec<String> = scan_agents(tmp.path())
.into_iter()
.map(|a| a.name)
.collect();
assert_eq!(
names,
vec!["engineer".to_string()],
"a bare `base.md` is a foundation file in every spelling (#4711)"
);
assert!(is_foundation_file("base"));
assert!(is_foundation_file("BASE"));
}
#[test]
fn base_prefixed_near_misses_survive_the_bare_base_rule() {
for stem in [
"baseline-analyzer",
"base64-decoder",
"basecamp-sync",
"based-reviewer",
] {
assert!(
!is_foundation_file(stem),
"`{stem}` is an ordinary agent name, not a foundation template (#4589)"
);
}
for stem in ["BASE-AGENT", "base-custom", "Base-Custom-Two"] {
assert!(
is_foundation_file(stem),
"`{stem}` is a foundation template"
);
}
}
#[test]
fn file_without_name_frontmatter_is_excluded_from_the_roster() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"shared-qa-preamble",
"---\nrole: qa\ndescription: Appended to all QA agents.\n---\n\n# Preamble\n",
);
write_agent(
tmp.path(),
"real-agent",
"---\nname: real-agent\ndescription: Does real work.\n---\n\n# Real\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(
agents.len(),
1,
"only the file declaring `name:` is a dispatchable agent (#4711)"
);
assert_eq!(agents[0].name, "real-agent");
assert_eq!(
agents[0].description.as_deref(),
Some("Does real work."),
"a real agent with `name:` frontmatter is still admitted unchanged"
);
}
#[test]
fn generate_authority_nonempty() {
let agents = vec![AgentSummary {
name: "engineer".to_string(),
role: "engineer".to_string(),
description: Some("Implements features.".to_string()),
model: Some("sonnet".to_string()),
extends_chain: vec![
"base-agent".to_string(),
"base-engineer".to_string(),
"engineer".to_string(),
],
}];
let md = generate_authority(&agents);
assert!(md.contains("## Delegation Authority"));
assert!(md.contains("### engineer"));
assert!(md.contains("base-agent → base-engineer → engineer"));
assert!(md.contains("**Model:** sonnet"));
}
#[test]
fn generate_authority_empty() {
let md = generate_authority(&[]);
assert!(md.contains("## Delegation Authority"));
assert!(md.to_lowercase().contains("no delegatable agents"));
}
#[test]
fn scan_preserves_url_in_description() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"docs-agent",
"---\nname: docs-agent\nrole: docs\ndescription: See https://docs.example.com/guide\n---\n\n# Docs\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(agents.len(), 1);
assert_eq!(
agents[0].description.as_deref(),
Some("See https://docs.example.com/guide"),
"URL in description must not be truncated"
);
}
#[test]
fn scan_preserves_bedrock_model_id() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"ml-agent",
"---\nname: ml-agent\nrole: ml\nmodel: bedrock/us.anthropic.claude-sonnet-4-6\n---\n\n# ML\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(agents.len(), 1);
assert_eq!(
agents[0].model.as_deref(),
Some("bedrock/us.anthropic.claude-sonnet-4-6"),
"bedrock model id must be preserved verbatim"
);
}
#[test]
fn scan_preserves_timestamp_in_description() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"timed-agent",
"---\nname: timed-agent\nrole: timer\ndescription: Deployed at 2026-06-05T14:31:34\n---\n\n# Timed\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(agents.len(), 1);
assert_eq!(
agents[0].description.as_deref(),
Some("Deployed at 2026-06-05T14:31:34"),
"timestamp in description must not be truncated"
);
}
#[test]
fn every_bundled_non_base_agent_reaches_the_roster() {
let assets = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/assets/agents");
let mut expected: Vec<String> = fs::read_dir(&assets)
.expect("bundled agent assets dir")
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("md"))
.filter_map(|p| {
p.file_stem()
.and_then(|s| s.to_str())
.filter(|stem| !is_foundation_file(stem))
.map(str::to_string)
})
.collect();
expected.sort();
let mut scanned: Vec<String> = scan_agents(&assets).into_iter().map(|a| a.name).collect();
scanned.sort();
let dropped: Vec<&String> = expected.iter().filter(|n| !scanned.contains(n)).collect();
assert!(
dropped.is_empty(),
"bundled agents silently dropped from every roster (check for a stray \
`role: base` in their frontmatter): {dropped:?}"
);
assert_eq!(
scanned.len(),
expected.len(),
"every non-BASE bundled agent must be delegatable"
);
}
#[test]
fn generate_authority_omits_self_referential_lines() {
let agents = vec![AgentSummary {
name: "ticketing".to_string(),
role: "ticketing".to_string(),
description: Some("Files tickets.".to_string()),
model: None,
extends_chain: vec!["ticketing".to_string()],
}];
let md = generate_authority(&agents);
assert!(md.contains("### ticketing"));
assert!(
!md.contains("**Role:**"),
"role duplicating the heading must be omitted"
);
assert!(
!md.contains("**Foundation:**"),
"a self-referential single-element chain must be omitted"
);
}
#[test]
fn roster_from_dirs_dedup_is_case_insensitive() {
let high = TempDir::new().unwrap();
let low = TempDir::new().unwrap();
write_agent(
high.path(),
"QA",
"---\nname: QA\nrole: qa\ndescription: PROJECT TIER\n---\n\n# QA\n",
);
write_agent(
low.path(),
"qa",
"---\nname: qa\nrole: qa\ndescription: USER TIER\n---\n\n# QA\n",
);
let roster = roster_from_dirs(&[high.path().to_path_buf(), low.path().to_path_buf()]);
assert_eq!(roster.len(), 1, "QA and qa are the same agent");
assert_eq!(roster[0].description.as_deref(), Some("PROJECT TIER"));
}
#[test]
fn deployed_agent_dirs_puts_project_tier_first() {
let dirs = deployed_agent_dirs(Path::new("/proj"));
assert_eq!(dirs[0], PathBuf::from("/proj/.claude/agents"));
assert!(
dirs.len() >= 2,
"the user-level tier(s) must also be consulted"
);
}
#[test]
fn deployed_agent_dirs_from_is_pure_and_ordered() {
let dirs = deployed_agent_dirs_from(
Path::new("/proj"),
Some(Path::new("/managed/claude-config")),
Path::new("/home/u/.claude/agents"),
);
assert_eq!(
dirs,
vec![
PathBuf::from("/proj/.claude/agents"),
PathBuf::from("/managed/claude-config/agents"),
PathBuf::from("/home/u/.claude/agents"),
]
);
}
#[test]
fn deployed_agent_dirs_from_skips_absent_managed_tier() {
let dirs = deployed_agent_dirs_from(
Path::new("/proj"),
None,
Path::new("/home/u/.claude/agents"),
);
assert_eq!(
dirs,
vec![
PathBuf::from("/proj/.claude/agents"),
PathBuf::from("/home/u/.claude/agents"),
]
);
}
#[test]
fn roster_from_dirs_dedupes_with_first_dir_winning() {
let high = TempDir::new().unwrap();
let low = TempDir::new().unwrap();
write_agent(
high.path(),
"qa",
"---\nname: qa\nrole: qa\ndescription: PROJECT TIER\n---\n\n# QA\n",
);
write_agent(
low.path(),
"qa",
"---\nname: qa\nrole: qa\ndescription: USER TIER\n---\n\n# QA\n",
);
write_agent(
low.path(),
"ops",
"---\nname: ops\nrole: ops\ndescription: USER TIER\n---\n\n# Ops\n",
);
let roster = roster_from_dirs(&[high.path().to_path_buf(), low.path().to_path_buf()]);
assert_eq!(roster.len(), 2, "qa must appear once, plus ops");
assert_eq!(roster[0].name, "ops", "roster is name-sorted");
assert_eq!(roster[1].name, "qa");
assert_eq!(roster[1].description.as_deref(), Some("PROJECT TIER"));
}
#[test]
fn roster_from_dirs_ignores_missing_dirs() {
let roster = roster_from_dirs(&[PathBuf::from("/nonexistent/agents")]);
assert!(roster.is_empty());
assert!(roster_from_dirs(&[]).is_empty());
}
#[test]
fn generate_authority_omits_the_harness_supplied_description() {
let agents = vec![AgentSummary {
name: "api-qa".to_string(),
role: "qa".to_string(),
description: Some(
"Specialized API and backend testing for REST, GraphQL, and \
server-side functionality"
.to_string(),
),
model: Some("sonnet".to_string()),
extends_chain: vec!["api-qa".to_string()],
}];
let md = generate_authority(&agents);
assert!(md.contains("### api-qa"));
assert!(
md.contains("**Role:** qa"),
"role is the roster's own value"
);
assert!(
md.contains("**Model:** sonnet"),
"model is the roster's own value"
);
assert!(
!md.contains("**Handles:**"),
"the harness-supplied description must not be re-emitted"
);
assert!(
!md.contains("Specialized API and backend testing"),
"the description text itself must not survive under another label"
);
}
#[test]
fn generate_authority_renders_an_agent_with_neither_role_nor_model() {
let agents = vec![
AgentSummary {
name: "alpha".to_string(),
role: "alpha".to_string(),
description: Some("ignored".to_string()),
model: None,
extends_chain: vec!["alpha".to_string()],
},
AgentSummary {
name: "beta".to_string(),
role: "beta".to_string(),
description: None,
model: None,
extends_chain: vec!["beta".to_string()],
},
];
let md = generate_authority(&agents);
assert!(md.contains("### alpha\n\n### beta\n"), "got: {md:?}");
assert!(!md.contains("(no description provided)"));
}
#[test]
fn block_scalar_description_is_folded() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"copyeditor",
"---\nname: copyeditor\nmodel: sonnet\ndescription: >\n \
Line/word-level copyedit pass.\n Assumes structure is sound.\n---\n\n# Copyeditor\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(agents.len(), 1);
assert_eq!(
agents[0].description.as_deref(),
Some("Line/word-level copyedit pass. Assumes structure is sound."),
"a folded scalar joins its lines with a space"
);
assert_eq!(agents[0].model.as_deref(), Some("sonnet"));
}
#[test]
fn literal_block_scalar_description_keeps_its_line_breaks() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"writer",
"---\nname: writer\ndescription: |-\n first line\n second line\nmodel: opus\n---\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(
agents[0].description.as_deref(),
Some("first line\nsecond line")
);
assert_eq!(
agents[0].model.as_deref(),
Some("opus"),
"the key AFTER the block scalar must still parse"
);
}
#[test]
fn block_scalar_body_stops_at_the_closing_fence() {
let tmp = TempDir::new().unwrap();
write_agent(
tmp.path(),
"proofreader",
"---\nname: proofreader\ndescription: >\n only this\n---\n\n# Proofreader\n\nBody prose.\n",
);
let agents = scan_agents(tmp.path());
assert_eq!(agents[0].description.as_deref(), Some("only this"));
}
#[test]
fn a_plain_value_beginning_with_an_angle_bracket_is_not_a_block_scalar() {
assert!(is_block_scalar_header(">"));
assert!(is_block_scalar_header("|"));
assert!(is_block_scalar_header(">-"));
assert!(is_block_scalar_header("|+"));
assert!(is_block_scalar_header(">2"));
assert!(!is_block_scalar_header("> quoted prose"));
assert!(!is_block_scalar_header("|pipe|table|"));
assert!(!is_block_scalar_header("sonnet"));
}