use crate::prelude::*;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum OpError {
#[error("Op execution failed: {0}")]
ExecutionFailed(String),
#[error("Op timeout after {timeout_ms}ms")]
Timeout { timeout_ms: u64 },
#[error("Context error: {0}")]
Context(String),
#[error("Batch op failed: {0}")]
BatchFailed(String),
#[error("{chain}")]
WrappedClassified {
chain: String,
code: String,
class: crate::failure::AttributionClass,
reason: String,
arg_urn: Option<String>,
},
#[error("Op aborted: {0}")]
Aborted(String),
#[error("Trigger error: {0}")]
Trigger(String),
#[error("{code}: {message}")]
Classified {
code: String,
class: crate::failure::AttributionClass,
message: String,
arg_urn: Option<String>,
},
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl OpError {
pub fn attribution_class(&self) -> crate::failure::AttributionClass {
match self {
Self::Classified { class, .. } => *class,
Self::WrappedClassified { class, .. } => *class,
_ => crate::failure::AttributionClass::Internal,
}
}
pub fn failure_code(&self) -> Option<&str> {
match self {
Self::Classified { code, .. } => Some(code),
Self::WrappedClassified { code, .. } => Some(code),
_ => None,
}
}
pub fn failure_arg_urn(&self) -> Option<&str> {
match self {
Self::Classified { arg_urn, .. } => arg_urn.as_deref(),
Self::WrappedClassified { arg_urn, .. } => arg_urn.as_deref(),
_ => None,
}
}
pub fn failure_reason(&self) -> String {
match self {
Self::Classified { message, .. } => message.clone(),
Self::WrappedClassified { reason, .. } => reason.clone(),
other => other.to_string(),
}
}
}
impl Clone for OpError {
fn clone(&self) -> Self {
match self {
Self::ExecutionFailed(msg) => Self::ExecutionFailed(msg.clone()),
Self::Timeout { timeout_ms } => Self::Timeout {
timeout_ms: *timeout_ms,
},
Self::Context(msg) => Self::Context(msg.clone()),
Self::BatchFailed(msg) => Self::BatchFailed(msg.clone()),
Self::WrappedClassified {
chain,
code,
class,
reason,
arg_urn,
} => Self::WrappedClassified {
chain: chain.clone(),
code: code.clone(),
class: *class,
reason: reason.clone(),
arg_urn: arg_urn.clone(),
},
Self::Aborted(msg) => Self::Aborted(msg.clone()),
Self::Trigger(msg) => Self::Trigger(msg.clone()),
Self::Classified {
code,
class,
message,
arg_urn,
} => Self::Classified {
code: code.clone(),
class: *class,
message: message.clone(),
arg_urn: arg_urn.clone(),
},
Self::Other(boxed_error) => Self::ExecutionFailed(format!("{}", boxed_error)),
}
}
}
impl From<serde_json::Error> for OpError {
fn from(e: serde_json::Error) -> Self {
OpError::Other(Box::new(e))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test0104_op_error_display_execution_failed() {
let err = OpError::ExecutionFailed("something broke".to_string());
assert_eq!(err.to_string(), "Op execution failed: something broke");
}
#[test]
fn test0105_op_error_display_timeout() {
let err = OpError::Timeout { timeout_ms: 250 };
assert_eq!(err.to_string(), "Op timeout after 250ms");
}
#[test]
fn test0106_op_error_display_context() {
let err = OpError::Context("missing key".to_string());
assert_eq!(err.to_string(), "Context error: missing key");
}
#[test]
fn test0107_op_error_display_aborted() {
let err = OpError::Aborted("user cancelled".to_string());
assert_eq!(err.to_string(), "Op aborted: user cancelled");
}
#[test]
fn test0108_op_error_clone_execution_failed() {
let err = OpError::ExecutionFailed("fail msg".to_string());
let cloned = err.clone();
assert_eq!(err.to_string(), cloned.to_string());
match cloned {
OpError::ExecutionFailed(msg) => assert_eq!(msg, "fail msg"),
_ => panic!("wrong variant"),
}
}
#[test]
fn test0109_op_error_clone_timeout() {
let err = OpError::Timeout { timeout_ms: 500 };
let cloned = err.clone();
match cloned {
OpError::Timeout { timeout_ms } => assert_eq!(timeout_ms, 500),
_ => panic!("wrong variant"),
}
}
#[test]
fn test0110_op_error_clone_other_converts_to_execution_failed() {
use std::io;
let io_err = io::Error::new(io::ErrorKind::NotFound, "file missing");
let err = OpError::Other(Box::new(io_err));
let cloned = err.clone();
match cloned {
OpError::ExecutionFailed(msg) => assert!(msg.contains("file missing")),
_ => panic!("expected ExecutionFailed from cloned Other"),
}
}
#[test]
fn test1901_classified_accessors() {
use crate::failure::AttributionClass;
let classified = OpError::Classified {
code: "CONTEXT_OVERFLOW".to_string(),
class: AttributionClass::Input,
message: "prompt too large".to_string(),
arg_urn: Some("media:prompt;textable".to_string()),
};
assert_eq!(classified.attribution_class(), AttributionClass::Input);
assert_eq!(classified.failure_code(), Some("CONTEXT_OVERFLOW"));
assert_eq!(classified.failure_reason(), "prompt too large");
assert_eq!(
classified.failure_arg_urn(),
Some("media:prompt;textable"),
"the emit source's argument attribution is served structurally"
);
assert_eq!(classified.to_string(), "CONTEXT_OVERFLOW: prompt too large");
let wrapped = OpError::WrappedClassified {
chain: "Op 3-generate failed: CONTEXT_OVERFLOW: prompt too large".to_string(),
code: "CONTEXT_OVERFLOW".to_string(),
class: AttributionClass::Input,
reason: "prompt too large".to_string(),
arg_urn: None,
};
assert_eq!(wrapped.attribution_class(), AttributionClass::Input);
assert_eq!(wrapped.failure_code(), Some("CONTEXT_OVERFLOW"));
assert_eq!(
wrapped.failure_arg_urn(),
None,
"no attribution declared means none served — never guessed"
);
assert_eq!(
wrapped.failure_reason(),
"prompt too large",
"the reason is the LEAF message, not the wrap chain"
);
assert_eq!(
wrapped.to_string(),
"Op 3-generate failed: CONTEXT_OVERFLOW: prompt too large",
"Display keeps the human chain"
);
let plain = OpError::ExecutionFailed("boom".to_string());
assert_eq!(plain.attribution_class(), AttributionClass::Internal);
assert_eq!(plain.failure_code(), None);
}
#[test]
fn test1902_clone_preserves_classification() {
use crate::failure::AttributionClass;
let original = OpError::WrappedClassified {
chain: "Op 'x' failed: GPU_OUT_OF_MEMORY: no VRAM".to_string(),
code: "GPU_OUT_OF_MEMORY".to_string(),
class: AttributionClass::Resource,
reason: "no VRAM".to_string(),
arg_urn: Some("media:model-spec;textable".to_string()),
};
let cloned = original.clone();
assert_eq!(cloned.attribution_class(), AttributionClass::Resource);
assert_eq!(cloned.failure_code(), Some("GPU_OUT_OF_MEMORY"));
assert_eq!(cloned.failure_reason(), "no VRAM");
assert_eq!(cloned.failure_arg_urn(), Some("media:model-spec;textable"));
}
#[test]
fn test0111_op_error_from_serde_json_error() {
let json_err = serde_json::from_str::<i32>("not_a_number").unwrap_err();
let op_err: OpError = json_err.into();
match op_err {
OpError::Other(_) => {}
_ => panic!("expected Other variant from serde_json::Error conversion"),
}
}
}