1use thiserror::Error;
14
15pub type Result<T> = std::result::Result<T, CodeError>;
17
18#[derive(Debug, Error)]
23pub enum CodeError {
24 #[error("Config error: {0}")]
26 Config(String),
27
28 #[error("LLM error: {0}")]
30 Llm(String),
31
32 #[error("Tool error: {tool}: {message}")]
34 Tool { tool: String, message: String },
35
36 #[error("Session error: {0}")]
38 Session(String),
39
40 #[error("Session '{session_id}' is closed")]
46 SessionClosed { session_id: String },
47
48 #[error("Budget exhausted on '{resource}': {reason}")]
52 BudgetExhausted { resource: String, reason: String },
53
54 #[error("Security error: {0}")]
56 Security(String),
57
58 #[error("Context error: {0}")]
60 Context(String),
61
62 #[error("MCP error: {0}")]
64 Mcp(String),
65
66 #[error("Queue error: {0}")]
68 Queue(String),
69
70 #[error("IO error: {0}")]
72 Io(#[from] std::io::Error),
73
74 #[error("Serialization error: {0}")]
76 Serialization(#[from] serde_json::Error),
77
78 #[error("{0:#}")]
84 Internal(#[from] anyhow::Error),
85}
86
87pub(crate) fn read_or_recover<T>(lock: &std::sync::RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
97 lock.read().unwrap_or_else(|p| p.into_inner())
98}
99
100pub(crate) fn write_or_recover<T>(
104 lock: &std::sync::RwLock<T>,
105) -> std::sync::RwLockWriteGuard<'_, T> {
106 lock.write().unwrap_or_else(|p| p.into_inner())
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_code_error_config() {
115 let err = CodeError::Config("missing API key".to_string());
116 assert!(err.to_string().contains("Config error"));
117 assert!(err.to_string().contains("missing API key"));
118 }
119
120 #[test]
121 fn test_code_error_llm() {
122 let err = CodeError::Llm("rate limited".to_string());
123 assert!(err.to_string().contains("LLM error"));
124 }
125
126 #[test]
127 fn test_code_error_tool() {
128 let err = CodeError::Tool {
129 tool: "bash".to_string(),
130 message: "command not found".to_string(),
131 };
132 let msg = err.to_string();
133 assert!(msg.contains("bash"));
134 assert!(msg.contains("command not found"));
135 }
136
137 #[test]
138 fn test_code_error_session() {
139 let err = CodeError::Session("not found".to_string());
140 assert!(err.to_string().contains("Session error"));
141 }
142
143 #[test]
144 fn test_code_error_security() {
145 let err = CodeError::Security("taint detected".to_string());
146 assert!(err.to_string().contains("Security error"));
147 }
148
149 #[test]
150 fn test_code_error_context() {
151 let err = CodeError::Context("provider failed".to_string());
152 assert!(err.to_string().contains("Context error"));
153 }
154
155 #[test]
156 fn test_code_error_mcp() {
157 let err = CodeError::Mcp("connection refused".to_string());
158 assert!(err.to_string().contains("MCP error"));
159 }
160
161 #[test]
162 fn test_code_error_queue() {
163 let err = CodeError::Queue("lane full".to_string());
164 assert!(err.to_string().contains("Queue error"));
165 }
166
167 #[test]
168 fn test_code_error_from_io() {
169 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
170 let err: CodeError = io_err.into();
171 assert!(matches!(err, CodeError::Io(_)));
172 assert!(err.to_string().contains("file missing"));
173 }
174
175 #[test]
176 fn test_code_error_from_serde_json() {
177 let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
178 let err: CodeError = json_err.into();
179 assert!(matches!(err, CodeError::Serialization(_)));
180 }
181
182 #[test]
183 fn test_code_error_from_anyhow() {
184 let anyhow_err = anyhow::anyhow!("something went wrong");
185 let err: CodeError = anyhow_err.into();
186 assert!(matches!(err, CodeError::Internal(_)));
187 assert!(err.to_string().contains("something went wrong"));
188 }
189
190 #[test]
191 fn test_code_error_question_mark_from_anyhow() {
192 fn inner() -> anyhow::Result<()> {
193 anyhow::bail!("inner error")
194 }
195
196 fn outer() -> Result<()> {
197 inner()?; Ok(())
199 }
200
201 let result = outer();
202 assert!(result.is_err());
203 let err = result.unwrap_err();
204 assert!(matches!(err, CodeError::Internal(_)));
205 }
206
207 #[test]
208 fn test_read_or_recover_normal() {
209 let lock = std::sync::RwLock::new(42);
210 let guard = read_or_recover(&lock);
211 assert_eq!(*guard, 42);
212 }
213
214 #[test]
215 fn test_write_or_recover_normal() {
216 let lock = std::sync::RwLock::new(42);
217 let mut guard = write_or_recover(&lock);
218 *guard = 99;
219 drop(guard);
220 assert_eq!(*read_or_recover(&lock), 99);
221 }
222
223 #[test]
224 fn test_read_or_recover_poisoned() {
225 let lock = std::sync::RwLock::new(42);
226 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
228 let _guard = lock.write().unwrap();
229 panic!("intentional poison");
230 }));
231 let guard = read_or_recover(&lock);
233 assert_eq!(*guard, 42);
234 }
235
236 #[test]
237 fn test_write_or_recover_poisoned() {
238 let lock = std::sync::RwLock::new(42);
239 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
240 let _guard = lock.write().unwrap();
241 panic!("intentional poison");
242 }));
243 let mut guard = write_or_recover(&lock);
244 *guard = 100;
245 assert_eq!(*guard, 100);
246 }
247}