openlatch-client 0.0.1

The open-source security layer for AI agents — client forwarder
Documentation
/// Integration tests for the openlatch-hook binary.
///
/// These tests verify the hook binary's stdin/stdout contract and fail-open
/// behavior without a running daemon.
use assert_cmd::Command;
use predicates::prelude::*;

/// Hook binary returns an allow verdict with expected JSON fields when the
/// daemon is unreachable (no daemon running on the test port).
#[test]
fn hook_binary_returns_allow_when_daemon_unreachable() {
    let input = r#"{"tool_name": "Write", "tool_input": {"path": "test.rs"}}"#;
    Command::cargo_bin("openlatch-hook")
        .unwrap()
        .write_stdin(input)
        .assert()
        .success()
        .stdout(predicate::str::contains("permissionDecision"))
        .stdout(predicate::str::contains("allow"));
}

/// Hook binary handles empty stdin gracefully — must return an allow verdict
/// and never panic or exit non-zero.
#[test]
fn hook_binary_handles_empty_stdin() {
    Command::cargo_bin("openlatch-hook")
        .unwrap()
        .write_stdin("")
        .assert()
        .success()
        .stdout(predicate::str::contains("allow"));
}

/// Hook binary handles malformed JSON on stdin gracefully — treat as unknown
/// event type, fall back to PreToolUse + allow, never crash.
#[test]
fn hook_binary_handles_malformed_json() {
    Command::cargo_bin("openlatch-hook")
        .unwrap()
        .write_stdin("this is not json {{{")
        .assert()
        .success()
        .stdout(predicate::str::contains("allow"));
}

/// Hook binary detects Stop events and returns "approve" instead of "allow"
/// per the Claude Code hook stdio contract.
#[test]
fn hook_binary_returns_approve_for_stop_event() {
    let input = r#"{"stopReason": "end_turn", "session_id": "abc"}"#;
    Command::cargo_bin("openlatch-hook")
        .unwrap()
        .write_stdin(input)
        .assert()
        .success()
        .stdout(predicate::str::contains("approve"));
}

/// Hook binary output is valid JSON parseable by serde_json.
#[test]
fn hook_binary_stdout_is_valid_json() {
    let input = r#"{"tool_name": "Bash", "tool_input": {"command": "ls"}}"#;
    let output = Command::cargo_bin("openlatch-hook")
        .unwrap()
        .write_stdin(input)
        .output()
        .expect("failed to run openlatch-hook");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("stdout not UTF-8");
    let _: serde_json::Value =
        serde_json::from_str(stdout.trim()).expect("stdout is not valid JSON");
}

/// Hook binary output includes the hookEventName field required by Claude Code.
#[test]
fn hook_binary_output_includes_hook_event_name() {
    let input = r#"{"tool_name": "Write", "tool_input": {"path": "foo.rs"}}"#;
    Command::cargo_bin("openlatch-hook")
        .unwrap()
        .write_stdin(input)
        .assert()
        .success()
        .stdout(predicate::str::contains("hookEventName"))
        .stdout(predicate::str::contains("PreToolUse"));
}

/// Binary size check — only meaningful in release mode.
///
/// Run with: cargo test --release -- --ignored hook_binary_size_under_1mb
#[test]
#[ignore]
fn hook_binary_size_under_1mb() {
    let output = std::process::Command::new("cargo")
        .args([
            "build",
            "--release",
            "--bin",
            "openlatch-hook",
            "--no-default-features",
        ])
        .output()
        .expect("cargo build failed");
    assert!(output.status.success(), "cargo build --release failed");

    let binary_path = if cfg!(windows) {
        std::path::PathBuf::from("target/release/openlatch-hook.exe")
    } else {
        std::path::PathBuf::from("target/release/openlatch-hook")
    };

    let metadata = std::fs::metadata(&binary_path)
        .unwrap_or_else(|_| panic!("binary not found at {:?}", binary_path));
    assert!(
        metadata.len() < 1_048_576,
        "hook binary is {}B, must be < 1MB",
        metadata.len()
    );
}