code-kb-core 1.6.1

Core library for code-kb AST fact querying, slicing, and progressive disclosure
Documentation
use code_kb_core::{
    Occurrence, Workspace, edit_file, find_julie_extract_binary, get_file, open_read_only,
    safe_tempdir, scan_workspace, search_symbols, start_watcher,
};
use std::fs;
use std::thread::sleep;
use std::time::Duration;

#[test]
fn test_background_watcher_incremental_sync() {
    let _extract_bin =
        find_julie_extract_binary().expect("julie-extract binary must be present for tests");

    let temp_dir = safe_tempdir();
    let root = temp_dir.path().to_path_buf();

    let src_dir = root.join("src");
    fs::create_dir_all(&src_dir).unwrap();
    let file1 = src_dir.join("greet.rs");
    fs::write(&file1, "pub fn greet() {}\n").unwrap();

    let ws = Workspace::new(root.clone());
    let db_path = root.join("watcher_test.db");

    // Initial scan
    scan_workspace(&ws, &db_path, true).expect("Initial scan failed");

    // Start Tier 3 file watcher
    let _watcher = start_watcher(ws.clone(), db_path.clone()).expect("Failed to start watcher");

    // Verify greet exists
    {
        let conn = open_read_only(&db_path).unwrap();
        let syms = search_symbols(&conn, "greet", None, false, 10).unwrap();
        assert_eq!(syms.len(), 1);
        assert_eq!(syms[0].name, "greet");
    }

    // 1. External edit to existing file: add farewell function
    fs::write(&file1, "pub fn greet() {}\npub fn farewell() {}\n").unwrap();

    // Wait for debouncer (150ms) and update execution
    let mut updated = false;
    for _ in 0..25 {
        sleep(Duration::from_millis(200));
        let conn = open_read_only(&db_path).unwrap();
        if let Ok(syms) = search_symbols(&conn, "farewell", None, false, 10)
            && !syms.is_empty()
        {
            updated = true;
            break;
        }
    }
    assert!(
        updated,
        "Watcher failed to detect external modification to greet.rs"
    );

    // 2. External creation of new file
    let file2 = src_dir.join("extra.rs");
    fs::write(&file2, "pub fn bonus_feature() {}\n").unwrap();

    let mut created = false;
    for _ in 0..25 {
        sleep(Duration::from_millis(200));
        let conn = open_read_only(&db_path).unwrap();
        if let Ok(syms) = search_symbols(&conn, "bonus_feature", None, false, 10)
            && !syms.is_empty()
        {
            created = true;
            break;
        }
    }
    assert!(created, "Watcher failed to detect new file extra.rs");

    // 3. External deletion of file
    fs::remove_file(&file2).unwrap();

    let mut deleted = false;
    for _ in 0..25 {
        sleep(Duration::from_millis(200));
        let conn = open_read_only(&db_path).unwrap();
        if let Ok(syms) = search_symbols(&conn, "bonus_feature", None, false, 10)
            && syms.is_empty()
        {
            deleted = true;
            break;
        }
    }
    assert!(deleted, "Watcher failed to detect deletion of extra.rs");
}

#[test]
fn test_watcher_updates_directory_named_targeted() {
    let _extract_bin =
        find_julie_extract_binary().expect("julie-extract binary must be present for tests");

    let temp_dir = safe_tempdir();
    let root = temp_dir.path().to_path_buf();
    let src_dir = root.join("targeted");
    fs::create_dir_all(&src_dir).unwrap();
    let file_path = src_dir.join("sample.rs");
    fs::write(&file_path, "pub fn before_update() {}\n").unwrap();

    let ws = Workspace::new(root.clone());
    let db_path = root.join("watcher_test.db");
    scan_workspace(&ws, &db_path, true).unwrap();
    let _watcher = start_watcher(ws, db_path.clone()).unwrap();

    fs::write(&file_path, "pub fn after_update() {}\n").unwrap();

    for _ in 0..25 {
        sleep(Duration::from_millis(200));
        let conn = open_read_only(&db_path).unwrap();
        if !search_symbols(&conn, "after_update", None, false, 10)
            .unwrap()
            .is_empty()
        {
            return;
        }
    }

    panic!("Watcher did not update a directory named targeted");
}

#[test]
fn test_watcher_respects_nested_ignore_files() {
    let _extract_bin =
        find_julie_extract_binary().expect("julie-extract binary must be present for tests");

    let temp_dir = safe_tempdir();
    let root = temp_dir.path().to_path_buf();
    let src_dir = root.join("src");
    fs::create_dir_all(&src_dir).unwrap();
    fs::write(src_dir.join(".gitignore"), "ignored.rs\n").unwrap();
    fs::write(src_dir.join(".julieignore"), "julie_ignored.rs\n").unwrap();

    let ws = Workspace::new(root.clone());
    let db_path = root.join("watcher_test.db");
    scan_workspace(&ws, &db_path, true).unwrap();
    let _watcher = start_watcher(ws, db_path.clone()).unwrap();

    fs::write(src_dir.join("ignored.rs"), "pub fn ignored_symbol() {}\n").unwrap();
    fs::write(
        src_dir.join("julie_ignored.rs"),
        "pub fn julie_ignored_symbol() {}\n",
    )
    .unwrap();
    fs::write(src_dir.join("tracked.rs"), "pub fn tracked_symbol() {}\n").unwrap();

    for _ in 0..25 {
        sleep(Duration::from_millis(200));
        let conn = open_read_only(&db_path).unwrap();
        if !search_symbols(&conn, "tracked_symbol", None, false, 10)
            .unwrap()
            .is_empty()
        {
            assert!(
                search_symbols(&conn, "ignored_symbol", None, false, 10)
                    .unwrap()
                    .is_empty()
            );
            assert!(
                search_symbols(&conn, "julie_ignored_symbol", None, false, 10)
                    .unwrap()
                    .is_empty()
            );
            return;
        }
    }

    panic!("Watcher did not index tracked file");
}

