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    // --- Catch-all ---
85    #[error("internal error: {0}")]
86    Internal(String),
87}
88
89/// Convenience alias for `Result<T, PunchError>`.
90pub 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}