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
37pub type Result<T> = std::result::Result<T, RavenClawsError>;
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn test_llm_error_variant() {
45        let err = RavenClawsError::Llm(crate::llm::LLMError::RequestFailed("timeout".to_string()));
46        assert_eq!(format!("{}", err), "LLM error: Request failed: timeout");
47    }
48
49    #[test]
50    fn test_config_error_variant() {
51        let err = RavenClawsError::Config(crate::config::ConfigError::ValidationError(
52            "bad field".to_string(),
53        ));
54        assert_eq!(
55            format!("{}", err),
56            "Configuration error: Invalid configuration: bad field"
57        );
58    }
59
60    #[test]
61    fn test_ravenfabric_error_variant() {
62        let err = RavenClawsError::RavenFabric("connection refused".to_string());
63        assert_eq!(format!("{}", err), "RavenFabric error: connection refused");
64    }
65
66    #[test]
67    fn test_command_execution_error_variant() {
68        let err = RavenClawsError::CommandExecution("command failed".to_string());
69        assert_eq!(
70            format!("{}", err),
71            "Command execution failed: command failed"
72        );
73    }
74
75    #[test]
76    fn test_security_violation_error_variant() {
77        let err = RavenClawsError::SecurityViolation("unauthorized access".to_string());
78        assert_eq!(
79            format!("{}", err),
80            "Security violation: unauthorized access"
81        );
82    }
83
84    #[test]
85    fn test_result_type_alias() {
86        let ok: i32 = 42;
87        assert_eq!(ok, 42);
88
89        let err: Result<i32> = Err(RavenClawsError::CommandExecution("fail".to_string()));
90        assert!(err.is_err());
91    }
92
93    #[tokio::test]
94    async fn test_network_error_variant() {
95        // Network error from reqwest — we can construct it via the From impl
96        // by creating a reqwest error. Since reqwest::Error is opaque, we
97        // test the variant via the Display trait.
98        let err = RavenClawsError::Network(
99            reqwest::Client::builder()
100                .build()
101                .unwrap()
102                .get("http://invalid.example.com")
103                .send()
104                .await
105                .unwrap_err(),
106        );
107        assert!(format!("{}", err).contains("Network error"));
108    }
109
110    #[test]
111    fn test_io_error_variant() {
112        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
113        let err = RavenClawsError::IO(io_err);
114        assert!(format!("{}", err).contains("IO error"));
115        assert!(format!("{}", err).contains("file not found"));
116    }
117
118    #[test]
119    fn test_error_is_debug() {
120        let err = RavenClawsError::CommandExecution("test".to_string());
121        let debug = format!("{:?}", err);
122        assert!(debug.contains("CommandExecution"));
123    }
124
125    #[test]
126    fn test_error_is_send() {
127        fn check_send<T: Send>() {}
128        check_send::<RavenClawsError>();
129    }
130
131    #[test]
132    fn test_error_is_sync() {
133        fn check_sync<T: Sync>() {}
134        check_sync::<RavenClawsError>();
135    }
136
137    #[test]
138    fn test_from_llm_error_conversion() {
139        let llm_err = crate::llm::LLMError::RequestFailed("timeout".to_string());
140        let err: RavenClawsError = llm_err.into();
141        assert!(format!("{}", err).contains("LLM error"));
142        assert!(format!("{}", err).contains("timeout"));
143    }
144
145    #[test]
146    fn test_from_config_error_conversion() {
147        let cfg_err = crate::config::ConfigError::ValidationError("bad config".to_string());
148        let err: RavenClawsError = cfg_err.into();
149        assert!(format!("{}", err).contains("Configuration error"));
150        assert!(format!("{}", err).contains("bad config"));
151    }
152
153    #[test]
154    fn test_from_io_error_conversion() {
155        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
156        let err: RavenClawsError = io_err.into();
157        assert!(format!("{}", err).contains("IO error"));
158        assert!(format!("{}", err).contains("permission denied"));
159    }
160
161    #[test]
162    fn test_error_source_chain() {
163        // RavenClawsError doesn't implement std::error::Error::source() directly
164        // for all variants, but the Display impl should contain the inner message
165        let inner = crate::llm::LLMError::AuthFailed;
166        let err = RavenClawsError::Llm(inner);
167        let display = format!("{}", err);
168        assert!(display.contains("Authentication failed"));
169    }
170
171    #[test]
172    fn test_ravenfabric_error_construction() {
173        let err = RavenClawsError::RavenFabric("connection timeout".to_string());
174        assert_eq!(format!("{}", err), "RavenFabric error: connection timeout");
175    }
176
177    #[test]
178    fn test_security_violation_construction() {
179        let err = RavenClawsError::SecurityViolation("invalid token".to_string());
180        assert_eq!(format!("{}", err), "Security violation: invalid token");
181    }
182
183    #[test]
184    #[allow(clippy::unnecessary_literal_unwrap)]
185    fn test_result_type_alias_ok() {
186        let result: Result<i32> = Ok(42);
187        assert!(result.is_ok());
188        assert_eq!(result.unwrap(), 42);
189    }
190
191    #[test]
192    #[allow(clippy::unnecessary_literal_unwrap)]
193    fn test_result_type_alias_err() {
194        let result: Result<i32> = Err(RavenClawsError::CommandExecution("fail".to_string()));
195        assert!(result.is_err());
196        assert_eq!(
197            format!("{}", result.unwrap_err()),
198            "Command execution failed: fail"
199        );
200    }
201
202    #[test]
203    fn test_error_into_boxed() {
204        // Verify RavenClawsError can be boxed (required for std::error::Error trait)
205        let err = RavenClawsError::CommandExecution("boxed".to_string());
206        let boxed: Box<dyn std::error::Error> = Box::new(err);
207        assert!(format!("{}", boxed).contains("Command execution failed"));
208    }
209
210    #[test]
211    fn test_error_into_string() {
212        let err = RavenClawsError::SecurityViolation("access denied".to_string());
213        let msg: String = err.to_string();
214        assert_eq!(msg, "Security violation: access denied");
215    }
216
217    #[test]
218    fn test_error_from_reqwest() {
219        // Verify the From<reqwest::Error> impl compiles and works
220        // We can't easily construct a reqwest::Error directly, but we can
221        // verify the From impl exists by checking the trait bounds
222        fn _check_from()
223        where
224            reqwest::Error: Into<RavenClawsError>,
225        {
226        }
227        // Compile-time check passes
228    }
229
230    #[test]
231    fn test_error_display_network_variant() {
232        // Network error display should contain the inner error message
233        let rt = tokio::runtime::Runtime::new().unwrap();
234        let err = rt.block_on(async {
235            reqwest::Client::builder()
236                .build()
237                .unwrap()
238                .get("http://invalid.example.com")
239                .send()
240                .await
241                .unwrap_err()
242        });
243        let raven_err = RavenClawsError::Network(err);
244        let display = format!("{}", raven_err);
245        assert!(display.contains("Network error"));
246        assert!(!display.is_empty());
247    }
248
249    #[test]
250    fn test_error_source_chain_io() {
251        // Test source chain: IO error wrapped in RavenClawsError
252        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
253        let err = RavenClawsError::IO(io_err);
254        let display = format!("{}", err);
255        assert!(display.contains("IO error"));
256        assert!(display.contains("file not found"));
257    }
258
259    #[test]
260    fn test_error_source_chain_config() {
261        let cfg_err = crate::config::ConfigError::ValidationError("invalid".to_string());
262        let err = RavenClawsError::Config(cfg_err);
263        let display = format!("{}", err);
264        assert!(display.contains("Configuration error"));
265        assert!(display.contains("invalid"));
266    }
267
268    #[test]
269    fn test_error_source_chain_llm() {
270        let llm_err = crate::llm::LLMError::RateLimited;
271        let err = RavenClawsError::Llm(llm_err);
272        let display = format!("{}", err);
273        assert!(display.contains("LLM error"));
274        assert!(display.contains("Rate limit exceeded"));
275    }
276
277    #[test]
278    fn test_error_clone_not_required() {
279        // RavenClawsError intentionally does not implement Clone.
280        // This test verifies that by checking it at compile time.
281        fn _check_no_clone<T>() {
282            // If this compiles, RavenClawsError does NOT implement Clone
283        }
284        _check_no_clone::<RavenClawsError>();
285    }
286}