git-iris 2.1.0

AI-powered Git workflow assistant for smart commits, code reviews, changelogs, and release notes
Documentation
use std::collections::BTreeSet;
use std::fs;

use tempfile::TempDir;

use crate::agents::tools::static_analysis::{
    StaticAnalyzer, executable_exists, select_analysis_commands, unavailable_analysis_summary,
};

fn availability(commands: &[&str]) -> impl Fn(&str) -> bool {
    let commands = commands.iter().copied().collect::<BTreeSet<_>>();
    move |command| commands.contains(command)
}

#[test]
fn static_analysis_auto_selects_installed_direct_linters() {
    let temp_dir = TempDir::new().expect("temp dir should be created");
    fs::write(
        temp_dir.path().join("Cargo.toml"),
        "[package]\nname='demo'\nversion='0.1.0'\n",
    )
    .expect("Cargo.toml should be written");
    fs::write(temp_dir.path().join("pyproject.toml"), "[tool.ruff]\n")
        .expect("pyproject should be written");
    fs::write(temp_dir.path().join("package.json"), "{}\n").expect("package should be written");
    fs::write(temp_dir.path().join("go.mod"), "module demo\n").expect("go.mod should be written");

    let commands = select_analysis_commands(
        temp_dir.path(),
        StaticAnalyzer::Auto,
        availability(&["cargo", "ruff", "biome", "golangci-lint", "go"]),
    );

    let names = commands
        .iter()
        .map(|command| command.name)
        .collect::<Vec<_>>();
    assert_eq!(
        names,
        vec![
            "Rust clippy",
            "Python ruff",
            "JavaScript/TypeScript biome",
            "Go golangci-lint"
        ]
    );
}

#[test]
fn static_analysis_uses_oxlint_and_go_vet_fallbacks() {
    let temp_dir = TempDir::new().expect("temp dir should be created");
    fs::write(temp_dir.path().join("package.json"), "{}\n").expect("package should be written");
    fs::write(temp_dir.path().join("go.mod"), "module demo\n").expect("go.mod should be written");

    let commands = select_analysis_commands(
        temp_dir.path(),
        StaticAnalyzer::Auto,
        availability(&["oxlint", "go"]),
    );

    assert_eq!(commands[0].executable, "oxlint");
    assert_eq!(commands[0].args, vec!["."]);
    assert_eq!(commands[1].executable, "go");
    assert_eq!(commands[1].args, vec!["vet", "./..."]);
}

#[test]
fn static_analysis_respects_requested_analyzer() {
    let temp_dir = TempDir::new().expect("temp dir should be created");
    fs::write(
        temp_dir.path().join("Cargo.toml"),
        "[package]\nname='demo'\nversion='0.1.0'\n",
    )
    .expect("Cargo.toml should be written");
    fs::write(temp_dir.path().join("pyproject.toml"), "[tool.ruff]\n")
        .expect("pyproject should be written");

    let commands = select_analysis_commands(
        temp_dir.path(),
        StaticAnalyzer::Python,
        availability(&["cargo", "ruff"]),
    );

    assert_eq!(commands.len(), 1);
    assert_eq!(commands[0].name, "Python ruff");
}

#[test]
fn static_analysis_returns_empty_without_markers_or_commands() {
    let temp_dir = TempDir::new().expect("temp dir should be created");

    let commands = select_analysis_commands(
        temp_dir.path(),
        StaticAnalyzer::Auto,
        availability(&["cargo", "ruff", "biome", "golangci-lint"]),
    );

    assert!(commands.is_empty());
}

#[test]
fn static_analysis_prefers_biome_over_oxlint() {
    let temp_dir = TempDir::new().expect("temp dir should be created");
    fs::write(temp_dir.path().join("package.json"), "{}\n").expect("package should be written");

    let commands = select_analysis_commands(
        temp_dir.path(),
        StaticAnalyzer::Javascript,
        availability(&["biome", "oxlint"]),
    );

    assert_eq!(commands.len(), 1);
    assert_eq!(commands[0].executable, "biome");
}

#[test]
fn static_analysis_prefers_golangci_lint_over_go_vet() {
    let temp_dir = TempDir::new().expect("temp dir should be created");
    fs::write(temp_dir.path().join("go.mod"), "module demo\n").expect("go.mod should be written");

    let commands = select_analysis_commands(
        temp_dir.path(),
        StaticAnalyzer::Go,
        availability(&["golangci-lint", "go"]),
    );

    assert_eq!(commands.len(), 1);
    assert_eq!(commands[0].executable, "golangci-lint");
}

