Skip to main content

starpod_hooks/
error.rs

1use thiserror::Error;
2
3/// Errors that can occur during hook operations.
4#[derive(Error, Debug)]
5pub enum HookError {
6    /// Invalid regex pattern in a hook matcher.
7    #[error("Invalid hook matcher regex: {0}")]
8    InvalidRegex(#[from] regex::Error),
9
10    /// Hook callback returned an error.
11    #[error("Hook callback failed: {0}")]
12    CallbackFailed(String),
13
14    /// Hook execution timed out.
15    #[error("Hook timed out after {0}s")]
16    Timeout(u64),
17
18    /// Serialization/deserialization error.
19    #[error("Serialization error: {0}")]
20    Serialization(#[from] serde_json::Error),
21
22    /// Circuit breaker is open — hook is temporarily disabled.
23    #[error("Circuit breaker open for hook '{0}'")]
24    CircuitBreakerOpen(String),
25
26    /// Hook eligibility check failed.
27    #[error("Eligibility check failed: {0}")]
28    Eligibility(String),
29
30    /// Hook discovery error.
31    #[error("Hook discovery error: {0}")]
32    Discovery(String),
33
34    /// Failed to parse a hook manifest file.
35    #[error("Failed to parse hook manifest at {path}: {reason}")]
36    ManifestParse { path: String, reason: String },
37
38    /// Hook command execution failed.
39    #[error("Hook command '{hook_name}' failed: {reason}")]
40    CommandExecution { hook_name: String, reason: String },
41
42    /// IO error.
43    #[error("IO error: {0}")]
44    Io(#[from] std::io::Error),
45}
46
47pub type Result<T> = std::result::Result<T, HookError>;
48
49#[cfg(test)]
50#[allow(clippy::invalid_regex)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn display_invalid_regex() {
56        let err = HookError::InvalidRegex(regex::Regex::new(r"[invalid").unwrap_err());
57        let msg = err.to_string();
58        assert!(msg.contains("Invalid hook matcher regex"), "got: {}", msg);
59    }
60
61    #[test]
62    fn display_callback_failed() {
63        let err = HookError::CallbackFailed("connection reset".into());
64        assert_eq!(err.to_string(), "Hook callback failed: connection reset");
65    }
66
67    #[test]
68    fn display_timeout() {
69        let err = HookError::Timeout(30);
70        assert_eq!(err.to_string(), "Hook timed out after 30s");
71    }
72
73    #[test]
74    fn from_regex_error() {
75        let regex_err = regex::Regex::new(r"[bad").unwrap_err();
76        let hook_err: HookError = regex_err.into();
77        assert!(matches!(hook_err, HookError::InvalidRegex(_)));
78    }
79
80    #[test]
81    fn from_serde_error() {
82        let serde_err = serde_json::from_str::<String>("not json").unwrap_err();
83        let hook_err: HookError = serde_err.into();
84        assert!(matches!(hook_err, HookError::Serialization(_)));
85    }
86
87    #[test]
88    fn display_circuit_breaker_open() {
89        let err = HookError::CircuitBreakerOpen("my-hook".into());
90        assert_eq!(err.to_string(), "Circuit breaker open for hook 'my-hook'");
91    }
92
93    #[test]
94    fn display_eligibility() {
95        let err = HookError::Eligibility("missing binary: eslint".into());
96        assert_eq!(
97            err.to_string(),
98            "Eligibility check failed: missing binary: eslint"
99        );
100    }
101
102    #[test]
103    fn display_discovery() {
104        let err = HookError::Discovery("bad glob".into());
105        assert_eq!(err.to_string(), "Hook discovery error: bad glob");
106    }
107
108    #[test]
109    fn display_manifest_parse() {
110        let err = HookError::ManifestParse {
111            path: "/hooks/bad/HOOK.md".into(),
112            reason: "invalid toml".into(),
113        };
114        assert_eq!(
115            err.to_string(),
116            "Failed to parse hook manifest at /hooks/bad/HOOK.md: invalid toml"
117        );
118    }
119
120    #[test]
121    fn display_command_execution() {
122        let err = HookError::CommandExecution {
123            hook_name: "lint".into(),
124            reason: "exit code 1".into(),
125        };
126        assert_eq!(err.to_string(), "Hook command 'lint' failed: exit code 1");
127    }
128
129    #[test]
130    fn from_io_error() {
131        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
132        let hook_err: HookError = io_err.into();
133        assert!(matches!(hook_err, HookError::Io(_)));
134        assert!(hook_err.to_string().contains("not found"));
135    }
136}