#[test]
fn test_watcher_does_not_reindex_on_reads() {
    let _extract_bin =
        find_julie_extract_binary().expect("julie-extract binary must be present for tests");

    let temp_dir = safe_tempdir();
    let root = temp_dir.path().to_path_buf();
    let src_dir = root.join("src");
    fs::create_dir_all(&src_dir).unwrap();

    let mut paths = Vec::new();
    for i in 0..10 {
        let file_path = src_dir.join(format!("file_{i}.rs"));
        fs::write(&file_path, format!("pub fn func_{i}() {{}}\n")).unwrap();
        paths.push(file_path);
    }

    let ws = Workspace::new(root.clone());
    let db_path = root.join("watcher_reads_test.db");
    scan_workspace(&ws, &db_path, true).unwrap();

    // Collect initial indexed_at timestamps
    let get_timestamps = || -> Vec<(String, String)> {
        let conn = open_read_only(&db_path).unwrap();
        let mut stmt = conn
            .prepare("SELECT path, indexed_at FROM files ORDER BY path ASC")
            .unwrap();
        let rows = stmt
            .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
            .unwrap();
        rows.map(|r| r.unwrap()).collect()
    };

    let initial_timestamps = get_timestamps();
    assert_eq!(initial_timestamps.len(), 10);

    let _watcher = start_watcher(ws, db_path.clone()).unwrap();

    // Read every file multiple times to simulate inspection/slicing
    for _ in 0..3 {
        for path in &paths {
            let _ = fs::read_to_string(path).unwrap();
        }
    }

    // Sleep for 500ms (longer than 150ms debounce window)
    sleep(Duration::from_millis(500));

    let final_timestamps = get_timestamps();
    assert_eq!(
        initial_timestamps, final_timestamps,
        "Reading files must not trigger watcher re-indexing"
    );
}

#[test]
fn test_watcher_refreshes_a_header_batch() {
    let _extract_bin =
        find_julie_extract_binary().expect("julie-extract binary must be present for tests");
    let temp_dir = safe_tempdir();
    let root = temp_dir.path().to_path_buf();
    let src_dir = root.join("src");
    fs::create_dir_all(&src_dir).unwrap();
    let first = src_dir.join("first.h");
    let second = src_dir.join("second.h");
    fs::write(&first, "class First { int old_value; };\n").unwrap();
    fs::write(&second, "class Second { int old_value; };\n").unwrap();

    let ws = Workspace::new(root);
    let db_path = ws.canonical_root.join("watcher_test.db");
    scan_workspace(&ws, &db_path, true).unwrap();
    let (first_hash, second_hash) = {
        let conn = open_read_only(&db_path).unwrap();
        (
            get_file(&conn, "src/first.h")
                .unwrap()
                .unwrap()
                .content_hash,
            get_file(&conn, "src/second.h")
                .unwrap()
                .unwrap()
                .content_hash,
        )
    };
    let _watcher = start_watcher(ws, db_path.clone()).unwrap();

    fs::write(&first, "class First { int new_value; };\n").unwrap();
    fs::write(&second, "class Second { int new_value; };\n").unwrap();

    for _ in 0..25 {
        sleep(Duration::from_millis(200));
        let conn = open_read_only(&db_path).unwrap();
        let current = (
            get_file(&conn, "src/first.h")
                .unwrap()
                .unwrap()
                .content_hash,
            get_file(&conn, "src/second.h")
                .unwrap()
                .unwrap()
                .content_hash,
        );
        if current.0 != first_hash && current.1 != second_hash {
            return;
        }
    }

    panic!("Watcher did not refresh both headers");
}

#[test]
fn test_watcher_leaves_a_header_edit_fresh() {
    let _extract_bin =
        find_julie_extract_binary().expect("julie-extract binary must be present for tests");
    let temp_dir = safe_tempdir();
    let root = temp_dir.path().to_path_buf();
    let src_dir = root.join("src");
    fs::create_dir_all(&src_dir).unwrap();
    let header = src_dir.join("widget.h");
    fs::write(&header, "class Widget { public: int old_value = 0; };\n").unwrap();

    let ws = Workspace::new(root);
    let db_path = ws.canonical_root.join("watcher_test.db");
    scan_workspace(&ws, &db_path, true).unwrap();
    let (original_hash, revisions_before): (String, i64) = {
        let conn = open_read_only(&db_path).unwrap();
        (
            get_file(&conn, "src/widget.h")
                .unwrap()
                .unwrap()
                .content_hash,
            conn.query_row("SELECT COUNT(*) FROM extraction_revisions", [], |row| {
                row.get(0)
            })
            .unwrap(),
        )
    };
    let _watcher = start_watcher(ws.clone(), db_path.clone()).unwrap();
    let conn = open_read_only(&db_path).unwrap();
    edit_file(
        &ws,
        &db_path,
        &conn,
        "src/widget.h",
        "old_value",
        "new_value",
        Occurrence::Only,
    )
    .unwrap();
    drop(conn);

    sleep(Duration::from_millis(700));
    let conn = open_read_only(&db_path).unwrap();
    assert_ne!(
        get_file(&conn, "src/widget.h")
            .unwrap()
            .unwrap()
            .content_hash,
        original_hash
    );
    let revisions_after: i64 = conn
        .query_row("SELECT COUNT(*) FROM extraction_revisions", [], |row| {
            row.get(0)
        })
        .unwrap();
    assert_eq!(revisions_after, revisions_before + 1);
}