Skip to main content

ravenclaws/
error.rs

1//! RavenClaws
2
3use thiserror::Error;
4
5/// Unified error type for RavenClaws.
6///
7/// # Stability
8/// This enum is `#[non_exhaustive]` — new variants may be added in minor releases.
9/// Match with a wildcard arm to handle future variants.
10#[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    /// Returns `true` if this error is transient and may succeed on retry.
47    #[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        // Network error from reqwest — we can construct it via the From impl
115        // by creating a reqwest error. Since reqwest::Error is opaque, we
116        // test the variant via the Display trait.
117        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        // RavenClawsError doesn't implement std::error::Error::source() directly
183        // for all variants, but the Display impl should contain the inner message
184        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        // Verify RavenClawsError can be boxed (required for std::error::Error trait)
300        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        // Verify the From<reqwest::Error> impl compiles and works
315        // We can't easily construct a reqwest::Error directly, but we can
316        // verify the From impl exists by checking the trait bounds
317        fn _check_from()
318        where
319            reqwest::Error: Into<RavenClawsError>,
320        {
321        }
322        // Compile-time check passes
323    }
324
325    #[test]
326    fn test_error_display_network_variant() {
327        // Network error display should contain the inner error message
328        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        // Test source chain: IO error wrapped in RavenClawsError
347        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        // RavenClawsError intentionally does not implement Clone.
375        // This test verifies that by checking it at compile time.
376        fn _check_no_clone<T>() {
377            // If this compiles, RavenClawsError does NOT implement Clone
378        }
379        _check_no_clone::<RavenClawsError>();
380    }
381}