repopilot 0.14.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use repopilot::baseline::key::stable_finding_key;
use repopilot::findings::types::{Evidence, Finding, FindingCategory, Severity};
use std::path::{Path, PathBuf};

#[test]
fn same_finding_produces_same_stable_key() {
    let finding = finding("architecture.large-file", "src/main.rs", 1);

    let first = stable_finding_key(&finding, Path::new("."));
    let second = stable_finding_key(&finding, Path::new("."));

    assert_eq!(first, second);
}

#[test]
fn path_separators_are_normalized() {
    let finding = finding("architecture.large-file", "src\\main.rs", 1);

    let key = stable_finding_key(&finding, Path::new("."));

    assert!(key.starts_with("architecture.large-file:src/main.rs:"));
}

#[test]
fn absolute_paths_are_not_leaked_into_key() {
    let root = PathBuf::from("/tmp/repopilot-project");
    let finding = finding(
        "security.secret-candidate",
        "/tmp/repopilot-project/src/config.rs",
        42,
    );

    let key = stable_finding_key(&finding, &root);

    assert!(key.starts_with("security.secret-candidate:src/config.rs:"));
    assert!(!key.contains("/tmp/repopilot-project"));
}

#[test]
fn key_includes_rule_id_path_and_stable_identity() {
    let finding = finding("code-marker.todo", "src/app.rs", 7);

    let key = stable_finding_key(&finding, Path::new("."));

    assert!(key.starts_with("code-marker.todo:src/app.rs:"));
    assert!(!key.ends_with(":7"));
}

#[test]
fn key_is_stable_when_only_line_number_shifts() {
    let before = finding("code-marker.todo", "src/app.rs", 7);
    let after = finding("code-marker.todo", "src/app.rs", 12);

    assert_eq!(
        stable_finding_key(&before, Path::new(".")),
        stable_finding_key(&after, Path::new("."))
    );
}

#[test]
fn key_masks_string_literal_value_changes_but_keeps_identifier_changes() {
    let first = finding_with_snippet(
        "security.secret-candidate",
        "src/config.rs",
        1,
        "const API_KEY: &str = \"abc12345\";",
    );
    let rotated_value = finding_with_snippet(
        "security.secret-candidate",
        "src/config.rs",
        8,
        "const API_KEY: &str = \"def67890\";",
    );
    let different_identifier = finding_with_snippet(
        "security.secret-candidate",
        "src/config.rs",
        2,
        "const ACCESS_TOKEN: &str = \"abc12345\";",
    );

    assert_eq!(
        stable_finding_key(&first, Path::new(".")),
        stable_finding_key(&rotated_value, Path::new("."))
    );
    assert_ne!(
        stable_finding_key(&first, Path::new(".")),
        stable_finding_key(&different_identifier, Path::new("."))
    );
}

fn finding(rule_id: &str, path: &str, line: usize) -> Finding {
    finding_with_snippet(rule_id, path, line, "snippet")
}

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