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, Evidence, FindingCategory, Severity};
use crate::risk::{RiskAssessment, RiskPriority};
use std::path::PathBuf;

fn finding(rule_id: &str, category: FindingCategory, severity: Severity) -> Finding {
    Finding {
        rule_id: rule_id.to_string(),
        category,
        severity,
        confidence: Confidence::High,
        ..Default::default()
    }
}

fn finding_with_path(
    rule_id: &str,
    category: FindingCategory,
    severity: Severity,
    path: &str,
) -> Finding {
    let mut finding = finding(rule_id, category, severity);
    finding.evidence = vec![Evidence {
        path: PathBuf::from(path),
        line_start: 1,
        line_end: None,
        snippet: "process.exit(1);".to_string(),
    }];
    finding
}

fn finding_with_priority(
    rule_id: &str,
    category: FindingCategory,
    severity: Severity,
    priority: RiskPriority,
) -> Finding {
    let mut finding = finding(rule_id, category, severity);
    finding.risk = RiskAssessment {
        priority,
        ..RiskAssessment::default()
    };
    finding
}

#[test]
fn default_profile_hides_testing_gaps_by_intent() {
    let finding = finding(
        "testing.source-without-test",
        FindingCategory::Testing,
        Severity::Low,
    );

    let decision = classify_visibility(&finding);

    assert_eq!(decision.intent, FindingIntent::TestingGap);
    assert!(!decision.visible_by_default);
}

#[test]
fn default_profile_hides_high_maintainability_signals() {
    let finding = finding(
        "architecture.large-file",
        FindingCategory::Architecture,
        Severity::High,
    );

    let decision = classify_visibility(&finding);

    assert_eq!(decision.intent, FindingIntent::Maintainability);
    assert!(!decision.visible_by_default);
}

#[test]
fn default_profile_hides_deep_relative_imports() {
    let finding = finding(
        "architecture.deep-relative-imports",
        FindingCategory::Architecture,
        Severity::High,
    );

    let decision = classify_visibility(&finding);

    assert_eq!(decision.intent, FindingIntent::Maintainability);
    assert!(!decision.visible_by_default);
}

#[test]
fn default_profile_hides_high_priority_broad_maintainability_heuristics() {
    let finding = finding_with_priority(
        "code-quality.long-function",
        FindingCategory::CodeQuality,
        Severity::Medium,
        RiskPriority::P1,
    );

    let decision = classify_visibility(&finding);

    assert_eq!(decision.intent, FindingIntent::Maintainability);
    assert!(!decision.visible_by_default);
}

#[test]
fn default_profile_keeps_stable_import_graph_architecture_risks() {
    let finding = finding_with_priority(
        "architecture.excessive-fan-out",
        FindingCategory::Architecture,
        Severity::Medium,
        RiskPriority::P2,
    );

    let decision = classify_visibility(&finding);

    assert_eq!(decision.intent, FindingIntent::ActionableRisk);
    assert!(decision.visible_by_default);
}

#[test]
fn default_profile_keeps_validated_secret_candidates() {
    let finding = finding(
        "security.secret-candidate",
        FindingCategory::Security,
        Severity::High,
    );

    let decision = classify_visibility(&finding);

    assert_eq!(decision.intent, FindingIntent::SecurityRisk);
    assert!(decision.visible_by_default);
}

#[test]
fn default_profile_hides_script_process_exit() {
    let finding = finding_with_path(
        "language.javascript.runtime-exit-risk",
        FindingCategory::CodeQuality,
        Severity::High,
        "scripts/verify-release.mjs",
    );

    let decision = classify_visibility(&finding);

    assert_eq!(decision.intent, FindingIntent::RuntimeRisk);
    assert!(!decision.visible_by_default);
}

#[test]
fn default_profile_hides_windows_script_process_exit() {
    let finding = finding_with_path(
        "language.javascript.runtime-exit-risk",
        FindingCategory::CodeQuality,
        Severity::High,
        r"tools\verify-release.mjs",
    );

    let decision = classify_visibility(&finding);

    assert_eq!(decision.intent, FindingIntent::RuntimeRisk);
    assert!(!decision.visible_by_default);
}

