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("Security error: {0}")]
42 Security(String),
43
44 #[error("Context error: {0}")]
46 Context(String),
47
48 #[error("MCP error: {0}")]
50 Mcp(String),
51
52 #[error("Queue error: {0}")]
54 Queue(String),
55
56 #[error("IO error: {0}")]
58 Io(#[from] std::io::Error),
59
60 #[error("Serialization error: {0}")]
62 Serialization(#[from] serde_json::Error),
63
64 #[error("{0:#}")]
70 Internal(#[from] anyhow::Error),
71}
72
73pub(crate) fn read_or_recover<T>(lock: &std::sync::RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
83 lock.read().unwrap_or_else(|p| p.into_inner())
84}
85
86pub(crate) fn write_or_recover<T>(
90 lock: &std::sync::RwLock<T>,
91) -> std::sync::RwLockWriteGuard<'_, T> {
92 lock.write().unwrap_or_else(|p| p.into_inner())
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_code_error_config() {
101 let err = CodeError::Config("missing API key".to_string());
102 assert!(err.to_string().contains("Config error"));
103 assert!(err.to_string().contains("missing API key"));
104 }
105
106 #[test]
107 fn test_code_error_llm() {
108 let err = CodeError::Llm("rate limited".to_string());
109 assert!(err.to_string().contains("LLM error"));
110 }
111
112 #[test]
113 fn test_code_error_tool() {
114 let err = CodeError::Tool {
115 tool: "bash".to_string(),
116 message: "command not found".to_string(),
117 };
118 let msg = err.to_string();
119 assert!(msg.contains("bash"));
120 assert!(msg.contains("command not found"));
121 }
122
123 #[test]
124 fn test_code_error_session() {
125 let err = CodeError::Session("not found".to_string());
126 assert!(err.to_string().contains("Session error"));
127 }
128
129 #[test]
130 fn test_code_error_security() {
131 let err = CodeError::Security("taint detected".to_string());
132 assert!(err.to_string().contains("Security error"));
133 }
134
135 #[test]
136 fn test_code_error_context() {
137 let err = CodeError::Context("provider failed".to_string());
138 assert!(err.to_string().contains("Context error"));
139 }
140
141 #[test]
142 fn test_code_error_mcp() {
143 let err = CodeError::Mcp("connection refused".to_string());
144 assert!(err.to_string().contains("MCP error"));
145 }
146
147 #[test]
148 fn test_code_error_queue() {
149 let err = CodeError::Queue("lane full".to_string());
150 assert!(err.to_string().contains("Queue error"));
151 }
152
153 #[test]
154 fn test_code_error_from_io() {
155 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
156 let err: CodeError = io_err.into();
157 assert!(matches!(err, CodeError::Io(_)));
158 assert!(err.to_string().contains("file missing"));
159 }
160
161 #[test]
162 fn test_code_error_from_serde_json() {
163 let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
164 let err: CodeError = json_err.into();
165 assert!(matches!(err, CodeError::Serialization(_)));
166 }
167
168 #[test]
169 fn test_code_error_from_anyhow() {
170 let anyhow_err = anyhow::anyhow!("something went wrong");
171 let err: CodeError = anyhow_err.into();
172 assert!(matches!(err, CodeError::Internal(_)));
173 assert!(err.to_string().contains("something went wrong"));
174 }
175
176 #[test]
177 fn test_code_error_question_mark_from_anyhow() {
178 fn inner() -> anyhow::Result<()> {
179 anyhow::bail!("inner error")
180 }
181
182 fn outer() -> Result<()> {
183 inner()?; Ok(())
185 }
186
187 let result = outer();
188 assert!(result.is_err());
189 let err = result.unwrap_err();
190 assert!(matches!(err, CodeError::Internal(_)));
191 }
192
193 #[test]
194 fn test_read_or_recover_normal() {
195 let lock = std::sync::RwLock::new(42);
196 let guard = read_or_recover(&lock);
197 assert_eq!(*guard, 42);
198 }
199
200 #[test]
201 fn test_write_or_recover_normal() {
202 let lock = std::sync::RwLock::new(42);
203 let mut guard = write_or_recover(&lock);
204 *guard = 99;
205 drop(guard);
206 assert_eq!(*read_or_recover(&lock), 99);
207 }
208
209 #[test]
210 fn test_read_or_recover_poisoned() {
211 let lock = std::sync::RwLock::new(42);
212 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
214 let _guard = lock.write().unwrap();
215 panic!("intentional poison");
216 }));
217 let guard = read_or_recover(&lock);
219 assert_eq!(*guard, 42);
220 }
221
222 #[test]
223 fn test_write_or_recover_poisoned() {
224 let lock = std::sync::RwLock::new(42);
225 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
226 let _guard = lock.write().unwrap();
227 panic!("intentional poison");
228 }));
229 let mut guard = write_or_recover(&lock);
230 *guard = 100;
231 assert_eq!(*guard, 100);
232 }
233}