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