codebase-graph 1.7.1

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use serde_json::json;
use std::{
    fs,
    path::{Path, PathBuf},
    process::{Command, Output},
};

fn bin() -> &'static str {
    env!("CARGO_BIN_EXE_codebase-graph")
}

fn unique_temp_dir(prefix: &str) -> PathBuf {
    std::env::temp_dir().join(format!(
        "{prefix}-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ))
}

fn run_cli(cwd: &Path, args: &[&str]) -> Output {
    Command::new(bin())
        .args(args)
        .current_dir(cwd)
        .output()
        .unwrap()
}

fn native_request(root: &Path, db: &Path, staging: &Path) -> PathBuf {
    let request_path = root.join("native-request.json");
    let payload = json!({
        "source_root": root,
        "repository_label": "native-request-identity",
        "mode": "full",
        "parser_version": "native-test",
        "manifest_schema_version": 1,
        "ontology": "code_ontology_v1",
        "previous_manifest": null,
        "profiles": [],
        "excluded_parts": [],
        "db_path": db,
        "include_fts": false,
        "semantic_enrichment": false,
        "semantic_provider_mode": "local_only",
        "schema_statements": [],
        "staging_dir": staging,
        "atomic_rebuild": true,
        "strict": true,
    });
    fs::write(&request_path, serde_json::to_vec_pretty(&payload).unwrap()).unwrap();
    request_path
}

fn assert_success(output: &Output) {
    assert!(
        output.status.success(),
        "command failed\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
fn standalone_native_request_uses_its_source_root_from_an_unrelated_cwd() {
    let root = unique_temp_dir("codebase-graph-native-request-source");
    let unrelated = unique_temp_dir("codebase-graph-native-request-cwd");
    fs::create_dir_all(&root).unwrap();
    fs::create_dir_all(&unrelated).unwrap();
    fs::write(root.join("expected.rs"), "pub fn expected() {}\n").unwrap();
    fs::write(unrelated.join("decoy.rs"), "pub fn decoy() {}\n").unwrap();
    let db = root.join("graph.ldb");
    let manifest = root.join("manifest.json");
    let staging = root.join("staging");
    let request = native_request(&root, &db, &staging);

    let output = run_cli(
        &unrelated,
        &[
            "build",
            "--native-request",
            request.to_str().unwrap(),
            "--db",
            db.to_str().unwrap(),
            "--manifest",
            manifest.to_str().unwrap(),
            "--no-git",
            "--no-fts",
            "--no-semantic-enrichment",
            "--json",
        ],
    );
    assert_success(&output);
    assert!(manifest.exists());
    assert!(db.exists());
    assert!(!unrelated.join(".codebaseGraph/manifest.json").exists());
    assert!(!unrelated
        .join(".codebaseGraph/storage/active.json")
        .exists());
    let manifest_value: serde_json::Value =
        serde_json::from_slice(&fs::read(&manifest).unwrap()).unwrap();
    assert!(manifest_value["files"]["expected.rs"].is_object());
    assert!(manifest_value["files"].get("decoy.rs").is_none());

    let _ = fs::remove_dir_all(root);
    let _ = fs::remove_dir_all(unrelated);
}

#[test]
fn bound_native_request_source_mismatch_is_rejected_before_direct_publication() {
    let source_root = unique_temp_dir("codebase-graph-native-request-bound-source");
    let bound_root = unique_temp_dir("codebase-graph-native-request-bound-repo");
    let unrelated = unique_temp_dir("codebase-graph-native-request-bound-cwd");
    fs::create_dir_all(&source_root).unwrap();
    fs::create_dir_all(&bound_root).unwrap();
    fs::create_dir_all(&unrelated).unwrap();
    let db = bound_root.join("graph.ldb");
    let manifest = bound_root.join("manifest.json");
    let staging = source_root.join("staging");
    let request = native_request(&source_root, &db, &staging);
    fs::write(&db, b"existing-db").unwrap();
    fs::write(&manifest, b"existing-manifest").unwrap();

    let output = run_cli(
        &unrelated,
        &[
            "build",
            "--native-request",
            request.to_str().unwrap(),
            "--repo-root",
            bound_root.to_str().unwrap(),
            "--db",
            db.to_str().unwrap(),
            "--manifest",
            manifest.to_str().unwrap(),
            "--no-git",
            "--no-fts",
            "--no-semantic-enrichment",
            "--json",
        ],
    );
    assert!(!output.status.success());
    let error = format!(
        "{}{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(error.contains("source root") && error.contains("does not match"));
    assert_eq!(fs::read(&db).unwrap(), b"existing-db");
    assert_eq!(fs::read(&manifest).unwrap(), b"existing-manifest");
    assert!(!bound_root
        .join(".codebaseGraph/storage/active.json")
        .exists());

    let _ = fs::remove_dir_all(source_root);
    let _ = fs::remove_dir_all(bound_root);
    let _ = fs::remove_dir_all(unrelated);
}