1use thiserror::Error;
8
9#[derive(Debug, Error)]
11pub enum Error {
12 #[error(transparent)]
13 Llm(#[from] LlmError),
14
15 #[error(transparent)]
16 Tool(#[from] ToolError),
17
18 #[error(transparent)]
19 Permission(#[from] PermissionError),
20
21 #[error(transparent)]
22 Config(#[from] ConfigError),
23
24 #[error(transparent)]
25 Io(#[from] std::io::Error),
26
27 #[error("{0}")]
28 Other(String),
29}
30
31#[derive(Debug, Error)]
33pub enum LlmError {
34 #[error("HTTP request failed: {0}")]
35 Http(String),
36
37 #[error("API error (status {status}): {body}")]
38 Api { status: u16, body: String },
39
40 #[error("Rate limited, retry after {retry_after_ms}ms")]
41 RateLimited { retry_after_ms: u64 },
42
43 #[error("Stream interrupted")]
44 StreamInterrupted,
45
46 #[error("Invalid response: {0}")]
47 InvalidResponse(String),
48
49 #[error("Authentication failed: {0}")]
50 AuthError(String),
51
52 #[error("Context window exceeded ({tokens} tokens)")]
53 ContextOverflow { tokens: usize },
54}
55
56#[derive(Debug, Error)]
58pub enum ToolError {
59 #[error("Permission denied: {0}")]
60 PermissionDenied(String),
61
62 #[error("Tool execution failed: {0}")]
63 ExecutionFailed(String),
64
65 #[error("Invalid input: {0}")]
66 InvalidInput(String),
67
68 #[error("Tool not found: {0}")]
69 NotFound(String),
70
71 #[error("IO error: {0}")]
72 Io(#[from] std::io::Error),
73
74 #[error("Operation cancelled")]
75 Cancelled,
76
77 #[error("Timeout after {0}ms")]
78 Timeout(u64),
79}
80
81#[derive(Debug, Error)]
83pub enum PermissionError {
84 #[error("Permission denied by rule: {0}")]
85 DeniedByRule(String),
86
87 #[error("User denied permission for {tool}: {reason}")]
88 UserDenied { tool: String, reason: String },
89}
90
91#[derive(Debug, Error)]
93pub enum ConfigError {
94 #[error("Config file error: {0}")]
95 FileError(String),
96
97 #[error("Invalid config value: {0}")]
98 InvalidValue(String),
99
100 #[error("TOML parse error: {0}")]
101 ParseError(#[from] toml::de::Error),
102}
103
104pub type Result<T> = std::result::Result<T, Error>;
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
114 fn error_display_llm_variant_delegates_to_llm_error() {
115 let err: Error = LlmError::StreamInterrupted.into();
116 assert_eq!(err.to_string(), "Stream interrupted");
117 }
118
119 #[test]
120 fn error_display_tool_variant_delegates_to_tool_error() {
121 let err: Error = ToolError::Cancelled.into();
122 assert_eq!(err.to_string(), "Operation cancelled");
123 }
124
125 #[test]
126 fn error_display_permission_variant_delegates_to_permission_error() {
127 let err: Error = PermissionError::DeniedByRule("no writes".into()).into();
128 assert_eq!(err.to_string(), "Permission denied by rule: no writes");
129 }
130
131 #[test]
132 fn error_display_config_variant_delegates_to_config_error() {
133 let err: Error = ConfigError::InvalidValue("bad timeout".into()).into();
134 assert_eq!(err.to_string(), "Invalid config value: bad timeout");
135 }
136
137 #[test]
138 fn error_display_io_variant() {
139 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
140 let err: Error = io_err.into();
141 assert_eq!(err.to_string(), "file missing");
142 }
143
144 #[test]
145 fn error_display_other_variant() {
146 let err = Error::Other("something went wrong".into());
147 assert_eq!(err.to_string(), "something went wrong");
148 }
149
150 #[test]
153 fn from_llm_error_to_error() {
154 let llm = LlmError::Http("connection reset".into());
155 let err: Error = llm.into();
156 assert!(matches!(err, Error::Llm(_)));
157 }
158
159 #[test]
160 fn from_tool_error_to_error() {
161 let tool = ToolError::NotFound("bash".into());
162 let err: Error = tool.into();
163 assert!(matches!(err, Error::Tool(_)));
164 }
165
166 #[test]
167 fn from_permission_error_to_error() {
168 let perm = PermissionError::DeniedByRule("rule_1".into());
169 let err: Error = perm.into();
170 assert!(matches!(err, Error::Permission(_)));
171 }
172
173 #[test]
174 fn from_config_error_to_error() {
175 let cfg = ConfigError::FileError("not found".into());
176 let err: Error = cfg.into();
177 assert!(matches!(err, Error::Config(_)));
178 }
179
180 #[test]
181 fn from_io_error_to_error() {
182 let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
183 let err: Error = io.into();
184 assert!(matches!(err, Error::Io(_)));
185 }
186
187 #[test]
190 fn llm_error_display_http() {
191 let err = LlmError::Http("timeout".into());
192 assert_eq!(err.to_string(), "HTTP request failed: timeout");
193 }
194
195 #[test]
196 fn llm_error_display_api() {
197 let err = LlmError::Api {
198 status: 429,
199 body: "too many requests".into(),
200 };
201 assert_eq!(err.to_string(), "API error (status 429): too many requests");
202 }
203
204 #[test]
205 fn llm_error_display_rate_limited() {
206 let err = LlmError::RateLimited {
207 retry_after_ms: 5000,
208 };
209 assert_eq!(err.to_string(), "Rate limited, retry after 5000ms");
210 }
211
212 #[test]
213 fn llm_error_display_stream_interrupted() {
214 let err = LlmError::StreamInterrupted;
215 assert_eq!(err.to_string(), "Stream interrupted");
216 }
217
218 #[test]
219 fn llm_error_display_invalid_response() {
220 let err = LlmError::InvalidResponse("missing field".into());
221 assert_eq!(err.to_string(), "Invalid response: missing field");
222 }
223
224 #[test]
225 fn llm_error_display_auth_error() {
226 let err = LlmError::AuthError("invalid key".into());
227 assert_eq!(err.to_string(), "Authentication failed: invalid key");
228 }
229
230 #[test]
231 fn llm_error_display_context_overflow() {
232 let err = LlmError::ContextOverflow { tokens: 200000 };
233 assert_eq!(err.to_string(), "Context window exceeded (200000 tokens)");
234 }
235
236 #[test]
239 fn tool_error_display_permission_denied() {
240 let err = ToolError::PermissionDenied("read /etc/shadow".into());
241 assert_eq!(err.to_string(), "Permission denied: read /etc/shadow");
242 }
243
244 #[test]
245 fn tool_error_display_execution_failed() {
246 let err = ToolError::ExecutionFailed("exit code 1".into());
247 assert_eq!(err.to_string(), "Tool execution failed: exit code 1");
248 }
249
250 #[test]
251 fn tool_error_display_invalid_input() {
252 let err = ToolError::InvalidInput("expected JSON".into());
253 assert_eq!(err.to_string(), "Invalid input: expected JSON");
254 }
255
256 #[test]
257 fn tool_error_display_not_found() {
258 let err = ToolError::NotFound("custom_tool".into());
259 assert_eq!(err.to_string(), "Tool not found: custom_tool");
260 }
261
262 #[test]
263 fn tool_error_display_io() {
264 let io = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe");
265 let err = ToolError::Io(io);
266 assert_eq!(err.to_string(), "IO error: broken pipe");
267 }
268
269 #[test]
270 fn tool_error_display_cancelled() {
271 let err = ToolError::Cancelled;
272 assert_eq!(err.to_string(), "Operation cancelled");
273 }
274
275 #[test]
276 fn tool_error_display_timeout() {
277 let err = ToolError::Timeout(30000);
278 assert_eq!(err.to_string(), "Timeout after 30000ms");
279 }
280
281 #[test]
282 fn tool_error_from_io_error() {
283 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
284 let err: ToolError = io.into();
285 assert!(matches!(err, ToolError::Io(_)));
286 }
287
288 #[test]
291 fn permission_error_display_denied_by_rule() {
292 let err = PermissionError::DeniedByRule("no shell access".into());
293 assert_eq!(
294 err.to_string(),
295 "Permission denied by rule: no shell access"
296 );
297 }
298
299 #[test]
300 fn permission_error_display_user_denied() {
301 let err = PermissionError::UserDenied {
302 tool: "Bash".into(),
303 reason: "looks dangerous".into(),
304 };
305 assert_eq!(
306 err.to_string(),
307 "User denied permission for Bash: looks dangerous"
308 );
309 }
310
311 #[test]
314 fn config_error_display_file_error() {
315 let err = ConfigError::FileError("config.toml not found".into());
316 assert_eq!(err.to_string(), "Config file error: config.toml not found");
317 }
318
319 #[test]
320 fn config_error_display_invalid_value() {
321 let err = ConfigError::InvalidValue("timeout must be positive".into());
322 assert_eq!(
323 err.to_string(),
324 "Invalid config value: timeout must be positive"
325 );
326 }
327
328 #[test]
329 fn config_error_from_toml_de_error() {
330 let bad_toml = "key = [unclosed";
332 let toml_err = toml::from_str::<toml::Value>(bad_toml).unwrap_err();
333 let err: ConfigError = toml_err.into();
334 assert!(matches!(err, ConfigError::ParseError(_)));
335 let display = err.to_string();
336 assert!(display.starts_with("TOML parse error:"));
337 }
338
339 #[test]
340 fn config_error_parse_error_propagates_to_top_level() {
341 let bad_toml = "= missing key";
342 let toml_err = toml::from_str::<toml::Value>(bad_toml).unwrap_err();
343 let config_err: ConfigError = toml_err.into();
344 let top_err: Error = config_err.into();
345 assert!(matches!(top_err, Error::Config(ConfigError::ParseError(_))));
346 }
347
348 #[test]
351 fn result_alias_ok() {
352 let r: Result<i32> = Ok(42);
353 assert!(r.is_ok());
354 }
355
356 #[test]
357 fn result_alias_err() {
358 let r: Result<i32> = Err(Error::Other("oops".into()));
359 assert!(r.is_err());
360 }
361}