#![cfg(test)]
use crate::commands::hook_install::{
InstallState, handle_hook_install_at, handle_hook_uninstall_at,
};
use serde_json::Value;
use std::path::Path;
const STATE_REL: &str = ".vipune/hook-install/state.json";
fn temp_home() -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::TempDir::new().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(&home).unwrap();
(dir, home)
}
fn write_settings(home: &Path, raw: &str) {
let claude_dir = home.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(claude_dir.join("settings.json"), raw).unwrap();
}
fn read_settings_value(home: &Path) -> Value {
let raw = std::fs::read_to_string(home.join(".claude").join("settings.json")).unwrap();
serde_json::from_str(&raw).unwrap()
}
fn assert_vipune_groups_present(value: &Value, expected_pre_tool_groups: usize) {
let hooks = value
.get("hooks")
.expect("hooks key must exist")
.as_object()
.unwrap();
for event in [
"SessionStart",
"UserPromptSubmit",
"PreToolUse",
"PostToolUse",
"PreCompact",
] {
let arr = hooks
.get(event)
.unwrap_or_else(|| panic!("event {event} missing from hooks map"))
.as_array()
.unwrap_or_else(|| panic!("event {event} is not an array"));
assert!(!arr.is_empty(), "event {event} array must not be empty");
}
let pre_tool = hooks.get("PreToolUse").unwrap().as_array().unwrap();
assert_eq!(
pre_tool.len(),
expected_pre_tool_groups,
"PreToolUse group count mismatch"
);
}
#[test]
fn test_fresh_install_creates_settings_and_all_events() {
let (_dir, home) = temp_home();
handle_hook_install_at(false, Some(&home)).unwrap();
let value = read_settings_value(&home);
assert_vipune_groups_present(&value, 1); }
#[test]
fn test_install_preserves_foreign_top_level_keys() {
let (_dir, home) = temp_home();
let foreign = serde_json::json!({
"model": "test-model",
"permissions": { "defaultMode": "auto" },
"hooks": {
"PreToolUse": [
{
"matcher": "Grep|Glob",
"hooks": [
{
"type": "command",
"command": "/opt/foreign-tool --check",
"timeout": 5
}
]
}
]
}
});
write_settings(&home, &foreign.to_string());
handle_hook_install_at(false, Some(&home)).unwrap();
let value = read_settings_value(&home);
assert_eq!(value.get("model").unwrap().as_str().unwrap(), "test-model");
assert!(value.get("permissions").is_some());
assert_vipune_groups_present(&value, 2);
let pre_tool = value
.get("hooks")
.unwrap()
.as_object()
.unwrap()
.get("PreToolUse")
.unwrap()
.as_array()
.unwrap();
let foreign_cmd = pre_tool[0].get("hooks").unwrap().as_array().unwrap()[0]
.get("command")
.unwrap()
.as_str()
.unwrap();
assert_eq!(foreign_cmd, "/opt/foreign-tool --check");
}
#[test]
fn test_install_is_idempotent_byte_stable() {
let (_dir, home) = temp_home();
let foreign = serde_json::json!({
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "/opt/other-tool" }
]
}
]
}
});
write_settings(&home, &foreign.to_string());
handle_hook_install_at(false, Some(&home)).unwrap();
let first = std::fs::read_to_string(home.join(".claude").join("settings.json")).unwrap();
handle_hook_install_at(false, Some(&home)).unwrap();
let second = std::fs::read_to_string(home.join(".claude").join("settings.json")).unwrap();
assert_eq!(first, second, "re-run must be byte-stable");
let value: Value = serde_json::from_str(&first).unwrap();
let arr = value
.get("hooks")
.unwrap()
.as_object()
.unwrap()
.get("UserPromptSubmit")
.unwrap()
.as_array()
.unwrap();
assert_eq!(arr.len(), 2);
}
#[test]
fn test_uninstall_removes_only_vipune_entries() {
let (_dir, home) = temp_home();
let foreign = serde_json::json!({
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "/opt/foreign-tool" }
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "/opt/other-tool" }
]
}
]
}
});
write_settings(&home, &foreign.to_string());
handle_hook_install_at(false, Some(&home)).unwrap();
handle_hook_uninstall_at(false, Some(&home)).unwrap();
let value = read_settings_value(&home);
let hooks = value
.get("hooks")
.expect("hooks key must still exist (foreign entries remain)")
.as_object()
.unwrap();
let pre_tool = hooks.get("PreToolUse").unwrap().as_array().unwrap();
assert_eq!(pre_tool.len(), 1);
let cmd = pre_tool[0].get("hooks").unwrap().as_array().unwrap()[0]
.get("command")
.unwrap()
.as_str()
.unwrap();
assert_eq!(cmd, "/opt/foreign-tool");
assert!(
hooks.contains_key("UserPromptSubmit"),
"foreign UserPromptSubmit entry must be preserved"
);
let ups = hooks.get("UserPromptSubmit").unwrap().as_array().unwrap();
assert_eq!(ups.len(), 1);
let ups_cmd = ups[0].get("hooks").unwrap().as_array().unwrap()[0]
.get("command")
.unwrap()
.as_str()
.unwrap();
assert_eq!(ups_cmd, "/opt/other-tool");
}
#[test]
fn test_uninstall_with_no_settings_file() {
let (_dir, home) = temp_home();
handle_hook_uninstall_at(false, Some(&home)).unwrap();
assert!(!home.join(".claude").join("settings.json").exists());
}
#[test]
fn test_state_sidecar_written_on_install() {
let (_dir, home) = temp_home();
let original = serde_json::json!({ "model": "x" });
write_settings(&home, &original.to_string());
handle_hook_install_at(false, Some(&home)).unwrap();
let state_path = home.join(STATE_REL);
assert!(
state_path.exists(),
"state sidecar must exist after install"
);
let state_raw = std::fs::read_to_string(&state_path).unwrap();
let state: InstallState = serde_json::from_str(&state_raw).unwrap();
let pre: Value = serde_json::from_str(state.pre_mutation.as_deref().unwrap()).unwrap();
assert_eq!(
pre, original,
"pre_mutation must equal the original settings"
);
}
#[test]
fn test_state_sidecar_removed_on_uninstall() {
let (_dir, home) = temp_home();
write_settings(&home, &serde_json::json!({}).to_string());
handle_hook_install_at(false, Some(&home)).unwrap();
handle_hook_uninstall_at(false, Some(&home)).unwrap();
assert!(!home.join(STATE_REL).exists());
}
#[test]
fn test_install_creates_missing_claude_dir() {
let (_dir, home) = temp_home();
assert!(!home.join(".claude").exists());
handle_hook_install_at(false, Some(&home)).unwrap();
assert!(home.join(".claude").join("settings.json").exists());
}