#[test]
fn static_analysis_returns_empty_when_requested_command_is_missing() {
    let temp_dir = TempDir::new().expect("temp dir should be created");
    fs::write(
        temp_dir.path().join("Cargo.toml"),
        "[package]\nname='demo'\nversion='0.1.0'\n",
    )
    .expect("Cargo.toml should be written");

    let commands =
        select_analysis_commands(temp_dir.path(), StaticAnalyzer::Rust, availability(&[]));

    assert!(commands.is_empty());
}

#[test]
fn static_analysis_explains_missing_commands_for_detected_projects() {
    let temp_dir = TempDir::new().expect("temp dir should be created");
    fs::write(
        temp_dir.path().join("Cargo.toml"),
        "[package]\nname='demo'\nversion='0.1.0'\n",
    )
    .expect("Cargo.toml should be written");
    fs::write(temp_dir.path().join("package.json"), "{}\n").expect("package should be written");

    let notes =
        unavailable_analysis_summary(temp_dir.path(), StaticAnalyzer::Auto, availability(&[]));

    assert_eq!(
        notes,
        vec![
            "Cargo.toml detected but cargo is not on PATH.",
            "package.json detected but biome and oxlint are not on PATH.",
        ]
    );
}

#[test]
fn static_analysis_explains_auto_mode_without_project_markers() {
    let temp_dir = TempDir::new().expect("temp dir should be created");

    let notes =
        unavailable_analysis_summary(temp_dir.path(), StaticAnalyzer::Auto, availability(&[]));

    assert_eq!(
        notes,
        vec!["No matching project markers detected for auto mode."]
    );
}

#[test]
fn static_analysis_runs_explicit_python_when_ruff_is_installed() {
    let temp_dir = TempDir::new().expect("temp dir should be created");

    let commands = select_analysis_commands(
        temp_dir.path(),
        StaticAnalyzer::Python,
        availability(&["ruff"]),
    );

    assert_eq!(commands.len(), 1);
    assert_eq!(commands[0].executable, "ruff");
    assert_eq!(commands[0].reason, "Python analyzer requested");
}

#[test]
fn static_analysis_runs_explicit_rust_without_project_marker() {
    let temp_dir = TempDir::new().expect("temp dir should be created");

    let commands = select_analysis_commands(
        temp_dir.path(),
        StaticAnalyzer::Rust,
        availability(&["cargo"]),
    );

    assert_eq!(commands.len(), 1);
    assert_eq!(commands[0].executable, "cargo");
    assert_eq!(commands[0].reason, "Rust analyzer requested");
}

#[test]
fn static_analysis_runs_explicit_javascript_without_project_marker() {
    let temp_dir = TempDir::new().expect("temp dir should be created");

    let commands = select_analysis_commands(
        temp_dir.path(),
        StaticAnalyzer::Javascript,
        availability(&["oxlint"]),
    );

    assert_eq!(commands.len(), 1);
    assert_eq!(commands[0].executable, "oxlint");
    assert_eq!(
        commands[0].reason,
        "JavaScript analyzer requested and oxlint is installed"
    );
}

#[test]
fn static_analysis_runs_explicit_go_without_project_marker() {
    let temp_dir = TempDir::new().expect("temp dir should be created");

    let commands =
        select_analysis_commands(temp_dir.path(), StaticAnalyzer::Go, availability(&["go"]));

    assert_eq!(commands.len(), 1);
    assert_eq!(commands[0].executable, "go");
    assert_eq!(
        commands[0].reason,
        "Go analyzer requested and go is installed"
    );
}

#[cfg(unix)]
#[test]
fn static_analysis_ignores_non_executable_files_on_unix() {
    use std::os::unix::fs::PermissionsExt;

    let temp_dir = TempDir::new().expect("temp dir should be created");
    let tool_path = temp_dir.path().join("fake-tool");
    fs::write(&tool_path, "#!/bin/sh\n").expect("tool should be written");
    fs::set_permissions(&tool_path, fs::Permissions::from_mode(0o644))
        .expect("tool permissions should be set");

    assert!(!executable_exists(&tool_path));

    fs::set_permissions(&tool_path, fs::Permissions::from_mode(0o755))
        .expect("tool permissions should be set");

    assert!(executable_exists(&tool_path));
}