Skip to main content

punch_types/
error.rs

1use thiserror::Error;
2
3/// Unified error type for the Punch system.
4#[derive(Debug, Error)]
5pub enum PunchError {
6    // --- Core subsystem errors ---
7    #[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    // --- Capability / auth errors ---
29    #[error("capability denied: {0}")]
30    CapabilityDenied(String),
31
32    #[error("authentication error: {0}")]
33    Auth(String),
34
35    // --- Tool / move errors ---
36    #[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    // --- Model / provider errors ---
46    #[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    // --- Memory errors ---
59    #[error("memory error: {0}")]
60    Memory(String),
61
62    #[error("knowledge graph error: {0}")]
63    KnowledgeGraph(String),
64
65    // --- Channel errors ---
66    #[error("channel error [{channel}]: {message}")]
67    Channel { channel: String, message: String },
68
69    // --- Event errors ---
70    #[error("event bus error: {0}")]
71    EventBus(String),
72
73    // --- MCP errors ---
74    #[error("mcp error [{server}]: {message}")]
75    Mcp { server: String, message: String },
76
77    // --- I/O and infrastructure ---
78    #[error("io error: {0}")]
79    Io(#[from] std::io::Error),
80
81    #[error("serialization error: {0}")]
82    Serialization(#[from] serde_json::Error),
83
84    // --- Marketplace errors ---
85    #[error("marketplace error: {0}")]
86    Marketplace(String),
87
88    // --- Catch-all ---
89    #[error("internal error: {0}")]
90    Internal(String),
91}
92
93/// Convenience alias for `Result<T, PunchError>`.
94pub 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}