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(transparent)]
95 Io(#[from] std::io::Error),
96}
97
98impl From<serde_json::Error> for MagiError {
99 fn from(err: serde_json::Error) -> Self {
100 MagiError::Deserialization(err.to_string())
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
112 fn test_provider_error_http_display_contains_status_and_body() {
113 let err = ProviderError::Http {
114 status: 500,
115 body: "Internal Server Error".to_string(),
116 };
117 let display = format!("{err}");
118 assert!(
119 display.contains("500"),
120 "Display should contain status code"
121 );
122 assert!(
123 display.contains("Internal Server Error"),
124 "Display should contain body"
125 );
126 }
127
128 #[test]
130 fn test_provider_error_network_display_contains_message() {
131 let err = ProviderError::Network {
132 message: "connection refused".to_string(),
133 };
134 let display = format!("{err}");
135 assert!(
136 display.contains("connection refused"),
137 "Display should contain message"
138 );
139 }
140
141 #[test]
143 fn test_provider_error_timeout_display_contains_message() {
144 let err = ProviderError::Timeout {
145 message: "exceeded 30s".to_string(),
146 };
147 let display = format!("{err}");
148 assert!(
149 display.contains("exceeded 30s"),
150 "Display should contain message"
151 );
152 }
153
154 #[test]
156 fn test_provider_error_auth_display_contains_message() {
157 let err = ProviderError::Auth {
158 message: "invalid api key".to_string(),
159 };
160 let display = format!("{err}");
161 assert!(
162 display.contains("invalid api key"),
163 "Display should contain message"
164 );
165 }
166
167 #[test]
169 fn test_provider_error_process_display_includes_exit_code_and_stderr() {
170 let err = ProviderError::Process {
171 exit_code: Some(1),
172 stderr: "segfault".to_string(),
173 };
174 let display = format!("{err}");
175 assert!(display.contains("1"), "Display should contain exit code");
176 assert!(
177 display.contains("segfault"),
178 "Display should contain stderr"
179 );
180 }
181
182 #[test]
184 fn test_provider_error_process_display_none_exit_code() {
185 let err = ProviderError::Process {
186 exit_code: None,
187 stderr: "killed".to_string(),
188 };
189 let display = format!("{err}");
190 assert!(display.contains("killed"), "Display should contain stderr");
191 }
192
193 #[test]
195 fn test_provider_error_nested_session_display() {
196 let err = ProviderError::NestedSession;
197 let display = format!("{err}");
198 assert!(!display.is_empty(), "Display should not be empty");
199 }
200
201 #[test]
205 fn test_magi_error_validation_contains_message() {
206 let err = MagiError::Validation("confidence out of range".to_string());
207 let display = format!("{err}");
208 assert!(
209 display.contains("confidence out of range"),
210 "Display should contain validation message"
211 );
212 }
213
214 #[test]
216 fn test_magi_error_insufficient_agents_formats_counts() {
217 let err = MagiError::InsufficientAgents {
218 succeeded: 1,
219 required: 2,
220 };
221 let display = format!("{err}");
222 assert!(
223 display.contains("1"),
224 "Display should contain succeeded count"
225 );
226 assert!(
227 display.contains("2"),
228 "Display should contain required count"
229 );
230 }
231
232 #[test]
234 fn test_magi_error_input_too_large_formats_size_and_max() {
235 let err = MagiError::InputTooLarge {
236 size: 2_000_000,
237 max: 1_048_576,
238 };
239 let display = format!("{err}");
240 assert!(
241 display.contains("2000000"),
242 "Display should contain actual size"
243 );
244 assert!(
245 display.contains("1048576"),
246 "Display should contain max size"
247 );
248 }
249
250 #[test]
254 fn test_from_provider_error_wraps_into_magi_error_provider() {
255 let pe = ProviderError::Timeout {
256 message: "timed out".to_string(),
257 };
258 let me: MagiError = pe.into();
259 assert!(
260 matches!(me, MagiError::Provider(_)),
261 "Should wrap into Provider variant"
262 );
263 }
264
265 #[test]
267 fn test_from_serde_json_error_produces_deserialization_variant() {
268 let result: Result<String, _> = serde_json::from_str("not json");
269 let serde_err = result.unwrap_err();
270 let me: MagiError = serde_err.into();
271 assert!(
272 matches!(me, MagiError::Deserialization(_)),
273 "Should produce Deserialization variant"
274 );
275 }
276
277 #[test]
279 fn test_from_io_error_produces_io_variant() {
280 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
281 let me: MagiError = io_err.into();
282 assert!(matches!(me, MagiError::Io(_)), "Should produce Io variant");
283 }
284}