lade 0.15.3

Automatically load secrets from your preferred vault as environment variables, and clear them once your shell command is over.
use super::config::{Agent, is_lade_hook};
use serde_json::Value;

const CMD: &str = "/usr/local/bin/lade hook";

#[test]
fn is_lade_hook_matches_bare_and_absolute() {
    assert!(is_lade_hook("lade hook"));
    assert!(is_lade_hook("/usr/local/bin/lade hook"));
    assert!(is_lade_hook("target/debug/lade hook"));
    assert!(is_lade_hook("lade.exe hook"));
    assert!(!is_lade_hook("lade inject"));
    assert!(!is_lade_hook("blade hook"));
    assert!(!is_lade_hook("ladehook"));
    assert!(!is_lade_hook(""));
}

#[test]
fn cursor_merge_from_empty_sets_version_and_hook() {
    let out = Agent::Cursor.merge("", CMD).unwrap();
    let v: Value = serde_json::from_str(&out).unwrap();
    assert_eq!(v["version"], 1);
    let arr = v["hooks"]["preToolUse"].as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["command"], CMD);
    assert_eq!(arr[0]["matcher"], "Shell");
    assert!(Agent::Cursor.has_hook(&out).unwrap());
}

#[test]
fn cursor_merge_is_idempotent() {
    let once = Agent::Cursor.merge("", CMD).unwrap();
    let twice = Agent::Cursor.merge(&once, "lade hook").unwrap();
    let v: Value = serde_json::from_str(&twice).unwrap();
    assert_eq!(v["hooks"]["preToolUse"].as_array().unwrap().len(), 1);
}

#[test]
fn cursor_merge_preserves_existing_hooks() {
    let existing = r#"{"version":1,"hooks":{"preToolUse":[{"command":"other tool","matcher":"Shell"}],"afterFileEdit":[{"command":"fmt"}]}}"#;
    let out = Agent::Cursor.merge(existing, CMD).unwrap();
    let v: Value = serde_json::from_str(&out).unwrap();
    let arr = v["hooks"]["preToolUse"].as_array().unwrap();
    assert_eq!(arr.len(), 2);
    assert!(arr.iter().any(|e| e["command"] == "other tool"));
    assert!(arr.iter().any(|e| e["command"] == CMD));
    assert_eq!(v["hooks"]["afterFileEdit"][0]["command"], "fmt");
}

#[test]
fn cursor_remove_keeps_other_hooks() {
    let merged = Agent::Cursor
        .merge(
            r#"{"hooks":{"preToolUse":[{"command":"other","matcher":"Shell"}]}}"#,
            CMD,
        )
        .unwrap();
    let cleaned = Agent::Cursor.remove(&merged).unwrap();
    let v: Value = serde_json::from_str(&cleaned).unwrap();
    let arr = v["hooks"]["preToolUse"].as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["command"], "other");
    assert!(!Agent::Cursor.has_hook(&cleaned).unwrap());
}

#[test]
fn claude_merge_from_empty_adds_bash_matcher() {
    let out = Agent::Claude.merge("", CMD).unwrap();
    let v: Value = serde_json::from_str(&out).unwrap();
    let arr = v["hooks"]["PreToolUse"].as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["matcher"], "Bash");
    assert_eq!(arr[0]["hooks"][0]["type"], "command");
    assert_eq!(arr[0]["hooks"][0]["command"], CMD);
    assert!(Agent::Claude.has_hook(&out).unwrap());
}

#[test]
fn claude_merge_is_idempotent() {
    let once = Agent::Claude.merge("", CMD).unwrap();
    let twice = Agent::Claude.merge(&once, "lade hook").unwrap();
    let v: Value = serde_json::from_str(&twice).unwrap();
    assert_eq!(v["hooks"]["PreToolUse"].as_array().unwrap().len(), 1);
}

#[test]
fn claude_merge_preserves_existing_settings() {
    let existing = r#"{"model":"sonnet","hooks":{"PreToolUse":[{"matcher":"Write","hooks":[{"type":"command","command":"guard"}]}]}}"#;
    let out = Agent::Claude.merge(existing, CMD).unwrap();
    let v: Value = serde_json::from_str(&out).unwrap();
    assert_eq!(v["model"], "sonnet");
    let arr = v["hooks"]["PreToolUse"].as_array().unwrap();
    assert_eq!(arr.len(), 2);
    assert!(arr.iter().any(|e| e["matcher"] == "Write"));
    assert!(arr.iter().any(|e| e["matcher"] == "Bash"));
}

#[test]
fn claude_remove_prunes_only_our_matcher() {
    let existing = r#"{"hooks":{"PreToolUse":[{"matcher":"Write","hooks":[{"type":"command","command":"guard"}]}]}}"#;
    let merged = Agent::Claude.merge(existing, CMD).unwrap();
    let cleaned = Agent::Claude.remove(&merged).unwrap();
    let v: Value = serde_json::from_str(&cleaned).unwrap();
    let arr = v["hooks"]["PreToolUse"].as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["matcher"], "Write");
    assert!(!Agent::Claude.has_hook(&cleaned).unwrap());
}

#[test]
fn has_hook_false_on_empty() {
    assert!(!Agent::Cursor.has_hook("").unwrap());
    assert!(!Agent::Claude.has_hook("   ").unwrap());
}

#[test]
fn merge_rejects_invalid_json() {
    assert!(Agent::Cursor.merge("{not json", CMD).is_err());
}

#[test]
fn merge_preserves_user_key_order() {
    let existing = r#"{"zebra":1,"alpha":2,"hooks":{"PreToolUse":[]}}"#;
    let out = Agent::Claude.merge(existing, CMD).unwrap();
    let zebra = out.find("zebra").expect("zebra kept");
    let alpha = out.find("alpha").expect("alpha kept");
    assert!(zebra < alpha, "user's original key order must be preserved");
}