use sidekick::handler::{classify_event, extract_mutations};
#[test]
fn claude_pre_tool_use_edit_extracts_file_path() {
let json = r#"{
"session_id": "s",
"transcript_path": "/tmp/t",
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "Edit",
"tool_input": { "file_path": "/p/src/lib.rs", "old_string": "a", "new_string": "b" }
}"#;
assert_eq!(classify_event(json).unwrap(), "PreToolUse");
assert_eq!(
extract_mutations(json).unwrap(),
vec![("edit".to_string(), "/p/src/lib.rs".to_string())]
);
}
#[test]
fn claude_pre_tool_use_write_extracts_file_path() {
let json = r#"{
"session_id": "s",
"transcript_path": "/tmp/t",
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": { "file_path": "/p/src/new.rs", "content": "hello" }
}"#;
assert_eq!(classify_event(json).unwrap(), "PreToolUse");
assert_eq!(
extract_mutations(json).unwrap(),
vec![("write".to_string(), "/p/src/new.rs".to_string())]
);
}
#[test]
fn claude_pre_tool_use_multiedit_extracts_file_path() {
let json = r#"{
"session_id": "s",
"transcript_path": "/tmp/t",
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "MultiEdit",
"tool_input": { "file_path": "/p/src/lib.rs" }
}"#;
assert_eq!(classify_event(json).unwrap(), "PreToolUse");
assert_eq!(
extract_mutations(json).unwrap(),
vec![("multiedit".to_string(), "/p/src/lib.rs".to_string())]
);
}
#[test]
fn claude_pre_tool_use_bash_does_not_engage() {
let json = r#"{
"session_id": "s",
"transcript_path": "/tmp/t",
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "ls -la", "description": "list" }
}"#;
assert_eq!(classify_event(json).unwrap(), "PreToolUse");
assert!(extract_mutations(json).unwrap().is_empty());
}
#[test]
fn claude_pre_tool_use_read_does_not_engage() {
let json = r#"{
"session_id": "s",
"transcript_path": "/tmp/t",
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "Read",
"tool_input": { "file_path": "/p/src/lib.rs" }
}"#;
assert_eq!(classify_event(json).unwrap(), "PreToolUse");
assert!(extract_mutations(json).unwrap().is_empty());
}
#[test]
fn claude_post_tool_use_routes_as_post_event() {
let json = r#"{
"session_id": "s",
"transcript_path": "/tmp/t",
"cwd": "/p",
"hook_event_name": "PostToolUse",
"tool_name": "Edit",
"tool_input": { "file_path": "/p/src/lib.rs" }
}"#;
assert_eq!(classify_event(json).unwrap(), "PostToolUse");
assert_eq!(
extract_mutations(json).unwrap(),
vec![("edit".to_string(), "/p/src/lib.rs".to_string())]
);
}
#[test]
fn claude_user_prompt_submit_routes_to_prompt_path() {
let json = r#"{
"session_id": "s",
"transcript_path": "/tmp/t",
"cwd": "/p",
"hook_event_name": "UserPromptSubmit",
"prompt": "do the thing"
}"#;
assert_eq!(classify_event(json).unwrap(), "UserPromptSubmit");
assert!(extract_mutations(json).unwrap().is_empty());
}
#[test]
fn claude_unknown_pascalcase_tool_does_not_engage() {
let json = r#"{
"session_id": "s",
"transcript_path": "/tmp/t",
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "WebFetch",
"tool_input": { "url": "https://example.com" }
}"#;
assert!(extract_mutations(json).unwrap().is_empty());
}
#[test]
fn codex_apply_patch_extracts_every_marker_kind() {
let json = r#"{
"session_id": "s",
"transcript_path": null,
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "apply_patch",
"tool_input": {
"patch": "*** Begin Patch\n*** Update File: src/lib.rs\n*** Add File: src/new.rs\n*** Delete File: src/old.rs\n*** Move to: src/moved.rs\n*** End Patch\n"
}
}"#;
assert_eq!(classify_event(json).unwrap(), "PreToolUse");
assert_eq!(
extract_mutations(json).unwrap(),
vec![
("edit".to_string(), "src/lib.rs".to_string()),
("edit".to_string(), "src/new.rs".to_string()),
("edit".to_string(), "src/old.rs".to_string()),
("edit".to_string(), "src/moved.rs".to_string()),
]
);
}
#[test]
fn codex_apply_patch_with_null_transcript_path_parses() {
let json = r#"{
"session_id": "s",
"turn_id": "t",
"transcript_path": null,
"cwd": "/p",
"model": "gpt-5.5",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "apply_patch",
"tool_use_id": "u",
"tool_input": { "patch": "*** Begin Patch\n*** Update File: a.rs\n*** End Patch\n" }
}"#;
assert_eq!(classify_event(json).unwrap(), "PreToolUse");
assert_eq!(
extract_mutations(json).unwrap(),
vec![("edit".to_string(), "a.rs".to_string())]
);
}
#[test]
fn codex_apply_patch_without_file_markers_does_not_engage() {
let json = r#"{
"session_id": "s",
"transcript_path": null,
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "apply_patch",
"tool_input": {
"patch": "*** Begin Patch\n@@ no header\n+content\n*** End Patch\n"
}
}"#;
assert!(extract_mutations(json).unwrap().is_empty());
}
#[test]
fn crush_pre_tool_use_edit_extracts_file_path() {
let json = r#"{
"event": "PreToolUse",
"session_id": "s",
"cwd": "/p",
"tool_name": "edit",
"tool_input": { "file_path": "/p/src/lib.rs", "old_string": "a", "new_string": "b" }
}"#;
assert_eq!(classify_event(json).unwrap(), "PreToolUse");
assert_eq!(
extract_mutations(json).unwrap(),
vec![("edit".to_string(), "/p/src/lib.rs".to_string())]
);
}
#[test]
fn crush_pre_tool_use_write_extracts_file_path() {
let json = r#"{
"event": "PreToolUse",
"session_id": "s",
"cwd": "/p",
"tool_name": "write",
"tool_input": { "file_path": "/p/src/new.rs", "content": "hello" }
}"#;
assert_eq!(
extract_mutations(json).unwrap(),
vec![("write".to_string(), "/p/src/new.rs".to_string())]
);
}
#[test]
fn crush_pre_tool_use_multiedit_extracts_file_path() {
let json = r#"{
"event": "PreToolUse",
"session_id": "s",
"cwd": "/p",
"tool_name": "multiedit",
"tool_input": {
"file_path": "/p/src/lib.rs",
"edits": [{ "old_string": "a", "new_string": "b" }]
}
}"#;
assert_eq!(
extract_mutations(json).unwrap(),
vec![("multiedit".to_string(), "/p/src/lib.rs".to_string())]
);
}
#[test]
fn crush_pre_tool_use_bash_does_not_engage() {
let json = r#"{
"event": "PreToolUse",
"session_id": "s",
"cwd": "/p",
"tool_name": "bash",
"tool_input": { "command": "ls -la" }
}"#;
assert!(extract_mutations(json).unwrap().is_empty());
}
#[test]
fn crush_unknown_lowercase_tool_does_not_engage() {
let json = r#"{
"event": "PreToolUse",
"session_id": "s",
"cwd": "/p",
"tool_name": "fetch_url",
"tool_input": { "url": "https://example.com" }
}"#;
assert!(extract_mutations(json).unwrap().is_empty());
}
#[test]
fn lowercase_edit_under_claude_shape_engages() {
let json = r#"{
"session_id": "s",
"transcript_path": null,
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "edit",
"tool_input": { "file_path": "/p/a.rs" }
}"#;
assert_eq!(
extract_mutations(json).unwrap(),
vec![("edit".to_string(), "/p/a.rs".to_string())]
);
}
#[test]
fn camelcase_file_path_field_is_accepted() {
let json = r#"{
"session_id": "s",
"transcript_path": null,
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "edit",
"tool_input": { "filePath": "/p/camel.rs" }
}"#;
assert_eq!(
extract_mutations(json).unwrap(),
vec![("edit".to_string(), "/p/camel.rs".to_string())]
);
}
#[test]
fn bare_path_field_is_accepted() {
let json = r#"{
"session_id": "s",
"transcript_path": null,
"cwd": "/p",
"hook_event_name": "PreToolUse",
"tool_name": "write",
"tool_input": { "path": "/p/bare.rs" }
}"#;
assert_eq!(
extract_mutations(json).unwrap(),
vec![("write".to_string(), "/p/bare.rs".to_string())]
);
}