repopilot 0.13.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use super::*;
use crate::findings::types::{Confidence, Severity};
use crate::scan::facts::FileFacts;
use std::path::PathBuf;

#[test]
fn ignores_unwrap_in_rust_tests() {
    let file = facts(
        "tests/parser_test.rs",
        "let value = parse().unwrap();",
        true,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert!(findings.is_empty());
}

#[test]
fn reports_unwrap_in_rust_library_code() {
    let file = facts(
        "src/domain/parser.rs",
        "let value = parse().unwrap();",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].rule_id, RULE_ID);
    assert_eq!(findings[0].severity, Severity::Medium);
    assert_eq!(findings[0].confidence, Confidence::High);
    assert!(findings[0].title.contains("unwrap()"));
    assert!(
        findings[0]
            .description
            .contains("confidence is HIGH because")
    );
    assert!(findings[0].recommendation.contains("Return `Result`"));
}

#[test]
fn reports_panic_in_domain_code_as_high() {
    let file = facts(
        "src/domain/user.rs",
        "panic!(\"invalid user state\");",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].severity, Severity::High);
    assert!(findings[0].title.contains("panic!"));
}

#[test]
fn reports_todo_in_production_code_as_high() {
    let file = facts(
        "src/service.rs",
        "todo!(\"implement payment flow\");",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].severity, Severity::High);
    assert!(findings[0].title.contains("todo!"));
}

#[test]
fn lowers_unwrap_severity_in_rust_cli_boundary() {
    let file = facts("src/main.rs", "let config = load_config().unwrap();", false);

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].severity, Severity::Low);
    assert_eq!(findings[0].confidence, Confidence::Low);
    assert!(findings[0].description.contains("CLI boundary"));
}

#[test]
fn ignores_commented_panic_patterns() {
    let file = facts(
        "src/lib.rs",
        "// let value = parse().unwrap();\n/// panic!(\"example\");\n/*\n * value.unwrap()\n */\n// panic!(\"old code\");",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert!(findings.is_empty());
}

#[test]
fn ignores_string_literal_panic_patterns() {
    let file = facts(
        "src/lib.rs",
        "let text = \"value.unwrap() and panic!(\\\"example\\\")\";",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert!(findings.is_empty());
}

#[test]
fn ignores_string_write_unwraps_in_report_renderers() {
    let file = facts(
        "src/output/markdown.rs",
        "pub fn render(output: &mut String) {\n    writeln!(output, \"# Report\").unwrap();\n}\n",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert!(findings.is_empty());
}

#[test]
fn ignores_multiline_string_write_unwraps_in_report_renderers() {
    let file = facts(
        "src/output/console.rs",
        "pub fn render(output: &mut String) {\n    writeln!(\n        output,\n        \"Findings: {}\",\n        3\n    )\n    .unwrap();\n}\n",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert!(findings.is_empty());
}

#[test]
fn still_reports_non_renderer_unwraps_in_output_modules() {
    let file = facts(
        "src/output/markdown.rs",
        "pub fn render(value: Option<&str>) -> &str {\n    value.unwrap()\n}\n",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].rule_id, RULE_ID);
}

#[test]
fn still_reports_unwraps_inside_renderer_format_arguments() {
    let file = facts(
        "src/output/console.rs",
        "pub fn render(output: &mut String, value: Option<&str>) {\n    writeln!(output, \"{}\", value.unwrap());\n}\n",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].rule_id, RULE_ID);
}

#[test]
fn does_not_report_functional_iterator_pipeline_without_panic_risk() {
    let file = facts(
        "src/domain/users.rs",
        "let names = users\n    .iter()\n    .filter(|user| user.is_active)\n    .map(|user| user.name.clone())\n    .collect::<Vec<_>>();\n",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert!(findings.is_empty());
}

#[test]
fn reports_expect_in_domain_code() {
    let file = facts(
        "src/domain/parser.rs",
        "let value = parse().expect(\"valid domain input\");",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].severity, Severity::Medium);
    assert_eq!(findings[0].confidence, Confidence::High);
    assert!(findings[0].title.contains("expect()"));
    assert!(findings[0].description.contains("Rust domain code"));
}

#[test]
fn ignores_valid_regex_expect_in_production_code() {
    let file = facts(
        "src/domain/parser.rs",
        "let matcher = Regex::new(r\"^[a-z]+$\").expect(\"valid parser regex\");",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert!(findings.is_empty());
}

#[test]
fn ignores_mutex_poison_invariant_expect() {
    let file = facts(
        "src/state.rs",
        "let guard = cache.lock().expect(\"cache mutex should not be poisoned\");",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert!(findings.is_empty());
}

#[test]
fn upgrades_external_parse_unwrap_to_high() {
    let file = facts(
        "src/domain/parser.rs",
        "let value = raw.parse::<u64>().unwrap();",
        false,
    );

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].severity, Severity::High);
    assert_eq!(findings[0].confidence, Confidence::High);
}

#[test]
fn reports_unimplemented_in_production_code() {
    let file = facts("src/lib.rs", "unimplemented!(\"missing adapter\");", false);

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert_eq!(findings.len(), 1);
    assert_eq!(findings[0].severity, Severity::High);
    assert!(findings[0].title.contains("unimplemented!"));
}

#[test]
fn does_not_run_on_non_rust_files() {
    let mut file = facts("src/app.ts", "panic!(\"not rust\");", false);
    file.language = Some("TypeScript".to_string());

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert!(findings.is_empty());
}

#[test]
fn does_not_run_without_file_content() {
    let mut file = facts("src/lib.rs", "panic!(\"missing content\");", false);
    file.content = None;

    let findings = RustPanicRiskAudit.audit(&file, &ScanConfig::default());

    assert!(findings.is_empty());
}

fn facts(path: &str, content: &str, has_inline_tests: bool) -> FileFacts {
    FileFacts {
        path: PathBuf::from(path),
        language: Some("Rust".to_string()),
        non_empty_lines: content.lines().count(),
        branch_count: 0,
        imports: Vec::new(),
        content: Some(content.to_string()),
        has_inline_tests,
    }
}