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 {
37        path: String,
38        reason: String,
39    },
40
41    /// Hook command execution failed.
42    #[error("Hook command '{hook_name}' failed: {reason}")]
43    CommandExecution {
44        hook_name: String,
45        reason: String,
46    },
47
48    /// IO error.
49    #[error("IO error: {0}")]
50    Io(#[from] std::io::Error),
51}
52
53pub type Result<T> = std::result::Result<T, HookError>;
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn display_invalid_regex() {
61        let err = HookError::InvalidRegex(regex::Regex::new("[invalid").unwrap_err());
62        let msg = err.to_string();
63        assert!(msg.contains("Invalid hook matcher regex"), "got: {}", msg);
64    }
65
66    #[test]
67    fn display_callback_failed() {
68        let err = HookError::CallbackFailed("connection reset".into());
69        assert_eq!(err.to_string(), "Hook callback failed: connection reset");
70    }
71
72    #[test]
73    fn display_timeout() {
74        let err = HookError::Timeout(30);
75        assert_eq!(err.to_string(), "Hook timed out after 30s");
76    }
77
78    #[test]
79    fn from_regex_error() {
80        let regex_err = regex::Regex::new("[bad").unwrap_err();
81        let hook_err: HookError = regex_err.into();
82        assert!(matches!(hook_err, HookError::InvalidRegex(_)));
83    }
84
85    #[test]
86    fn from_serde_error() {
87        let serde_err = serde_json::from_str::<String>("not json").unwrap_err();
88        let hook_err: HookError = serde_err.into();
89        assert!(matches!(hook_err, HookError::Serialization(_)));
90    }
91
92    #[test]
93    fn display_circuit_breaker_open() {
94        let err = HookError::CircuitBreakerOpen("my-hook".into());
95        assert_eq!(err.to_string(), "Circuit breaker open for hook 'my-hook'");
96    }
97
98    #[test]
99    fn display_eligibility() {
100        let err = HookError::Eligibility("missing binary: eslint".into());
101        assert_eq!(err.to_string(), "Eligibility check failed: missing binary: eslint");
102    }
103
104    #[test]
105    fn display_discovery() {
106        let err = HookError::Discovery("bad glob".into());
107        assert_eq!(err.to_string(), "Hook discovery error: bad glob");
108    }
109
110    #[test]
111    fn display_manifest_parse() {
112        let err = HookError::ManifestParse {
113            path: "/hooks/bad/HOOK.md".into(),
114            reason: "invalid toml".into(),
115        };
116        assert_eq!(
117            err.to_string(),
118            "Failed to parse hook manifest at /hooks/bad/HOOK.md: invalid toml"
119        );
120    }
121
122    #[test]
123    fn display_command_execution() {
124        let err = HookError::CommandExecution {
125            hook_name: "lint".into(),
126            reason: "exit code 1".into(),
127        };
128        assert_eq!(err.to_string(), "Hook command 'lint' failed: exit code 1");
129    }
130
131    #[test]
132    fn from_io_error() {
133        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
134        let hook_err: HookError = io_err.into();
135        assert!(matches!(hook_err, HookError::Io(_)));
136        assert!(hook_err.to_string().contains("not found"));
137    }
138}