longline 0.15.4

System-installed safety hook for Claude Code
Documentation
mod support;
use std::path::PathBuf;
use support::cli::run_subcommand;
use support::paths::rules_path;

#[test]
fn test_e2e_check_from_file() {
    let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("target")
        .join("test-tmp");
    let _ = std::fs::create_dir_all(&dir);
    let file = dir.join("test-commands-subcmd.txt");
    std::fs::write(&file, "ls -la\nrm -rf /\nchmod 777 /tmp/f\n").unwrap();

    let result = run_subcommand(&["check", "--config", &rules_path(), file.to_str().unwrap()]);
    assert_eq!(result.exit_code, 0);
    assert!(
        result.stdout.contains("DECISION"),
        "Should have header: {}",
        result.stdout
    );
    assert!(
        result.stdout.contains("allow"),
        "Should have allow: {}",
        result.stdout
    );
    assert!(
        result.stdout.contains("deny"),
        "Should have deny: {}",
        result.stdout
    );
    assert!(
        result.stdout.contains("ask"),
        "Should have ask: {}",
        result.stdout
    );

    let _ = std::fs::remove_file(&file);
}

#[test]
fn test_e2e_check_filter_deny() {
    let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("target")
        .join("test-tmp");
    let _ = std::fs::create_dir_all(&dir);
    let file = dir.join("test-commands-filter-subcmd.txt");
    std::fs::write(&file, "ls -la\nrm -rf /\nchmod 777 /tmp/f\n").unwrap();

    let result = run_subcommand(&[
        "check",
        "--config",
        &rules_path(),
        "--filter",
        "deny",
        file.to_str().unwrap(),
    ]);
    assert_eq!(result.exit_code, 0);
    assert!(
        result.stdout.contains("deny"),
        "Should have deny: {}",
        result.stdout
    );
    assert!(
        result.stdout.contains("rm -rf /"),
        "Should contain denied command: {}",
        result.stdout
    );
    assert!(
        !result.stdout.contains("ls -la"),
        "Should not contain allowed command: {}",
        result.stdout
    );
    assert!(
        !result.stdout.contains("chmod 777"),
        "Should not contain ask command: {}",
        result.stdout
    );

    let _ = std::fs::remove_file(&file);
}

#[test]
fn test_e2e_check_skips_comments_and_blanks() {
    let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("target")
        .join("test-tmp");
    let _ = std::fs::create_dir_all(&dir);
    let file = dir.join("test-commands-comments-subcmd.txt");
    std::fs::write(&file, "# this is a comment\n\nls -la\n").unwrap();

    let result = run_subcommand(&["check", "--config", &rules_path(), file.to_str().unwrap()]);
    assert_eq!(result.exit_code, 0);
    assert!(
        result.stdout.contains("ls -la"),
        "Should contain the ls command: {}",
        result.stdout
    );
    assert!(
        !result.stdout.contains("comment"),
        "Should not contain comment text: {}",
        result.stdout
    );
    let cmd_count = result.stdout.matches("ls -la").count();
    assert_eq!(
        cmd_count, 1,
        "Should have exactly 1 result, got {}: {}",
        cmd_count, result.stdout
    );

    let _ = std::fs::remove_file(&file);
}

#[test]
fn test_e2e_check_with_dir_uses_project_rules() {
    let dir = tempfile::TempDir::new().unwrap();
    std::fs::create_dir_all(dir.path().join(".git")).unwrap();
    let claude_dir = dir.path().join(".claude");
    std::fs::create_dir_all(&claude_dir).unwrap();
    std::fs::write(
        claude_dir.join("longline.yaml"),
        r#"
rules:
  - id: project-ask-on-push
    level: high
    match:
      command: git
      args:
        any_of: ["push"]
    decision: ask
    reason: "Requires approval before pushing"
"#,
    )
    .unwrap();

    let cmd_file = dir.path().join("cmds.txt");
    std::fs::write(&cmd_file, "git push origin main\n").unwrap();

    let result = run_subcommand(&[
        "check",
        "--config",
        &rules_path(),
        "--dir",
        dir.path().to_str().unwrap(),
        cmd_file.to_str().unwrap(),
    ]);
    assert_eq!(result.exit_code, 0);
    assert!(
        result.stdout.contains("project-ask-on-push"),
        "Should match project rule: {}",
        result.stdout
    );
}