1use thiserror::Error;
6
7#[derive(Debug, Clone, Error)]
12pub enum ProviderError {
13 #[error("http error {status}: {body}")]
15 Http {
16 status: u16,
18 body: String,
20 },
21
22 #[error("network error: {message}")]
24 Network {
25 message: String,
27 },
28
29 #[error("timeout: {message}")]
31 Timeout {
32 message: String,
34 },
35
36 #[error("auth error: {message}")]
38 Auth {
39 message: String,
41 },
42
43 #[error("process error (exit_code={exit_code:?}): {stderr}")]
45 Process {
46 exit_code: Option<i32>,
48 stderr: String,
50 },
51
52 #[error("nested session detected: cannot launch CLI provider from within an existing session")]
54 NestedSession,
55}
56
57#[derive(Debug, Error)]
62pub enum MagiError {
63 #[error("validation error: {0}")]
65 Validation(String),
66
67 #[error(transparent)]
69 Provider(#[from] ProviderError),
70
71 #[error("insufficient agents: {succeeded} succeeded, {required} required")]
73 InsufficientAgents {
74 succeeded: usize,
76 required: usize,
78 },
79
80 #[error("deserialization error: {0}")]
82 Deserialization(String),
83
84 #[error("input too large: {size} bytes exceeds maximum of {max} bytes")]
86 InputTooLarge {
87 size: usize,
89 max: usize,
91 },
92
93 #[error("invalid input: {reason}")]
95 InvalidInput {
96 reason: String,
98 },
99
100 #[error(transparent)]
102 Io(#[from] std::io::Error),
103}
104
105impl From<serde_json::Error> for MagiError {
106 fn from(err: serde_json::Error) -> Self {
107 MagiError::Deserialization(err.to_string())
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
119 fn test_provider_error_http_display_contains_status_and_body() {
120 let err = ProviderError::Http {
121 status: 500,
122 body: "Internal Server Error".to_string(),
123 };
124 let display = format!("{err}");
125 assert!(
126 display.contains("500"),
127 "Display should contain status code"
128 );
129 assert!(
130 display.contains("Internal Server Error"),
131 "Display should contain body"
132 );
133 }
134
135 #[test]
137 fn test_provider_error_network_display_contains_message() {
138 let err = ProviderError::Network {
139 message: "connection refused".to_string(),
140 };
141 let display = format!("{err}");
142 assert!(
143 display.contains("connection refused"),
144 "Display should contain message"
145 );
146 }
147
148 #[test]
150 fn test_provider_error_timeout_display_contains_message() {
151 let err = ProviderError::Timeout {
152 message: "exceeded 30s".to_string(),
153 };
154 let display = format!("{err}");
155 assert!(
156 display.contains("exceeded 30s"),
157 "Display should contain message"
158 );
159 }
160
161 #[test]
163 fn test_provider_error_auth_display_contains_message() {
164 let err = ProviderError::Auth {
165 message: "invalid api key".to_string(),
166 };
167 let display = format!("{err}");
168 assert!(
169 display.contains("invalid api key"),
170 "Display should contain message"
171 );
172 }
173
174 #[test]
176 fn test_provider_error_process_display_includes_exit_code_and_stderr() {
177 let err = ProviderError::Process {
178 exit_code: Some(1),
179 stderr: "segfault".to_string(),
180 };
181 let display = format!("{err}");
182 assert!(display.contains("1"), "Display should contain exit code");
183 assert!(
184 display.contains("segfault"),
185 "Display should contain stderr"
186 );
187 }
188
189 #[test]
191 fn test_provider_error_process_display_none_exit_code() {
192 let err = ProviderError::Process {
193 exit_code: None,
194 stderr: "killed".to_string(),
195 };
196 let display = format!("{err}");
197 assert!(display.contains("killed"), "Display should contain stderr");
198 }
199
200 #[test]
202 fn test_provider_error_nested_session_display() {
203 let err = ProviderError::NestedSession;
204 let display = format!("{err}");
205 assert!(!display.is_empty(), "Display should not be empty");
206 }
207
208 #[test]
212 fn test_magi_error_validation_contains_message() {
213 let err = MagiError::Validation("confidence out of range".to_string());
214 let display = format!("{err}");
215 assert!(
216 display.contains("confidence out of range"),
217 "Display should contain validation message"
218 );
219 }
220
221 #[test]
223 fn test_magi_error_insufficient_agents_formats_counts() {
224 let err = MagiError::InsufficientAgents {
225 succeeded: 1,
226 required: 2,
227 };
228 let display = format!("{err}");
229 assert!(
230 display.contains("1"),
231 "Display should contain succeeded count"
232 );
233 assert!(
234 display.contains("2"),
235 "Display should contain required count"
236 );
237 }
238
239 #[test]
241 fn test_magi_error_input_too_large_formats_size_and_max() {
242 let err = MagiError::InputTooLarge {
243 size: 2_000_000,
244 max: 1_048_576,
245 };
246 let display = format!("{err}");
247 assert!(
248 display.contains("2000000"),
249 "Display should contain actual size"
250 );
251 assert!(
252 display.contains("1048576"),
253 "Display should contain max size"
254 );
255 }
256
257 #[test]
261 fn test_from_provider_error_wraps_into_magi_error_provider() {
262 let pe = ProviderError::Timeout {
263 message: "timed out".to_string(),
264 };
265 let me: MagiError = pe.into();
266 assert!(
267 matches!(me, MagiError::Provider(_)),
268 "Should wrap into Provider variant"
269 );
270 }
271
272 #[test]
274 fn test_from_serde_json_error_produces_deserialization_variant() {
275 let result: Result<String, _> = serde_json::from_str("not json");
276 let serde_err = result.unwrap_err();
277 let me: MagiError = serde_err.into();
278 assert!(
279 matches!(me, MagiError::Deserialization(_)),
280 "Should produce Deserialization variant"
281 );
282 }
283
284 #[test]
286 fn test_from_io_error_produces_io_variant() {
287 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
288 let me: MagiError = io_err.into();
289 assert!(matches!(me, MagiError::Io(_)), "Should produce Io variant");
290 }
291}