1use thiserror::Error;
4
5#[derive(Error, Debug)]
11#[non_exhaustive]
12pub enum RavenClawsError {
13 #[error("LLM error: {0}")]
14 Llm(#[from] crate::llm::LLMError),
15
16 #[error("Configuration error: {0}")]
17 Config(#[from] crate::config::ConfigError),
18
19 #[error("RavenFabric error: {0}")]
20 #[allow(dead_code)]
21 RavenFabric(String),
22
23 #[error("Network error: {0}")]
24 Network(#[from] reqwest::Error),
25
26 #[error("IO error: {0}")]
27 IO(#[from] std::io::Error),
28
29 #[error("Command execution failed: {0}")]
30 CommandExecution(String),
31
32 #[error("Security violation: {0}")]
33 #[allow(dead_code)]
34 SecurityViolation(String),
35
36 #[error("Agent failed: {0}")]
37 #[allow(dead_code)]
38 AgentFailed(String),
39
40 #[error("Self-healing error: {0}")]
41 #[allow(dead_code)]
42 HealingError(String),
43}
44
45impl RavenClawsError {
46 #[allow(dead_code)]
48 pub fn is_transient(&self) -> bool {
49 matches!(
50 self,
51 RavenClawsError::Llm(e) if e.is_transient(),
52 )
53 }
54}
55
56pub type Result<T> = std::result::Result<T, RavenClawsError>;
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn test_llm_error_variant() {
64 let err = RavenClawsError::Llm(crate::llm::LLMError::RequestFailed("timeout".to_string()));
65 assert_eq!(format!("{}", err), "LLM error: Request failed: timeout");
66 }
67
68 #[test]
69 fn test_config_error_variant() {
70 let err = RavenClawsError::Config(crate::config::ConfigError::ValidationError(
71 "bad field".to_string(),
72 ));
73 assert_eq!(
74 format!("{}", err),
75 "Configuration error: Invalid configuration: bad field"
76 );
77 }
78
79 #[test]
80 fn test_ravenfabric_error_variant() {
81 let err = RavenClawsError::RavenFabric("connection refused".to_string());
82 assert_eq!(format!("{}", err), "RavenFabric error: connection refused");
83 }
84
85 #[test]
86 fn test_command_execution_error_variant() {
87 let err = RavenClawsError::CommandExecution("command failed".to_string());
88 assert_eq!(
89 format!("{}", err),
90 "Command execution failed: command failed"
91 );
92 }
93
94 #[test]
95 fn test_security_violation_error_variant() {
96 let err = RavenClawsError::SecurityViolation("unauthorized access".to_string());
97 assert_eq!(
98 format!("{}", err),
99 "Security violation: unauthorized access"
100 );
101 }
102
103 #[test]
104 fn test_result_type_alias() {
105 let ok: i32 = 42;
106 assert_eq!(ok, 42);
107
108 let err: Result<i32> = Err(RavenClawsError::CommandExecution("fail".to_string()));
109 assert!(err.is_err());
110 }
111
112 #[tokio::test]
113 async fn test_network_error_variant() {
114 let err = RavenClawsError::Network(
118 reqwest::Client::builder()
119 .build()
120 .unwrap()
121 .get("http://invalid.example.com")
122 .send()
123 .await
124 .unwrap_err(),
125 );
126 assert!(format!("{}", err).contains("Network error"));
127 }
128
129 #[test]
130 fn test_io_error_variant() {
131 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
132 let err = RavenClawsError::IO(io_err);
133 assert!(format!("{}", err).contains("IO error"));
134 assert!(format!("{}", err).contains("file not found"));
135 }
136
137 #[test]
138 fn test_error_is_debug() {
139 let err = RavenClawsError::CommandExecution("test".to_string());
140 let debug = format!("{:?}", err);
141 assert!(debug.contains("CommandExecution"));
142 }
143
144 #[test]
145 fn test_error_is_send() {
146 fn check_send<T: Send>() {}
147 check_send::<RavenClawsError>();
148 }
149
150 #[test]
151 fn test_error_is_sync() {
152 fn check_sync<T: Sync>() {}
153 check_sync::<RavenClawsError>();
154 }
155
156 #[test]
157 fn test_from_llm_error_conversion() {
158 let llm_err = crate::llm::LLMError::RequestFailed("timeout".to_string());
159 let err: RavenClawsError = llm_err.into();
160 assert!(format!("{}", err).contains("LLM error"));
161 assert!(format!("{}", err).contains("timeout"));
162 }
163
164 #[test]
165 fn test_from_config_error_conversion() {
166 let cfg_err = crate::config::ConfigError::ValidationError("bad config".to_string());
167 let err: RavenClawsError = cfg_err.into();
168 assert!(format!("{}", err).contains("Configuration error"));
169 assert!(format!("{}", err).contains("bad config"));
170 }
171
172 #[test]
173 fn test_from_io_error_conversion() {
174 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
175 let err: RavenClawsError = io_err.into();
176 assert!(format!("{}", err).contains("IO error"));
177 assert!(format!("{}", err).contains("permission denied"));
178 }
179
180 #[test]
181 fn test_error_source_chain() {
182 let inner = crate::llm::LLMError::AuthFailed;
185 let err = RavenClawsError::Llm(inner);
186 let display = format!("{}", err);
187 assert!(display.contains("Authentication failed"));
188 }
189
190 #[test]
191 fn test_ravenfabric_error_construction() {
192 let err = RavenClawsError::RavenFabric("connection timeout".to_string());
193 assert_eq!(format!("{}", err), "RavenFabric error: connection timeout");
194 }
195
196 #[test]
197 fn test_security_violation_construction() {
198 let err = RavenClawsError::SecurityViolation("invalid token".to_string());
199 assert_eq!(format!("{}", err), "Security violation: invalid token");
200 }
201
202 #[test]
203 fn test_agent_failed_variant() {
204 let err = RavenClawsError::AgentFailed("worker-1 crashed".to_string());
205 assert_eq!(format!("{}", err), "Agent failed: worker-1 crashed");
206 }
207
208 #[test]
209 fn test_healing_error_variant() {
210 let err = RavenClawsError::HealingError("circuit breaker open".to_string());
211 assert_eq!(
212 format!("{}", err),
213 "Self-healing error: circuit breaker open"
214 );
215 }
216
217 #[test]
218 fn test_is_transient_llm_request_failed() {
219 let llm_err = crate::llm::LLMError::RequestFailed("timeout".to_string());
220 assert!(llm_err.is_transient());
221 }
222
223 #[test]
224 fn test_is_transient_llm_rate_limited() {
225 let llm_err = crate::llm::LLMError::RateLimited;
226 assert!(llm_err.is_transient());
227 }
228
229 #[test]
230 fn test_is_transient_llm_circuit_breaker() {
231 let llm_err = crate::llm::LLMError::CircuitBreakerOpen("openai".to_string());
232 assert!(llm_err.is_transient());
233 }
234
235 #[test]
236 fn test_is_not_transient_llm_auth_failed() {
237 let llm_err = crate::llm::LLMError::AuthFailed;
238 assert!(!llm_err.is_transient());
239 }
240
241 #[test]
242 fn test_is_not_transient_llm_invalid_response() {
243 let llm_err = crate::llm::LLMError::InvalidResponse("bad json".to_string());
244 assert!(!llm_err.is_transient());
245 }
246
247 #[test]
248 fn test_is_not_transient_llm_token_budget() {
249 let llm_err = crate::llm::LLMError::TokenBudgetExceeded;
250 assert!(!llm_err.is_transient());
251 }
252
253 #[test]
254 fn test_is_not_transient_llm_all_providers_failed() {
255 let llm_err = crate::llm::LLMError::AllProvidersFailed;
256 assert!(!llm_err.is_transient());
257 }
258
259 #[test]
260 fn test_ravenclaws_error_is_transient_via_llm() {
261 let llm_err = crate::llm::LLMError::RequestFailed("timeout".to_string());
262 let err = RavenClawsError::Llm(llm_err);
263 assert!(err.is_transient());
264 }
265
266 #[test]
267 fn test_ravenclaws_error_is_not_transient_for_non_llm() {
268 let err = RavenClawsError::CommandExecution("fail".to_string());
269 assert!(!err.is_transient());
270 }
271
272 #[test]
273 fn test_ravenclaws_error_is_not_transient_for_agent_failed() {
274 let err = RavenClawsError::AgentFailed("crashed".to_string());
275 assert!(!err.is_transient());
276 }
277
278 #[test]
279 #[allow(clippy::unnecessary_literal_unwrap)]
280 fn test_result_type_alias_ok() {
281 let result: Result<i32> = Ok(42);
282 assert!(result.is_ok());
283 assert_eq!(result.unwrap(), 42);
284 }
285
286 #[test]
287 #[allow(clippy::unnecessary_literal_unwrap)]
288 fn test_result_type_alias_err() {
289 let result: Result<i32> = Err(RavenClawsError::CommandExecution("fail".to_string()));
290 assert!(result.is_err());
291 assert_eq!(
292 format!("{}", result.unwrap_err()),
293 "Command execution failed: fail"
294 );
295 }
296
297 #[test]
298 fn test_error_into_boxed() {
299 let err = RavenClawsError::CommandExecution("boxed".to_string());
301 let boxed: Box<dyn std::error::Error> = Box::new(err);
302 assert!(format!("{}", boxed).contains("Command execution failed"));
303 }
304
305 #[test]
306 fn test_error_into_string() {
307 let err = RavenClawsError::SecurityViolation("access denied".to_string());
308 let msg: String = err.to_string();
309 assert_eq!(msg, "Security violation: access denied");
310 }
311
312 #[test]
313 fn test_error_from_reqwest() {
314 fn _check_from()
318 where
319 reqwest::Error: Into<RavenClawsError>,
320 {
321 }
322 }
324
325 #[test]
326 fn test_error_display_network_variant() {
327 let rt = tokio::runtime::Runtime::new().unwrap();
329 let err = rt.block_on(async {
330 reqwest::Client::builder()
331 .build()
332 .unwrap()
333 .get("http://invalid.example.com")
334 .send()
335 .await
336 .unwrap_err()
337 });
338 let raven_err = RavenClawsError::Network(err);
339 let display = format!("{}", raven_err);
340 assert!(display.contains("Network error"));
341 assert!(!display.is_empty());
342 }
343
344 #[test]
345 fn test_error_source_chain_io() {
346 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
348 let err = RavenClawsError::IO(io_err);
349 let display = format!("{}", err);
350 assert!(display.contains("IO error"));
351 assert!(display.contains("file not found"));
352 }
353
354 #[test]
355 fn test_error_source_chain_config() {
356 let cfg_err = crate::config::ConfigError::ValidationError("invalid".to_string());
357 let err = RavenClawsError::Config(cfg_err);
358 let display = format!("{}", err);
359 assert!(display.contains("Configuration error"));
360 assert!(display.contains("invalid"));
361 }
362
363 #[test]
364 fn test_error_source_chain_llm() {
365 let llm_err = crate::llm::LLMError::RateLimited;
366 let err = RavenClawsError::Llm(llm_err);
367 let display = format!("{}", err);
368 assert!(display.contains("LLM error"));
369 assert!(display.contains("Rate limit exceeded"));
370 }
371
372 #[test]
373 fn test_error_clone_not_required() {
374 fn _check_no_clone<T>() {
377 }
379 _check_no_clone::<RavenClawsError>();
380 }
381}