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 {
37 path: String,
38 reason: String,
39 },
40
41 #[error("Hook command '{hook_name}' failed: {reason}")]
43 CommandExecution {
44 hook_name: String,
45 reason: String,
46 },
47
48 #[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}