repopilot 0.20.0

Local-first CLI for reviewing Git changes, security boundaries, and blast radius before merge.
Documentation
use super::*;
use crate::audits::context::{
    AuditContext, FileRole, LanguageKind, ProgrammingParadigm, RuntimeKind,
};

#[test]
fn suppresses_rust_unwrap_in_tests() {
    let context = rust_context(vec![FileRole::Test], vec![RuntimeKind::RustLibrary]);

    let decision = decide_for_audit_context(
        "language.rust.panic-risk",
        &context,
        Severity::Medium,
        Some("rust.unwrap"),
    );

    assert!(decision.is_suppressed());
}

#[test]
fn upgrades_rust_panic_in_domain_code() {
    let context = rust_context(vec![FileRole::Domain], vec![RuntimeKind::RustLibrary]);

    let decision = decide_for_audit_context(
        "language.rust.panic-risk",
        &context,
        Severity::Medium,
        Some("rust.panic"),
    );

    assert_eq!(decision.action, RuleDecisionAction::Upgrade);
    assert_eq!(decision.severity, Severity::High);
}

#[test]
fn downgrades_rust_unwrap_at_cli_boundary() {
    let context = rust_context(Vec::new(), vec![RuntimeKind::RustCli]);

    let decision = decide_for_audit_context(
        "language.rust.panic-risk",
        &context,
        Severity::Medium,
        Some("rust.unwrap"),
    );

    assert_eq!(decision.action, RuleDecisionAction::Downgrade);
    assert_eq!(decision.severity, Severity::Low);
}

#[test]
fn functional_paradigm_does_not_suppress_or_create_a_problem() {
    let context = AuditContext {
        language: LanguageKind::Rust,
        frameworks: Vec::new(),
        roles: vec![FileRole::Domain],
        paradigms: vec![ProgrammingParadigm::Functional],
        runtimes: vec![RuntimeKind::RustLibrary],
        is_test: false,
    };

    let decision = decide_for_audit_context(
        "code-quality.complex-file",
        &context,
        Severity::Medium,
        None,
    );

    assert_eq!(decision.action, RuleDecisionAction::Apply);
    assert_eq!(decision.severity, Severity::Medium);
}

#[test]
fn suppresses_rust_rule_for_python_context() {
    let context = AuditContext {
        language: LanguageKind::Python,
        frameworks: Vec::new(),
        roles: Vec::new(),
        paradigms: vec![ProgrammingParadigm::Unknown],
        runtimes: Vec::new(),
        is_test: false,
    };

    let decision =
        decide_for_audit_context("language.rust.panic-risk", &context, Severity::Medium, None);

    assert!(decision.is_suppressed());
}

#[test]
fn suppresses_react_native_rule_for_plain_react_context() {
    use crate::audits::context::FrameworkKind;

    let context = AuditContext {
        language: LanguageKind::TypeScript,
        frameworks: vec![FrameworkKind::React],
        roles: Vec::new(),
        paradigms: Vec::new(),
        runtimes: Vec::new(),
        is_test: false,
    };

    let decision = decide_for_audit_context(
        "framework.react-native.inline-style",
        &context,
        Severity::Medium,
        None,
    );

    assert!(decision.is_suppressed());
}

#[test]
fn applies_rule_when_no_knowledge_entry_exists() {
    let decision = decide_for_audit_context(
        "nonexistent.unknown.rule",
        &rust_context(Vec::new(), Vec::new()),
        Severity::High,
        None,
    );

    assert_eq!(decision.action, RuleDecisionAction::Apply);
    assert_eq!(decision.severity, Severity::High);
}

#[test]
fn suppresses_low_signal_for_rules_with_flag() {
    let decision = decide(&RuleMatchContext {
        rule_id: "language.rust.panic-risk",
        languages: &["rust"],
        frameworks: &[],
        roles: &[],
        paradigms: &[],
        runtimes: &[],
        is_test: false,
        is_low_signal: true,
        signal: None,
        base_severity: Severity::Medium,
    });

    assert!(decision.is_suppressed());
}

fn rust_context(roles: Vec<FileRole>, runtimes: Vec<RuntimeKind>) -> AuditContext {
    AuditContext {
        language: LanguageKind::Rust,
        frameworks: Vec::new(),
        roles,
        paradigms: vec![ProgrammingParadigm::Unknown],
        runtimes,
        is_test: false,
    }
}

#[test]
fn trace_preserves_ordered_matching_overrides_and_severity_transitions() {
    let context = AuditContext {
        language: LanguageKind::Kotlin,
        frameworks: Vec::new(),
        roles: vec![FileRole::Domain, FileRole::TestSupport],
        paradigms: vec![ProgrammingParadigm::ObjectOriented],
        runtimes: vec![RuntimeKind::Jvm],
        is_test: false,
    };
    let trace = decide_for_audit_context_with_trace(
        "language.managed.fatal-exception-risk",
        &context,
        Severity::Medium,
        None,
    );
    assert_eq!(trace.decision.action, RuleDecisionAction::Downgrade);
    assert_eq!(trace.decision.severity, Severity::Low);
    let applied = trace
        .steps
        .iter()
        .filter(|step| {
            step.stage == DecisionTraceStage::Override
                && step.status == DecisionTraceStatus::Applied
        })
        .collect::<Vec<_>>();
    assert_eq!(applied.len(), 2);
    assert_eq!(applied[0].override_index, Some(1));
    assert_eq!(applied[0].severity_before, Severity::Medium);
    assert_eq!(applied[0].severity_after, Severity::High);
    assert_eq!(applied[1].override_index, Some(2));
    assert_eq!(applied[1].severity_before, Severity::High);
    assert_eq!(applied[1].severity_after, Severity::Low);
}

