use super::support::*;
#[test]
fn child_prompt_reuses_parent_agents_and_filtered_skills_without_tool_inventory() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let instructions = vec![InstructionFile {
kind: InstructionSourceKind::Repository,
path: PathBuf::from("AGENTS.md"),
content: "AGENTS.md rules".to_string(),
}];
let mut discovered_skills = SkillDiscovery::default();
for name in ["test-skill", "disabled-skill"] {
discovered_skills.skills.insert(
name.to_string(),
DiscoveredSkill {
name: name.to_string(),
path: PathBuf::from(format!(".agents/skills/{name}/SKILL.md")),
frontmatter: BTreeMap::new(),
},
);
}
let skills = filter_enabled_skills(
&discovered_skills,
&BTreeSet::from(["disabled-skill".to_string()]),
);
let agent = AgentSession::new("model", &instructions, &skills).with_context_budget(
crate::context::ContextBudget {
enabled: false,
..Default::default()
},
);
let cfg = SubagentRunConfig {
parent_agent: agent,
provider: provider.clone(),
provider_override: None,
parent_tools: ToolRuntime::new(temp.path()).unwrap(),
parent_cwd: temp.path().to_path_buf(),
cancellation: AgentCancellation::default(),
profiles: BTreeMap::new(),
subagent_profiles_prompt: None,
sessions_root: None,
parent_session_id: None,
depth: 0,
parent_activity_id: None,
activity_sender: None,
inherited_hooks: None,
semantic_progress_timeout: SUBAGENT_PROVIDER_STREAM_NO_SEMANTIC_PROGRESS_TIMEOUT,
schema_validation_max_retries: 2,
compaction: None,
};
run_subagents(
SubagentsArgs {
concurrency: None,
tasks: vec![SubagentTask {
intent: "inspect".into(),
agent: Some("label".into()),
identity: None,
context: Some("ctx".into()),
cwd: None,
}],
},
cfg,
)
.unwrap();
let requests = provider.requests.lock().unwrap();
assert!(
requests[0].messages()[0]
.content
.contains("AGENTS.md rules")
);
assert!(requests[0].messages()[0].content.contains("test-skill"));
assert!(!requests[0].messages()[0].content.contains("disabled-skill"));
assert!(requests[0].messages()[0].content.contains("<Tools>"));
assert!(!requests[0].messages()[0].content.contains("**Tool Name**"));
assert!(
requests[0].messages()[1]
.content
.contains("Agent label/persona: label")
);
}
#[test]
fn subagent_with_subagents_tool_receives_available_identity_list() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let mut cfg = config(provider.clone(), temp.path());
cfg.profiles.insert(
"frontend-dev".to_string(),
profile("frontend-dev", "PROFILE BODY MUST NOT LEAK IN LIST"),
);
cfg.subagent_profiles_prompt = Some(
"<Subagent-Identities>\n- `frontend-dev` — UI work\n</Subagent-Identities>".to_string(),
);
run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "inspect available identities".into(),
agent: None,
identity: None,
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
let requests = provider.requests.lock().unwrap();
let system = &requests[0].messages()[0].content;
assert!(system.contains("<Subagent-Identities>"), "{system}");
assert!(system.contains("`frontend-dev` — UI work"), "{system}");
assert!(
!system.contains("PROFILE BODY MUST NOT LEAK IN LIST"),
"{system}"
);
}
#[test]
fn subagent_at_subagents_tool_depth_limit_does_not_receive_identity_list() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let mut cfg = config(provider.clone(), temp.path());
cfg.parent_tools = ToolRuntime::new_with_settings(
temp.path(),
crate::tools::ToolSettings {
subagents: crate::tools::SubagentsToolSettings {
max_depth: 2,
..crate::tools::SubagentsToolSettings::default()
},
..crate::tools::ToolSettings::default()
},
)
.unwrap();
cfg.depth = 1;
cfg.profiles.insert(
"frontend-dev".to_string(),
profile("frontend-dev", "PROFILE BODY"),
);
cfg.subagent_profiles_prompt = Some(
"<Subagent-Identities>\n- `frontend-dev` — UI work\n</Subagent-Identities>".to_string(),
);
run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "at max depth".into(),
agent: None,
identity: None,
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
let requests = provider.requests.lock().unwrap();
let system = &requests[0].messages()[0].content;
assert!(!system.contains("<Subagent-Identities>"), "{system}");
assert!(!requests[0].subagents_tool_enabled());
}
#[test]
fn selected_identity_receives_profile_prompt_and_available_identity_list() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let mut cfg = config(provider.clone(), temp.path());
cfg.profiles.insert(
"frontend-dev".to_string(),
profile("frontend-dev", "PROFILE BODY ONLY FOR FRONTEND"),
);
cfg.subagent_profiles_prompt = Some(
"<Subagent-Identities>\n- `frontend-dev` — UI work\n</Subagent-Identities>".to_string(),
);
run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "inspect ui".into(),
agent: None,
identity: Some("frontend-dev".into()),
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
let requests = provider.requests.lock().unwrap();
let system = &requests[0].messages()[0].content;
assert!(
system.contains("PROFILE BODY ONLY FOR FRONTEND"),
"{system}"
);
assert!(system.contains("<Subagent-Identities>"), "{system}");
assert!(system.contains("`frontend-dev` — UI work"), "{system}");
}
#[test]
fn selected_identity_appends_profile_prompt_to_child_system_only() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let mut cfg = config(provider.clone(), temp.path());
let mut selected = profile("frontend-dev", "PROFILE BODY ONLY FOR FRONTEND");
selected.name = "Frontend Developer".to_string();
cfg.profiles.insert("frontend-dev".to_string(), selected);
let output = run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "inspect ui".into(),
agent: Some("legacy label".into()),
identity: Some("frontend-dev".into()),
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
assert_eq!(output.results[0].identity.as_deref(), Some("frontend-dev"));
let requests = provider.requests.lock().unwrap();
let system = &requests[0].messages()[0].content;
assert!(system.contains("PROFILE BODY ONLY FOR FRONTEND"));
assert!(
system.contains(
"<Your-Role Persona=Frontend Developer>\nYou must always follow the instructions of your assigned persona:\nPROFILE BODY ONLY FOR FRONTEND\n</Your-Role>"
),
"{system}"
);
assert!(
!requests[0].messages()[1]
.content
.contains("PROFILE BODY ONLY FOR FRONTEND")
);
assert!(
requests[0].messages()[1]
.content
.contains("Agent label/persona: legacy label")
);
}
#[test]
fn nested_selected_identities_append_exact_wrappers_and_keep_ancestor_prompt() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let mut cfg = config(provider, temp.path());
let mut builder = profile("builder", "unused");
builder.name = "Builder".to_string();
builder.prompt = "# Builder\nBuilder instructions".to_string();
cfg.profiles.insert("builder".to_string(), builder);
let mut reviewer = profile("reviewer", "unused");
reviewer.name = "Reviewer".to_string();
reviewer.prompt = "# Reviewer\nReviewer instructions".to_string();
cfg.profiles.insert("reviewer".to_string(), reviewer);
let base_prompt = cfg.parent_agent.system_prompt().to_string();
let builder_child = child_agent_for_task(
&SubagentTask {
intent: "build".to_string(),
agent: None,
identity: Some("builder".to_string()),
context: None,
cwd: None,
},
temp.path(),
&cfg,
)
.unwrap();
let mut nested_cfg = cfg.clone();
nested_cfg.parent_agent = builder_child.agent.clone();
let reviewer_child = child_agent_for_task(
&SubagentTask {
intent: "review".to_string(),
agent: None,
identity: Some("reviewer".to_string()),
context: None,
cwd: None,
},
temp.path(),
&nested_cfg,
)
.unwrap();
let builder_role = "<Your-Role Persona=Builder>\nYou must always follow the instructions of your assigned persona:\n# Builder\nBuilder instructions\n</Your-Role>";
let reviewer_role = "<Your-Role Persona=Reviewer>\nYou must always follow the instructions of your assigned persona:\n# Reviewer\nReviewer instructions\n</Your-Role>";
assert_eq!(
builder_child.agent.system_prompt(),
format!("{base_prompt}\n\n{builder_role}")
);
assert_eq!(
reviewer_child.agent.system_prompt(),
format!("{base_prompt}\n\n{builder_role}\n\n{reviewer_role}")
);
}
#[test]
fn selected_identity_filters_disabled_tools_from_child_provider_schema() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let mut cfg = config(provider.clone(), temp.path());
let mut selected = profile("frontend-dev", "PROFILE BODY");
selected.disabled_tools = HashSet::from(["shell".to_string(), "read".to_string()]);
cfg.profiles.insert("frontend-dev".to_string(), selected);
run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "inspect ui".into(),
agent: None,
identity: Some("frontend-dev".into()),
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
let requests = provider.requests.lock().unwrap();
let request = &requests[0];
let definitions = request.tool_definitions_json();
let names = definitions
.as_array()
.unwrap()
.iter()
.filter_map(|definition| definition.get("name")?.as_str())
.collect::<Vec<_>>();
assert!(!names.contains(&"bash"), "{names:?}");
assert!(!names.contains(&"read"), "{names:?}");
assert!(names.contains(&"write"), "{names:?}");
}
#[test]
fn selected_profile_disabling_subagents_hides_schema_and_identity_prompt() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let mut cfg = config(provider.clone(), temp.path());
let mut selected = profile("frontend-dev", "PROFILE BODY");
selected.disabled_tools = HashSet::from(["subagents".to_string()]);
cfg.profiles.insert("frontend-dev".to_string(), selected);
cfg.subagent_profiles_prompt =
Some("<Subagents>\n- `frontend-dev` — frontend work\n</Subagents>".to_string());
run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "inspect ui".into(),
agent: None,
identity: Some("frontend-dev".into()),
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
let requests = provider.requests.lock().unwrap();
let request = &requests[0];
let system = &request.messages()[0].content;
assert!(system.contains("PROFILE BODY"), "{system}");
assert!(!system.contains("<Subagents>"), "{system}");
assert!(!request.subagents_tool_enabled());
let definitions = request.tool_definitions_json();
let names = definitions
.as_array()
.unwrap()
.iter()
.filter_map(|definition| definition.get("name")?.as_str())
.collect::<Vec<_>>();
assert!(!names.contains(&"subagents"), "{names:?}");
}
#[test]
fn selected_identity_model_and_reasoning_override_route_child_provider_request() {
let temp = tempfile::TempDir::new().unwrap();
let paths = crate::config::McPaths::from_root(temp.path().join("mc"));
let mut settings = crate::config::Settings::default();
settings.custom_providers.insert(
"local-ai".to_string(),
crate::config::CustomProviderConfig {
label: "Local AI".to_string(),
base_url: "http://localhost:8080/v1".to_string(),
fast_mode: None,
api_key_env_var: None,
models_dev_provider: None,
use_responses_endpoint: false,
supports_text_verbosity: false,
reasoning_protocol: crate::config::CustomReasoningProtocol::default(),
extra_models: Vec::new(),
request_headers: Default::default(),
},
);
crate::config::write_settings(&paths, &settings).unwrap();
let mut entry = crate::model_catalog::ModelCatalogEntry::new("local-ai", "gpt-test");
entry.reasoning_efforts = Some(crate::thinking::ThinkingLevel::EFFORT_GENERIC.to_vec());
crate::model_catalog::write_catalog_cache_for_configured_provider(&paths, "local-ai", &[entry])
.unwrap();
let parent_provider = Arc::new(CountingProvider::new("never"));
let override_provider = Arc::new(CountingProvider::new("never"));
let resolver_provider = override_provider.clone();
let resolver_calls = Arc::new(AtomicUsize::new(0));
let resolver_calls_for_closure = Arc::clone(&resolver_calls);
let expected_cwd = temp.path().canonicalize().unwrap();
let resolver_config =
crate::config::load_effective_provider_selection(&paths, "local-ai", "gpt-test").unwrap();
let mut cfg = config(parent_provider.clone(), temp.path());
cfg.provider_override = Some(SubagentProviderOverride::new(
paths,
Arc::new(move |selection, cwd| {
resolver_calls_for_closure.fetch_add(1, Ordering::SeqCst);
assert_eq!(cwd.canonicalize().unwrap(), expected_cwd);
assert_eq!(selection.provider, "local-ai");
assert_eq!(selection.model, "gpt-test");
let provider: Arc<dyn Provider> = resolver_provider.clone();
Ok(crate::subagents::ResolvedProviderOverride {
provider,
scope: crate::thinking::ThinkingCapabilityScope::Custom(
crate::config::CustomReasoningProtocol::GptLike,
),
active_config: resolver_config.clone(),
context_budget: crate::context::ContextBudget::default(),
settings: settings.clone(),
})
}),
));
let mut frontend = profile("frontend-dev", "PROFILE BODY ONLY FOR FRONTEND");
frontend.model = Some(profiles::SubagentModelOverride {
provider: "local-ai".to_string(),
model: "gpt-test".to_string(),
});
frontend.reasoning = Some(crate::thinking::ThinkingLevel::High);
cfg.profiles.insert("frontend-dev".to_string(), frontend);
let output = run_subagents(
SubagentsArgs {
concurrency: Some(2),
tasks: (0..2)
.map(|_| SubagentTask {
intent: "inspect ui".into(),
agent: None,
identity: Some("frontend-dev".into()),
context: None,
cwd: None,
})
.collect(),
},
cfg,
)
.unwrap();
assert_eq!(resolver_calls.load(Ordering::SeqCst), 1);
assert!(
output
.results
.iter()
.all(|result| result.status == SubagentStatus::Completed)
);
assert!(parent_provider.requests.lock().unwrap().is_empty());
let requests = override_provider.requests.lock().unwrap();
assert_eq!(requests.len(), 2);
let request = &requests[0];
assert_eq!(request.model, "gpt-test");
assert_eq!(request.thinking_level, crate::thinking::ThinkingLevel::High);
assert!(request.send_default_reasoning_summary());
assert!(
request.messages()[0]
.content
.contains("PROFILE BODY ONLY FOR FRONTEND")
);
}
#[test]
fn selected_primary_agent_is_excluded_from_subagent_identity_child_prompt() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let mut cfg = config(provider.clone(), temp.path());
let primary = crate::primary_agents::PrimaryAgentProfile {
id: "tars".to_string(),
name: "TARS".to_string(),
description: "Tactical".to_string(),
path: PathBuf::new(),
prompt: "PRIMARY AGENT SECRET BODY MUST NOT LEAK".to_string(),
};
let main_agent = crate::agent::runner::append_primary_agent_to_main_prompt(
cfg.parent_agent.clone(),
Some(&primary),
);
assert!(
main_agent
.system_prompt()
.contains("PRIMARY AGENT SECRET BODY")
);
assert!(
!cfg.parent_agent
.system_prompt()
.contains("PRIMARY AGENT SECRET BODY")
);
cfg.profiles.insert(
"frontend-dev".to_string(),
profile("frontend-dev", "SUBAGENT IDENTITY BODY"),
);
run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "inspect ui".into(),
agent: None,
identity: Some("frontend-dev".into()),
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
let requests = provider.requests.lock().unwrap();
let system = &requests[0].messages()[0].content;
assert!(system.contains("SUBAGENT IDENTITY BODY"), "{system}");
assert!(!system.contains("PRIMARY AGENT SECRET BODY"), "{system}");
assert!(
!requests[0].messages()[1]
.content
.contains("PRIMARY AGENT SECRET BODY")
);
}
#[test]
fn no_identity_preserves_base_child_prompt_without_profiles_or_discovery_metadata() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let mut cfg = config(provider.clone(), temp.path());
let base_system_prompt = cfg.parent_agent.system_prompt().to_string();
cfg.profiles.insert(
"frontend-dev".to_string(),
profile("frontend-dev", "PROFILE BODY MUST NOT LEAK"),
);
run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "generic".into(),
agent: None,
identity: None,
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
let requests = provider.requests.lock().unwrap();
let system = &requests[0].messages()[0].content;
assert_eq!(system, &base_system_prompt);
assert!(!system.contains("PROFILE BODY MUST NOT LEAK"));
assert!(!system.contains("Discoverable subagent identities"));
}
#[test]
fn unknown_identity_fails_without_provider_call() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let output = run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "generic".into(),
agent: None,
identity: Some("missing".into()),
context: None,
cwd: None,
}],
},
config(provider.clone(), temp.path()),
)
.unwrap();
assert_eq!(output.summary.failed, 1);
assert_eq!(output.results[0].identity.as_deref(), Some("missing"));
assert!(
output.results[0]
.error
.as_deref()
.unwrap()
.contains("identity 'missing' is unavailable")
);
assert!(provider.requests.lock().unwrap().is_empty());
}
#[cfg(unix)]
#[test]
fn discover_subagent_profiles_rejects_symlinked_root() {
let temp = tempfile::TempDir::new().unwrap();
let real = temp.path().join("real");
std::fs::create_dir(&real).unwrap();
std::fs::write(
real.join("frontend.md"),
"---\nname: Frontend\ndescription: UI work\n---\nPrompt body\n",
)
.unwrap();
let linked = temp.path().join("linked");
std::os::unix::fs::symlink(&real, &linked).unwrap();
let discovery = profiles::discover_subagent_profiles(&linked);
assert!(discovery.profiles.is_empty(), "{:?}", discovery.profiles);
assert_eq!(discovery.diagnostics.len(), 1);
assert!(
discovery.diagnostics[0].message.contains("symlink"),
"{}",
discovery.diagnostics[0].message
);
}
#[test]
fn discover_subagent_profiles_missing_root_is_empty() {
let temp = tempfile::TempDir::new().unwrap();
let discovery = profiles::discover_subagent_profiles(&temp.path().join("missing"));
assert!(discovery.profiles.is_empty());
assert!(
discovery.diagnostics.is_empty(),
"{:?}",
discovery.diagnostics
);
}
#[test]
fn parent_identity_metadata_can_be_appended_without_exposing_bodies_or_mutating_base() {
let temp = tempfile::TempDir::new().unwrap();
std::fs::write(
temp.path().join("frontend.md"),
"---\nname: Frontend\ndescription: UI work\n---\nSECRET PROFILE BODY\n",
)
.unwrap();
let discovery = profiles::discover_subagent_profiles(temp.path());
let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
let base_prompt = agent.system_prompt().to_string();
let parent = agent.with_appended_system_prompt(
&profiles::render_subagent_profiles_prompt(None, &discovery)
.unwrap()
.unwrap(),
);
assert_eq!(agent.system_prompt(), base_prompt);
assert!(parent.system_prompt().contains("frontend"));
assert!(parent.system_prompt().contains("UI work"));
assert!(!parent.system_prompt().contains("SECRET PROFILE BODY"));
assert!(
!agent
.system_prompt()
.contains("Discoverable subagent identities")
);
}