repopilot 0.9.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use super::header::risk_level;
use super::{VibeCategory, VibeOptions, render};
use crate::findings::types::{Evidence, Finding, FindingCategory, Severity};
use crate::scan::types::ScanSummary;
use std::path::PathBuf;

fn make_finding(
    rule_id: &str,
    title: &str,
    severity: Severity,
    category: FindingCategory,
    path: &str,
    line: usize,
) -> Finding {
    Finding {
        id: rule_id.to_string(),
        rule_id: rule_id.to_string(),
        recommendation: Finding::recommendation_for_rule_id(rule_id),
        title: title.to_string(),
        description: format!("Description for {title}"),
        category,
        severity,
        confidence: Default::default(),
        evidence: vec![Evidence {
            path: PathBuf::from(path),
            line_start: line,
            line_end: None,
            snippet: format!("// snippet for {title}"),
        }],
        workspace_package: None,
        docs_url: None,
    }
}

fn make_summary(findings: Vec<Finding>) -> ScanSummary {
    ScanSummary {
        root_path: PathBuf::from("/my-project"),
        files_discovered: 0,
        files_count: 42,
        lines_of_code: 3000,
        directories_count: 10,
        languages: vec![],
        findings,
        ..Default::default()
    }
}

#[test]
fn renders_header_with_risk_high() {
    let findings = vec![make_finding(
        "security.secret",
        "Hardcoded secret",
        Severity::Critical,
        FindingCategory::Security,
        "src/auth.rs",
        42,
    )];
    let summary = make_summary(findings);
    let output = render(&summary, &VibeOptions::default());
    assert!(output.contains("# RepoPilot Vibe Check — my-project"));
    assert!(output.contains("🔴 HIGH"));
    assert!(output.contains("1 critical"));
}

#[test]
fn renders_no_header_when_flag_set() {
    let summary = make_summary(vec![]);
    let opts = VibeOptions {
        no_header: true,
        ..Default::default()
    };
    let output = render(&summary, &opts);
    assert!(!output.contains("# RepoPilot Vibe Check"));
}

#[test]
fn focus_filters_to_security_only() {
    let findings = vec![
        make_finding(
            "security.secret",
            "Hardcoded secret",
            Severity::Critical,
            FindingCategory::Security,
            "src/auth.rs",
            1,
        ),
        make_finding(
            "architecture.large-file",
            "Large file",
            Severity::Medium,
            FindingCategory::Architecture,
            "src/big.rs",
            1,
        ),
    ];
    let summary = make_summary(findings);
    let opts = VibeOptions {
        focus: Some(VibeCategory::Security),
        ..Default::default()
    };
    let output = render(&summary, &opts);
    assert!(output.contains("Security"));
    assert!(!output.contains("Architecture"));
}

#[test]
fn focus_quality_includes_testing_and_code_quality() {
    let findings = vec![
        make_finding(
            "code-quality.todo",
            "TODO marker",
            Severity::Low,
            FindingCategory::CodeQuality,
            "src/a.rs",
            1,
        ),
        make_finding(
            "testing.missing-tests",
            "Missing tests",
            Severity::Medium,
            FindingCategory::Testing,
            "src/b.rs",
            1,
        ),
        make_finding(
            "security.secret",
            "Secret",
            Severity::Critical,
            FindingCategory::Security,
            "src/c.rs",
            1,
        ),
    ];
    let summary = make_summary(findings);
    let opts = VibeOptions {
        focus: Some(VibeCategory::Quality),
        ..Default::default()
    };
    let output = render(&summary, &opts);
    assert!(output.contains("Code Quality") || output.contains("Testing"));
    assert!(!output.contains("Security"));
}

#[test]
fn small_budget_renders_truncation_notice() {
    let findings: Vec<Finding> = (0..8)
        .map(|i| {
            make_finding(
                "security.secret",
                &format!("Hardcoded secret {i}"),
                Severity::High,
                FindingCategory::Security,
                &format!("src/auth_{i}.rs"),
                i + 1,
            )
        })
        .collect();
    let summary = make_summary(findings);
    let opts = VibeOptions {
        budget_tokens: 20,
        no_header: true,
        ..Default::default()
    };
    let output = render(&summary, &opts);
    assert!(output.contains("Output truncated to stay within token budget"));
}

