1#[derive(Debug, thiserror::Error)]
8#[non_exhaustive]
9pub enum Error {
10 #[error("LLM error: {message}")]
12 Llm {
13 message: String,
15 #[source]
17 source: Option<Box<dyn std::error::Error + Send + Sync>>,
18 },
19
20 #[error("Validation error: {message}")]
22 Validation {
23 field: Option<String>,
25 message: String,
27 },
28
29 #[error("I/O error: {0}")]
31 Io(#[from] std::io::Error),
32
33 #[error("Serialization error: {0}")]
35 Serialization(#[from] serde_json::Error),
36
37 #[error("Step timed out after {seconds}s")]
39 Timeout {
40 seconds: u64,
42 },
43
44 #[error("Maximum revisions exceeded: {attempts} attempts")]
46 MaxRevisionsExceeded {
47 attempts: u32,
49 },
50
51 #[error("Database error: {0}")]
53 Database(#[from] sqlx::Error),
54
55 #[error("Configuration error: {message}")]
57 Config {
58 message: String,
60 },
61
62 #[error("Workflow not found: {id}")]
64 WorkflowNotFound {
65 id: String,
67 },
68
69 #[error("Step not found: {id}")]
71 StepNotFound {
72 id: String,
74 },
75}
76
77pub type Result<T> = std::result::Result<T, Error>;
81
82impl Error {
83 pub fn is_retryable(&self) -> bool {
88 match self {
89 Error::Llm { .. } => true, Error::Io(_) => true,
91 Error::Database(_) => true, Error::Timeout { .. } => true,
93 Error::Validation { .. } => false, Error::MaxRevisionsExceeded { .. } => false,
95 Error::Serialization(_) => false,
96 Error::Config { .. } => false,
97 Error::WorkflowNotFound { .. } => false,
98 Error::StepNotFound { .. } => false,
99 }
100 }
101
102 pub fn llm<S: Into<String>>(message: S) -> Self {
104 Error::Llm {
105 message: message.into(),
106 source: None,
107 }
108 }
109
110 pub fn llm_with_source<S, E>(message: S, source: E) -> Self
112 where
113 S: Into<String>,
114 E: std::error::Error + Send + Sync + 'static,
115 {
116 Error::Llm {
117 message: message.into(),
118 source: Some(Box::new(source)),
119 }
120 }
121
122 pub fn validation<S: Into<String>>(message: S) -> Self {
124 Error::Validation {
125 field: None,
126 message: message.into(),
127 }
128 }
129
130 pub fn validation_field<F, M>(field: F, message: M) -> Self
132 where
133 F: Into<String>,
134 M: Into<String>,
135 {
136 Error::Validation {
137 field: Some(field.into()),
138 message: message.into(),
139 }
140 }
141
142 pub fn config<S: Into<String>>(message: S) -> Self {
144 Error::Config {
145 message: message.into(),
146 }
147 }
148}
149
150#[cfg(test)]
151#[allow(clippy::unwrap_used)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_error_display() {
157 let err = Error::llm("Rate limit exceeded");
158 assert_eq!(err.to_string(), "LLM error: Rate limit exceeded");
159 }
160
161 #[test]
162 fn test_retryable_classification() {
163 assert!(Error::llm("test").is_retryable());
164 assert!(Error::Timeout { seconds: 30 }.is_retryable());
165 assert!(!Error::validation("test").is_retryable());
166 assert!(!Error::MaxRevisionsExceeded { attempts: 3 }.is_retryable());
167 }
168
169 #[test]
170 fn test_validation_error_with_field() {
171 let err = Error::validation_field("topic", "must not be empty");
172 let Error::Validation { field, message } = err else {
173 unreachable!("Expected Validation error variant");
174 };
175 assert_eq!(field, Some("topic".to_string()));
176 assert_eq!(message, "must not be empty");
177 }
178
179 #[test]
180 fn test_max_revisions_exceeded() {
181 let err = Error::MaxRevisionsExceeded { attempts: 5 };
182 assert_eq!(err.to_string(), "Maximum revisions exceeded: 5 attempts");
183 }
184
185 #[test]
186 fn test_error_implements_send_sync() {
187 fn assert_send_sync<T: Send + Sync>() {}
188 assert_send_sync::<Error>();
189 }
190
191 #[test]
192 fn test_config_error() {
193 let err = Error::config("Invalid API endpoint");
194 assert_eq!(err.to_string(), "Configuration error: Invalid API endpoint");
195 assert!(!err.is_retryable());
196 }
197
198 #[test]
199 fn test_workflow_not_found() {
200 let err = Error::WorkflowNotFound {
201 id: "wf-123".to_string(),
202 };
203 assert_eq!(err.to_string(), "Workflow not found: wf-123");
204 assert!(!err.is_retryable());
205 }
206
207 #[test]
208 fn test_step_not_found() {
209 let err = Error::StepNotFound {
210 id: "step-456".to_string(),
211 };
212 assert_eq!(err.to_string(), "Step not found: step-456");
213 assert!(!err.is_retryable());
214 }
215
216 #[test]
217 fn test_llm_error_with_source() {
218 let io_error = std::io::Error::other("network failure");
219 let err = Error::llm_with_source("API call failed", Box::new(io_error));
220 assert!(err.to_string().contains("API call failed"));
221 assert!(err.is_retryable());
222 }
223
224 #[test]
225 fn test_io_error_is_retryable() {
226 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
227 let err: Error = io_error.into();
228 assert!(err.is_retryable());
229 }
230
231 #[test]
232 fn test_serde_error_not_retryable() {
233 let json = "{invalid json}";
234 let serde_err = serde_json::from_str::<serde_json::Value>(json).unwrap_err();
235 let err: Error = serde_err.into();
236 assert!(!err.is_retryable());
237 }
238
239 #[test]
240 fn test_timeout_error_display() {
241 let err = Error::Timeout { seconds: 60 };
242 assert_eq!(err.to_string(), "Step timed out after 60s");
243 }
244
245 #[test]
246 fn test_validation_without_field() {
247 let err = Error::validation("Generic validation failure");
248 let Error::Validation { field, message } = err else {
249 unreachable!("Expected Validation error");
250 };
251 assert_eq!(field, None);
252 assert_eq!(message, "Generic validation failure");
253 }
254}