mati_core/hooks/decide/
file_changed.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct FileChangedPayload {
13 pub session_id: String,
14 pub transcript_path: String,
15 pub cwd: String,
16 #[serde(default)]
18 pub prompt_id: Option<String>,
19 pub hook_event_name: String,
20 pub file_path: String,
24 pub event: String,
26}
27
28pub fn parse_file_changed(input: &serde_json::Value) -> Option<FileChangedPayload> {
33 let payload: FileChangedPayload = serde_json::from_value(input.clone()).ok()?;
34 (payload.hook_event_name == "FileChanged").then_some(payload)
35}
36
37impl FileChangedPayload {
38 pub fn drives_reparse(&self) -> bool {
48 matches!(self.event.as_str(), "add" | "change")
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use serde_json::json;
56
57 fn payload() -> serde_json::Value {
58 json!({
59 "session_id": "session-123",
60 "transcript_path": "/tmp/transcript.jsonl",
61 "cwd": "/private/tmp/repo",
62 "prompt_id": "prompt-456",
63 "hook_event_name": "FileChanged",
64 "file_path": "/tmp/repo/src/main.rs",
65 "event": "change"
66 })
67 }
68
69 #[test]
70 fn parses_captured_platform_shape() {
71 let parsed = parse_file_changed(&payload()).expect("payload should parse");
72 assert_eq!(parsed.file_path, "/tmp/repo/src/main.rs");
73 assert_eq!(parsed.event, "change");
74 assert_eq!(parsed.prompt_id.as_deref(), Some("prompt-456"));
75 }
76
77 #[test]
80 fn parses_without_prompt_id() {
81 let mut value = payload();
82 value.as_object_mut().unwrap().remove("prompt_id");
83 assert!(parse_file_changed(&value).is_some());
84 }
85
86 #[test]
87 fn rejects_other_hook_events() {
88 let mut value = payload();
89 value["hook_event_name"] = json!("SessionStart");
90 assert!(parse_file_changed(&value).is_none());
91 }
92
93 #[test]
94 fn rejects_missing_or_wrongly_typed_fields() {
95 let mut missing = payload();
96 missing.as_object_mut().unwrap().remove("file_path");
97 assert!(parse_file_changed(&missing).is_none());
98
99 let mut wrong_type = payload();
100 wrong_type["event"] = json!(3);
101 assert!(parse_file_changed(&wrong_type).is_none());
102 }
103
104 #[test]
106 fn accepts_unknown_fields() {
107 let mut value = payload();
108 value["future_platform_field"] = json!(true);
109 assert!(parse_file_changed(&value).is_some());
110 }
111
112 #[test]
113 fn only_add_and_change_drive_a_reparse() {
114 for (event, expected) in [
115 ("change", true),
116 ("add", true),
117 ("unlink", false),
118 ("addDir", false),
119 ] {
120 let mut value = payload();
121 value["event"] = json!(event);
122 let parsed = parse_file_changed(&value).expect("payload should parse");
123 assert_eq!(parsed.drives_reparse(), expected, "event {event}");
124 }
125 }
126}