#[test]
fn risk_level_moderate_for_one_high() {
    let findings = [make_finding(
        "arch.coupling",
        "High coupling",
        Severity::High,
        FindingCategory::Architecture,
        "src/a.rs",
        1,
    )];
    let refs: Vec<&Finding> = findings.iter().collect();
    assert_eq!(risk_level(&refs), "🟡 MODERATE");
}

#[test]
fn risk_level_elevated_for_three_high() {
    let findings: Vec<Finding> = (0..3)
        .map(|i| {
            make_finding(
                "arch.coupling",
                "High coupling",
                Severity::High,
                FindingCategory::Architecture,
                "src/a.rs",
                i,
            )
        })
        .collect();
    let refs: Vec<&Finding> = findings.iter().collect();
    assert_eq!(risk_level(&refs), "🟠 ELEVATED");
}

#[test]
fn risk_level_low_for_no_high() {
    let findings = [make_finding(
        "code.todo",
        "TODO",
        Severity::Low,
        FindingCategory::CodeQuality,
        "src/a.rs",
        1,
    )];
    let refs: Vec<&Finding> = findings.iter().collect();
    assert_eq!(risk_level(&refs), "🟢 LOW");
}

#[test]
fn risk_level_moderate_for_many_medium_findings() {
    let findings: Vec<Finding> = (0..10)
        .map(|index| {
            make_finding(
                "architecture.large-file",
                "Large file",
                Severity::Medium,
                FindingCategory::Architecture,
                "src/a.rs",
                index + 1,
            )
        })
        .collect();
    let refs: Vec<&Finding> = findings.iter().collect();
    assert_eq!(risk_level(&refs), "🟡 MODERATE");
}

#[test]
fn token_estimate_in_footer() {
    let summary = make_summary(vec![]);
    let output = render(&summary, &VibeOptions::default());
    assert!(output.contains("tokens"));
    assert!(output.contains("budget: 4096"));
}

#[test]
fn top_recommendations_omitted_when_no_high_findings() {
    let findings = vec![make_finding(
        "code.todo",
        "TODO marker",
        Severity::Low,
        FindingCategory::CodeQuality,
        "src/a.rs",
        1,
    )];
    let summary = make_summary(findings);
    let output = render(&summary, &VibeOptions::default());
    assert!(!output.contains("## Top Recommendations"));
}

#[test]
fn top_recommendations_include_medium_clusters_when_no_high_findings() {
    let findings = vec![
        make_finding(
            "architecture.large-file",
            "Large file detected",
            Severity::Medium,
            FindingCategory::Architecture,
            "src/a.rs",
            1,
        ),
        make_finding(
            "architecture.large-file",
            "Large file detected",
            Severity::Medium,
            FindingCategory::Architecture,
            "src/b.rs",
            1,
        ),
    ];
    let summary = make_summary(findings);
    let output = render(&summary, &VibeOptions::default());
    assert!(output.contains("## Top Recommendations"));
    assert!(output.contains("MEDIUM 2 finding(s)"));
}

#[test]
fn top_recommendations_shown_for_high_findings() {
    let findings = vec![make_finding(
        "security.secret",
        "Hardcoded secret",
        Severity::High,
        FindingCategory::Security,
        "src/a.rs",
        5,
    )];
    let summary = make_summary(findings);
    let output = render(&summary, &VibeOptions::default());
    assert!(output.contains("## Top Recommendations"));
}

#[test]
fn empty_scan_renders_without_panic() {
    let summary = make_summary(vec![]);
    let output = render(&summary, &VibeOptions::default());
    assert!(output.contains("RepoPilot Vibe Check"));
    assert!(output.contains("0 findings"));
}

#[test]
fn vibe_category_from_str() {
    assert_eq!("security".parse(), Ok(VibeCategory::Security));
    assert_eq!("arch".parse(), Ok(VibeCategory::Architecture));
    assert_eq!("quality".parse(), Ok(VibeCategory::Quality));
    assert_eq!("framework".parse(), Ok(VibeCategory::Framework));
    assert_eq!("all".parse(), Ok(VibeCategory::All));
    assert_eq!("unknown".parse::<VibeCategory>(), Err(()));
}