use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileChangedPayload {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(default)]
pub prompt_id: Option<String>,
pub hook_event_name: String,
pub file_path: String,
pub event: String,
}
pub fn parse_file_changed(input: &serde_json::Value) -> Option<FileChangedPayload> {
let payload: FileChangedPayload = serde_json::from_value(input.clone()).ok()?;
(payload.hook_event_name == "FileChanged").then_some(payload)
}
impl FileChangedPayload {
pub fn drives_reparse(&self) -> bool {
matches!(self.event.as_str(), "add" | "change")
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn payload() -> serde_json::Value {
json!({
"session_id": "session-123",
"transcript_path": "/tmp/transcript.jsonl",
"cwd": "/private/tmp/repo",
"prompt_id": "prompt-456",
"hook_event_name": "FileChanged",
"file_path": "/tmp/repo/src/main.rs",
"event": "change"
})
}
#[test]
fn parses_captured_platform_shape() {
let parsed = parse_file_changed(&payload()).expect("payload should parse");
assert_eq!(parsed.file_path, "/tmp/repo/src/main.rs");
assert_eq!(parsed.event, "change");
assert_eq!(parsed.prompt_id.as_deref(), Some("prompt-456"));
}
#[test]
fn parses_without_prompt_id() {
let mut value = payload();
value.as_object_mut().unwrap().remove("prompt_id");
assert!(parse_file_changed(&value).is_some());
}
#[test]
fn rejects_other_hook_events() {
let mut value = payload();
value["hook_event_name"] = json!("SessionStart");
assert!(parse_file_changed(&value).is_none());
}
#[test]
fn rejects_missing_or_wrongly_typed_fields() {
let mut missing = payload();
missing.as_object_mut().unwrap().remove("file_path");
assert!(parse_file_changed(&missing).is_none());
let mut wrong_type = payload();
wrong_type["event"] = json!(3);
assert!(parse_file_changed(&wrong_type).is_none());
}
#[test]
fn accepts_unknown_fields() {
let mut value = payload();
value["future_platform_field"] = json!(true);
assert!(parse_file_changed(&value).is_some());
}
#[test]
fn only_add_and_change_drive_a_reparse() {
for (event, expected) in [
("change", true),
("add", true),
("unlink", false),
("addDir", false),
] {
let mut value = payload();
value["event"] = json!(event);
let parsed = parse_file_changed(&value).expect("payload should parse");
assert_eq!(parsed.drives_reparse(), expected, "event {event}");
}
}
}