1use thiserror::Error;
2
3#[derive(Error, Debug)]
5pub enum HookError {
6 #[error("Invalid hook matcher regex: {0}")]
8 InvalidRegex(#[from] regex::Error),
9
10 #[error("Hook callback failed: {0}")]
12 CallbackFailed(String),
13
14 #[error("Hook timed out after {0}s")]
16 Timeout(u64),
17
18 #[error("Serialization error: {0}")]
20 Serialization(#[from] serde_json::Error),
21
22 #[error("Circuit breaker open for hook '{0}'")]
24 CircuitBreakerOpen(String),
25
26 #[error("Eligibility check failed: {0}")]
28 Eligibility(String),
29
30 #[error("Hook discovery error: {0}")]
32 Discovery(String),
33
34 #[error("Failed to parse hook manifest at {path}: {reason}")]
36 ManifestParse { path: String, reason: String },
37
38 #[error("Hook command '{hook_name}' failed: {reason}")]
40 CommandExecution { hook_name: String, reason: String },
41
42 #[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}