1pub use nucel_agent_core::{
45 AgentCapabilities, AgentCost, AgentError, AgentExecutor, AgentResponse, AgentSession,
46 AvailabilityStatus, ExecutorType, PermissionMode, Result, SessionMetadata, SpawnConfig,
47};
48
49pub use nucel_agent_claude_code::ClaudeCodeExecutor;
51pub use nucel_agent_codex::CodexExecutor;
52pub use nucel_agent_opencode::OpencodeExecutor;
53
54pub fn build_executor(
62 provider: &str,
63 api_key_or_url: Option<String>,
64) -> Option<Box<dyn AgentExecutor>> {
65 match provider {
66 "claude-code" | "claude_code" | "claudecode" => Some(Box::new(ClaudeCodeExecutor::new())),
67 "codex" => Some(Box::new(CodexExecutor::new())),
68 "opencode" => {
69 let mut exec = OpencodeExecutor::new();
70 if let Some(url) = api_key_or_url {
71 exec = OpencodeExecutor::with_base_url(url);
72 }
73 Some(Box::new(exec))
74 }
75 _ => None,
76 }
77}
78
79pub fn available_providers() -> &'static [&'static str] {
81 &["claude-code", "codex", "opencode"]
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn build_claude_code_executor() {
90 let exec = build_executor("claude-code", None).unwrap();
91 assert_eq!(exec.executor_type(), ExecutorType::ClaudeCode);
92 }
93
94 #[test]
95 fn build_codex_executor() {
96 let exec = build_executor("codex", None).unwrap();
97 assert_eq!(exec.executor_type(), ExecutorType::Codex);
98 }
99
100 #[test]
101 fn build_opencode_executor() {
102 let exec = build_executor("opencode", None).unwrap();
103 assert_eq!(exec.executor_type(), ExecutorType::OpenCode);
104 }
105
106 #[test]
107 fn build_opencode_with_url() {
108 let exec = build_executor("opencode", Some("http://my-server:8080".into())).unwrap();
109 assert_eq!(exec.executor_type(), ExecutorType::OpenCode);
110 }
111
112 #[test]
113 fn unknown_provider_returns_none() {
114 assert!(build_executor("gpt-4", None).is_none());
115 }
116
117 #[test]
118 fn claude_code_aliases_work() {
119 assert!(build_executor("claude_code", None).is_some());
120 assert!(build_executor("claudecode", None).is_some());
121 }
122
123 #[test]
124 fn available_providers_list() {
125 let providers = available_providers();
126 assert_eq!(providers.len(), 3);
127 assert!(providers.contains(&"claude-code"));
128 assert!(providers.contains(&"codex"));
129 assert!(providers.contains(&"opencode"));
130 }
131
132 #[test]
133 fn build_executor_empty_string_returns_none() {
134 assert!(build_executor("", None).is_none());
135 }
136
137 #[test]
138 fn build_executor_case_sensitive() {
139 assert!(build_executor("Claude-Code", None).is_none());
140 assert!(build_executor("CODEX", None).is_none());
141 assert!(build_executor("OpenCode", None).is_none());
142 }
143
144 #[test]
145 fn all_executors_have_capabilities() {
146 for provider in available_providers() {
147 let exec = build_executor(provider, None).unwrap();
148 let caps = exec.capabilities();
149 assert!(caps.token_usage, "{provider} should support token_usage");
151 assert!(caps.autonomous_mode, "{provider} should support autonomous_mode");
153 }
154 }
155
156 #[test]
157 fn all_executors_report_availability() {
158 for provider in available_providers() {
159 let exec = build_executor(provider, None).unwrap();
160 let status = exec.availability();
161 if !status.available {
163 assert!(status.reason.is_some(), "{provider} unavailable but no reason");
164 }
165 }
166 }
167
168 #[test]
169 fn claude_code_api_key_ignored_by_build_executor() {
170 let exec = build_executor("claude-code", Some("sk-test".into())).unwrap();
172 assert_eq!(exec.executor_type(), ExecutorType::ClaudeCode);
173 }
174
175 #[test]
176 fn codex_api_key_ignored_by_build_executor() {
177 let exec = build_executor("codex", Some("sk-test".into())).unwrap();
178 assert_eq!(exec.executor_type(), ExecutorType::Codex);
179 }
180}