guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
use std::{fs, process::Command};

use tempfile::TempDir;

/// Test that commands actually execute by spawning a separate process
/// This test checks what should be the core functionality of the hook executor
#[tokio::test]
async fn test_commands_actually_execute() {
    // Save original directory (in case previous tests left us in a deleted temp dir)
    let original_dir = std::env::current_dir().unwrap_or_else(|_| {
        // If current dir is invalid, use the project root
        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
    });
    struct DirGuard(std::path::PathBuf);
    impl Drop for DirGuard {
        fn drop(&mut self) {
            let _ = std::env::set_current_dir(&self.0);
        }
    }
    let _guard = DirGuard(original_dir.clone());

    // Create a temporary directory for testing
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let temp_path = temp_dir.path();

    // Initialize git repo
    Command::new("git")
        .args(["init"])
        .current_dir(temp_path)
        .output()
        .expect("Failed to init git repo");

    Command::new("git")
        .args(["config", "user.name", "Test User"])
        .current_dir(temp_path)
        .output()
        .expect("Failed to set git user.name");

    Command::new("git")
        .args(["config", "user.email", "test@example.com"])
        .current_dir(temp_path)
        .output()
        .expect("Failed to set git user.email");

    // Create a test file that will be created by our hook command
    let output_file = temp_path.join("hook_executed.txt");

    // Create guardy.yaml with a simple command that creates a file
    let config_content = format!(
        r#"hooks:
  pre-commit:
    parallel: true
    commands:
      test-echo:
        run: echo "HOOK EXECUTED" > {}
        priority: 1
"#,
        output_file.display()
    );

    let config_path = temp_path.join("guardy.yaml");
    fs::write(&config_path, config_content).expect("Failed to write config");

    // Create a test file and stage it
    let test_file = temp_path.join("test.txt");
    fs::write(&test_file, "test content").expect("Failed to create test file");

    Command::new("git")
        .args(["add", "test.txt"])
        .current_dir(temp_path)
        .output()
        .expect("Failed to stage test file");

    // Build the guardy binary path
    // When running under cargo llvm-cov, the binary is in a different location
    let guardy_bin = if let Ok(cargo_bin_path) = std::env::var("CARGO_BIN_EXE_guardy") {
        // This env var is set by cargo test and points to the test binary
        std::path::PathBuf::from(cargo_bin_path)
    } else {
        // Fallback: try to build it ourselves
        let potential_paths = vec![
            // Try llvm-cov target directory first
            original_dir
                .join("target")
                .join("llvm-cov-target")
                .join("debug")
                .join("guardy"),
            // Then regular debug directory
            original_dir.join("target").join("debug").join("guardy"),
            // Try workspace root target directory
            original_dir
                .parent()
                .and_then(|p| p.parent())
                .map(|p| p.join("target").join("debug").join("guardy"))
                .unwrap_or_default(),
        ];

        let mut found_bin = None;
        for path in &potential_paths {
            if path.exists() {
                found_bin = Some(path.clone());
                break;
            }
        }

        if found_bin.is_none() {
            // Last resort: try to build it
            let build_output = Command::new("cargo")
                .args(["build", "--bin", "guardy"])
                .current_dir(&original_dir)
                .output()
                .expect("Failed to build guardy binary");

            if !build_output.status.success() {
                panic!(
                    "Failed to build guardy binary. Stdout: {}\nStderr: {}",
                    String::from_utf8_lossy(&build_output.stdout),
                    String::from_utf8_lossy(&build_output.stderr)
                );
            }

            // After building, try to find it again
            for path in &potential_paths {
                if path.exists() {
                    found_bin = Some(path.clone());
                    break;
                }
            }
        }

        found_bin.expect("Could not find or build guardy binary")
    };

    // Verify the binary exists
    assert!(
        guardy_bin.exists(),
        "Guardy binary not found at: {}",
        guardy_bin.display()
    );

    // Run the hook using the guardy binary with custom config and tracing
    let output = Command::new(&guardy_bin)
        .args([
            "--config",
            config_path.to_str().unwrap(),
            "hooks",
            "run",
            "pre-commit",
        ])
        .current_dir(temp_path)
        .env("RUST_LOG", "trace")
        .output()
        .expect("Failed to run guardy hooks");

    println!("Guardy output: {}", String::from_utf8_lossy(&output.stdout));
    println!("Guardy stderr: {}", String::from_utf8_lossy(&output.stderr));

    // Let's also check what the config file actually contains
    println!("Config file contents:");
    println!("{}", fs::read_to_string(&config_path).unwrap());

    // Check that the command executed successfully
    assert!(
        output.status.success(),
        "Guardy hook execution should succeed"
    );

    // Most importantly: Check that the file was actually created by the command
    assert!(
        output_file.exists(),
        "Hook command should have created output file at {}",
        output_file.display()
    );

    // Verify the content
    let content = fs::read_to_string(&output_file).expect("Failed to read hook output file");
    assert!(
        content.trim() == "HOOK EXECUTED",
        "Hook output file should contain expected content, got: '{}'",
        content.trim()
    );

    println!("✅ Hook command actually executed and created expected file");
}

// TODO: Add more comprehensive tests using subprocess approach
// - Test multiple commands execute in priority order
// - Test failed command handling
// - Test continue_on_error behavior