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    #[error("Kubernetes error: {0}")]
45    #[allow(dead_code)]
46    K8s(String),
47}
48
49impl RavenClawsError {
50    /// Returns `true` if this error is transient and may succeed on retry.
51    #[allow(dead_code)]
52    pub fn is_transient(&self) -> bool {
53        matches!(
54            self,
55            RavenClawsError::Llm(e) if e.is_transient(),
56        )
57    }
58}
59
60pub type Result<T> = std::result::Result<T, RavenClawsError>;
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_llm_error_variant() {
68        let err = RavenClawsError::Llm(crate::llm::LLMError::RequestFailed("timeout".to_string()));
69        assert_eq!(format!("{}", err), "LLM error: Request failed: timeout");
70    }
71
72    #[test]
73    fn test_config_error_variant() {
74        let err = RavenClawsError::Config(crate::config::ConfigError::ValidationError(
75            "bad field".to_string(),
76        ));
77        assert_eq!(
78            format!("{}", err),
79            "Configuration error: Invalid configuration: bad field"
80        );
81    }
82
83    #[test]
84    fn test_ravenfabric_error_variant() {
85        let err = RavenClawsError::RavenFabric("connection refused".to_string());
86        assert_eq!(format!("{}", err), "RavenFabric error: connection refused");
87    }
88
89    #[test]
90    fn test_command_execution_error_variant() {
91        let err = RavenClawsError::CommandExecution("command failed".to_string());
92        assert_eq!(
93            format!("{}", err),
94            "Command execution failed: command failed"
95        );
96    }
97
98    #[test]
99    fn test_security_violation_error_variant() {
100        let err = RavenClawsError::SecurityViolation("unauthorized access".to_string());
101        assert_eq!(
102            format!("{}", err),
103            "Security violation: unauthorized access"
104        );
105    }
106
107    #[test]
108    fn test_result_type_alias() {
109        let ok: i32 = 42;
110        assert_eq!(ok, 42);
111
112        let err: Result<i32> = Err(RavenClawsError::CommandExecution("fail".to_string()));
113        assert!(err.is_err());
114    }
115
116    #[tokio::test]
117    async fn test_network_error_variant() {
118        // Network error from reqwest — we can construct it via the From impl
119        // by creating a reqwest error. Since reqwest::Error is opaque, we
120        // test the variant via the Display trait.
121        let err = RavenClawsError::Network(
122            reqwest::Client::builder()
123                .build()
124                .unwrap()
125                .get("http://invalid.example.com")
126                .send()
127                .await
128                .unwrap_err(),
129        );
130        assert!(format!("{}", err).contains("Network error"));
131    }
132
133    #[test]
134    fn test_io_error_variant() {
135        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
136        let err = RavenClawsError::IO(io_err);
137        assert!(format!("{}", err).contains("IO error"));
138        assert!(format!("{}", err).contains("file not found"));
139    }
140
141    #[test]
142    fn test_error_is_debug() {
143        let err = RavenClawsError::CommandExecution("test".to_string());
144        let debug = format!("{:?}", err);
145        assert!(debug.contains("CommandExecution"));
146    }
147
148    #[test]
149    fn test_error_is_send() {
150        fn check_send<T: Send>() {}
151        check_send::<RavenClawsError>();
152    }
153
154    #[test]
155    fn test_error_is_sync() {
156        fn check_sync<T: Sync>() {}
157        check_sync::<RavenClawsError>();
158    }
159
160    #[test]
161    fn test_from_llm_error_conversion() {
162        let llm_err = crate::llm::LLMError::RequestFailed("timeout".to_string());
163        let err: RavenClawsError = llm_err.into();
164        assert!(format!("{}", err).contains("LLM error"));
165        assert!(format!("{}", err).contains("timeout"));
166    }
167
168    #[test]
169    fn test_from_config_error_conversion() {
170        let cfg_err = crate::config::ConfigError::ValidationError("bad config".to_string());
171        let err: RavenClawsError = cfg_err.into();
172        assert!(format!("{}", err).contains("Configuration error"));
173        assert!(format!("{}", err).contains("bad config"));
174    }
175
176    #[test]
177    fn test_from_io_error_conversion() {
178        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
179        let err: RavenClawsError = io_err.into();
180        assert!(format!("{}", err).contains("IO error"));
181        assert!(format!("{}", err).contains("permission denied"));
182    }
183
184    #[test]
185    fn test_error_source_chain() {
186        // RavenClawsError doesn't implement std::error::Error::source() directly
187        // for all variants, but the Display impl should contain the inner message
188        let inner = crate::llm::LLMError::AuthFailed;
189        let err = RavenClawsError::Llm(inner);
190        let display = format!("{}", err);
191        assert!(display.contains("Authentication failed"));
192    }
193
194    #[test]
195    fn test_ravenfabric_error_construction() {
196        let err = RavenClawsError::RavenFabric("connection timeout".to_string());
197        assert_eq!(format!("{}", err), "RavenFabric error: connection timeout");
198    }
199
200    #[test]
201    fn test_security_violation_construction() {
202        let err = RavenClawsError::SecurityViolation("invalid token".to_string());
203        assert_eq!(format!("{}", err), "Security violation: invalid token");
204    }
205
206    #[test]
207    fn test_agent_failed_variant() {
208        let err = RavenClawsError::AgentFailed("worker-1 crashed".to_string());
209        assert_eq!(format!("{}", err), "Agent failed: worker-1 crashed");
210    }
211
212    #[test]
213    fn test_healing_error_variant() {
214        let err = RavenClawsError::HealingError("circuit breaker open".to_string());
215        assert_eq!(
216            format!("{}", err),
217            "Self-healing error: circuit breaker open"
218        );
219    }
220
221    #[test]
222    fn test_is_transient_llm_request_failed() {
223        let llm_err = crate::llm::LLMError::RequestFailed("timeout".to_string());
224        assert!(llm_err.is_transient());
225    }
226
227    #[test]
228    fn test_is_transient_llm_rate_limited() {
229        let llm_err = crate::llm::LLMError::RateLimited;
230        assert!(llm_err.is_transient());
231    }
232
233    #[test]
234    fn test_is_transient_llm_circuit_breaker() {
235        let llm_err = crate::llm::LLMError::CircuitBreakerOpen("openai".to_string());
236        assert!(llm_err.is_transient());
237    }
238
239    #[test]
240    fn test_is_not_transient_llm_auth_failed() {
241        let llm_err = crate::llm::LLMError::AuthFailed;
242        assert!(!llm_err.is_transient());
243    }
244
245    #[test]
246    fn test_is_not_transient_llm_invalid_response() {
247        let llm_err = crate::llm::LLMError::InvalidResponse("bad json".to_string());
248        assert!(!llm_err.is_transient());
249    }
250
251    #[test]
252    fn test_is_not_transient_llm_token_budget() {
253        let llm_err = crate::llm::LLMError::TokenBudgetExceeded;
254        assert!(!llm_err.is_transient());
255    }
256
257    #[test]
258    fn test_is_not_transient_llm_all_providers_failed() {
259        let llm_err = crate::llm::LLMError::AllProvidersFailed;
260        assert!(!llm_err.is_transient());
261    }
262
263    #[test]
264    fn test_ravenclaws_error_is_transient_via_llm() {
265        let llm_err = crate::llm::LLMError::RequestFailed("timeout".to_string());
266        let err = RavenClawsError::Llm(llm_err);
267        assert!(err.is_transient());
268    }
269
270    #[test]
271    fn test_ravenclaws_error_is_not_transient_for_non_llm() {
272        let err = RavenClawsError::CommandExecution("fail".to_string());
273        assert!(!err.is_transient());
274    }
275
276    #[test]
277    fn test_ravenclaws_error_is_not_transient_for_agent_failed() {
278        let err = RavenClawsError::AgentFailed("crashed".to_string());
279        assert!(!err.is_transient());
280    }
281
282    #[test]
283    #[allow(clippy::unnecessary_literal_unwrap)]
284    fn test_result_type_alias_ok() {
285        let result: Result<i32> = Ok(42);
286        assert!(result.is_ok());
287        assert_eq!(result.unwrap(), 42);
288    }
289
290    #[test]
291    #[allow(clippy::unnecessary_literal_unwrap)]
292    fn test_result_type_alias_err() {
293        let result: Result<i32> = Err(RavenClawsError::CommandExecution("fail".to_string()));
294        assert!(result.is_err());
295        assert_eq!(
296            format!("{}", result.unwrap_err()),
297            "Command execution failed: fail"
298        );
299    }
300
301    #[test]
302    fn test_error_into_boxed() {
303        // Verify RavenClawsError can be boxed (required for std::error::Error trait)
304        let err = RavenClawsError::CommandExecution("boxed".to_string());
305        let boxed: Box<dyn std::error::Error> = Box::new(err);
306        assert!(format!("{}", boxed).contains("Command execution failed"));
307    }
308
309    #[test]
310    fn test_error_into_string() {
311        let err = RavenClawsError::SecurityViolation("access denied".to_string());
312        let msg: String = err.to_string();
313        assert_eq!(msg, "Security violation: access denied");
314    }
315
316    #[test]
317    fn test_error_from_reqwest() {
318        // Verify the From<reqwest::Error> impl compiles and works
319        // We can't easily construct a reqwest::Error directly, but we can
320        // verify the From impl exists by checking the trait bounds
321        fn _check_from()
322        where
323            reqwest::Error: Into<RavenClawsError>,
324        {
325        }
326        // Compile-time check passes
327    }
328
329    #[test]
330    fn test_error_display_network_variant() {
331        // Network error display should contain the inner error message
332        let rt = tokio::runtime::Runtime::new().unwrap();
333        let err = rt.block_on(async {
334            reqwest::Client::builder()
335                .build()
336                .unwrap()
337                .get("http://invalid.example.com")
338                .send()
339                .await
340                .unwrap_err()
341        });
342        let raven_err = RavenClawsError::Network(err);
343        let display = format!("{}", raven_err);
344        assert!(display.contains("Network error"));
345        assert!(!display.is_empty());
346    }
347
348    #[test]
349    fn test_error_source_chain_io() {
350        // Test source chain: IO error wrapped in RavenClawsError
351        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
352        let err = RavenClawsError::IO(io_err);
353        let display = format!("{}", err);
354        assert!(display.contains("IO error"));
355        assert!(display.contains("file not found"));
356    }
357
358    #[test]
359    fn test_error_source_chain_config() {
360        let cfg_err = crate::config::ConfigError::ValidationError("invalid".to_string());
361        let err = RavenClawsError::Config(cfg_err);
362        let display = format!("{}", err);
363        assert!(display.contains("Configuration error"));
364        assert!(display.contains("invalid"));
365    }
366
367    #[test]
368    fn test_error_source_chain_llm() {
369        let llm_err = crate::llm::LLMError::RateLimited;
370        let err = RavenClawsError::Llm(llm_err);
371        let display = format!("{}", err);
372        assert!(display.contains("LLM error"));
373        assert!(display.contains("Rate limit exceeded"));
374    }
375
376    #[test]
377    fn test_error_clone_not_required() {
378        // RavenClawsError intentionally does not implement Clone.
379        // This test verifies that by checking it at compile time.
380        fn _check_no_clone<T>() {
381            // If this compiles, RavenClawsError does NOT implement Clone
382        }
383        _check_no_clone::<RavenClawsError>();
384    }
385}