Skip to main content

minion_engine/
error.rs

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