1use crate::prelude::*;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
5pub enum OpError {
6 #[error("Op execution failed: {0}")]
7 ExecutionFailed(String),
8
9 #[error("Op timeout after {timeout_ms}ms")]
10 Timeout { timeout_ms: u64 },
11
12 #[error("Context error: {0}")]
13 Context(String),
14
15 #[error("Batch op failed: {0}")]
16 BatchFailed(String),
17
18 #[error("{chain}")]
24 WrappedClassified {
25 chain: String,
26 code: String,
27 class: crate::failure::AttributionClass,
28 reason: String,
29 arg_urn: Option<String>,
34 },
35
36 #[error("Op aborted: {0}")]
37 Aborted(String),
38
39 #[error("Trigger error: {0}")]
40 Trigger(String),
41
42 #[error("{code}: {message}")]
49 Classified {
50 code: String,
51 class: crate::failure::AttributionClass,
52 message: String,
53 arg_urn: Option<String>,
58 },
59
60 #[error(transparent)]
61 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
62}
63
64impl OpError {
65 pub fn attribution_class(&self) -> crate::failure::AttributionClass {
69 match self {
70 Self::Classified { class, .. } => *class,
71 Self::WrappedClassified { class, .. } => *class,
72 _ => crate::failure::AttributionClass::Internal,
73 }
74 }
75
76 pub fn failure_code(&self) -> Option<&str> {
79 match self {
80 Self::Classified { code, .. } => Some(code),
81 Self::WrappedClassified { code, .. } => Some(code),
82 _ => None,
83 }
84 }
85
86 pub fn failure_arg_urn(&self) -> Option<&str> {
91 match self {
92 Self::Classified { arg_urn, .. } => arg_urn.as_deref(),
93 Self::WrappedClassified { arg_urn, .. } => arg_urn.as_deref(),
94 _ => None,
95 }
96 }
97
98 pub fn failure_reason(&self) -> String {
101 match self {
102 Self::Classified { message, .. } => message.clone(),
103 Self::WrappedClassified { reason, .. } => reason.clone(),
104 other => other.to_string(),
105 }
106 }
107}
108
109impl Clone for OpError {
110 fn clone(&self) -> Self {
111 match self {
112 Self::ExecutionFailed(msg) => Self::ExecutionFailed(msg.clone()),
113 Self::Timeout { timeout_ms } => Self::Timeout {
114 timeout_ms: *timeout_ms,
115 },
116 Self::Context(msg) => Self::Context(msg.clone()),
117 Self::BatchFailed(msg) => Self::BatchFailed(msg.clone()),
118 Self::WrappedClassified {
119 chain,
120 code,
121 class,
122 reason,
123 arg_urn,
124 } => Self::WrappedClassified {
125 chain: chain.clone(),
126 code: code.clone(),
127 class: *class,
128 reason: reason.clone(),
129 arg_urn: arg_urn.clone(),
130 },
131 Self::Aborted(msg) => Self::Aborted(msg.clone()),
132 Self::Trigger(msg) => Self::Trigger(msg.clone()),
133 Self::Classified {
134 code,
135 class,
136 message,
137 arg_urn,
138 } => Self::Classified {
139 code: code.clone(),
140 class: *class,
141 message: message.clone(),
142 arg_urn: arg_urn.clone(),
143 },
144 Self::Other(boxed_error) => Self::ExecutionFailed(format!("{}", boxed_error)),
145 }
146 }
147}
148
149impl From<serde_json::Error> for OpError {
150 fn from(e: serde_json::Error) -> Self {
151 OpError::Other(Box::new(e))
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
161 fn test0104_op_error_display_execution_failed() {
162 let err = OpError::ExecutionFailed("something broke".to_string());
163 assert_eq!(err.to_string(), "Op execution failed: something broke");
164 }
165
166 #[test]
168 fn test0105_op_error_display_timeout() {
169 let err = OpError::Timeout { timeout_ms: 250 };
170 assert_eq!(err.to_string(), "Op timeout after 250ms");
171 }
172
173 #[test]
175 fn test0106_op_error_display_context() {
176 let err = OpError::Context("missing key".to_string());
177 assert_eq!(err.to_string(), "Context error: missing key");
178 }
179
180 #[test]
182 fn test0107_op_error_display_aborted() {
183 let err = OpError::Aborted("user cancelled".to_string());
184 assert_eq!(err.to_string(), "Op aborted: user cancelled");
185 }
186
187 #[test]
189 fn test0108_op_error_clone_execution_failed() {
190 let err = OpError::ExecutionFailed("fail msg".to_string());
191 let cloned = err.clone();
192 assert_eq!(err.to_string(), cloned.to_string());
193 match cloned {
194 OpError::ExecutionFailed(msg) => assert_eq!(msg, "fail msg"),
195 _ => panic!("wrong variant"),
196 }
197 }
198
199 #[test]
201 fn test0109_op_error_clone_timeout() {
202 let err = OpError::Timeout { timeout_ms: 500 };
203 let cloned = err.clone();
204 match cloned {
205 OpError::Timeout { timeout_ms } => assert_eq!(timeout_ms, 500),
206 _ => panic!("wrong variant"),
207 }
208 }
209
210 #[test]
212 fn test0110_op_error_clone_other_converts_to_execution_failed() {
213 use std::io;
214 let io_err = io::Error::new(io::ErrorKind::NotFound, "file missing");
215 let err = OpError::Other(Box::new(io_err));
216 let cloned = err.clone();
217 match cloned {
219 OpError::ExecutionFailed(msg) => assert!(msg.contains("file missing")),
220 _ => panic!("expected ExecutionFailed from cloned Other"),
221 }
222 }
223
224 #[test]
228 fn test1901_classified_accessors() {
229 use crate::failure::AttributionClass;
230
231 let classified = OpError::Classified {
232 code: "CONTEXT_OVERFLOW".to_string(),
233 class: AttributionClass::Input,
234 message: "prompt too large".to_string(),
235 arg_urn: Some("media:prompt;textable".to_string()),
236 };
237 assert_eq!(classified.attribution_class(), AttributionClass::Input);
238 assert_eq!(classified.failure_code(), Some("CONTEXT_OVERFLOW"));
239 assert_eq!(classified.failure_reason(), "prompt too large");
240 assert_eq!(
241 classified.failure_arg_urn(),
242 Some("media:prompt;textable"),
243 "the emit source's argument attribution is served structurally"
244 );
245 assert_eq!(classified.to_string(), "CONTEXT_OVERFLOW: prompt too large");
246
247 let wrapped = OpError::WrappedClassified {
248 chain: "Op 3-generate failed: CONTEXT_OVERFLOW: prompt too large".to_string(),
249 code: "CONTEXT_OVERFLOW".to_string(),
250 class: AttributionClass::Input,
251 reason: "prompt too large".to_string(),
252 arg_urn: None,
253 };
254 assert_eq!(wrapped.attribution_class(), AttributionClass::Input);
255 assert_eq!(wrapped.failure_code(), Some("CONTEXT_OVERFLOW"));
256 assert_eq!(
257 wrapped.failure_arg_urn(),
258 None,
259 "no attribution declared means none served — never guessed"
260 );
261 assert_eq!(
262 wrapped.failure_reason(),
263 "prompt too large",
264 "the reason is the LEAF message, not the wrap chain"
265 );
266 assert_eq!(
267 wrapped.to_string(),
268 "Op 3-generate failed: CONTEXT_OVERFLOW: prompt too large",
269 "Display keeps the human chain"
270 );
271
272 let plain = OpError::ExecutionFailed("boom".to_string());
273 assert_eq!(plain.attribution_class(), AttributionClass::Internal);
274 assert_eq!(plain.failure_code(), None);
275 }
276
277 #[test]
280 fn test1902_clone_preserves_classification() {
281 use crate::failure::AttributionClass;
282
283 let original = OpError::WrappedClassified {
284 chain: "Op 'x' failed: GPU_OUT_OF_MEMORY: no VRAM".to_string(),
285 code: "GPU_OUT_OF_MEMORY".to_string(),
286 class: AttributionClass::Resource,
287 reason: "no VRAM".to_string(),
288 arg_urn: Some("media:model-spec;textable".to_string()),
289 };
290 let cloned = original.clone();
291 assert_eq!(cloned.attribution_class(), AttributionClass::Resource);
292 assert_eq!(cloned.failure_code(), Some("GPU_OUT_OF_MEMORY"));
293 assert_eq!(cloned.failure_reason(), "no VRAM");
294 assert_eq!(cloned.failure_arg_urn(), Some("media:model-spec;textable"));
295 }
296
297 #[test]
299 fn test0111_op_error_from_serde_json_error() {
300 let json_err = serde_json::from_str::<i32>("not_a_number").unwrap_err();
301 let op_err: OpError = json_err.into();
302 match op_err {
304 OpError::Other(_) => {}
305 _ => panic!("expected Other variant from serde_json::Error conversion"),
306 }
307 }
308}