Skip to main content

minion_engine/
error.rs

1use thiserror::Error;
2
3use crate::control_flow::ControlFlow;
4
5#[derive(Error, Debug)]
6#[allow(dead_code)]
7pub enum StepError {
8    #[error("Step failed: {0}")]
9    Fail(String),
10
11    #[error("Control flow: {0:?}")]
12    ControlFlow(ControlFlow),
13
14    #[error("Timeout after {0:?}")]
15    Timeout(std::time::Duration),
16
17    #[error("Template error: {0}")]
18    Template(String),
19
20    #[error("Sandbox error: {message} (image: {image})")]
21    Sandbox {
22        message: String,
23        image: String,
24    },
25
26    #[error("Config error in '{field}': {message}")]
27    Config {
28        field: String,
29        message: String,
30    },
31
32    #[error("{0}")]
33    Other(#[from] anyhow::Error),
34}
35
36#[allow(dead_code)]
37impl StepError {
38    /// Create a sandbox error with image context
39    pub fn sandbox(message: impl Into<String>, image: impl Into<String>) -> Self {
40        StepError::Sandbox {
41            message: message.into(),
42            image: image.into(),
43        }
44    }
45
46    /// Create a config validation error
47    pub fn config(field: impl Into<String>, message: impl Into<String>) -> Self {
48        StepError::Config {
49            field: field.into(),
50            message: message.into(),
51        }
52    }
53
54    /// Returns true if this error is a timeout
55    pub fn is_timeout(&self) -> bool {
56        matches!(self, StepError::Timeout(_))
57    }
58
59    /// Returns true if this error is a control flow signal (not a real error)
60    pub fn is_control_flow(&self) -> bool {
61        matches!(self, StepError::ControlFlow(_))
62    }
63
64    /// Returns a human-friendly error category for logging
65    pub fn category(&self) -> &'static str {
66        match self {
67            StepError::Fail(_) => "step_failure",
68            StepError::ControlFlow(_) => "control_flow",
69            StepError::Timeout(_) => "timeout",
70            StepError::Template(_) => "template",
71            StepError::Sandbox { .. } => "sandbox",
72            StepError::Config { .. } => "config",
73            StepError::Other(_) => "internal",
74        }
75    }
76}
77
78impl From<ControlFlow> for StepError {
79    fn from(cf: ControlFlow) -> Self {
80        StepError::ControlFlow(cf)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_error_categories() {
90        let err = StepError::Fail("test".into());
91        assert_eq!(err.category(), "step_failure");
92        assert!(!err.is_timeout());
93        assert!(!err.is_control_flow());
94
95        let err = StepError::Timeout(std::time::Duration::from_secs(30));
96        assert_eq!(err.category(), "timeout");
97        assert!(err.is_timeout());
98
99        let err = StepError::sandbox("container crashed", "node:20");
100        assert_eq!(err.category(), "sandbox");
101        assert_eq!(
102            err.to_string(),
103            "Sandbox error: container crashed (image: node:20)"
104        );
105
106        let err = StepError::config("sandbox.image", "image not found");
107        assert_eq!(err.category(), "config");
108        assert_eq!(
109            err.to_string(),
110            "Config error in 'sandbox.image': image not found"
111        );
112    }
113
114    #[test]
115    fn test_error_display() {
116        let err = StepError::Fail("connection refused".into());
117        assert_eq!(err.to_string(), "Step failed: connection refused");
118
119        let err = StepError::Template("bad syntax in {{name}}".into());
120        assert_eq!(err.to_string(), "Template error: bad syntax in {{name}}");
121    }
122}