atomcode_core/hook/
config.rs1use super::{HookConfig, HookEvent};
2
3pub fn matches_tool(matcher: &Option<String>, tool_name: &str) -> bool {
11 match matcher {
12 None => true,
13 Some(pattern) => {
14 if pattern == "*" {
15 return true;
16 }
17 if let Some(prefix) = pattern.strip_suffix('*') {
18 tool_name.starts_with(prefix)
19 } else {
20 pattern == tool_name
21 }
22 }
23 }
24}
25
26pub fn matching_hooks<'a>(
32 hooks: &'a [HookConfig],
33 event: HookEvent,
34 tool_name: Option<&str>,
35) -> Vec<&'a HookConfig> {
36 hooks
37 .iter()
38 .filter(|h| h.event == event)
39 .filter(|h| match tool_name {
40 Some(name) => matches_tool(&h.matcher, name),
41 None => true,
43 })
44 .collect()
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use crate::hook::{HookConfig, HookEvent};
51
52 #[test]
55 fn none_matcher_matches_all() {
56 assert!(matches_tool(&None, "bash"));
57 assert!(matches_tool(&None, "edit_file"));
58 assert!(matches_tool(&None, "anything"));
59 }
60
61 #[test]
62 fn star_matcher_matches_all() {
63 let m = Some("*".to_string());
64 assert!(matches_tool(&m, "bash"));
65 assert!(matches_tool(&m, "edit_file"));
66 assert!(matches_tool(&m, "write_file"));
67 }
68
69 #[test]
70 fn exact_match_works() {
71 let m = Some("bash".to_string());
72 assert!(matches_tool(&m, "bash"));
73 assert!(!matches_tool(&m, "grep"));
74 assert!(!matches_tool(&m, "bash_extra"));
75 }
76
77 #[test]
78 fn prefix_wildcard_works() {
79 let m = Some("edit_*".to_string());
80 assert!(matches_tool(&m, "edit_file"));
81 assert!(matches_tool(&m, "edit_config"));
82 assert!(!matches_tool(&m, "write_file"));
83 assert!(!matches_tool(&m, "edit")); }
85
86 fn make_hook(event: HookEvent, matcher: Option<&str>, cmd: &str) -> HookConfig {
89 HookConfig {
90 event,
91 matcher: matcher.map(String::from),
92 command: cmd.to_string(),
93 timeout_ms: 10_000,
94 plugin_root: None,
95 }
96 }
97
98 #[test]
99 fn matching_hooks_filters_by_event() {
100 let hooks = vec![
101 make_hook(HookEvent::PreToolUse, None, "pre.sh"),
102 make_hook(HookEvent::PostToolUse, None, "post.sh"),
103 make_hook(HookEvent::SessionStart, None, "start.sh"),
104 ];
105
106 let matched = matching_hooks(&hooks, HookEvent::PreToolUse, Some("bash"));
107 assert_eq!(matched.len(), 1);
108 assert_eq!(matched[0].command, "pre.sh");
109 }
110
111 #[test]
112 fn matching_hooks_filters_by_tool_name() {
113 let hooks = vec![
114 make_hook(HookEvent::PreToolUse, Some("bash"), "bash-hook.sh"),
115 make_hook(HookEvent::PreToolUse, Some("edit_*"), "edit-hook.sh"),
116 make_hook(HookEvent::PreToolUse, None, "catch-all.sh"),
117 ];
118
119 let matched = matching_hooks(&hooks, HookEvent::PreToolUse, Some("bash"));
121 assert_eq!(matched.len(), 2);
122 assert_eq!(matched[0].command, "bash-hook.sh");
123 assert_eq!(matched[1].command, "catch-all.sh");
124
125 let matched = matching_hooks(&hooks, HookEvent::PreToolUse, Some("edit_file"));
127 assert_eq!(matched.len(), 2);
128 assert_eq!(matched[0].command, "edit-hook.sh");
129 assert_eq!(matched[1].command, "catch-all.sh");
130
131 let matched = matching_hooks(&hooks, HookEvent::PreToolUse, Some("grep"));
133 assert_eq!(matched.len(), 1);
134 assert_eq!(matched[0].command, "catch-all.sh");
135 }
136
137 #[test]
138 fn session_events_with_no_tool_name() {
139 let hooks = vec![
140 make_hook(HookEvent::SessionStart, Some("bash"), "should-match.sh"),
141 make_hook(HookEvent::SessionStart, None, "also-match.sh"),
142 make_hook(HookEvent::PreToolUse, None, "wrong-event.sh"),
143 ];
144
145 let matched = matching_hooks(&hooks, HookEvent::SessionStart, None);
147 assert_eq!(matched.len(), 2);
148 assert_eq!(matched[0].command, "should-match.sh");
149 assert_eq!(matched[1].command, "also-match.sh");
150 }
151}