1use thiserror::Error;
2
3#[derive(Debug, Error)]
5pub enum PunchError {
6 #[error("configuration error: {0}")]
8 Config(String),
9
10 #[error("fighter error: {0}")]
11 Fighter(String),
12
13 #[error("gorilla error: {0}")]
14 Gorilla(String),
15
16 #[error("troop error: {0}")]
17 Troop(String),
18
19 #[error("tenant error: {0}")]
20 Tenant(String),
21
22 #[error("quota exceeded: {0}")]
23 QuotaExceeded(String),
24
25 #[error("bout error: {0}")]
26 Bout(String),
27
28 #[error("capability denied: {0}")]
30 CapabilityDenied(String),
31
32 #[error("authentication error: {0}")]
33 Auth(String),
34
35 #[error("tool error [{tool}]: {message}")]
37 Tool { tool: String, message: String },
38
39 #[error("tool not found: {0}")]
40 ToolNotFound(String),
41
42 #[error("tool timeout: {tool} after {timeout_ms}ms")]
43 ToolTimeout { tool: String, timeout_ms: u64 },
44
45 #[error("provider error [{provider}]: {message}")]
47 Provider { provider: String, message: String },
48
49 #[error("rate limited by {provider}, retry after {retry_after_ms}ms")]
50 RateLimited {
51 provider: String,
52 retry_after_ms: u64,
53 },
54
55 #[error("model context length exceeded: {used} / {limit} tokens")]
56 ContextOverflow { used: u64, limit: u64 },
57
58 #[error("memory error: {0}")]
60 Memory(String),
61
62 #[error("knowledge graph error: {0}")]
63 KnowledgeGraph(String),
64
65 #[error("channel error [{channel}]: {message}")]
67 Channel { channel: String, message: String },
68
69 #[error("event bus error: {0}")]
71 EventBus(String),
72
73 #[error("mcp error [{server}]: {message}")]
75 Mcp { server: String, message: String },
76
77 #[error("io error: {0}")]
79 Io(#[from] std::io::Error),
80
81 #[error("serialization error: {0}")]
82 Serialization(#[from] serde_json::Error),
83
84 #[error("internal error: {0}")]
86 Internal(String),
87}
88
89pub type PunchResult<T> = Result<T, PunchError>;
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn test_config_error_display() {
98 let err = PunchError::Config("missing api_key".to_string());
99 assert_eq!(err.to_string(), "configuration error: missing api_key");
100 }
101
102 #[test]
103 fn test_fighter_error_display() {
104 let err = PunchError::Fighter("failed to spawn".to_string());
105 assert_eq!(err.to_string(), "fighter error: failed to spawn");
106 }
107
108 #[test]
109 fn test_gorilla_error_display() {
110 let err = PunchError::Gorilla("schedule invalid".to_string());
111 assert_eq!(err.to_string(), "gorilla error: schedule invalid");
112 }
113
114 #[test]
115 fn test_bout_error_display() {
116 let err = PunchError::Bout("session expired".to_string());
117 assert_eq!(err.to_string(), "bout error: session expired");
118 }
119
120 #[test]
121 fn test_capability_denied_display() {
122 let err = PunchError::CapabilityDenied("file_write(/etc)".to_string());
123 assert_eq!(err.to_string(), "capability denied: file_write(/etc)");
124 }
125
126 #[test]
127 fn test_auth_error_display() {
128 let err = PunchError::Auth("invalid token".to_string());
129 assert_eq!(err.to_string(), "authentication error: invalid token");
130 }
131
132 #[test]
133 fn test_tool_error_display() {
134 let err = PunchError::Tool {
135 tool: "web_fetch".to_string(),
136 message: "timeout".to_string(),
137 };
138 assert_eq!(err.to_string(), "tool error [web_fetch]: timeout");
139 }
140
141 #[test]
142 fn test_tool_not_found_display() {
143 let err = PunchError::ToolNotFound("nonexistent_tool".to_string());
144 assert_eq!(err.to_string(), "tool not found: nonexistent_tool");
145 }
146
147 #[test]
148 fn test_tool_timeout_display() {
149 let err = PunchError::ToolTimeout {
150 tool: "shell_exec".to_string(),
151 timeout_ms: 30000,
152 };
153 assert_eq!(err.to_string(), "tool timeout: shell_exec after 30000ms");
154 }
155
156 #[test]
157 fn test_provider_error_display() {
158 let err = PunchError::Provider {
159 provider: "anthropic".to_string(),
160 message: "server error".to_string(),
161 };
162 assert_eq!(err.to_string(), "provider error [anthropic]: server error");
163 }
164
165 #[test]
166 fn test_rate_limited_display() {
167 let err = PunchError::RateLimited {
168 provider: "openai".to_string(),
169 retry_after_ms: 5000,
170 };
171 assert_eq!(
172 err.to_string(),
173 "rate limited by openai, retry after 5000ms"
174 );
175 }
176
177 #[test]
178 fn test_context_overflow_display() {
179 let err = PunchError::ContextOverflow {
180 used: 150000,
181 limit: 128000,
182 };
183 assert_eq!(
184 err.to_string(),
185 "model context length exceeded: 150000 / 128000 tokens"
186 );
187 }
188
189 #[test]
190 fn test_memory_error_display() {
191 let err = PunchError::Memory("db locked".to_string());
192 assert_eq!(err.to_string(), "memory error: db locked");
193 }
194
195 #[test]
196 fn test_channel_error_display() {
197 let err = PunchError::Channel {
198 channel: "slack".to_string(),
199 message: "auth failed".to_string(),
200 };
201 assert_eq!(err.to_string(), "channel error [slack]: auth failed");
202 }
203
204 #[test]
205 fn test_event_bus_error_display() {
206 let err = PunchError::EventBus("queue full".to_string());
207 assert_eq!(err.to_string(), "event bus error: queue full");
208 }
209
210 #[test]
211 fn test_mcp_error_display() {
212 let err = PunchError::Mcp {
213 server: "filesystem".to_string(),
214 message: "connection refused".to_string(),
215 };
216 assert_eq!(
217 err.to_string(),
218 "mcp error [filesystem]: connection refused"
219 );
220 }
221
222 #[test]
223 fn test_internal_error_display() {
224 let err = PunchError::Internal("unexpected state".to_string());
225 assert_eq!(err.to_string(), "internal error: unexpected state");
226 }
227
228 #[test]
229 fn test_from_io_error() {
230 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
231 let punch_err: PunchError = io_err.into();
232 assert!(punch_err.to_string().contains("file missing"));
233 assert!(matches!(punch_err, PunchError::Io(_)));
234 }
235
236 #[test]
237 fn test_from_serde_error() {
238 let serde_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
239 let punch_err: PunchError = serde_err.into();
240 assert!(matches!(punch_err, PunchError::Serialization(_)));
241 }
242
243 #[test]
244 fn test_punch_result_ok() {
245 let result: PunchResult<i32> = Ok(42);
246 assert_eq!(result.unwrap(), 42);
247 }
248
249 #[test]
250 fn test_punch_result_err() {
251 let result: PunchResult<i32> = Err(PunchError::Internal("fail".to_string()));
252 assert!(result.is_err());
253 }
254
255 #[test]
256 fn test_error_debug_impl() {
257 let err = PunchError::Config("test".to_string());
258 let debug = format!("{:?}", err);
259 assert!(debug.contains("Config"));
260 assert!(debug.contains("test"));
261 }
262
263 #[test]
264 fn test_empty_string_errors() {
265 let err = PunchError::Config(String::new());
266 assert_eq!(err.to_string(), "configuration error: ");
267 }
268
269 #[test]
270 fn test_knowledge_graph_error_display() {
271 let err = PunchError::KnowledgeGraph("cycle detected".to_string());
272 assert_eq!(err.to_string(), "knowledge graph error: cycle detected");
273 }
274}