repopilot 0.7.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use serde_json::Value;
use std::fs;
use std::process::Command;
use tempfile::tempdir;

fn repopilot() -> Command {
    Command::new(env!("CARGO_BIN_EXE_repopilot"))
}

#[test]
fn baseline_create_creates_default_file_and_parent_directory() {
    let temp = tempdir().expect("failed to create temp dir");
    write_project_with_secret(temp.path(), "config");

    let output = repopilot()
        .args(["baseline", "create", "."])
        .current_dir(temp.path())
        .output()
        .expect("failed to run baseline create");

    assert!(output.status.success());
    assert!(temp.path().join(".repopilot").is_dir());
    assert!(temp.path().join(".repopilot/baseline.json").is_file());

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("RepoPilot Baseline"));
    assert!(stdout.contains("Scanned path: ."));
    assert!(stdout.contains("Baseline written to: .repopilot/baseline.json"));
    assert!(stdout.contains("- High: 1"));
}

#[test]
fn baseline_create_does_not_overwrite_existing_file_without_force() {
    let temp = tempdir().expect("failed to create temp dir");
    write_project_with_secret(temp.path(), "config");
    fs::create_dir_all(temp.path().join(".repopilot")).expect("failed to create baseline dir");
    let baseline_path = temp.path().join(".repopilot/baseline.json");
    fs::write(&baseline_path, "sentinel\n").expect("failed to write sentinel baseline");

    let output = repopilot()
        .args(["baseline", "create", "."])
        .current_dir(temp.path())
        .output()
        .expect("failed to run baseline create");

    assert!(
        !output.status.success(),
        "should exit with non-zero when baseline exists and --force is not set"
    );
    assert_eq!(
        fs::read_to_string(&baseline_path).expect("failed to read baseline"),
        "sentinel\n",
        "existing baseline file must not be modified"
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("--force"),
        "error message should mention --force"
    );
}

#[test]
fn baseline_create_overwrites_existing_file_with_force() {
    let temp = tempdir().expect("failed to create temp dir");
    write_project_with_secret(temp.path(), "config");
    fs::create_dir_all(temp.path().join(".repopilot")).expect("failed to create baseline dir");
    let baseline_path = temp.path().join(".repopilot/baseline.json");
    fs::write(&baseline_path, "sentinel\n").expect("failed to write sentinel baseline");

    let output = repopilot()
        .args(["baseline", "create", ".", "--force"])
        .current_dir(temp.path())
        .output()
        .expect("failed to run baseline create");

    assert!(output.status.success());
    let baseline: Value =
        serde_json::from_str(&fs::read_to_string(&baseline_path).expect("failed to read baseline"))
            .expect("baseline should be JSON");
    assert_eq!(baseline["schema_version"], 1);
    assert_ne!(baseline, Value::String("sentinel\n".to_string()));
}

#[test]
fn baseline_create_supports_explicit_output_path() {
    let temp = tempdir().expect("failed to create temp dir");
    write_project_with_secret(temp.path(), "config");
    let baseline_path = temp.path().join("baseline.json");

    let output = repopilot()
        .args(["baseline", "create", ".", "--output"])
        .arg(&baseline_path)
        .current_dir(temp.path())
        .output()
        .expect("failed to run baseline create");

    assert!(output.status.success());
    assert!(baseline_path.is_file());
}

#[test]
fn baseline_create_stores_stable_keys_and_repo_relative_paths() {
    let temp = tempdir().expect("failed to create temp dir");
    write_project_with_secret(temp.path(), "config");

    let output = repopilot()
        .args(["baseline", "create", "."])
        .current_dir(temp.path())
        .output()
        .expect("failed to run baseline create");

    assert!(output.status.success());

    let baseline: Value = serde_json::from_str(
        &fs::read_to_string(temp.path().join(".repopilot/baseline.json"))
            .expect("failed to read baseline"),
    )
    .expect("baseline should be JSON");
    let findings = baseline["findings"]
        .as_array()
        .expect("findings should be an array");
    let secret = findings
        .iter()
        .find(|finding| finding["rule_id"] == "security.secret-candidate")
        .expect("expected secret finding");

    assert_eq!(secret["key"], "security.secret-candidate:src/config.rs:1");
    assert_eq!(secret["path"], "src/config.rs");
    assert!(
        !secret["key"]
            .as_str()
            .unwrap()
            .contains(temp.path().to_str().unwrap())
    );
}

fn write_project_with_secret(root: &std::path::Path, module: &str) {
    fs::create_dir_all(root.join("src")).expect("failed to create src dir");
    fs::create_dir_all(root.join("tests")).expect("failed to create tests dir");
    fs::write(
        root.join(format!("src/{module}.rs")),
        "const API_KEY: &str = \"abc12345\";\n",
    )
    .expect("failed to write source file");
    fs::write(
        root.join(format!("tests/{module}.rs")),
        "fn covers_module() {}\n",
    )
    .expect("failed to write test file");
}