use anyhow::Result;
use serde_json::Value;
use tokio::fs;
use crate::common::{ManifestBuilder, TestProject};
#[tokio::test]
async fn test_hooks_install_and_format() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("hooks").await?;
let hooks_dir = source_repo.path.join("hooks");
fs::create_dir_all(&hooks_dir).await?;
let session_hook = serde_json::json!({
"events": ["SessionStart"],
"type": "command",
"command": "echo 'Session started'",
"description": "Session start hook"
});
fs::write(hooks_dir.join("session-start.json"), serde_json::to_string_pretty(&session_hook)?)
.await?;
let pre_tool_hook = serde_json::json!({
"events": ["PreToolUse"],
"matcher": "Bash|Write",
"type": "command",
"command": "echo 'Before tool use'",
"timeout": 5000,
"description": "Pre-tool use hook"
});
fs::write(hooks_dir.join("pre-tool-use.json"), serde_json::to_string_pretty(&pre_tool_hook)?)
.await?;
source_repo.commit_all("Add test hooks")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest_content = ManifestBuilder::new()
.add_source("hooks", &source_url)
.add_hook("session-hook", |d| d.source("hooks").path("hooks/session-start.json"))
.add_hook("tool-hook", |d| d.source("hooks").path("hooks/pre-tool-use.json"))
.build();
project.write_manifest(&manifest_content).await?;
let output = project.run_agpm(&["install"])?;
output.assert_success();
output.assert_stdout_contains("✓ Configured 2 hooks");
let settings_path = project.project_path().join(".claude/settings.local.json");
let settings_content = fs::read_to_string(&settings_path).await?;
let settings: Value = serde_json::from_str(&settings_content)?;
let hooks = settings.get("hooks").expect("Should have hooks section");
let session_start =
hooks.get("SessionStart").expect("Should have SessionStart").as_array().unwrap();
assert_eq!(session_start.len(), 1);
assert!(session_start[0].get("matcher").is_none(), "SessionStart should not have matcher");
let session_commands = session_start[0].get("hooks").unwrap().as_array().unwrap();
assert_eq!(session_commands.len(), 1);
assert_eq!(
session_commands[0].get("command").unwrap().as_str().unwrap(),
"echo 'Session started'"
);
let pre_tool_use = hooks.get("PreToolUse").expect("Should have PreToolUse").as_array().unwrap();
assert_eq!(pre_tool_use.len(), 1);
assert_eq!(pre_tool_use[0].get("matcher").unwrap().as_str().unwrap(), "Bash|Write");
let pre_tool_commands = pre_tool_use[0].get("hooks").unwrap().as_array().unwrap();
assert_eq!(pre_tool_commands.len(), 1);
assert_eq!(
pre_tool_commands[0].get("command").unwrap().as_str().unwrap(),
"echo 'Before tool use'"
);
assert_eq!(pre_tool_commands[0].get("timeout").unwrap().as_u64().unwrap(), 5000);
Ok(())
}
#[tokio::test]
async fn test_hooks_deduplication() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("hooks").await?;
let hooks_dir = source_repo.path.join("hooks");
fs::create_dir_all(&hooks_dir).await?;
let session_hook = serde_json::json!({
"events": ["SessionStart"],
"type": "command",
"command": "agpm update",
"description": "Update AGPM"
});
fs::write(hooks_dir.join("hook1.json"), serde_json::to_string_pretty(&session_hook)?).await?;
fs::write(hooks_dir.join("hook2.json"), serde_json::to_string_pretty(&session_hook)?).await?;
source_repo.commit_all("Add duplicate hooks")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest_content = ManifestBuilder::new()
.add_source("hooks", &source_url)
.add_hook("first-hook", |d| d.source("hooks").path("hooks/hook1.json"))
.add_hook("second-hook", |d| d.source("hooks").path("hooks/hook2.json"))
.build();
project.write_manifest(&manifest_content).await?;
let output = project.run_agpm(&["install"])?;
output.assert_success();
output.assert_stdout_contains("✓ Configured 2 hooks (1 changed)");
let settings_path = project.project_path().join(".claude/settings.local.json");
let settings_content = fs::read_to_string(&settings_path).await?;
let settings: Value = serde_json::from_str(&settings_content)?;
let hooks = settings.get("hooks").unwrap();
let session_start = hooks.get("SessionStart").unwrap().as_array().unwrap();
assert_eq!(session_start.len(), 1);
let hook_commands = session_start[0].get("hooks").unwrap().as_array().unwrap();
assert_eq!(hook_commands.len(), 1, "Identical hooks should be deduplicated");
assert_eq!(hook_commands[0].get("command").unwrap().as_str().unwrap(), "agpm update");
Ok(())
}
#[tokio::test]
async fn test_hooks_unknown_event_type() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("hooks").await?;
let hooks_dir = source_repo.path.join("hooks");
fs::create_dir_all(&hooks_dir).await?;
let future_hook = serde_json::json!({
"events": ["FutureEvent"],
"type": "command",
"command": "echo 'future event'",
"description": "Testing future event type"
});
fs::write(hooks_dir.join("future-hook.json"), serde_json::to_string_pretty(&future_hook)?)
.await?;
source_repo.commit_all("Add future hook")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest_content = ManifestBuilder::new()
.add_source("hooks", &source_url)
.add_hook("future-hook", |d| d.source("hooks").path("hooks/future-hook.json"))
.build();
project.write_manifest(&manifest_content).await?;
let output = project.run_agpm(&["install"])?;
output.assert_success();
output.assert_stdout_contains("✓ Configured 1 hook");
let settings_path = project.project_path().join(".claude/settings.local.json");
let settings_content = fs::read_to_string(&settings_path).await?;
let settings: Value = serde_json::from_str(&settings_content)?;
let hooks = settings.get("hooks").expect("Should have hooks section");
let future_event =
hooks.get("FutureEvent").expect("Should have FutureEvent").as_array().unwrap();
assert_eq!(future_event.len(), 1);
assert!(future_event[0].get("matcher").is_none(), "FutureEvent should not have matcher");
let commands = future_event[0].get("hooks").unwrap().as_array().unwrap();
assert_eq!(commands.len(), 1);
assert_eq!(commands[0].get("command").unwrap().as_str().unwrap(), "echo 'future event'");
Ok(())
}
#[tokio::test]
async fn test_hooks_empty_no_message() -> Result<()> {
let project = TestProject::new().await?;
let manifest_content = r#"
[sources]
# No sources
[hooks]
# No hooks
"#;
project.write_manifest(manifest_content).await?;
let output = project.run_agpm(&["install"])?;
output.assert_success();
assert!(!output.stdout.contains("Configured"));
assert!(!output.stdout.contains("hook"));
let settings_path = project.project_path().join(".claude/settings.local.json");
if settings_path.exists() {
let settings_content = fs::read_to_string(&settings_path).await?;
let settings: Value = serde_json::from_str(&settings_content)?;
if let Some(hooks) = settings.get("hooks") {
let hooks_obj = hooks.as_object().unwrap();
assert!(hooks_obj.is_empty(), "Hooks section should be empty");
}
}
Ok(())
}
#[tokio::test]
async fn test_hooks_no_change_no_message() -> Result<()> {
let project = TestProject::new().await?;
let source_repo = project.create_source_repo("hooks").await?;
let hooks_dir = source_repo.path.join("hooks");
fs::create_dir_all(&hooks_dir).await?;
let session_hook = serde_json::json!({
"events": ["SessionStart"],
"type": "command",
"command": "echo 'test hook'",
"description": "Test hook"
});
fs::write(hooks_dir.join("session-start.json"), serde_json::to_string_pretty(&session_hook)?)
.await?;
source_repo.commit_all("Add test hook")?;
let source_url = source_repo.bare_file_url(project.sources_path()).await?;
let manifest_content = ManifestBuilder::new()
.add_source("hooks", &source_url)
.add_hook("session-hook", |d| d.source("hooks").path("hooks/session-start.json"))
.build();
project.write_manifest(&manifest_content).await?;
let output1 = project.run_agpm(&["install"])?;
output1.assert_success();
output1.assert_stdout_contains("✓ Configured 1 hook (1 changed)");
let output2 = project.run_agpm(&["install"])?;
output2.assert_success();
output2.assert_stdout_contains("✓ Configured 1 hook (0 changed)");
Ok(())
}