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