#[test]
fn default_profile_keeps_source_process_exit() {
    let finding = finding_with_path(
        "language.javascript.runtime-exit-risk",
        FindingCategory::CodeQuality,
        Severity::High,
        "src/runtime/server.ts",
    );

    let decision = classify_visibility(&finding);

    assert_eq!(decision.intent, FindingIntent::RuntimeRisk);
    assert!(decision.visible_by_default);
}

#[test]
fn hidden_suggestion_summary_groups_by_intent_rule_and_reason() {
    let findings = vec![
        finding(
            "architecture.large-file",
            FindingCategory::Architecture,
            Severity::High,
        ),
        finding(
            "architecture.large-file",
            FindingCategory::Architecture,
            Severity::High,
        ),
        finding(
            "testing.source-without-test",
            FindingCategory::Testing,
            Severity::Low,
        ),
    ];

    let summaries = build_hidden_suggestion_summaries(&findings);

    assert_eq!(summaries.len(), 2);
    assert_eq!(summaries[0].rule_id, "architecture.large-file");
    assert_eq!(summaries[0].intent, "maintainability");
    assert_eq!(summaries[0].count, 2);
    assert_eq!(summaries[1].rule_id, "testing.source-without-test");
    assert_eq!(summaries[1].intent, "testing-gap");
    assert_eq!(summaries[1].count, 1);
}

#[test]
fn apply_default_profile_recomputes_visible_counts_and_hidden_breakdown() {
    let visible = finding(
        "security.secret-candidate",
        FindingCategory::Security,
        Severity::High,
    );
    let hidden = finding(
        "architecture.large-file",
        FindingCategory::Architecture,
        Severity::High,
    );

    let mut summary = ScanSummary {
        hidden_suggestions: Vec::new(),
        non_empty_lines: 1_000,
        findings: vec![visible, hidden],
        ..Default::default()
    };

    apply_visibility_profile(&mut summary, FindingVisibilityProfile::Default);

    assert_eq!(summary.visibility_profile.as_deref(), Some("default"));
    assert_eq!(summary.raw_findings_count, 2);
    assert_eq!(summary.visible_findings_count, 1);
    assert_eq!(summary.hidden_suggestions_count, 1);
    assert_eq!(summary.raw_signal_quality.findings_total, 2);
    assert_eq!(summary.visible_signal_quality.findings_total, 1);
    assert_eq!(summary.signal_quality.findings_total, 1);
    assert_eq!(summary.hidden_suggestions.len(), 1);
    assert_eq!(summary.hidden_suggestions[0].intent, "maintainability");
    assert_eq!(
        summary.hidden_suggestions[0].rule_id,
        "architecture.large-file"
    );
    assert_eq!(summary.findings.len(), 1);
    assert_eq!(summary.findings[0].rule_id, "security.secret-candidate");
}

#[test]
fn strict_profile_keeps_all_findings_and_clears_hidden_breakdown() {
    let mut summary = ScanSummary {
        non_empty_lines: 1_000,
        hidden_suggestions: vec![HiddenSuggestionSummary {
            intent: "maintainability".to_string(),
            rule_id: "architecture.large-file".to_string(),
            category: "architecture".to_string(),
            reason: "example".to_string(),
            count: 1,
        }],
        findings: vec![
            finding(
                "security.secret-candidate",
                FindingCategory::Security,
                Severity::High,
            ),
            finding(
                "architecture.large-file",
                FindingCategory::Architecture,
                Severity::High,
            ),
        ],
        ..Default::default()
    };

    apply_visibility_profile(&mut summary, FindingVisibilityProfile::Strict);

    assert_eq!(summary.visibility_profile.as_deref(), Some("strict"));
    assert_eq!(summary.raw_findings_count, 2);
    assert_eq!(summary.visible_findings_count, 2);
    assert_eq!(summary.hidden_suggestions_count, 0);
    assert!(summary.hidden_suggestions.is_empty());
    assert_eq!(summary.findings.len(), 2);
}