Skip to main content

aether_cli/
resolve.rs

1use crate::error::CliError;
2use aether_core::agent_spec::AgentSpec;
3use aether_project::AgentCatalog;
4use std::path::Path;
5
6pub fn resolve_agent_spec(catalog: &AgentCatalog, agent_name: Option<&str>, cwd: &Path) -> Result<AgentSpec, CliError> {
7    match agent_name {
8        Some(name) => catalog.resolve(name, cwd).map_err(|e| CliError::AgentError(e.to_string())),
9
10        None => {
11            if let Some(first) = catalog.user_invocable().next() {
12                catalog.resolve(&first.name, cwd).map_err(|e| CliError::AgentError(e.to_string()))
13            } else {
14                let model = "anthropic:claude-sonnet-4-5".parse().map_err(|e: String| CliError::ModelError(e))?;
15
16                Ok(catalog.resolve_default(&model, None, cwd))
17            }
18        }
19    }
20}
21
22#[cfg(test)]
23mod tests {
24    use super::*;
25    use aether_project::load_agent_catalog;
26
27    fn write_file(dir: &std::path::Path, path: &str, content: &str) {
28        let full = dir.join(path);
29        if let Some(parent) = full.parent() {
30            std::fs::create_dir_all(parent).unwrap();
31        }
32        std::fs::write(full, content).unwrap();
33    }
34
35    fn setup_catalog(settings_json: &str) -> (tempfile::TempDir, AgentCatalog) {
36        let dir = tempfile::tempdir().unwrap();
37        write_file(dir.path(), "PROMPT.md", "Be helpful");
38        write_file(dir.path(), ".aether/settings.json", settings_json);
39        let catalog = load_agent_catalog(dir.path()).unwrap();
40        (dir, catalog)
41    }
42
43    #[test]
44    fn resolve_with_explicit_name() {
45        let (dir, catalog) = setup_catalog(
46            r#"{"agents": [
47                {"name": "first", "description": "First", "model": "anthropic:claude-sonnet-4-5", "userInvocable": true, "prompts": ["PROMPT.md"]},
48                {"name": "second", "description": "Second", "model": "anthropic:claude-sonnet-4-5", "userInvocable": true, "prompts": ["PROMPT.md"]}
49            ]}"#,
50        );
51        let spec = resolve_agent_spec(&catalog, Some("second"), dir.path()).unwrap();
52        assert_eq!(spec.name, "second");
53    }
54
55    #[test]
56    fn resolve_auto_selects_first_user_invocable() {
57        let (dir, catalog) = setup_catalog(
58            r#"{"agents": [
59                {"name": "internal", "description": "Internal", "model": "anthropic:claude-sonnet-4-5", "agentInvocable": true, "prompts": ["PROMPT.md"]},
60                {"name": "visible", "description": "Visible", "model": "anthropic:claude-sonnet-4-5", "userInvocable": true, "prompts": ["PROMPT.md"]}
61            ]}"#,
62        );
63        let spec = resolve_agent_spec(&catalog, None, dir.path()).unwrap();
64        assert_eq!(spec.name, "visible");
65    }
66
67    #[test]
68    fn resolve_falls_back_to_default() {
69        let dir = tempfile::tempdir().unwrap();
70        let catalog = AgentCatalog::empty(dir.path().to_path_buf());
71        let spec = resolve_agent_spec(&catalog, None, dir.path()).unwrap();
72        assert_eq!(spec.name, "__default__");
73    }
74
75    #[test]
76    fn resolve_unknown_name_errors() {
77        let (dir, catalog) = setup_catalog(
78            r#"{"agents": [
79                {"name": "alpha", "description": "Alpha", "model": "anthropic:claude-sonnet-4-5", "userInvocable": true, "prompts": ["PROMPT.md"]}
80            ]}"#,
81        );
82        let result = resolve_agent_spec(&catalog, Some("nonexistent"), dir.path());
83        assert!(result.is_err());
84    }
85}