ralph_workflow/agents/
validation.rs1use crate::agents::fallback::FallbackConfig;
11use crate::agents::opencode_api::ApiCatalog;
12use crate::agents::opencode_resolver::OpenCodeResolver;
13
14pub fn validate_opencode_agents(
26 fallback: &FallbackConfig,
27 catalog: &ApiCatalog,
28) -> Result<(), String> {
29 let resolver = OpenCodeResolver::new(catalog.clone());
30 let mut errors = Vec::new();
31
32 let all_agents: Vec<&str> = fallback
34 .get_fallbacks(crate::agents::AgentRole::Developer)
35 .iter()
36 .chain(
37 fallback
38 .get_fallbacks(crate::agents::AgentRole::Reviewer)
39 .iter(),
40 )
41 .map(std::string::String::as_str)
42 .collect();
43
44 for agent_name in all_agents {
46 if let Some((provider, model)) = parse_opencode_ref(agent_name) {
47 if let Err(e) = resolver.validate(&provider, &model) {
48 let msg = resolver.format_error(&e, agent_name);
49 errors.push(msg);
50 }
51 }
52 }
53
54 if errors.is_empty() {
55 Ok(())
56 } else {
57 Err(errors.join("\n\n"))
58 }
59}
60
61fn parse_opencode_ref(agent_name: &str) -> Option<(String, String)> {
65 if !agent_name.starts_with("opencode/") {
66 return None;
67 }
68
69 let parts: Vec<&str> = agent_name.split('/').collect();
70 if parts.len() != 3 {
71 return None;
72 }
73
74 let provider = parts[1].to_string();
75 let model = parts[2].to_string();
76
77 Some((provider, model))
78}
79
80#[must_use]
82pub fn get_opencode_refs(fallback: &FallbackConfig) -> Vec<String> {
83 fallback
84 .get_fallbacks(crate::agents::AgentRole::Developer)
85 .iter()
86 .chain(
87 fallback
88 .get_fallbacks(crate::agents::AgentRole::Reviewer)
89 .iter(),
90 )
91 .filter(|name| name.starts_with("opencode/"))
92 .cloned()
93 .collect()
94}
95
96#[cfg(test)]
98fn count_opencode_refs(fallback: &FallbackConfig) -> usize {
99 fallback
100 .get_fallbacks(crate::agents::AgentRole::Developer)
101 .iter()
102 .chain(
103 fallback
104 .get_fallbacks(crate::agents::AgentRole::Reviewer)
105 .iter(),
106 )
107 .filter(|name| name.starts_with("opencode/"))
108 .count()
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::agents::opencode_api::{Model, Provider};
115 use std::collections::HashMap;
116
117 fn mock_catalog() -> ApiCatalog {
118 let mut providers = HashMap::new();
119 providers.insert(
120 "anthropic".to_string(),
121 Provider {
122 id: "anthropic".to_string(),
123 name: "Anthropic".to_string(),
124 description: "Anthropic Claude models".to_string(),
125 },
126 );
127
128 let mut models = HashMap::new();
129 models.insert(
130 "anthropic".to_string(),
131 vec![Model {
132 id: "claude-sonnet-4-5".to_string(),
133 name: "Claude Sonnet 4.5".to_string(),
134 description: "Latest Claude Sonnet".to_string(),
135 context_length: Some(200_000),
136 }],
137 );
138
139 ApiCatalog {
140 providers,
141 models,
142 cached_at: Some(chrono::Utc::now()),
143 ttl_seconds: 86400,
144 }
145 }
146
147 fn create_fallback_with_refs(refs: &[&str]) -> FallbackConfig {
148 FallbackConfig {
149 developer: refs.iter().map(|s| (*s).to_string()).collect(),
150 ..FallbackConfig::default()
151 }
152 }
153
154 #[test]
155 fn test_parse_opencode_ref_valid() {
156 let result = parse_opencode_ref("opencode/anthropic/claude-sonnet-4-5");
157 assert_eq!(
158 result,
159 Some(("anthropic".to_string(), "claude-sonnet-4-5".to_string()))
160 );
161 }
162
163 #[test]
164 fn test_parse_opencode_ref_invalid() {
165 assert_eq!(parse_opencode_ref("claude"), None);
166 assert_eq!(parse_opencode_ref("opencode"), None);
167 assert_eq!(parse_opencode_ref("opencode/anthropic"), None);
168 assert_eq!(parse_opencode_ref("ccs/glm"), None);
169 }
170
171 #[test]
172 fn test_validate_opencode_agents_valid() {
173 let catalog = mock_catalog();
174 let fallback = create_fallback_with_refs(&["opencode/anthropic/claude-sonnet-4-5"]);
175
176 let result = validate_opencode_agents(&fallback, &catalog);
177 assert!(result.is_ok());
178 }
179
180 #[test]
181 fn test_validate_opencode_agents_invalid_provider() {
182 let catalog = mock_catalog();
183 let fallback = create_fallback_with_refs(&["opencode/unknown/claude-sonnet-4-5"]);
184
185 let result = validate_opencode_agents(&fallback, &catalog);
186 assert!(result.is_err());
187 assert!(result.unwrap_err().contains("unknown"));
188 }
189
190 #[test]
191 fn test_validate_opencode_agents_invalid_model() {
192 let catalog = mock_catalog();
193 let fallback = create_fallback_with_refs(&["opencode/anthropic/unknown-model"]);
194
195 let result = validate_opencode_agents(&fallback, &catalog);
196 assert!(result.is_err());
197 assert!(result.unwrap_err().contains("unknown-model"));
198 }
199
200 #[test]
201 fn test_count_opencode_refs() {
202 let fallback = create_fallback_with_refs(&[
203 "opencode/anthropic/claude-sonnet-4-5",
204 "claude",
205 "opencode/openai/gpt-4",
206 ]);
207
208 let count = count_opencode_refs(&fallback);
209 assert_eq!(count, 2);
210 }
211
212 #[test]
213 fn test_get_opencode_refs() {
214 let fallback = create_fallback_with_refs(&[
215 "opencode/anthropic/claude-sonnet-4-5",
216 "claude",
217 "opencode/openai/gpt-4",
218 ]);
219
220 let refs = get_opencode_refs(&fallback);
221 assert_eq!(refs.len(), 2);
222 assert!(refs.contains(&"opencode/anthropic/claude-sonnet-4-5".to_string()));
223 assert!(refs.contains(&"opencode/openai/gpt-4".to_string()));
224 }
225}