#[test]
fn trace_records_the_applicability_check_that_suppressed_a_rule() {
    let context = AuditContext {
        language: LanguageKind::Python,
        frameworks: Vec::new(),
        roles: Vec::new(),
        paradigms: vec![ProgrammingParadigm::Unknown],
        runtimes: vec![RuntimeKind::Python],
        is_test: false,
    };
    let trace = decide_for_audit_context_with_trace(
        "language.rust.panic-risk",
        &context,
        Severity::Medium,
        Some("rust.panic"),
    );
    assert!(trace.decision.is_suppressed());
    let failed = trace
        .steps
        .iter()
        .find(|step| step.status == DecisionTraceStatus::Failed)
        .expect("failed applicability step");
    assert_eq!(failed.stage, DecisionTraceStage::Applicability);
    assert_eq!(failed.label, "language");
    assert_eq!(failed.action, Some(RuleDecisionAction::Suppress));
}

#[test]
fn traced_and_compatibility_decisions_remain_identical() {
    let context = rust_context(vec![FileRole::Domain], vec![RuntimeKind::RustLibrary]);
    let compatibility = decide_for_audit_context(
        "language.rust.panic-risk",
        &context,
        Severity::Medium,
        Some("rust.panic"),
    );
    let traced = decide_for_audit_context_with_trace(
        "language.rust.panic-risk",
        &context,
        Severity::Medium,
        Some("rust.panic"),
    );
    assert_eq!(compatibility, traced.decision);
}

#[test]
fn disabled_trace_recorder_does_not_materialize_steps() {
    let mut materialized = false;
    let mut recorder = TraceRecorder::disabled();

    recorder.push(|| {
        materialized = true;
        DecisionTraceStep {
            stage: DecisionTraceStage::RuleLookup,
            status: DecisionTraceStatus::Matched,
            label: "should-not-be-built".to_string(),
            criteria: vec!["should-not-be-built".to_string()],
            action: None,
            severity_before: Severity::Info,
            severity_after: Severity::Info,
            reason: "should-not-be-built".to_string(),
            override_index: None,
        }
    });

    assert!(!materialized);
}

#[test]
fn applied_file_decision_preserves_replayable_provenance() {
    use crate::findings::provenance::KnowledgeDecisionAction;
    use crate::findings::types::{Evidence, FindingCategory};
    use std::path::PathBuf;

    let file = FileFacts {
        path: PathBuf::from("src/domain/service.rs"),
        language: Some("Rust".to_string()),
        non_empty_lines: 1,
        branch_count: 0,
        imports: Vec::new(),
        content: Some("pub fn run() { panic!(\"boom\"); }\n".to_string()),
        has_inline_tests: false,
        in_executable_package: false,
        deferred_imports: Vec::new(),
    };
    let base_severity = Severity::Medium;
    let signal = "rust.panic";
    let expected = decide_for_file(
        "language.rust.panic-risk",
        &file,
        base_severity,
        Some(signal),
    );
    assert!(!expected.is_suppressed());

    let finding = Finding {
        rule_id: "language.rust.panic-risk".to_string(),
        category: FindingCategory::CodeQuality,
        severity: base_severity,
        evidence: vec![Evidence {
            path: file.path.clone(),
            line_start: 1,
            line_end: None,
            snippet: "panic!(\"boom\")".to_string(),
        }],
        ..Finding::default()
    };

    let mut applied = apply_file_decision("language.rust.panic-risk", &file, finding, Some(signal))
        .expect("finding should remain visible");
    applied.populate_rule_metadata();

    let provenance = applied
        .provenance
        .knowledge_decision
        .as_ref()
        .expect("knowledge decision provenance");
    assert_eq!(provenance.base_severity, base_severity);
    assert_eq!(provenance.signal.as_deref(), Some(signal));
    assert_eq!(provenance.decided_severity, expected.severity);
    assert_eq!(
        provenance.action,
        match expected.action {
            RuleDecisionAction::Apply => KnowledgeDecisionAction::Apply,
            RuleDecisionAction::Suppress => KnowledgeDecisionAction::Suppress,
            RuleDecisionAction::Downgrade => KnowledgeDecisionAction::Downgrade,
            RuleDecisionAction::Upgrade => KnowledgeDecisionAction::Upgrade,
        }
    );
    assert_eq!(applied.provenance.detector, "language.rust.panic-risk");

    let json = serde_json::to_value(&applied).expect("serialize finding");
    assert_eq!(json["provenance"]["knowledge_decision"]["signal"], signal);
    assert_eq!(
        json["provenance"]["knowledge_decision"]["base_severity"],
        base_severity.label()
    );
}

#[test]
fn default_provenance_omits_absent_knowledge_decision() {
    let value = serde_json::to_value(crate::findings::provenance::FindingProvenance::default())
        .expect("serialize provenance");
    assert!(value.get("knowledge_decision").is_none());
}