#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum CanoError {
TaskExecution(String),
Store(String),
Workflow(String),
Configuration(String),
RetryExhausted {
attempts: u32,
source: Box<CanoError>,
},
Timeout(String),
WorkflowTimeout {
elapsed: std::time::Duration,
limit: std::time::Duration,
},
CircuitOpen(String),
RateLimited {
tier: String,
retry_after: std::time::Duration,
},
CheckpointStore(String),
CompensationFailed {
errors: Vec<CanoError>,
},
Generic(String),
ResourceNotFound(String),
ResourceTypeMismatch(String),
ResourceDuplicateKey(String),
WorkflowVersionMismatch {
stored: u32,
expected: u32,
},
OrphanedCompensation {
task_id: std::sync::Arc<str>,
output_blob: Vec<u8>,
},
WithStateContext {
state: String,
attempt: u32,
transitions_so_far: Vec<String>,
source: Box<CanoError>,
},
}
impl CanoError {
pub fn task_execution<S: Into<String>>(msg: S) -> Self {
CanoError::TaskExecution(msg.into())
}
pub fn store<S: Into<String>>(msg: S) -> Self {
CanoError::Store(msg.into())
}
pub fn workflow<S: Into<String>>(msg: S) -> Self {
CanoError::Workflow(msg.into())
}
pub fn configuration<S: Into<String>>(msg: S) -> Self {
CanoError::Configuration(msg.into())
}
pub fn retry_exhausted(attempts: u32, source: CanoError) -> Self {
CanoError::RetryExhausted {
attempts,
source: Box::new(source),
}
}
pub fn timeout<S: Into<String>>(msg: S) -> Self {
CanoError::Timeout(msg.into())
}
pub fn workflow_timeout(elapsed: std::time::Duration, limit: std::time::Duration) -> Self {
CanoError::WorkflowTimeout { elapsed, limit }
}
pub fn circuit_open<S: Into<String>>(msg: S) -> Self {
CanoError::CircuitOpen(msg.into())
}
pub fn rate_limited<S: Into<String>>(tier: S, retry_after: std::time::Duration) -> Self {
CanoError::RateLimited {
tier: tier.into(),
retry_after,
}
}
pub fn checkpoint_store<S: Into<String>>(msg: S) -> Self {
CanoError::CheckpointStore(msg.into())
}
pub fn compensation_failed(errors: Vec<CanoError>) -> Self {
let mut flat = Vec::with_capacity(errors.len());
for e in errors {
match e {
CanoError::CompensationFailed { errors: inner } => flat.extend(inner),
other => flat.push(other),
}
}
CanoError::CompensationFailed { errors: flat }
}
pub fn generic<S: Into<String>>(msg: S) -> Self {
CanoError::Generic(msg.into())
}
pub fn resource_not_found<S: Into<String>>(msg: S) -> Self {
CanoError::ResourceNotFound(msg.into())
}
pub fn resource_type_mismatch<S: Into<String>>(msg: S) -> Self {
CanoError::ResourceTypeMismatch(msg.into())
}
pub fn resource_duplicate_key<S: Into<String>>(msg: S) -> Self {
CanoError::ResourceDuplicateKey(msg.into())
}
pub fn workflow_version_mismatch(stored: u32, expected: u32) -> Self {
CanoError::WorkflowVersionMismatch { stored, expected }
}
pub fn orphaned_compensation(
task_id: impl Into<std::sync::Arc<str>>,
output_blob: Vec<u8>,
) -> Self {
CanoError::OrphanedCompensation {
task_id: task_id.into(),
output_blob,
}
}
pub fn with_state_context(
state: impl Into<String>,
attempt: u32,
transitions_so_far: Vec<String>,
source: CanoError,
) -> Self {
match source {
CanoError::WithStateContext { .. } => source,
CanoError::CompensationFailed { mut errors } => {
if let Some(first) = errors.first_mut() {
let owned = std::mem::replace(first, CanoError::Generic(String::new()));
*first = Self::with_state_context(state, attempt, transitions_so_far, owned);
}
CanoError::CompensationFailed { errors }
}
other => CanoError::WithStateContext {
state: state.into(),
attempt,
transitions_so_far,
source: Box::new(other),
},
}
}
pub fn message(&self) -> &str {
match self {
CanoError::TaskExecution(msg) => msg,
CanoError::Store(msg) => msg,
CanoError::Workflow(msg) => msg,
CanoError::Configuration(msg) => msg,
CanoError::RetryExhausted { source, .. } => source.message(),
CanoError::Timeout(msg) => msg,
CanoError::WorkflowTimeout { .. } => "workflow total timeout exceeded",
CanoError::CircuitOpen(msg) => msg,
CanoError::RateLimited { .. } => "rate limited",
CanoError::CheckpointStore(msg) => msg,
CanoError::CompensationFailed { errors } => errors
.first()
.map_or("compensation failed", CanoError::message),
CanoError::Generic(msg) => msg,
CanoError::ResourceNotFound(msg) => msg,
CanoError::ResourceTypeMismatch(msg) => msg,
CanoError::ResourceDuplicateKey(msg) => msg,
CanoError::WorkflowVersionMismatch { .. } => {
"workflow version mismatch (stored vs expected)"
}
CanoError::OrphanedCompensation { .. } => {
"compensation entry has no registered compensator"
}
CanoError::WithStateContext { source, .. } => source.message(),
}
}
pub fn category(&self) -> &'static str {
match self {
CanoError::WithStateContext { source, .. } => source.category(),
other => other.outer_category(),
}
}
pub fn inner(&self) -> &CanoError {
match self {
CanoError::WithStateContext { source, .. } => source.as_ref(),
other => other,
}
}
pub fn outer_category(&self) -> &'static str {
match self {
CanoError::TaskExecution(_) => "task_execution",
CanoError::Store(_) => "store",
CanoError::Workflow(_) => "workflow",
CanoError::Configuration(_) => "configuration",
CanoError::RetryExhausted { .. } => "retry_exhausted",
CanoError::Timeout(_) => "timeout",
CanoError::WorkflowTimeout { .. } => "workflow_timeout",
CanoError::CircuitOpen(_) => "circuit_open",
CanoError::RateLimited { .. } => "rate_limited",
CanoError::CheckpointStore(_) => "checkpoint_store",
CanoError::CompensationFailed { .. } => "compensation_failed",
CanoError::Generic(_) => "generic",
CanoError::ResourceNotFound(_) => "resource_not_found",
CanoError::ResourceTypeMismatch(_) => "resource_type_mismatch",
CanoError::ResourceDuplicateKey(_) => "resource_duplicate_key",
CanoError::WorkflowVersionMismatch { .. } => "workflow_version_mismatch",
CanoError::OrphanedCompensation { .. } => "orphaned_compensation",
CanoError::WithStateContext { .. } => "with_state_context",
}
}
}
impl std::fmt::Display for CanoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CanoError::TaskExecution(msg) => write!(f, "Task execution error: {msg}"),
CanoError::Store(msg) => write!(f, "Store error: {msg}"),
CanoError::Workflow(msg) => write!(f, "Workflow error: {msg}"),
CanoError::Configuration(msg) => write!(f, "Configuration error: {msg}"),
CanoError::RetryExhausted { attempts, source } => {
write!(f, "Retry exhausted after {attempts} attempt(s): {source}")
}
CanoError::Timeout(msg) => write!(f, "Timeout error: {msg}"),
CanoError::WorkflowTimeout { elapsed, limit } => write!(
f,
"Workflow total timeout exceeded: elapsed={elapsed:?} limit={limit:?}"
),
CanoError::CircuitOpen(msg) => write!(f, "Circuit open: {msg}"),
CanoError::RateLimited { tier, retry_after } => {
write!(
f,
"Rate limited by tier `{tier}`: retry after {retry_after:?}"
)
}
CanoError::CheckpointStore(msg) => write!(f, "Checkpoint store error: {msg}"),
CanoError::CompensationFailed { errors } => {
write!(f, "Compensation failed ({} error(s))", errors.len())?;
for (i, e) in errors.iter().enumerate() {
if i == 0 {
write!(f, "; original: {e}")?;
} else {
write!(f, "; compensation #{i}: {e}")?;
}
}
Ok(())
}
CanoError::Generic(msg) => write!(f, "Error: {msg}"),
CanoError::ResourceNotFound(msg) => write!(f, "Resource not found: {msg}"),
CanoError::ResourceTypeMismatch(msg) => write!(f, "Resource type mismatch: {msg}"),
CanoError::ResourceDuplicateKey(msg) => write!(f, "Resource duplicate key: {msg}"),
CanoError::WorkflowVersionMismatch { stored, expected } => write!(
f,
"Workflow version mismatch: stored={stored}, expected={expected}"
),
CanoError::OrphanedCompensation {
task_id,
output_blob,
} => write!(
f,
"No compensator registered for task {task_id:?} — output_blob ({} bytes) was kept for manual recovery",
output_blob.len()
),
CanoError::WithStateContext {
state,
attempt,
transitions_so_far,
source,
} => {
write!(f, "state={state} attempt={attempt} path=[")?;
for (i, s) in transitions_so_far.iter().enumerate() {
if i == 0 {
write!(f, "{s}")?;
} else {
write!(f, ", {s}")?;
}
}
write!(f, "] caused by: {source}")
}
}
}
}
impl std::error::Error for CanoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
CanoError::WithStateContext { source, .. }
| CanoError::RetryExhausted { source, .. } => {
Some(source.as_ref() as &(dyn std::error::Error + 'static))
}
_ => None,
}
}
}
impl PartialEq for CanoError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(CanoError::TaskExecution(a), CanoError::TaskExecution(b)) => a == b,
(CanoError::Store(a), CanoError::Store(b)) => a == b,
(CanoError::Workflow(a), CanoError::Workflow(b)) => a == b,
(CanoError::Configuration(a), CanoError::Configuration(b)) => a == b,
(
CanoError::RetryExhausted {
attempts: a_attempts,
source: a_source,
},
CanoError::RetryExhausted {
attempts: b_attempts,
source: b_source,
},
) => a_attempts == b_attempts && a_source == b_source,
(CanoError::Timeout(a), CanoError::Timeout(b)) => a == b,
(
CanoError::WorkflowTimeout {
elapsed: e1,
limit: l1,
},
CanoError::WorkflowTimeout {
elapsed: e2,
limit: l2,
},
) => e1 == e2 && l1 == l2,
(CanoError::CircuitOpen(a), CanoError::CircuitOpen(b)) => a == b,
(
CanoError::RateLimited {
tier: t1,
retry_after: r1,
},
CanoError::RateLimited {
tier: t2,
retry_after: r2,
},
) => t1 == t2 && r1 == r2,
(CanoError::CheckpointStore(a), CanoError::CheckpointStore(b)) => a == b,
(
CanoError::CompensationFailed { errors: a },
CanoError::CompensationFailed { errors: b },
) => a == b,
(CanoError::Generic(a), CanoError::Generic(b)) => a == b,
(CanoError::ResourceNotFound(a), CanoError::ResourceNotFound(b)) => a == b,
(CanoError::ResourceTypeMismatch(a), CanoError::ResourceTypeMismatch(b)) => a == b,
(CanoError::ResourceDuplicateKey(a), CanoError::ResourceDuplicateKey(b)) => a == b,
(
CanoError::WorkflowVersionMismatch {
stored: s1,
expected: e1,
},
CanoError::WorkflowVersionMismatch {
stored: s2,
expected: e2,
},
) => s1 == s2 && e1 == e2,
(
CanoError::OrphanedCompensation {
task_id: t1,
output_blob: b1,
},
CanoError::OrphanedCompensation {
task_id: t2,
output_blob: b2,
},
) => t1 == t2 && b1 == b2,
(
CanoError::WithStateContext {
state: s1,
attempt: a1,
transitions_so_far: t1,
source: src1,
},
CanoError::WithStateContext {
state: s2,
attempt: a2,
transitions_so_far: t2,
source: src2,
},
) => s1 == s2 && a1 == a2 && t1 == t2 && src1 == src2,
_ => false,
}
}
}
impl Eq for CanoError {}
impl From<Box<dyn std::error::Error + Send + Sync>> for CanoError {
fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
CanoError::Generic(err.to_string())
}
}
impl From<&str> for CanoError {
fn from(err: &str) -> Self {
CanoError::Generic(err.to_string())
}
}
impl From<String> for CanoError {
fn from(err: String) -> Self {
CanoError::Generic(err)
}
}
impl From<std::io::Error> for CanoError {
fn from(err: std::io::Error) -> Self {
CanoError::Generic(format!("IO error: {err}"))
}
}
impl From<crate::store::error::StoreError> for CanoError {
fn from(err: crate::store::error::StoreError) -> Self {
CanoError::store(err.to_string())
}
}
pub type CanoResult<TState> = Result<TState, CanoError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let error = CanoError::task_execution("Test error");
assert_eq!(error.message(), "Test error");
assert_eq!(error.category(), "task_execution");
}
#[test]
fn test_error_display() {
let error = CanoError::TaskExecution("Test error".to_string());
assert_eq!(format!("{error}"), "Task execution error: Test error");
}
#[test]
fn test_error_conversions() {
let error1: CanoError = "Test error".into();
let error2: CanoError = "Test error".to_string().into();
match (&error1, &error2) {
(CanoError::Generic(msg1), CanoError::Generic(msg2)) => {
assert_eq!(msg1, msg2);
}
_ => panic!("Expected Generic errors"),
}
}
#[test]
fn test_error_categories() {
assert_eq!(
CanoError::TaskExecution("".to_string()).category(),
"task_execution"
);
assert_eq!(CanoError::store("".to_string()).category(), "store");
assert_eq!(CanoError::Workflow("".to_string()).category(), "workflow");
}
#[test]
fn test_store_error_conversion() {
use crate::store::error::StoreError;
let store_error = StoreError::key_not_found("test_key");
let cano_error: CanoError = store_error.into();
match cano_error {
CanoError::Store(msg) => {
assert!(msg.contains("test_key"));
assert!(msg.contains("not found"));
}
_ => panic!("Expected store error variant"),
}
let store_error = StoreError::type_mismatch("type error");
let cano_error: CanoError = store_error.into();
assert_eq!(cano_error.category(), "store");
let store_error = StoreError::lock_error("lock failed");
let cano_error: CanoError = store_error.into();
assert_eq!(cano_error.category(), "store");
assert!(cano_error.message().contains("lock failed"));
}
#[test]
fn test_io_error_maps_to_generic() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let cano_err: CanoError = io_err.into();
assert_eq!(cano_err.category(), "generic");
assert!(cano_err.message().contains("IO error"));
assert!(cano_err.message().contains("file missing"));
}
#[test]
fn test_partial_eq_same_variant_same_message() {
let a = CanoError::TaskExecution("oops".to_string());
let b = CanoError::TaskExecution("oops".to_string());
assert_eq!(a, b);
}
#[test]
fn test_partial_eq_same_variant_different_message() {
let a = CanoError::TaskExecution("a".to_string());
let b = CanoError::TaskExecution("b".to_string());
assert_ne!(a, b);
}
#[test]
fn test_partial_eq_different_variants() {
let a = CanoError::TaskExecution("msg".to_string());
let b = CanoError::Workflow("msg".to_string());
assert_ne!(a, b);
}
#[test]
fn test_resource_not_found_constructor_and_category() {
let err = CanoError::resource_not_found("missing key");
assert_eq!(err.message(), "missing key");
assert_eq!(err.category(), "resource_not_found");
assert_eq!(format!("{err}"), "Resource not found: missing key");
}
#[test]
fn test_resource_type_mismatch_constructor_and_category() {
let err = CanoError::resource_type_mismatch("wrong type");
assert_eq!(err.message(), "wrong type");
assert_eq!(err.category(), "resource_type_mismatch");
assert_eq!(format!("{err}"), "Resource type mismatch: wrong type");
}
#[test]
fn test_resource_variants_partial_eq() {
let a = CanoError::ResourceNotFound("k".to_string());
let b = CanoError::ResourceNotFound("k".to_string());
assert_eq!(a, b);
let c = CanoError::ResourceNotFound("k".to_string());
let d = CanoError::ResourceTypeMismatch("k".to_string());
assert_ne!(c, d);
let e = CanoError::ResourceTypeMismatch("t".to_string());
let f = CanoError::ResourceTypeMismatch("t".to_string());
assert_eq!(e, f);
}
#[test]
fn test_timeout_constructor_and_category() {
let err = CanoError::timeout("attempt deadline reached");
assert_eq!(err.message(), "attempt deadline reached");
assert_eq!(err.category(), "timeout");
assert_eq!(format!("{err}"), "Timeout error: attempt deadline reached");
}
#[test]
fn test_timeout_partial_eq() {
let a = CanoError::timeout("d");
let b = CanoError::timeout("d");
assert_eq!(a, b);
let c = CanoError::timeout("x");
let d = CanoError::timeout("y");
assert_ne!(c, d);
let timeout = CanoError::timeout("k");
let workflow = CanoError::workflow("k");
assert_ne!(timeout, workflow);
}
#[test]
fn test_circuit_open_constructor_and_category() {
let err = CanoError::circuit_open("breaker tripped");
assert_eq!(err.message(), "breaker tripped");
assert_eq!(err.category(), "circuit_open");
assert_eq!(format!("{err}"), "Circuit open: breaker tripped");
}
#[test]
fn test_circuit_open_partial_eq() {
let a = CanoError::circuit_open("x");
let b = CanoError::circuit_open("x");
assert_eq!(a, b);
let c = CanoError::circuit_open("x");
let d = CanoError::circuit_open("y");
assert_ne!(c, d);
let timeout = CanoError::timeout("x");
let circuit = CanoError::circuit_open("x");
assert_ne!(timeout, circuit);
}
#[test]
fn test_checkpoint_store_constructor_and_category() {
let err = CanoError::checkpoint_store("disk full");
assert_eq!(err.message(), "disk full");
assert_eq!(err.category(), "checkpoint_store");
assert_eq!(format!("{err}"), "Checkpoint store error: disk full");
}
#[test]
fn test_checkpoint_store_partial_eq() {
let a = CanoError::checkpoint_store("x");
let b = CanoError::checkpoint_store("x");
assert_eq!(a, b);
let c = CanoError::checkpoint_store("x");
let d = CanoError::checkpoint_store("y");
assert_ne!(c, d);
let circuit = CanoError::circuit_open("x");
let checkpoint = CanoError::checkpoint_store("x");
assert_ne!(circuit, checkpoint);
}
#[test]
fn test_compensation_failed_constructor_category_and_message() {
let err = CanoError::compensation_failed(vec![
CanoError::task_execution("charge declined"),
CanoError::generic("refund timed out"),
]);
assert_eq!(err.category(), "compensation_failed");
assert_eq!(err.message(), "charge declined");
let shown = format!("{err}");
assert!(shown.contains("Compensation failed (2 error(s))"));
assert!(shown.contains("original: Task execution error: charge declined"));
assert!(shown.contains("compensation #1: Error: refund timed out"));
assert_eq!(
CanoError::compensation_failed(vec![]).message(),
"compensation failed"
);
}
#[test]
fn test_compensation_failed_partial_eq() {
let a = CanoError::compensation_failed(vec![CanoError::generic("x")]);
let b = CanoError::compensation_failed(vec![CanoError::generic("x")]);
assert_eq!(a, b);
let c = CanoError::compensation_failed(vec![CanoError::generic("x")]);
let d =
CanoError::compensation_failed(vec![CanoError::generic("x"), CanoError::generic("y")]);
assert_ne!(c, d);
assert_ne!(
CanoError::compensation_failed(vec![CanoError::generic("x")]),
CanoError::generic("x")
);
}
#[test]
fn test_resource_not_found_distinct_from_resource_type_mismatch() {
let not_found = CanoError::resource_not_found("key");
let mismatch = CanoError::resource_type_mismatch("key");
assert_ne!(not_found, mismatch);
}
#[test]
fn test_workflow_version_mismatch_constructor_category_and_display() {
let err = CanoError::workflow_version_mismatch(1, 2);
assert_eq!(err.category(), "workflow_version_mismatch");
assert!(err.message().contains("stored"));
let shown = format!("{err}");
assert!(shown.contains("Workflow version mismatch"));
assert!(shown.contains("stored=1"));
assert!(shown.contains("expected=2"));
}
#[test]
fn test_workflow_version_mismatch_partial_eq() {
let a = CanoError::workflow_version_mismatch(0, 1);
let b = CanoError::workflow_version_mismatch(0, 1);
assert_eq!(a, b);
let c = CanoError::workflow_version_mismatch(1, 2);
assert_ne!(a, c);
}
#[test]
fn test_with_state_context_constructor_category_and_message() {
let inner = CanoError::task_execution("connection refused");
let err = CanoError::with_state_context(
"Process",
2,
vec![
"Start".to_string(),
"Fetch".to_string(),
"Process".to_string(),
],
inner.clone(),
);
assert_eq!(err.category(), "task_execution");
assert_eq!(err.outer_category(), "with_state_context");
assert_eq!(err.message(), "connection refused");
let shown = format!("{err}");
assert!(shown.contains("state=Process"));
assert!(shown.contains("attempt=2"));
assert!(shown.contains("path=[Start, Fetch, Process]"));
}
#[test]
fn test_with_state_context_wraps_errors_zero_through_compensation_failed() {
let cf = CanoError::compensation_failed(vec![
CanoError::generic("x"),
CanoError::task_execution("comp failed"),
]);
let wrapped = CanoError::with_state_context("S", 1, vec!["S".into()], cf);
match wrapped {
CanoError::CompensationFailed { errors } => {
assert_eq!(errors.len(), 2);
match &errors[0] {
CanoError::WithStateContext {
state,
attempt,
transitions_so_far,
source,
} => {
assert_eq!(state, "S");
assert_eq!(*attempt, 1);
assert_eq!(transitions_so_far, &vec!["S".to_string()]);
assert!(matches!(**source, CanoError::Generic(_)));
}
other => panic!("errors[0] must be WithStateContext, got {other:?}"),
}
assert!(errors[1].message().contains("comp failed"));
}
other => panic!("expected CompensationFailed envelope, got {other:?}"),
}
}
#[test]
fn test_with_state_context_handles_empty_compensation_failed_envelope() {
let cf = CanoError::CompensationFailed { errors: vec![] };
let wrapped = CanoError::with_state_context("S", 1, vec!["S".into()], cf.clone());
assert_eq!(wrapped, cf);
}
#[test]
fn test_with_state_context_source_chain() {
use std::error::Error;
let inner = CanoError::task_execution("boom");
let err = CanoError::with_state_context("S", 1, vec!["S".to_string()], inner);
let src = err.source().expect("must expose inner via Error::source");
assert!(src.to_string().contains("boom"));
}
#[test]
fn test_retry_exhausted_constructor_category_and_message() {
let inner = CanoError::task_execution("connection refused");
let err = CanoError::retry_exhausted(3, inner.clone());
assert_eq!(err.category(), "retry_exhausted");
assert_eq!(err.message(), "connection refused");
let shown = format!("{err}");
assert!(shown.contains("Retry exhausted after 3 attempt(s)"));
assert!(shown.contains("connection refused"));
}
#[test]
fn test_retry_exhausted_partial_eq() {
let a = CanoError::retry_exhausted(2, CanoError::task_execution("x"));
let b = CanoError::retry_exhausted(2, CanoError::task_execution("x"));
assert_eq!(a, b);
let c = CanoError::retry_exhausted(3, CanoError::task_execution("x"));
assert_ne!(a, c, "different attempts must not compare equal");
let d = CanoError::retry_exhausted(2, CanoError::task_execution("y"));
assert_ne!(a, d, "different sources must not compare equal");
}
#[test]
fn test_retry_exhausted_source_chain() {
use std::error::Error;
let err = CanoError::retry_exhausted(5, CanoError::task_execution("boom"));
let src = err.source().expect("must expose inner via Error::source");
assert!(src.to_string().contains("boom"));
}
#[test]
fn test_workflow_timeout_constructor_category_and_display() {
use std::time::Duration;
let err =
CanoError::workflow_timeout(Duration::from_millis(150), Duration::from_millis(100));
assert_eq!(err.category(), "workflow_timeout");
assert_eq!(err.message(), "workflow total timeout exceeded");
let shown = format!("{err}");
assert!(shown.contains("Workflow total timeout exceeded"));
assert!(shown.contains("150ms"));
assert!(shown.contains("100ms"));
}
#[test]
fn test_workflow_timeout_partial_eq() {
use std::time::Duration;
let a = CanoError::workflow_timeout(Duration::from_secs(1), Duration::from_secs(2));
let b = CanoError::workflow_timeout(Duration::from_secs(1), Duration::from_secs(2));
assert_eq!(a, b);
let c = CanoError::workflow_timeout(Duration::from_secs(3), Duration::from_secs(2));
assert_ne!(a, c, "different limit must not compare equal");
let d = CanoError::workflow_timeout(Duration::from_secs(1), Duration::from_secs(5));
assert_ne!(a, d, "different elapsed must not compare equal");
let attempt_timeout = CanoError::timeout("workflow total timeout exceeded");
assert_ne!(a, attempt_timeout);
}
}