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 let mut config = FallbackConfig::default();
145 config.developer = refs.iter().map(|s| s.to_string()).collect();
146 config
147 }
148
149 #[test]
150 fn test_parse_opencode_ref_valid() {
151 let result = parse_opencode_ref("opencode/anthropic/claude-sonnet-4-5");
152 assert_eq!(
153 result,
154 Some(("anthropic".to_string(), "claude-sonnet-4-5".to_string()))
155 );
156 }
157
158 #[test]
159 fn test_parse_opencode_ref_invalid() {
160 assert_eq!(parse_opencode_ref("claude"), None);
161 assert_eq!(parse_opencode_ref("opencode"), None);
162 assert_eq!(parse_opencode_ref("opencode/anthropic"), None);
163 assert_eq!(parse_opencode_ref("ccs/glm"), None);
164 }
165
166 #[test]
167 fn test_validate_opencode_agents_valid() {
168 let catalog = mock_catalog();
169 let fallback = create_fallback_with_refs(vec!["opencode/anthropic/claude-sonnet-4-5"]);
170
171 let result = validate_opencode_agents(&fallback, &catalog);
172 assert!(result.is_ok());
173 }
174
175 #[test]
176 fn test_validate_opencode_agents_invalid_provider() {
177 let catalog = mock_catalog();
178 let fallback = create_fallback_with_refs(vec!["opencode/unknown/claude-sonnet-4-5"]);
179
180 let result = validate_opencode_agents(&fallback, &catalog);
181 assert!(result.is_err());
182 assert!(result.unwrap_err().contains("unknown"));
183 }
184
185 #[test]
186 fn test_validate_opencode_agents_invalid_model() {
187 let catalog = mock_catalog();
188 let fallback = create_fallback_with_refs(vec!["opencode/anthropic/unknown-model"]);
189
190 let result = validate_opencode_agents(&fallback, &catalog);
191 assert!(result.is_err());
192 assert!(result.unwrap_err().contains("unknown-model"));
193 }
194
195 #[test]
196 fn test_count_opencode_refs() {
197 let fallback = create_fallback_with_refs(vec![
198 "opencode/anthropic/claude-sonnet-4-5",
199 "claude",
200 "opencode/openai/gpt-4",
201 ]);
202
203 let count = count_opencode_refs(&fallback);
204 assert_eq!(count, 2);
205 }
206
207 #[test]
208 fn test_get_opencode_refs() {
209 let fallback = create_fallback_with_refs(vec![
210 "opencode/anthropic/claude-sonnet-4-5",
211 "claude",
212 "opencode/openai/gpt-4",
213 ]);
214
215 let refs = get_opencode_refs(&fallback);
216 assert_eq!(refs.len(), 2);
217 assert!(refs.contains(&"opencode/anthropic/claude-sonnet-4-5".to_string()));
218 assert!(refs.contains(&"opencode/openai/gpt-4".to_string()));
219 }
220}