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("marketplace error: {0}")]
86 Marketplace(String),
87
88 #[error("internal error: {0}")]
90 Internal(String),
91}
92
93pub type PunchResult<T> = Result<T, PunchError>;
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn test_config_error_display() {
102 let err = PunchError::Config("missing api_key".to_string());
103 assert_eq!(err.to_string(), "configuration error: missing api_key");
104 }
105
106 #[test]
107 fn test_fighter_error_display() {
108 let err = PunchError::Fighter("failed to spawn".to_string());
109 assert_eq!(err.to_string(), "fighter error: failed to spawn");
110 }
111
112 #[test]
113 fn test_gorilla_error_display() {
114 let err = PunchError::Gorilla("schedule invalid".to_string());
115 assert_eq!(err.to_string(), "gorilla error: schedule invalid");
116 }
117
118 #[test]
119 fn test_bout_error_display() {
120 let err = PunchError::Bout("session expired".to_string());
121 assert_eq!(err.to_string(), "bout error: session expired");
122 }
123
124 #[test]
125 fn test_capability_denied_display() {
126 let err = PunchError::CapabilityDenied("file_write(/etc)".to_string());
127 assert_eq!(err.to_string(), "capability denied: file_write(/etc)");
128 }
129
130 #[test]
131 fn test_auth_error_display() {
132 let err = PunchError::Auth("invalid token".to_string());
133 assert_eq!(err.to_string(), "authentication error: invalid token");
134 }
135
136 #[test]
137 fn test_tool_error_display() {
138 let err = PunchError::Tool {
139 tool: "web_fetch".to_string(),
140 message: "timeout".to_string(),
141 };
142 assert_eq!(err.to_string(), "tool error [web_fetch]: timeout");
143 }
144
145 #[test]
146 fn test_tool_not_found_display() {
147 let err = PunchError::ToolNotFound("nonexistent_tool".to_string());
148 assert_eq!(err.to_string(), "tool not found: nonexistent_tool");
149 }
150
151 #[test]
152 fn test_tool_timeout_display() {
153 let err = PunchError::ToolTimeout {
154 tool: "shell_exec".to_string(),
155 timeout_ms: 30000,
156 };
157 assert_eq!(err.to_string(), "tool timeout: shell_exec after 30000ms");
158 }
159
160 #[test]
161 fn test_provider_error_display() {
162 let err = PunchError::Provider {
163 provider: "anthropic".to_string(),
164 message: "server error".to_string(),
165 };
166 assert_eq!(err.to_string(), "provider error [anthropic]: server error");
167 }
168
169 #[test]
170 fn test_rate_limited_display() {
171 let err = PunchError::RateLimited {
172 provider: "openai".to_string(),
173 retry_after_ms: 5000,
174 };
175 assert_eq!(
176 err.to_string(),
177 "rate limited by openai, retry after 5000ms"
178 );
179 }
180
181 #[test]
182 fn test_context_overflow_display() {
183 let err = PunchError::ContextOverflow {
184 used: 150000,
185 limit: 128000,
186 };
187 assert_eq!(
188 err.to_string(),
189 "model context length exceeded: 150000 / 128000 tokens"
190 );
191 }
192
193 #[test]
194 fn test_memory_error_display() {
195 let err = PunchError::Memory("db locked".to_string());
196 assert_eq!(err.to_string(), "memory error: db locked");
197 }
198
199 #[test]
200 fn test_channel_error_display() {
201 let err = PunchError::Channel {
202 channel: "slack".to_string(),
203 message: "auth failed".to_string(),
204 };
205 assert_eq!(err.to_string(), "channel error [slack]: auth failed");
206 }
207
208 #[test]
209 fn test_event_bus_error_display() {
210 let err = PunchError::EventBus("queue full".to_string());
211 assert_eq!(err.to_string(), "event bus error: queue full");
212 }
213
214 #[test]
215 fn test_mcp_error_display() {
216 let err = PunchError::Mcp {
217 server: "filesystem".to_string(),
218 message: "connection refused".to_string(),
219 };
220 assert_eq!(
221 err.to_string(),
222 "mcp error [filesystem]: connection refused"
223 );
224 }
225
226 #[test]
227 fn test_marketplace_error_display() {
228 let err = PunchError::Marketplace("skill not found".to_string());
229 assert_eq!(err.to_string(), "marketplace error: skill not found");
230 }
231
232 #[test]
233 fn test_internal_error_display() {
234 let err = PunchError::Internal("unexpected state".to_string());
235 assert_eq!(err.to_string(), "internal error: unexpected state");
236 }
237
238 #[test]
239 fn test_from_io_error() {
240 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
241 let punch_err: PunchError = io_err.into();
242 assert!(punch_err.to_string().contains("file missing"));
243 assert!(matches!(punch_err, PunchError::Io(_)));
244 }
245
246 #[test]
247 fn test_from_serde_error() {
248 let serde_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
249 let punch_err: PunchError = serde_err.into();
250 assert!(matches!(punch_err, PunchError::Serialization(_)));
251 }
252
253 #[test]
254 fn test_punch_result_ok() {
255 let result: PunchResult<i32> = Ok(42);
256 assert_eq!(result.unwrap(), 42);
257 }
258
259 #[test]
260 fn test_punch_result_err() {
261 let result: PunchResult<i32> = Err(PunchError::Internal("fail".to_string()));
262 assert!(result.is_err());
263 }
264
265 #[test]
266 fn test_error_debug_impl() {
267 let err = PunchError::Config("test".to_string());
268 let debug = format!("{:?}", err);
269 assert!(debug.contains("Config"));
270 assert!(debug.contains("test"));
271 }
272
273 #[test]
274 fn test_empty_string_errors() {
275 let err = PunchError::Config(String::new());
276 assert_eq!(err.to_string(), "configuration error: ");
277 }
278
279 #[test]
280 fn test_knowledge_graph_error_display() {
281 let err = PunchError::KnowledgeGraph("cycle detected".to_string());
282 assert_eq!(err.to_string(), "knowledge graph error: cycle detected");
283 }
284}