codebase-graph 1.5.1

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use super::build::{build, SearchIndexBuildRequest};
use super::model::POSTINGS_SUFFIX;
use super::{search, sidecar_path, validate};
use serde_json::json;
use std::fs::{self, OpenOptions};
use std::io::{Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};

#[test]
fn sidecar_build_and_search_use_deterministic_bm25_ties() {
    let fixture = Fixture::new("ranking");
    fixture.write_nodes(
        "function",
        &[
            node("fn:a", "alpha", "src/a.rs"),
            node("fn:b", "alpha", "src/b.rs"),
            node("fn:c", "unrelated", "src/c.rs"),
        ],
    );
    let built = build(fixture.request(4096)).unwrap();
    validate(&fixture.db_path, &built.metadata).unwrap();

    let hits = search(&fixture.db_path, &built.metadata, "alpha", "semantic", 10).unwrap();
    assert_eq!(
        hits.iter().map(|hit| hit.id.as_str()).collect::<Vec<_>>(),
        vec!["fn:a", "fn:b"]
    );
    assert_eq!(hits[0].score, hits[1].score);
    let top = search(&fixture.db_path, &built.metadata, "alpha", "semantic", 1).unwrap();
    assert_eq!(top[0].id, "fn:a");
}

#[test]
fn checksum_validation_rejects_corrupt_postings() {
    let fixture = Fixture::new("corruption");
    fixture.write_nodes("function", &[node("fn:a", "alpha", "src/a.rs")]);
    let built = build(fixture.request(4096)).unwrap();
    let postings_path = sidecar_path(&fixture.db_path, POSTINGS_SUFFIX);
    let mut postings = OpenOptions::new().write(true).open(postings_path).unwrap();
    postings.seek(SeekFrom::End(-1)).unwrap();
    postings.write_all(&[0xff]).unwrap();
    postings.flush().unwrap();

    let error = validate(&fixture.db_path, &built.metadata).unwrap_err();
    assert!(error.to_string().contains("checksum mismatch"));
}

#[test]
fn occurrence_sorting_stays_within_the_configured_chunk() {
    let fixture = Fixture::new("bounded");
    let nodes = (0..200)
        .map(|index| {
            node(
                &format!("fn:{index:04}"),
                &format!("token_{index:04} common"),
                &format!("src/{index:04}.rs"),
            )
        })
        .collect::<Vec<_>>();
    fixture.write_nodes("function", &nodes);
    let built = build(fixture.request(4096)).unwrap();

    assert!(built.spill_bytes > 0);
    assert!(built.high_water_bytes <= 4096);
    validate(&fixture.db_path, &built.metadata).unwrap();
}

#[test]
fn builder_consumes_all_staging_chunks_in_sequence() {
    let fixture = Fixture::new("staging-chunks");
    fixture.write_nodes("function", &[node("fn:a", "alpha", "src/a.rs")]);
    fixture.write_nodes_chunk("function", 1, &[node("fn:b", "bravo", "src/b.rs")]);
    let built = build(fixture.request(4096)).unwrap();

    let hits = search(&fixture.db_path, &built.metadata, "bravo", "semantic", 10).unwrap();
    assert_eq!(
        hits.iter().map(|hit| hit.id.as_str()).collect::<Vec<_>>(),
        vec!["fn:b"]
    );
}

#[test]
fn tokenizer_indexes_unicode_identifiers_case_insensitively() {
    let fixture = Fixture::new("unicode");
    fixture.write_nodes(
        "function",
        &[node("fn:unicode", "ÜberService", "src/unicode.rs")],
    );
    let built = build(fixture.request(4096)).unwrap();

    let hits = search(
        &fixture.db_path,
        &built.metadata,
        "überservice",
        "semantic",
        10,
    )
    .unwrap();
    assert_eq!(hits[0].id, "fn:unicode");
}

fn node(id: &str, label: &str, path: &str) -> serde_json::Value {
    json!({
        "id": id,
        "label": label,
        "path": path,
        "qualified_name": label,
        "summary": label,
        "text": label,
        "tree_sitter_node_type": "function_item"
    })
}

struct Fixture {
    root: PathBuf,
    staging_dir: PathBuf,
    db_path: PathBuf,
}

impl Fixture {
    fn new(name: &str) -> Self {
        let root = std::env::temp_dir().join(format!(
            "codebase-graph-search-{name}-{}-{}",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        let staging_dir = root.join("staging");
        fs::create_dir_all(&staging_dir).unwrap();
        Self {
            db_path: root.join("graph.ldb"),
            staging_dir,
            root,
        }
    }

    fn write_nodes(&self, table: &str, nodes: &[serde_json::Value]) {
        fs::write(
            self.staging_dir.join(format!("{table}.json")),
            serde_json::to_vec(nodes).unwrap(),
        )
        .unwrap();
    }

    fn write_nodes_chunk(&self, table: &str, index: usize, nodes: &[serde_json::Value]) {
        fs::write(
            self.staging_dir.join(format!("{table}__{index:06}.json")),
            serde_json::to_vec(nodes).unwrap(),
        )
        .unwrap();
    }

    fn request(&self, chunk_bytes: usize) -> SearchIndexBuildRequest<'_> {
        SearchIndexBuildRequest {
            db_path: &self.db_path,
            staging_dir: &self.staging_dir,
            chunk_bytes,
        }
    }
}

impl Drop for Fixture {
    fn drop(&mut self) {
        if self.root.is_dir() {
            fs::remove_dir_all(&self.root).unwrap();
        }
    }
}

#[allow(dead_code)]
fn _assert_path(_: &Path) {}