use super::*;
use crate::config::LintConfig;
use crate::diagnostics::DiagnosticLevel;
fn validate(content: &str) -> Vec<Diagnostic> {
let validator = HooksValidator;
validator.validate(Path::new("settings.json"), content, &LintConfig::default())
}
fn validate_with_config(content: &str, config: &LintConfig) -> Vec<Diagnostic> {
let validator = HooksValidator;
validator.validate(Path::new("settings.json"), content, config)
}
#[test]
fn test_cc_hk_006_command_hook_missing_command() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-006")
.collect();
assert_eq!(cc_hk_006.len(), 1);
assert_eq!(cc_hk_006[0].level, DiagnosticLevel::Error);
assert!(
cc_hk_006[0]
.message
.contains("missing required 'command' field")
);
}
#[test]
fn test_cc_hk_006_command_hook_with_command_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo hello" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-006")
.collect();
assert_eq!(cc_hk_006.len(), 0);
}
#[test]
fn test_cc_hk_006_multiple_command_hooks_missing_command() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command" },
{ "type": "command", "command": "valid" },
{ "type": "command" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-006")
.collect();
assert_eq!(cc_hk_006.len(), 2);
}
#[test]
fn test_cc_hk_007_prompt_hook_missing_prompt() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-007")
.collect();
assert_eq!(cc_hk_007.len(), 1);
assert_eq!(cc_hk_007[0].level, DiagnosticLevel::Error);
assert!(
cc_hk_007[0]
.message
.contains("missing required 'prompt' field")
);
}
#[test]
fn test_cc_hk_007_prompt_hook_with_prompt_ok() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "Summarize the session" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-007")
.collect();
assert_eq!(cc_hk_007.len(), 0);
}
#[test]
fn test_cc_hk_007_mixed_hooks_one_missing_prompt() {
let content = r#"{
"hooks": {
"SubagentStop": [
{
"hooks": [
{ "type": "prompt", "prompt": "valid prompt" },
{ "type": "prompt" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-007")
.collect();
assert_eq!(cc_hk_007.len(), 1);
}
#[test]
fn test_cc_hk_008_script_file_not_found() {
let content = r#"{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "bash scripts/nonexistent.sh" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-008")
.collect();
assert_eq!(cc_hk_008.len(), 1);
assert_eq!(cc_hk_008[0].level, DiagnosticLevel::Error);
assert!(cc_hk_008[0].message.contains("Script file not found"));
}
#[test]
fn test_cc_hk_008_system_command_no_script_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo 'logging tool use'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-008")
.collect();
assert_eq!(cc_hk_008.len(), 0);
}
#[test]
fn test_cc_hk_008_env_var_with_unresolvable_path_skipped() {
let content = r#"{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "$HOME/scripts/setup.sh" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-008")
.collect();
assert_eq!(cc_hk_008.len(), 0);
}
#[test]
fn test_cc_hk_008_python_script_not_found() {
let content = r#"{
"hooks": {
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "python hooks/logger.py" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-008")
.collect();
assert_eq!(cc_hk_008.len(), 1);
assert!(cc_hk_008[0].message.contains("logger.py"));
}
#[test]
fn test_cc_hk_008_url_not_treated_as_script() {
let content = r#"{
"hooks": {
"Setup": [
{
"hooks": [
{ "type": "command", "command": "curl https://example.com/install.sh | bash" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-008")
.collect();
assert_eq!(cc_hk_008.len(), 0);
}
#[test]
fn test_cc_hk_009_rm_rf_root() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "rm -rf /" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-009")
.collect();
assert_eq!(cc_hk_009.len(), 1);
assert_eq!(cc_hk_009[0].level, DiagnosticLevel::Warning);
assert!(cc_hk_009[0].message.contains("dangerous"));
}
#[test]
fn test_cc_hk_009_git_reset_hard() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "git reset --hard HEAD" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-009")
.collect();
assert_eq!(cc_hk_009.len(), 1);
assert!(cc_hk_009[0].message.contains("Hard reset"));
}
#[test]
fn test_cc_hk_009_curl_pipe_bash() {
let content = r#"{
"hooks": {
"Setup": [
{
"hooks": [
{ "type": "command", "command": "curl https://example.com/install.sh | bash" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-009")
.collect();
assert_eq!(cc_hk_009.len(), 1);
assert!(cc_hk_009[0].message.contains("security risk"));
}
#[test]
fn test_cc_hk_009_git_push_force() {
let content = r#"{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "git push origin main --force" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-009")
.collect();
assert_eq!(cc_hk_009.len(), 1);
assert!(cc_hk_009[0].message.contains("Force push"));
}
#[test]
fn test_cc_hk_009_drop_database() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "psql -c 'DROP DATABASE production'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-009")
.collect();
assert_eq!(cc_hk_009.len(), 1);
assert!(cc_hk_009[0].message.contains("irreversible"));
}
#[test]
fn test_cc_hk_009_chmod_777() {
let content = r#"{
"hooks": {
"Setup": [
{
"hooks": [
{ "type": "command", "command": "chmod 777 /var/www" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-009")
.collect();
assert_eq!(cc_hk_009.len(), 1);
assert!(cc_hk_009[0].message.contains("full access"));
}
#[test]
fn test_cc_hk_009_safe_command_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo 'logging'" },
{ "type": "command", "command": "git status" },
{ "type": "command", "command": "npm test" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-009")
.collect();
assert_eq!(cc_hk_009.len(), 0);
}
#[test]
fn test_valid_hooks_no_errors() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo 'pre-bash'" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "Summarize the work done" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let rule_errors: Vec<_> = diagnostics
.iter()
.filter(|d| {
d.rule.starts_with("CC-HK-006")
|| d.rule.starts_with("CC-HK-007")
|| d.rule.starts_with("CC-HK-009")
})
.collect();
assert_eq!(rule_errors.len(), 0);
}
#[test]
fn test_empty_hooks_ok() {
let content = r#"{ "hooks": {} }"#;
let diagnostics = validate(content);
assert!(diagnostics.is_empty());
}
#[test]
fn test_settings_with_other_fields() {
let content = r#"{
"permissions": { "allow": ["Read"] },
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "echo 'started'" }
]
}
]
},
"model": "sonnet"
}"#;
let diagnostics = validate(content);
let parse_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-012")
.collect();
assert_eq!(parse_errors.len(), 0);
}
#[test]
fn test_invalid_json_parse_error() {
let content = r#"{ invalid json }"#;
let diagnostics = validate(content);
let parse_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-012")
.collect();
assert_eq!(parse_errors.len(), 1);
}
#[test]
fn test_extract_script_paths_sh() {
let validator = HooksValidator;
let paths = validator.extract_script_paths("bash scripts/hook.sh");
assert_eq!(paths, vec!["scripts/hook.sh"]);
}
#[test]
fn test_extract_script_paths_py() {
let validator = HooksValidator;
let paths = validator.extract_script_paths("python /path/to/script.py arg1 arg2");
assert_eq!(paths, vec!["/path/to/script.py"]);
}
#[test]
fn test_extract_script_paths_env_var() {
let validator = HooksValidator;
let paths = validator.extract_script_paths("$CLAUDE_PROJECT_DIR/hooks/setup.sh");
assert_eq!(paths, vec!["$CLAUDE_PROJECT_DIR/hooks/setup.sh"]);
}
#[test]
fn test_extract_script_paths_no_script() {
let validator = HooksValidator;
let paths = validator.extract_script_paths("echo 'hello world'");
assert!(paths.is_empty());
}
#[test]
fn test_extract_script_paths_multiple() {
let validator = HooksValidator;
let paths = validator.extract_script_paths("./first.sh && ./second.sh");
assert_eq!(paths.len(), 2);
assert!(paths.contains(&"./first.sh".to_string()));
assert!(paths.contains(&"./second.sh".to_string()));
}
#[test]
fn test_extract_script_paths_quoted() {
let validator = HooksValidator;
let paths = validator.extract_script_paths("bash \"$CLAUDE_PROJECT_DIR/hooks/test.sh\"");
assert_eq!(paths, vec!["$CLAUDE_PROJECT_DIR/hooks/test.sh"]);
}
#[test]
fn test_has_unresolved_env_vars() {
let validator = HooksValidator;
assert!(!validator.has_unresolved_env_vars("./script.sh"));
assert!(!validator.has_unresolved_env_vars("$CLAUDE_PROJECT_DIR/script.sh"));
assert!(validator.has_unresolved_env_vars("$HOME/script.sh"));
assert!(validator.has_unresolved_env_vars("$CLAUDE_PROJECT_DIR/$HOME/script.sh"));
}
#[test]
fn test_dangerous_pattern_case_insensitive() {
let validator = HooksValidator;
assert!(validator.check_dangerous_patterns("RM -RF /").is_some());
assert!(
validator
.check_dangerous_patterns("Git Reset --Hard")
.is_some()
);
assert!(
validator
.check_dangerous_patterns("DROP DATABASE test")
.is_some()
);
}
#[test]
fn test_fixture_valid_settings() {
let content = include_str!("../../../../../tests/fixtures/valid/hooks/settings.json");
let diagnostics = validate(content);
let errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule.starts_with("CC-HK-00"))
.collect();
assert!(errors.is_empty());
}
#[test]
fn test_fixture_missing_command() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/hooks/missing-command-field/settings.json"
);
let diagnostics = validate(content);
let cc_hk_006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-006")
.collect();
assert!(!cc_hk_006.is_empty());
}
#[test]
fn test_fixture_missing_prompt() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/hooks/missing-prompt-field/settings.json"
);
let diagnostics = validate(content);
let cc_hk_007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-007")
.collect();
assert!(!cc_hk_007.is_empty());
}
#[test]
fn test_fixture_dangerous_commands() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/hooks/dangerous-commands/settings.json"
);
let diagnostics = validate(content);
let cc_hk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-009")
.collect();
assert!(cc_hk_009.len() >= 3);
}
#[test]
fn test_cc_hk_001_invalid_event_name() {
let content = r#"{
"hooks": {
"InvalidEvent": [
{
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 1);
assert_eq!(cc_hk_001[0].level, DiagnosticLevel::Error);
assert!(cc_hk_001[0].message.contains("Invalid hook event"));
assert!(cc_hk_001[0].message.contains("InvalidEvent"));
}
#[test]
fn test_cc_hk_001_wrong_case_event_name() {
let content = r#"{
"hooks": {
"pretooluse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 1);
assert!(
cc_hk_001[0]
.suggestion
.as_ref()
.unwrap()
.contains("PreToolUse")
);
assert!(
cc_hk_001[0]
.suggestion
.as_ref()
.unwrap()
.contains("case-sensitive")
);
}
#[test]
fn test_cc_hk_001_valid_event_name() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 0);
}
#[test]
fn test_cc_hk_001_multiple_invalid_events() {
let content = r#"{
"hooks": {
"InvalidOne": [
{
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
],
"InvalidTwo": [
{
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 2);
}
#[test]
fn test_fixture_invalid_event() {
let content =
include_str!("../../../../../tests/fixtures/invalid/hooks/invalid-event/settings.json");
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 2);
}
#[test]
fn test_cc_hk_002_prompt_on_pretooluse_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "prompt", "prompt": "allowed here" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-002")
.collect();
assert_eq!(cc_hk_002.len(), 0);
}
#[test]
fn test_cc_hk_002_prompt_on_session_start() {
let content = r#"{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "prompt", "prompt": "not allowed here" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-002")
.collect();
assert_eq!(cc_hk_002.len(), 1);
assert_eq!(cc_hk_002[0].level, DiagnosticLevel::Error);
assert!(cc_hk_002[0].message.contains("not supported on"));
}
#[test]
fn test_cc_hk_002_prompt_on_stop_ok() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "this is valid" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-002")
.collect();
assert_eq!(cc_hk_002.len(), 0);
}
#[test]
fn test_cc_hk_002_prompt_on_subagent_stop_ok() {
let content = r#"{
"hooks": {
"SubagentStop": [
{
"hooks": [
{ "type": "prompt", "prompt": "this is valid" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-002")
.collect();
assert_eq!(cc_hk_002.len(), 0);
}
#[test]
fn test_fixture_prompt_on_wrong_event() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/hooks/prompt-on-wrong-event/settings.json"
);
let diagnostics = validate(content);
let cc_hk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-002")
.collect();
assert_eq!(cc_hk_002.len(), 2);
}
#[test]
fn test_cc_hk_003_missing_matcher_pretooluse() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_003: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-003")
.collect();
assert_eq!(cc_hk_003.len(), 1);
assert_eq!(cc_hk_003[0].level, DiagnosticLevel::Info);
assert!(cc_hk_003[0].message.contains("has no matcher"));
}
#[test]
fn test_cc_hk_003_missing_matcher_permission_request() {
let content = r#"{
"hooks": {
"PermissionRequest": [
{
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_003: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-003")
.collect();
assert_eq!(cc_hk_003.len(), 1);
assert_eq!(cc_hk_003[0].level, DiagnosticLevel::Info);
assert!(cc_hk_003[0].message.contains("has no matcher"));
}
#[test]
fn test_cc_hk_003_missing_matcher_posttooluse() {
let content = r#"{
"hooks": {
"PostToolUse": [
{
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_003: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-003")
.collect();
assert_eq!(cc_hk_003.len(), 1);
assert_eq!(cc_hk_003[0].level, DiagnosticLevel::Info);
assert!(cc_hk_003[0].message.contains("has no matcher"));
}
#[test]
fn test_cc_hk_003_missing_matcher_posttoolusefailure() {
let content = r#"{
"hooks": {
"PostToolUseFailure": [
{
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_003: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-003")
.collect();
assert_eq!(cc_hk_003.len(), 1);
assert_eq!(cc_hk_003[0].level, DiagnosticLevel::Info);
assert!(cc_hk_003[0].message.contains("has no matcher"));
}
#[test]
fn test_cc_hk_003_with_matcher_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_003: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-003")
.collect();
assert_eq!(cc_hk_003.len(), 0);
}
#[test]
fn test_fixture_missing_matcher() {
let content =
include_str!("../../../../../tests/fixtures/invalid/hooks/missing-matcher/settings.json");
let diagnostics = validate(content);
let cc_hk_003: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-003")
.collect();
assert_eq!(cc_hk_003.len(), 4);
assert!(
cc_hk_003.iter().all(|d| d.level == DiagnosticLevel::Info),
"All CC-HK-003 diagnostics should be Info level"
);
}
#[test]
fn test_cc_hk_004_matcher_on_stop() {
let content = r#"{
"hooks": {
"Stop": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-004")
.collect();
let cc_hk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-018")
.collect();
assert_eq!(cc_hk_004.len(), 0, "Stop should not trigger CC-HK-004");
assert_eq!(cc_hk_018.len(), 1, "Stop should trigger CC-HK-018");
assert_eq!(cc_hk_018[0].level, DiagnosticLevel::Info);
}
#[test]
fn test_cc_hk_004_matcher_on_session_start() {
let content = r#"{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-004")
.collect();
assert_eq!(cc_hk_004.len(), 0, "SessionStart now supports matchers");
}
#[test]
fn test_cc_hk_004_no_matcher_on_stop_ok() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-004")
.collect();
assert_eq!(cc_hk_004.len(), 0);
}
#[test]
fn test_cc_hk_004_has_safe_fix_when_unique_matcher_line() {
let content = r#"{
"hooks": {
"TaskCompleted": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_004 = diagnostics
.iter()
.find(|d| d.rule == "CC-HK-004")
.expect("CC-HK-004 should be reported");
assert!(cc_hk_004.has_fixes());
let fix = &cc_hk_004.fixes[0];
assert!(fix.safe);
assert_eq!(fix.replacement, "");
}
#[test]
fn test_fixture_matcher_on_wrong_event() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/hooks/matcher-on-wrong-event/settings.json"
);
let diagnostics = validate(content);
let cc_hk_004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-004")
.collect();
assert_eq!(cc_hk_004.len(), 0);
let cc_hk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-018")
.collect();
assert_eq!(cc_hk_018.len(), 2);
}
#[test]
fn test_cc_hk_005_missing_type_field() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "command": "echo 'missing type'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-005")
.collect();
assert_eq!(cc_hk_005.len(), 1);
assert_eq!(cc_hk_005[0].level, DiagnosticLevel::Error);
assert!(
cc_hk_005[0]
.message
.contains("missing required 'type' field")
);
}
#[test]
fn test_cc_hk_005_multiple_missing_type() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "command": "echo 'missing type 1'" },
{ "prompt": "missing type 2" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-005")
.collect();
assert_eq!(cc_hk_005.len(), 2);
}
#[test]
fn test_cc_hk_005_with_type_ok() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "echo 'has type'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-005")
.collect();
assert_eq!(cc_hk_005.len(), 0);
}
#[test]
fn test_fixture_missing_type_field() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/hooks/missing-type-field/settings.json"
);
let diagnostics = validate(content);
let cc_hk_005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-005")
.collect();
assert_eq!(cc_hk_005.len(), 3);
}
#[test]
fn test_find_closest_event_exact_case_match() {
let closest = find_closest_event("pretooluse");
assert!(closest.suggestion.contains("PreToolUse"));
assert!(closest.suggestion.contains("case-sensitive"));
assert_eq!(closest.corrected_event, Some("PreToolUse".to_string()));
assert!(closest.is_case_fix);
}
#[test]
fn test_find_closest_event_partial_match() {
let closest = find_closest_event("tool");
assert!(closest.suggestion.contains("Did you mean"));
assert!(closest.corrected_event.is_some());
assert!(!closest.is_case_fix);
}
#[test]
fn test_find_closest_event_no_match() {
let closest = find_closest_event("CompletelyInvalid");
assert!(closest.suggestion.contains("Valid events are"));
assert!(closest.corrected_event.is_none());
}
#[test]
fn test_cc_hk_001_case_fix_has_safe_fix() {
let content = r#"{
"hooks": {
"pretooluse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 1);
assert!(cc_hk_001[0].has_fixes());
let fix = &cc_hk_001[0].fixes[0];
assert!(fix.safe); assert_eq!(fix.replacement, "\"PreToolUse\"");
}
#[test]
fn test_cc_hk_001_case_fix_teammateidle() {
let content = r#"{
"hooks": {
"teammateidle": [
{
"hooks": [
{ "type": "command", "command": "echo idle" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 1);
assert!(cc_hk_001[0].has_fixes());
let fix = &cc_hk_001[0].fixes[0];
assert!(fix.safe); assert_eq!(fix.replacement, "\"TeammateIdle\"");
}
#[test]
fn test_cc_hk_001_case_fix_taskcompleted() {
let content = r#"{
"hooks": {
"taskcompleted": [
{
"hooks": [
{ "type": "command", "command": "echo done" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 1);
assert!(cc_hk_001[0].has_fixes());
let fix = &cc_hk_001[0].fixes[0];
assert!(fix.safe); assert_eq!(fix.replacement, "\"TaskCompleted\"");
}
#[test]
fn test_cc_hk_001_typo_teammate_partial_match() {
let content = r#"{
"hooks": {
"Teammate": [
{
"hooks": [
{ "type": "command", "command": "echo idle" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 1);
assert!(cc_hk_001[0].has_fixes());
let fix = &cc_hk_001[0].fixes[0];
assert!(!fix.safe); assert_eq!(fix.replacement, "\"TeammateIdle\"");
}
#[test]
fn test_cc_hk_001_typo_fix_not_safe() {
let content = r#"{
"hooks": {
"tool": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 1);
assert!(cc_hk_001[0].has_fixes());
let fix = &cc_hk_001[0].fixes[0];
assert!(!fix.safe); }
#[test]
fn test_cc_hk_001_no_fix_for_completely_invalid() {
let content = r#"{
"hooks": {
"XyzAbc123": [
{
"hooks": [
{ "type": "command", "command": "echo 'test'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 1);
assert!(!cc_hk_001[0].has_fixes());
}
#[test]
fn test_cc_hk_001_fix_correct_byte_position() {
let content = r#"{"hooks": {"stop": [{"hooks": [{"type": "command", "command": "echo"}]}]}}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 1);
assert!(cc_hk_001[0].has_fixes());
let fix = &cc_hk_001[0].fixes[0];
let mut fixed = content.to_string();
fixed.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert!(fixed.contains("\"Stop\""));
assert!(!fixed.contains("\"stop\""));
}
#[test]
fn test_find_event_key_position() {
let content = r#"{"hooks": {"InvalidEvent": []}}"#;
let pos = find_event_key_position(content, "InvalidEvent");
assert!(pos.is_some());
let (start, end) = pos.unwrap();
assert_eq!(&content[start..end], "\"InvalidEvent\"");
}
#[test]
fn test_find_event_key_position_not_found() {
let content = r#"{"hooks": {"ValidEvent": []}}"#;
let pos = find_event_key_position(content, "NotPresent");
assert!(pos.is_none());
}
#[test]
fn test_cc_hk_010_command_hook_no_timeout() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 1);
assert_eq!(cc_hk_010[0].level, DiagnosticLevel::Warning);
assert!(cc_hk_010[0].message.contains("no timeout specified"));
}
#[test]
fn test_cc_hk_010_prompt_hook_no_timeout() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "Summarize session" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 1);
assert_eq!(cc_hk_010[0].level, DiagnosticLevel::Warning);
assert!(cc_hk_010[0].message.contains("Prompt/agent hook"));
}
#[test]
fn test_cc_hk_010_with_timeout_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 0);
}
#[test]
fn test_cc_hk_010_command_timeout_exceeds_default() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": 700 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 1);
assert_eq!(cc_hk_010[0].level, DiagnosticLevel::Warning);
assert!(cc_hk_010[0].message.contains("exceeding 600s default"));
}
#[test]
fn test_cc_hk_010_command_timeout_at_default_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": 600 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 0);
}
#[test]
fn test_cc_hk_010_prompt_timeout_exceeds_default() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "test prompt", "timeout": 45 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 1);
assert_eq!(cc_hk_010[0].level, DiagnosticLevel::Warning);
assert!(cc_hk_010[0].message.contains("exceeding 30s default"));
}
#[test]
fn test_cc_hk_010_prompt_timeout_at_default_ok() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "test prompt", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 0);
}
#[test]
fn test_cc_hk_010_multiple_hooks_mixed() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "echo 'no timeout'" },
{ "type": "command", "command": "echo 'with timeout'", "timeout": 30 },
{ "type": "command", "command": "echo 'also no timeout'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 2);
}
#[test]
fn test_fixture_no_timeout() {
let content =
include_str!("../../../../../tests/fixtures/invalid/hooks/no-timeout/settings.json");
let diagnostics = validate(content);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 2);
}
#[test]
fn test_cc_hk_011_negative_timeout() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": -5 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 1);
assert_eq!(cc_hk_011[0].level, DiagnosticLevel::Error);
assert!(cc_hk_011[0].message.contains("Invalid timeout"));
}
#[test]
fn test_cc_hk_011_has_unsafe_fix_for_invalid_timeout() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": -5 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011 = diagnostics
.iter()
.find(|d| d.rule == "CC-HK-011")
.expect("CC-HK-011 should be reported");
assert!(cc_hk_011.has_fixes());
let fix = &cc_hk_011.fixes[0];
assert_eq!(fix.replacement, "30");
assert!(!fix.safe);
}
#[test]
fn test_cc_hk_011_zero_timeout() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": 0 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 1);
assert_eq!(cc_hk_011[0].level, DiagnosticLevel::Error);
}
#[test]
fn test_cc_hk_011_float_zero_timeout() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": 0.0 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 1);
assert_eq!(cc_hk_011[0].level, DiagnosticLevel::Error);
}
#[test]
fn test_cc_hk_011_float_timeout() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": 30.5 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 1);
assert_eq!(cc_hk_011[0].level, DiagnosticLevel::Error);
}
#[test]
fn test_cc_hk_011_whole_float_invalid() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": 30.0 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 1);
assert_eq!(cc_hk_011[0].level, DiagnosticLevel::Error);
}
#[test]
fn test_cc_hk_011_string_timeout() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "echo test", "timeout": "thirty" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 1);
assert_eq!(cc_hk_011[0].level, DiagnosticLevel::Error);
}
#[test]
fn test_cc_hk_011_null_timeout() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "echo test", "timeout": null }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 1);
assert_eq!(cc_hk_011[0].level, DiagnosticLevel::Error);
}
#[test]
fn test_cc_hk_011_positive_timeout_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 0);
}
#[test]
fn test_cc_hk_011_multiple_invalid_timeouts() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "echo 'negative'", "timeout": -5 },
{ "type": "command", "command": "echo 'zero'", "timeout": 0 },
{ "type": "command", "command": "echo 'valid'", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 2);
}
#[test]
fn test_cc_hk_011_missing_timeout_not_triggered() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 0);
}
#[test]
fn test_fixture_invalid_timeout() {
let content =
include_str!("../../../../../tests/fixtures/invalid/hooks/invalid-timeout/settings.json");
let diagnostics = validate(content);
let cc_hk_011: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-011")
.collect();
assert_eq!(cc_hk_011.len(), 2);
}
#[test]
fn test_config_disabled_hooks_category_returns_empty() {
let mut config = LintConfig::default();
config.rules_mut().hooks = false;
let content = r#"{
"hooks": {
"InvalidEvent": [
{
"hooks": [
{ "type": "command", "command": "echo test" }
]
}
]
}
}"#;
let validator = HooksValidator;
let diagnostics = validator.validate(Path::new("settings.json"), content, &config);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(cc_hk_001.len(), 0);
}
#[test]
fn test_config_disabled_specific_hook_rule() {
let mut config = LintConfig::default();
config.rules_mut().disabled_rules = vec!["CC-HK-006".to_string()];
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command" }
]
}
]
}
}"#;
let validator = HooksValidator;
let diagnostics = validator.validate(Path::new("settings.json"), content, &config);
let cc_hk_006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-006")
.collect();
assert_eq!(cc_hk_006.len(), 0);
}
#[test]
fn test_config_cursor_target_disables_hooks_rules() {
use crate::config::TargetTool;
let mut config = LintConfig::default();
config.set_target(TargetTool::Cursor);
let content = r#"{
"hooks": {
"InvalidEvent": [
{
"hooks": [
{ "type": "command", "command": "echo test" }
]
}
]
}
}"#;
let validator = HooksValidator;
let diagnostics = validator.validate(Path::new("settings.json"), content, &config);
let hook_rules: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule.starts_with("CC-HK-"))
.collect();
assert_eq!(hook_rules.len(), 0);
}
#[test]
fn test_config_dangerous_pattern_disabled() {
let mut config = LintConfig::default();
config.rules_mut().disabled_rules = vec!["CC-HK-009".to_string()];
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "rm -rf /" }
]
}
]
}
}"#;
let validator = HooksValidator;
let diagnostics = validator.validate(Path::new("settings.json"), content, &config);
let cc_hk_009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-009")
.collect();
assert_eq!(cc_hk_009.len(), 0);
}
#[test]
fn test_cc_hk_010_assumption_when_version_not_pinned() {
let config = LintConfig::default();
assert!(!config.is_claude_code_version_pinned());
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test" }
]
}
]
}
}"#;
let validator = HooksValidator;
let diagnostics = validator.validate(Path::new("settings.json"), content, &config);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 1);
assert!(cc_hk_010[0].assumption.is_some());
let assumption = cc_hk_010[0].assumption.as_ref().unwrap();
assert!(assumption.contains("Assumes Claude Code default timeout behavior"));
assert!(assumption.contains("[tool_versions]"));
}
#[test]
fn test_cc_hk_010_no_assumption_when_version_pinned() {
let mut config = LintConfig::default();
config.tool_versions_mut().claude_code = Some("1.0.0".to_string());
assert!(config.is_claude_code_version_pinned());
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test" }
]
}
]
}
}"#;
let validator = HooksValidator;
let diagnostics = validator.validate(Path::new("settings.json"), content, &config);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 1);
assert!(cc_hk_010[0].assumption.is_none());
}
#[test]
fn test_cc_hk_010_prompt_assumption_when_version_not_pinned() {
let config = LintConfig::default();
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "Summarize session" }
]
}
]
}
}"#;
let validator = HooksValidator;
let diagnostics = validator.validate(Path::new("settings.json"), content, &config);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 1);
assert!(cc_hk_010[0].assumption.is_some());
}
#[test]
fn test_cc_hk_010_exceeds_default_assumption_when_version_not_pinned() {
let config = LintConfig::default();
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "timeout": 700 }
]
}
]
}
}"#;
let validator = HooksValidator;
let diagnostics = validator.validate(Path::new("settings.json"), content, &config);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 1);
assert!(cc_hk_010[0].assumption.is_some());
}
#[test]
fn test_cc_hk_012_invalid_json_syntax() {
let content = r#"{ "hooks": { invalid syntax } }"#;
let diagnostics = validate(content);
let parse_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-012")
.collect();
assert_eq!(parse_errors.len(), 1);
assert!(parse_errors[0].message.contains("Failed to parse"));
}
#[test]
fn test_cc_hk_012_truncated_json() {
let content = r#"{"hooks": {"Stop": [{"hooks": [{"type":"command""#;
let diagnostics = validate(content);
let parse_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-012")
.collect();
assert_eq!(parse_errors.len(), 1);
}
#[test]
fn test_cc_hk_012_empty_file() {
let content = "";
let diagnostics = validate(content);
let parse_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-012")
.collect();
assert_eq!(parse_errors.len(), 1);
}
#[test]
fn test_cc_hk_012_valid_json_no_error() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "echo done" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let parse_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-012")
.collect();
assert_eq!(parse_errors.len(), 0);
}
#[test]
fn test_cc_hk_012_disabled() {
let mut config = LintConfig::default();
config.rules_mut().disabled_rules = vec!["CC-HK-012".to_string()];
let content = r#"{ invalid json }"#;
let validator = HooksValidator;
let diagnostics = validator.validate(Path::new("settings.json"), content, &config);
assert!(!diagnostics.iter().any(|d| d.rule == "CC-HK-012"));
}
#[test]
fn test_cc_hk_012_missing_required_field_hooks_key() {
let content = r#"{"model": "sonnet"}"#;
let diagnostics = validate(content);
assert!(!diagnostics.iter().any(|d| d.rule == "CC-HK-012"));
}
#[test]
fn test_cc_hk_012_null_value() {
let content = r#"{"hooks": null}"#;
let diagnostics = validate(content);
let parse_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-012")
.collect();
assert_eq!(parse_errors.len(), 1);
}
#[test]
fn test_cc_hk_012_array_instead_of_object() {
let content = r#"["hooks", "array"]"#;
let diagnostics = validate(content);
let parse_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-012")
.collect();
assert_eq!(parse_errors.len(), 1);
}
#[test]
fn test_cc_hk_001_all_valid_events() {
let tool_events = ["PreToolUse", "PostToolUse", "PermissionRequest"];
let non_tool_events = [
"Stop",
"SubagentStop",
"SessionStart",
"TeammateIdle",
"TaskCompleted",
];
for event in tool_events {
let content = format!(
r#"{{
"hooks": {{
"{}": [
{{
"matcher": "Bash",
"hooks": [{{ "type": "command", "command": "echo test" }}]
}}
]
}}
}}"#,
event
);
let diagnostics = validate(&content);
let hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert!(
hk_001.is_empty(),
"Tool event '{}' with matcher should be valid",
event
);
}
for event in non_tool_events {
let content = format!(
r#"{{
"hooks": {{
"{}": [
{{
"hooks": [{{ "type": "command", "command": "echo test" }}]
}}
]
}}
}}"#,
event
);
let diagnostics = validate(&content);
let hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert!(
hk_001.is_empty(),
"Non-tool event '{}' without matcher should be valid",
event
);
}
}
#[test]
fn test_cc_hk_003_all_tool_events_hint_matcher() {
let tool_events = HooksSchema::TOOL_EVENTS;
for event in tool_events {
let content = format!(
r#"{{
"hooks": {{
"{}": [
{{
"hooks": [{{ "type": "command", "command": "echo test" }}]
}}
]
}}
}}"#,
event
);
let diagnostics = validate(&content);
let hk_003: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-003")
.collect();
assert_eq!(
hk_003.len(),
1,
"Event '{}' without matcher should get CC-HK-003 hint",
event
);
assert_eq!(
hk_003[0].level,
DiagnosticLevel::Info,
"Event '{}' CC-HK-003 should be Info level",
event
);
}
}
#[test]
fn test_cc_hk_004_non_tool_events_reject_matcher() {
let non_tool_events = ["TeammateIdle", "TaskCompleted"];
for event in non_tool_events {
let content = format!(
r#"{{
"hooks": {{
"{}": [
{{
"matcher": "Bash",
"hooks": [{{ "type": "command", "command": "echo test" }}]
}}
]
}}
}}"#,
event
);
let diagnostics = validate(&content);
let hk_004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-004")
.collect();
assert_eq!(
hk_004.len(),
1,
"Event '{}' should reject matcher but didn't get CC-HK-004",
event
);
}
}
#[test]
fn test_cc_hk_002_prompt_allowed_events() {
let prompt_allowed = HooksSchema::PROMPT_EVENTS;
assert_eq!(
prompt_allowed.len(),
8,
"Expected exactly 8 prompt-allowed events"
);
for event in prompt_allowed {
let content = format!(
r#"{{
"hooks": {{
"{}": [
{{
"hooks": [{{ "type": "prompt", "prompt": "Summarize" }}]
}}
]
}}
}}"#,
event
);
let diagnostics = validate(&content);
let hk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-002")
.collect();
assert!(
hk_002.is_empty(),
"Prompt on '{}' should be valid but got CC-HK-002",
event
);
}
}
#[test]
fn test_cc_hk_002_prompt_on_task_completed_ok() {
let content = r#"{
"hooks": {
"TaskCompleted": [
{
"hooks": [
{ "type": "prompt", "prompt": "verify task completion" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-002")
.collect();
assert_eq!(cc_hk_002.len(), 0);
}
#[test]
fn test_cc_hk_002_prompt_disallowed_events() {
let prompt_disallowed = [
"SessionStart",
"SessionEnd",
"Notification",
"SubagentStart",
"PreCompact",
"Setup",
"TeammateIdle",
];
for event in prompt_disallowed {
let content = format!(
r#"{{
"hooks": {{
"{}": [
{{
"hooks": [{{ "type": "prompt", "prompt": "Test" }}]
}}
]
}}
}}"#,
event
);
let diagnostics = validate(&content);
let hk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-002")
.collect();
assert_eq!(
hk_002.len(),
1,
"Prompt on '{}' should be invalid but didn't get CC-HK-002",
event
);
}
}
#[test]
fn test_cc_hk_002_agent_disallowed_events() {
let agent_disallowed = [
"Setup",
"SessionStart",
"SessionEnd",
"Notification",
"SubagentStart",
"PreCompact",
"TeammateIdle",
];
for event in agent_disallowed {
let content = format!(
r#"{{
"hooks": {{
"{}": [
{{
"hooks": [{{ "type": "agent", "prompt": "Test $ARGUMENTS" }}]
}}
]
}}
}}"#,
event
);
let diagnostics = validate(&content);
let hk_002: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-002")
.collect();
assert_eq!(
hk_002.len(),
1,
"Agent on '{}' should be invalid but didn't get CC-HK-002",
event
);
}
}
#[test]
fn test_cc_hk_013_async_on_prompt_hook() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "Summarize $ARGUMENTS", "async": true }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-013")
.collect();
assert_eq!(cc_hk_013.len(), 1);
assert_eq!(cc_hk_013[0].level, DiagnosticLevel::Error);
assert!(cc_hk_013[0].message.contains("async"));
assert!(cc_hk_013[0].message.contains("prompt"));
}
#[test]
fn test_cc_hk_013_async_on_agent_hook() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "agent", "prompt": "Review $ARGUMENTS", "async": true }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-013")
.collect();
assert_eq!(cc_hk_013.len(), 1);
assert!(cc_hk_013[0].message.contains("agent"));
}
#[test]
fn test_cc_hk_013_async_on_command_hook_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "async": true }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-013")
.collect();
assert_eq!(cc_hk_013.len(), 0);
}
#[test]
fn test_fixture_async_on_non_command() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/hooks/async-on-non-command/settings.json"
);
let diagnostics = validate(content);
let cc_hk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-013")
.collect();
assert_eq!(cc_hk_013.len(), 2);
}
#[test]
fn test_cc_hk_014_once_in_settings() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "echo done", "once": true }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-014")
.collect();
assert_eq!(cc_hk_014.len(), 1);
assert_eq!(cc_hk_014[0].level, DiagnosticLevel::Warning);
assert!(cc_hk_014[0].message.contains("once"));
}
#[test]
fn test_cc_hk_014_no_once_ok() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "echo done" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-014")
.collect();
assert_eq!(cc_hk_014.len(), 0);
}
#[test]
fn test_fixture_once_in_settings() {
let content =
include_str!("../../../../../tests/fixtures/invalid/hooks/once-in-settings/settings.json");
let diagnostics = validate(content);
let cc_hk_014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-014")
.collect();
assert_eq!(cc_hk_014.len(), 2);
}
#[test]
fn test_cc_hk_015_model_on_command() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test", "model": "claude-sonnet-4-5-20250929" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-015")
.collect();
assert_eq!(cc_hk_015.len(), 1);
assert_eq!(cc_hk_015[0].level, DiagnosticLevel::Warning);
assert!(cc_hk_015[0].message.contains("model"));
}
#[test]
fn test_cc_hk_015_model_on_prompt_ok() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "Summarize $ARGUMENTS", "model": "claude-sonnet-4-5-20250929" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-015")
.collect();
assert_eq!(cc_hk_015.len(), 0);
}
#[test]
fn test_cc_hk_015_model_on_agent_ok() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "agent", "prompt": "Review $ARGUMENTS", "model": "claude-sonnet-4-5-20250929" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-015")
.collect();
assert_eq!(cc_hk_015.len(), 0);
}
#[test]
fn test_cc_hk_015_no_model_on_command_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-015")
.collect();
assert_eq!(cc_hk_015.len(), 0);
}
#[test]
fn test_fixture_model_on_command() {
let content =
include_str!("../../../../../tests/fixtures/invalid/hooks/model-on-command/settings.json");
let diagnostics = validate(content);
let cc_hk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-015")
.collect();
assert_eq!(cc_hk_015.len(), 1);
}
#[test]
fn test_cc_hk_016_agent_type_valid() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "agent", "prompt": "Review $ARGUMENTS" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-016")
.collect();
assert_eq!(cc_hk_016.len(), 0);
}
#[test]
fn test_cc_hk_016_mcp_tool_type_valid() {
let content = r#"{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "mcp_tool", "server": "my_server", "tool": "do_thing" }
]
}
]
}
}"#;
let diagnostics = validate(content);
assert!(
diagnostics.is_empty(),
"valid mcp_tool hook should produce zero diagnostics, got: {:?}",
diagnostics
);
}
#[test]
fn test_cc_hk_016_unknown_type() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "webhook", "url": "https://example.com" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-016")
.collect();
assert_eq!(cc_hk_016.len(), 1);
assert_eq!(cc_hk_016[0].level, DiagnosticLevel::Error);
assert!(cc_hk_016[0].message.contains("webhook"));
}
#[test]
fn test_cc_hk_016_command_and_prompt_still_valid() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "echo done" },
{ "type": "prompt", "prompt": "Summarize $ARGUMENTS" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-016")
.collect();
assert_eq!(cc_hk_016.len(), 0);
}
#[test]
fn test_fixture_unknown_hook_type() {
let content =
include_str!("../../../../../tests/fixtures/invalid/hooks/unknown-hook-type/settings.json");
let diagnostics = validate(content);
let cc_hk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-016")
.collect();
assert_eq!(cc_hk_016.len(), 1);
}
#[test]
fn test_cc_hk_016_non_string_type() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": 123, "command": "echo bad" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-016")
.collect();
assert_eq!(
cc_hk_016.len(),
1,
"Non-string type value should trigger CC-HK-016"
);
}
#[test]
fn test_cc_hk_017_prompt_missing_arguments() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "Summarize what happened in this session" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_017: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-017")
.collect();
assert_eq!(cc_hk_017.len(), 1);
assert_eq!(cc_hk_017[0].level, DiagnosticLevel::Warning);
assert!(cc_hk_017[0].message.contains("$ARGUMENTS"));
}
#[test]
fn test_cc_hk_017_prompt_with_arguments_ok() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt", "prompt": "Summarize: $ARGUMENTS" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_017: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-017")
.collect();
assert_eq!(cc_hk_017.len(), 0);
}
#[test]
fn test_cc_hk_017_agent_missing_arguments() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "agent", "prompt": "Review the session" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_017: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-017")
.collect();
assert_eq!(cc_hk_017.len(), 1);
}
#[test]
fn test_cc_hk_017_agent_with_arguments_ok() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "agent", "prompt": "Review: $ARGUMENTS" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_017: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-017")
.collect();
assert_eq!(cc_hk_017.len(), 0);
}
#[test]
fn test_cc_hk_017_no_prompt_field_no_trigger() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "prompt" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_017: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-017")
.collect();
assert_eq!(cc_hk_017.len(), 0);
}
#[test]
fn test_fixture_prompt_missing_arguments() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/hooks/prompt-missing-arguments/settings.json"
);
let diagnostics = validate(content);
let cc_hk_017: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-017")
.collect();
assert_eq!(cc_hk_017.len(), 1);
}
#[test]
fn test_cc_hk_018_matcher_on_user_prompt_submit() {
let content = r#"{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo submitted" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-018")
.collect();
assert_eq!(cc_hk_018.len(), 1);
assert_eq!(cc_hk_018[0].level, DiagnosticLevel::Info);
assert!(cc_hk_018[0].message.contains("silently ignored"));
assert!(cc_hk_018[0].message.contains("UserPromptSubmit"));
}
#[test]
fn test_cc_hk_018_matcher_on_stop() {
let content = r#"{
"hooks": {
"Stop": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo done" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-018")
.collect();
assert_eq!(cc_hk_018.len(), 1);
assert!(cc_hk_018[0].message.contains("Stop"));
}
#[test]
fn test_cc_hk_018_matcher_on_pretooluse_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo test" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-018")
.collect();
assert_eq!(cc_hk_018.len(), 0);
}
#[test]
fn test_cc_hk_018_no_matcher_ok() {
let content = r#"{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "echo submitted" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-018")
.collect();
assert_eq!(cc_hk_018.len(), 0);
}
#[test]
fn test_fixture_matcher_on_ignored_event() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/hooks/matcher-on-ignored-event/settings.json"
);
let diagnostics = validate(content);
let cc_hk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-018")
.collect();
assert_eq!(cc_hk_018.len(), 2);
}
#[test]
fn test_agent_hook_on_stop_valid() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "agent", "prompt": "Review session $ARGUMENTS", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
assert!(!diagnostics.iter().any(|d| d.rule == "CC-HK-016"));
assert!(!diagnostics.iter().any(|d| d.rule == "CC-HK-002"));
}
#[test]
fn test_agent_hook_on_pretooluse_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "agent", "prompt": "Review $ARGUMENTS", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
assert!(!diagnostics.iter().any(|d| d.rule == "CC-HK-002"));
}
#[test]
fn test_agent_hook_missing_prompt() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "agent" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-007")
.collect();
assert_eq!(cc_hk_007.len(), 1);
}
#[test]
fn test_agent_hook_timeout_policy() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "agent", "prompt": "Review $ARGUMENTS" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-010")
.collect();
assert_eq!(cc_hk_010.len(), 1);
}
#[test]
fn test_cc_hk_013_has_autofix_when_unique() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"async": true,
"prompt": "hello"
}
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-013")
.collect();
assert_eq!(cc_hk_013.len(), 1);
assert!(
cc_hk_013[0].has_fixes(),
"CC-HK-013 should have auto-fix when async is unique"
);
assert!(cc_hk_013[0].fixes[0].safe, "CC-HK-013 fix should be safe");
assert!(
cc_hk_013[0].fixes[0].replacement.is_empty(),
"CC-HK-013 fix should be a deletion"
);
}
#[test]
fn test_cc_hk_013_no_autofix_when_duplicate() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"async": true,
"prompt": "hello"
},
{
"type": "prompt",
"async": false,
"prompt": "world"
}
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_013: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-013")
.collect();
assert_eq!(cc_hk_013.len(), 2);
let no_fix = cc_hk_013.iter().any(|d| !d.has_fixes());
assert!(
no_fix,
"CC-HK-013 should not have auto-fix when async appears multiple times"
);
}
#[test]
fn test_cc_hk_015_has_autofix_when_unique() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo hi",
"model": "sonnet"
}
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-015")
.collect();
assert_eq!(cc_hk_015.len(), 1);
assert!(
cc_hk_015[0].has_fixes(),
"CC-HK-015 should have auto-fix when model is unique"
);
assert!(cc_hk_015[0].fixes[0].safe, "CC-HK-015 fix should be safe");
assert!(
cc_hk_015[0].fixes[0].replacement.is_empty(),
"CC-HK-015 fix should be a deletion"
);
}
#[test]
fn test_cc_hk_015_no_autofix_when_duplicate() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo hi",
"model": "sonnet"
},
{
"type": "command",
"command": "echo bye",
"model": "opus"
}
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_015: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-015")
.collect();
assert_eq!(cc_hk_015.len(), 2);
let no_fix = cc_hk_015.iter().any(|d| !d.has_fixes());
assert!(
no_fix,
"CC-HK-015 should not have auto-fix when model appears multiple times"
);
}
#[test]
fn test_cc_hk_018_has_autofix_when_unique() {
let content = r#"{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "test-tool",
"hooks": [
{ "type": "command", "command": "echo hi" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-018")
.collect();
assert_eq!(cc_hk_018.len(), 1);
assert!(
cc_hk_018[0].has_fixes(),
"CC-HK-018 should have auto-fix when matcher is unique"
);
assert!(cc_hk_018[0].fixes[0].safe, "CC-HK-018 fix should be safe");
assert!(
cc_hk_018[0].fixes[0].replacement.is_empty(),
"CC-HK-018 fix should be a deletion"
);
}
#[test]
fn test_cc_hk_018_no_autofix_when_duplicate_matcher() {
let content = r#"{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "test-tool",
"hooks": [
{ "type": "command", "command": "echo hi" }
]
},
{
"matcher": "test-tool",
"hooks": [
{ "type": "command", "command": "echo bye" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_018: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-018")
.collect();
assert_eq!(cc_hk_018.len(), 2);
assert!(
cc_hk_018.iter().all(|d| !d.has_fixes()),
"CC-HK-018 should not have auto-fix when matcher is duplicated"
);
}
#[test]
fn test_cc_hk_016_autofix_case_insensitive() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "Command", "command": "echo hi" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-016")
.collect();
assert_eq!(cc_hk_016.len(), 1);
assert!(
cc_hk_016[0].has_fixes(),
"CC-HK-016 should have auto-fix for case-insensitive match"
);
assert!(
!cc_hk_016[0].fixes[0].safe,
"CC-HK-016 fix should be unsafe"
);
assert_eq!(
cc_hk_016[0].fixes[0].replacement, "command",
"CC-HK-016 fix should replace with 'command'"
);
}
#[test]
fn test_cc_hk_016_no_autofix_nonsense() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "xyzzy", "command": "echo hi" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-016")
.collect();
assert_eq!(cc_hk_016.len(), 1);
assert!(
!cc_hk_016[0].has_fixes(),
"CC-HK-016 should not have auto-fix for nonsense value"
);
}
#[test]
fn test_cc_hk_016_autofix_targets_correct_bytes() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "Prompt", "prompt": "hello" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-016")
.collect();
assert_eq!(cc_hk_016.len(), 1);
assert!(cc_hk_016[0].has_fixes());
let fix = &cc_hk_016[0].fixes[0];
assert_eq!(fix.replacement, "prompt");
let targeted = &content[fix.start_byte..fix.end_byte];
assert_eq!(
targeted, "Prompt",
"Fix should target the exact value bytes"
);
}
#[test]
fn test_cc_hk_016_no_autofix_when_duplicate_type() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "Command", "command": "echo first" },
{ "type": "Command", "command": "echo second" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-016")
.collect();
assert_eq!(cc_hk_016.len(), 2, "Should flag both invalid types");
for diag in &cc_hk_016 {
assert!(
!diag.has_fixes(),
"CC-HK-016 should not have auto-fix when type value appears multiple times"
);
}
}
#[test]
fn test_cc_hk_016_no_autofix_for_non_string() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": 123, "command": "echo bad" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_016: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-016")
.collect();
assert_eq!(cc_hk_016.len(), 1);
assert!(
!cc_hk_016[0].has_fixes(),
"CC-HK-016 should not have auto-fix for non-string type values"
);
}
#[test]
fn test_cc_hk_012_has_suggestion_on_invalid_json() {
let content = r#"{ "hooks": { invalid syntax } }"#;
let diagnostics = validate(content);
let parse_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-012")
.collect();
assert_eq!(parse_errors.len(), 1);
assert!(
parse_errors[0].suggestion.is_some(),
"CC-HK-012 should have a suggestion"
);
assert!(
parse_errors[0]
.suggestion
.as_ref()
.unwrap()
.contains("Validate JSON syntax"),
"CC-HK-012 suggestion should mention JSON syntax"
);
}
#[test]
fn test_cc_hk_012_has_suggestion_on_schema_mismatch() {
let content = r#"{"hooks": null}"#;
let diagnostics = validate(content);
let parse_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-012")
.collect();
assert_eq!(parse_errors.len(), 1);
assert!(
parse_errors[0].suggestion.is_some(),
"CC-HK-012 schema mismatch should also have a suggestion"
);
}
#[test]
fn test_cc_hk_019_setup_triggers_deprecation_warning() {
let content = r#"{
"hooks": {
"Setup": [
{
"hooks": [
{ "type": "command", "command": "echo setup", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-019")
.collect();
assert_eq!(cc_hk_019.len(), 1);
assert_eq!(cc_hk_019[0].level, DiagnosticLevel::Warning);
assert!(
cc_hk_019[0].message.contains("Deprecated"),
"Should mention deprecation, got: {}",
cc_hk_019[0].message
);
assert!(
cc_hk_019[0].message.contains("Setup"),
"Should mention Setup event, got: {}",
cc_hk_019[0].message
);
assert!(
cc_hk_019[0].message.contains("SessionStart"),
"Should suggest SessionStart replacement, got: {}",
cc_hk_019[0].message
);
}
#[test]
fn test_cc_hk_019_session_start_no_warning() {
let content = r#"{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "echo start", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-019")
.collect();
assert_eq!(
cc_hk_019.len(),
0,
"SessionStart should NOT trigger CC-HK-019"
);
}
#[test]
fn test_cc_hk_019_has_autofix() {
let content = r#"{
"hooks": {
"Setup": [
{
"hooks": [
{ "type": "command", "command": "echo setup", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-019")
.collect();
assert_eq!(cc_hk_019.len(), 1);
assert!(cc_hk_019[0].has_fixes(), "CC-HK-019 should have auto-fix");
let fix = &cc_hk_019[0].fixes[0];
assert!(
fix.replacement.contains("SessionStart"),
"Fix should replace with SessionStart, got: {}",
fix.replacement
);
}
#[test]
fn test_cc_hk_019_fix_is_unsafe() {
let content = r#"{
"hooks": {
"Setup": [
{
"hooks": [
{ "type": "command", "command": "echo setup", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-019")
.collect();
assert_eq!(cc_hk_019.len(), 1);
assert!(cc_hk_019[0].has_fixes(), "CC-HK-019 should have auto-fix");
let fix = &cc_hk_019[0].fixes[0];
assert!(!fix.safe, "CC-HK-019 fix should be unsafe (safe=false)");
}
#[test]
fn test_cc_hk_019_can_be_disabled() {
let content = r#"{
"hooks": {
"Setup": [
{
"hooks": [
{ "type": "command", "command": "echo setup", "timeout": 30 }
]
}
]
}
}"#;
let config = LintConfig::builder()
.disable_rule("CC-HK-019")
.build()
.unwrap();
let validator = HooksValidator;
let diagnostics = validator.validate(Path::new("settings.json"), content, &config);
let cc_hk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-019")
.collect();
assert_eq!(
cc_hk_019.len(),
0,
"CC-HK-019 should not fire when disabled"
);
}
#[test]
fn test_cc_hk_019_fixture_deprecated_setup_event() {
let content = include_str!(
"../../../../../tests/fixtures/invalid/hooks/deprecated-setup-event/settings.json"
);
let diagnostics = validate(content);
let cc_hk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-019")
.collect();
assert_eq!(cc_hk_019.len(), 1, "Fixture should trigger CC-HK-019");
assert_eq!(cc_hk_019[0].level, DiagnosticLevel::Warning);
}
#[test]
fn test_cc_hk_019_setup_does_not_trigger_cc_hk_001() {
let content = r#"{
"hooks": {
"Setup": [
{
"hooks": [
{ "type": "command", "command": "echo setup", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
assert_eq!(
cc_hk_001.len(),
0,
"Setup is a valid event - CC-HK-001 should NOT fire"
);
}
#[test]
fn test_cc_hk_019_case_variants_trigger_cc_hk_001_not_019() {
for variant in &["setup", "SetUp", "SETUP", "Setups"] {
let content = format!(
r#"{{
"hooks": {{
"{}": [
{{
"hooks": [
{{ "type": "command", "command": "echo test", "timeout": 30 }}
]
}}
]
}}
}}"#,
variant
);
let diagnostics = validate(&content);
let cc_hk_001: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-001")
.collect();
let cc_hk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-019")
.collect();
assert_eq!(
cc_hk_001.len(),
1,
"'{}' should trigger CC-HK-001 (invalid event)",
variant
);
assert_eq!(
cc_hk_019.len(),
0,
"'{}' should NOT trigger CC-HK-019 (not exact match for deprecated)",
variant
);
}
}
#[test]
fn test_cc_hk_019_suggestion_field() {
let content = r#"{
"hooks": {
"Setup": [
{
"hooks": [
{ "type": "command", "command": "echo setup", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-019")
.collect();
assert_eq!(cc_hk_019.len(), 1);
assert!(
cc_hk_019[0].suggestion.is_some(),
"CC-HK-019 should have a suggestion"
);
let suggestion = cc_hk_019[0].suggestion.as_ref().unwrap();
assert!(
suggestion.contains("SessionStart"),
"Suggestion should mention SessionStart, got: {}",
suggestion
);
}
#[test]
fn test_cc_hk_019_autofix_application() {
let content = r#"{
"hooks": {
"Setup": [
{
"hooks": [
{ "type": "command", "command": "echo setup", "timeout": 30 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_019: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-019")
.collect();
assert_eq!(cc_hk_019.len(), 1);
assert!(cc_hk_019[0].has_fixes());
let fix = &cc_hk_019[0].fixes[0];
let mut fixed_content = content.to_string();
fixed_content.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert!(
fixed_content.contains("\"SessionStart\""),
"Fixed content should contain SessionStart"
);
assert!(
!fixed_content.contains("\"Setup\""),
"Fixed content should not contain Setup"
);
let re_diagnostics = validate(&fixed_content);
let re_019: Vec<_> = re_diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-019")
.collect();
assert_eq!(re_019.len(), 0, "After fix, CC-HK-019 should not fire");
}
#[test]
fn test_cc_hk_020_http_hook_missing_url() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "http" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_020: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-020")
.collect();
assert_eq!(cc_hk_020.len(), 1);
assert_eq!(cc_hk_020[0].level, DiagnosticLevel::Error);
assert!(
cc_hk_020[0]
.message
.contains("missing required 'url' field")
);
}
#[test]
fn test_cc_hk_020_http_hook_with_url_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "http", "url": "https://example.com/hook" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_020: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-020")
.collect();
assert_eq!(cc_hk_020.len(), 0);
}
#[test]
fn test_cc_hk_020_http_hook_empty_url_string() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "http", "url": "" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_020: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-020")
.collect();
assert_eq!(cc_hk_020.len(), 1, "Empty url string should fire CC-HK-020");
assert_eq!(cc_hk_020[0].level, DiagnosticLevel::Error);
}
#[test]
fn test_cc_hk_020_http_hook_non_string_url() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "http", "url": 123 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_020: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-020")
.collect();
assert_eq!(cc_hk_020.len(), 1, "Non-string url should fire CC-HK-020");
assert_eq!(cc_hk_020[0].level, DiagnosticLevel::Error);
}
#[test]
fn test_cc_hk_021_if_field_on_non_tool_event() {
let content = r#"{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "echo done", "if": "tool === 'Bash'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_021: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-021")
.collect();
assert_eq!(cc_hk_021.len(), 1);
assert_eq!(cc_hk_021[0].level, DiagnosticLevel::Warning);
assert!(cc_hk_021[0].message.contains("non-tool event"));
}
#[test]
fn test_cc_hk_021_if_field_on_pre_tool_use_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo check", "if": "tool === 'Bash'" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_021: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-021")
.collect();
assert_eq!(cc_hk_021.len(), 0);
}
#[test]
fn test_cc_hk_021_if_field_empty_string_on_pre_tool_use() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo check", "if": "" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_021: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-021")
.collect();
assert_eq!(
cc_hk_021.len(),
1,
"Empty 'if' string on PreToolUse should fire CC-HK-021"
);
assert_eq!(cc_hk_021[0].level, DiagnosticLevel::Warning);
}
#[test]
fn test_cc_hk_021_if_field_non_string_on_pre_tool_use() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo check", "if": 42 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_021: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-021")
.collect();
assert_eq!(
cc_hk_021.len(),
1,
"Non-string 'if' on PreToolUse should fire CC-HK-021"
);
assert_eq!(cc_hk_021[0].level, DiagnosticLevel::Warning);
}
#[test]
fn test_cc_hk_022_invalid_shell_value() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo hi", "shell": "zsh" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_022: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-022")
.collect();
assert_eq!(cc_hk_022.len(), 1);
assert_eq!(cc_hk_022[0].level, DiagnosticLevel::Warning);
assert!(cc_hk_022[0].message.contains("invalid 'shell' value"));
}
#[test]
fn test_cc_hk_022_valid_shell_bash_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo hi", "shell": "bash" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_022: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-022")
.collect();
assert_eq!(cc_hk_022.len(), 0);
}
#[test]
fn test_cc_hk_022_valid_shell_powershell_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo hi", "shell": "powershell" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_022: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-022")
.collect();
assert_eq!(cc_hk_022.len(), 0);
}
#[test]
fn test_cc_hk_023_once_not_boolean() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo hi", "once": "yes" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_023: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-023")
.collect();
assert_eq!(cc_hk_023.len(), 1);
assert_eq!(cc_hk_023[0].level, DiagnosticLevel::Info);
assert!(cc_hk_023[0].message.contains("non-boolean 'once' field"));
}
#[test]
fn test_cc_hk_023_once_boolean_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo hi", "once": true }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_023: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-023")
.collect();
assert_eq!(cc_hk_023.len(), 0);
}
#[test]
fn test_cc_hk_023_once_numeric() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo hi", "once": 1 }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_023: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-023")
.collect();
assert_eq!(cc_hk_023.len(), 1);
}
#[test]
fn test_cc_hk_024_headers_env_var_without_allowed() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "http",
"url": "https://example.com/hook",
"headers": { "Authorization": "$TOKEN" }
}
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_024: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-024")
.collect();
assert_eq!(cc_hk_024.len(), 1);
assert_eq!(cc_hk_024[0].level, DiagnosticLevel::Warning);
assert!(
cc_hk_024[0]
.message
.contains("$VAR interpolation but no 'allowedEnvVars'")
);
}
#[test]
fn test_cc_hk_024_headers_env_var_with_allowed_ok() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "http",
"url": "https://example.com/hook",
"headers": { "Authorization": "$TOKEN" },
"allowedEnvVars": ["TOKEN"]
}
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_024: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-024")
.collect();
assert_eq!(cc_hk_024.len(), 0);
}
#[test]
fn test_cc_hk_024_env_var_in_url_only_no_fire() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "http",
"url": "https://example.com/$SERVICE/hook"
}
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_hk_024: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-024")
.collect();
assert_eq!(cc_hk_024.len(), 0);
}
#[test]
fn test_cc_hk_026_missing_server_flags() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "mcp_tool", "tool": "security_scan" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let hits: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-026")
.collect();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].level, DiagnosticLevel::Error);
assert!(hits[0].message.contains("missing required 'server' field"));
}
#[test]
fn test_cc_hk_026_empty_server_flags() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "mcp_tool", "server": "", "tool": "x" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let hits: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-026")
.collect();
assert_eq!(hits.len(), 1);
}
#[test]
fn test_cc_hk_026_non_string_server_flags() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "mcp_tool", "server": 123, "tool": "x" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let hits: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-026")
.collect();
assert_eq!(hits.len(), 1);
}
#[test]
fn test_cc_hk_027_missing_tool_flags() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "mcp_tool", "server": "my_server" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let hits: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-027")
.collect();
assert_eq!(hits.len(), 1);
assert!(hits[0].message.contains("missing required 'tool' field"));
}
#[test]
fn test_cc_hk_027_empty_tool_flags() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "mcp_tool", "server": "my_server", "tool": "" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let hits: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-027")
.collect();
assert_eq!(hits.len(), 1);
}
#[test]
fn test_cc_hk_mcp_tool_valid_hook_does_not_flag() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "mcp_tool",
"server": "my_server",
"tool": "security_scan",
"input": { "file_path": "${tool_input.file_path}" }
}
]
}
]
}
}"#;
let diagnostics = validate(content);
let hits: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-026" || d.rule == "CC-HK-027")
.collect();
assert!(
hits.is_empty(),
"valid mcp_tool hook must not flag, got {:?}",
hits.iter()
.map(|d| (&d.rule, &d.message))
.collect::<Vec<_>>()
);
}
#[test]
fn test_cc_hk_mcp_tool_does_not_trigger_on_command_hook() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo hi" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let hits: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-026" || d.rule == "CC-HK-027")
.collect();
assert!(hits.is_empty());
}
#[test]
fn test_cc_hk_026_can_be_disabled_independently_of_027() {
let mut config = LintConfig::default();
config.rules_mut().disabled_rules = vec!["CC-HK-026".to_string()];
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{ "type": "mcp_tool" }
]
}
]
}
}"#;
let diagnostics = validate_with_config(content, &config);
let cc_026: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-026")
.collect();
let cc_027: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-027")
.collect();
assert!(
cc_026.is_empty(),
"CC-HK-026 disabled - no diagnostic expected"
);
assert_eq!(
cc_027.len(),
1,
"CC-HK-027 still enabled - 'tool missing' diagnostic expected"
);
}
#[test]
fn test_cc_hk_027_can_be_disabled_independently_of_026() {
let mut config = LintConfig::default();
config.rules_mut().disabled_rules = vec!["CC-HK-027".to_string()];
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{ "type": "mcp_tool" }
]
}
]
}
}"#;
let diagnostics = validate_with_config(content, &config);
let cc_026: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-026")
.collect();
let cc_027: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-027")
.collect();
assert_eq!(cc_026.len(), 1);
assert!(cc_027.is_empty());
}
#[test]
fn test_cc_hk_mcp_tool_both_missing_flags_both() {
let content = r#"{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{ "type": "mcp_tool" }
]
}
]
}
}"#;
let diagnostics = validate(content);
let cc_026: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-026")
.collect();
let cc_027: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-HK-027")
.collect();
assert_eq!(cc_026.len(), 1);
assert_eq!(cc_027.len(), 1);
}