use super::*;
use crate::agent::{DetectAgentsGuard, classifier::TaskCategory};
use crate::types::{TaskBudget, TaskDifficulty, TaskRigor, TaskUrgency};
use std::sync::Arc;
fn isolated() -> (
tempfile::TempDir,
crate::paths::AidHomeGuard,
crate::live_quota::CacheDirGuard,
) {
let dir = tempfile::tempdir().expect("home");
let home = crate::paths::AidHomeGuard::set(dir.path());
let cache = crate::live_quota::CacheDirGuard::set(dir.path());
crate::agent::codex::cli_config::set_test_codex_home(Some(dir.path().join("no-codex")));
(dir, home, cache)
}
fn args() -> RunArgs {
RunArgs {
agent_name: "oz".into(),
prompt: "Compare documentation".into(),
kind: Some(TaskCategory::Refactoring),
declared_difficulty: Some(TaskDifficulty::Complex),
declared_budget: Some(TaskBudget::Premium),
declared_urgency: Some(TaskUrgency::Background),
declared_rigor: Some(TaskRigor::Standard),
..Default::default()
}
}
fn hold(dir: &std::path::Path, route: &str) {
std::fs::write(
dir.join(format!("rate-limit-{route}")),
"hold: manual\nmessage: quota exhausted\n",
)
.expect("hold");
}
fn expected(store: &Store, args: &RunArgs) -> Option<AdviceCandidate> {
let declared = DeclaredTaskProfile {
difficulty: args.declared_difficulty.unwrap_or_default(),
budget: args.declared_budget.unwrap_or_default(),
urgency: args.declared_urgency.unwrap_or_default(),
rigor: args.declared_rigor.unwrap_or_default(),
};
crate::cmd::advise::build_report(
Some(store),
&args.prompt,
declared,
args.kind,
args.team.as_deref(),
0,
None,
)
.candidates
.into_iter()
.find(|c| c.agent != args.agent_name && c.agent != "claude" && c.launchable(None))
}
#[test]
fn automatic_uses_first_report_route_with_complete_profile_kind_team_and_store() {
let (_dir, _home, _cache) = isolated();
let _fleet = DetectAgentsGuard::set(vec![
AgentKind::Codex,
AgentKind::Droid,
AgentKind::Claude,
AgentKind::Oz,
]);
std::fs::create_dir(crate::team::teams_dir()).expect("teams");
std::fs::write(crate::team::teams_dir().join("routes.toml"),
"[team]\nid = 'routes'\ndisplay_name = 'Routes'\npreferred_agents = ['claude', 'droid']\ndefault_agent = 'claude'\n").expect("team");
crate::agent_config::save_agent_default_model("droid", Some("gpt-5.3-codex")).expect("model");
let store = Store::open_memory().expect("store");
let args = RunArgs {
team: Some("routes".into()),
..args()
};
let snapshot = report(Some(&store), &args);
assert_eq!(snapshot.declared.difficulty, TaskDifficulty::Complex);
assert_eq!(snapshot.inferred.kind, TaskCategory::Refactoring);
assert!(
snapshot
.candidates
.iter()
.any(|c| c.agent == "claude" && c.eligible)
);
let selected = automatic_candidate(Some(&store), &args).expect("peer");
assert_eq!(Some(selected.clone()), expected(&store, &args));
assert_ne!(selected.agent, "claude");
assert_ne!(selected.agent, "oz");
let mut child = args.clone();
apply_candidate(&mut child, &selected);
assert_eq!(child.kind, args.kind);
assert_eq!(child.team, args.team);
assert_eq!(report(Some(&store), &child).declared, snapshot.declared);
}
#[test]
fn automatic_excludes_missing_disabled_auth_failed_below_floor_and_exhausted() {
let (_dir, _home, _cache) = isolated();
let _fleet = DetectAgentsGuard::set(vec![
AgentKind::Codex,
AgentKind::Droid,
AgentKind::Qwen,
AgentKind::Grok,
]);
crate::agent_config::save_agent_disabled("grok", true).expect("disable");
crate::auth_marker::record_failure_at(AgentKind::Droid, "Not signed in.", chrono::Local::now());
let args = RunArgs {
agent_name: "codex".into(),
..args()
};
let all = report(None, &args).candidates;
let candidate = |name: &str| all.iter().find(|c| c.agent == name).expect("candidate");
assert!(
candidate("agy")
.exclusion_codes
.contains(&"not_installed".into())
);
assert!(
candidate("droid")
.exclusion_codes
.contains(&"auth_failed".into())
);
assert!(
candidate("qwen")
.exclusion_codes
.contains(&"below_floor".into())
);
assert!(!all.iter().any(|c| c.agent == "grok"));
assert!(automatic_candidate(None, &args).is_none());
}
#[test]
fn automatic_excludes_selected_model_group_holds_even_background_and_advisory_rescue() {
let (dir, _home, _cache) = isolated();
let _fleet = DetectAgentsGuard::set(vec![AgentKind::Droid, AgentKind::Codex]);
crate::agent_config::save_agent_default_model("droid", Some("gpt-5.3-codex")).expect("model");
hold(dir.path(), "droid--standard");
let args = RunArgs {
agent_name: "codex".into(),
..args()
};
let all = report(None, &args);
assert_eq!(
all.candidates
.iter()
.find(|c| c.agent == "droid")
.expect("droid")
.quota
.status,
"held"
);
assert!(automatic_candidate(None, &args).is_none());
hold(dir.path(), "codex");
assert!(
report(None, &args).recommended.is_some(),
"held routes can remain advisory"
);
assert!(automatic_candidate(None, &args).is_none());
}
#[test]
fn automatic_excludes_superseded_gemini_and_claude_but_explicit_gemini_still_runs() {
let (dir, _home, _cache) = isolated();
let _fleet = DetectAgentsGuard::set(vec![
AgentKind::Gemini,
AgentKind::Antigravity,
AgentKind::Claude,
]);
let store = Arc::new(Store::open_memory().expect("store"));
let args = RunArgs {
kind: Some(TaskCategory::Research),
..args()
};
assert!(report(Some(&store), &args).candidates.iter().any(|c|
c.agent == "gemini" && c.exclusion_codes.contains(&"superseded_by_agy".into())));
assert_eq!(
automatic_candidate(Some(&store), &args)
.expect("peer")
.agent,
"agy"
);
hold(dir.path(), "oz");
let mut explicit = RunArgs {
cascade: vec!["gemini".into(), "claude".into()],
..args
};
let setup =
super::super::run_dispatch_resolve::resolve_agent_setup(&store, &mut explicit, None)
.expect("explicit Gemini");
assert_eq!(setup.agent_kind, AgentKind::Gemini);
assert_eq!(explicit.cascade, ["claude"]);
assert_ne!(explicit.model_source, ModelSource::Advised);
}
#[test]
fn resolver_excludes_uppercase_and_alias_primary_by_builtin_kind() {
let (dir, _home, _cache) = isolated();
let store = Arc::new(Store::open_memory().expect("store"));
let _fleet = DetectAgentsGuard::set(vec![AgentKind::Antigravity, AgentKind::Codex]);
crate::agent_config::save_agent_default_model("codex", Some("gpt-6-sol")).expect("model");
let _served = crate::agent::model_validation::MockServedModelsGuard::set(AgentKind::Codex, None);
for name in ["AGY", "antigravity", "ANTIGRAVITY"] {
hold(dir.path(), "agy");
let mut child = RunArgs { agent_name: name.into(), ..args() };
let setup = super::super::run_dispatch_resolve::resolve_agent_setup(&store, &mut child, None)
.expect("other builtin");
assert_eq!(setup.agent_kind, AgentKind::Codex, "{name}");
assert_eq!(child.model_source, ModelSource::Advised);
std::fs::remove_file(dir.path().join("rate-limit-agy")).expect("clear");
}
let only = DetectAgentsGuard::set(vec![AgentKind::Antigravity]);
for name in ["AGY", "antigravity", "ANTIGRAVITY"] {
let child = RunArgs { agent_name: name.into(), kind: Some(TaskCategory::Research), ..args() };
assert!(automatic_candidate(Some(&store), &child).is_none(), "{name}");
}
drop(only);
let only = DetectAgentsGuard::set(vec![AgentKind::Codex]);
assert!(automatic_candidate(Some(&store), &RunArgs { agent_name: "CODEX".into(), ..args() }).is_none());
drop(only);
}
#[test]
fn selected_route_materializes_and_persists_default_profiles_without_changing_kind_team() {
let (_dir, _home, _cache) = isolated();
let _fleet = DetectAgentsGuard::set(vec![AgentKind::Codex]);
let store = Store::open_memory().expect("store");
let mut child = RunArgs { agent_name: "oz".into(), prompt: "Refactor scheduler".into(),
kind: Some(TaskCategory::Refactoring), ..Default::default() };
let expected = report(Some(&store), &child).declared;
let selected = automatic_candidate(Some(&store), &child).expect("peer");
apply_candidate(&mut child, &selected);
let restored = RunArgs::from_dispatch_args_json(&child.dispatch_args_json().expect("save")).expect("restore");
assert_eq!(restored.declared_difficulty, Some(expected.difficulty));
assert_eq!(restored.declared_budget, Some(expected.budget));
assert_eq!(restored.declared_urgency, Some(expected.urgency));
assert_eq!(restored.declared_rigor, Some(expected.rigor));
assert_eq!(restored.kind, child.kind);
assert_eq!(restored.team, child.team);
assert_eq!(restored.model_source, ModelSource::Advised);
}
#[path = "advice_route_resolve_tests.rs"]
mod resolve_tests;