vtcode-indexer 0.181.6

Workspace file indexer, fuzzy search, and markdown-backed storage for VT Code
Documentation
use super::{
    FileIndexCache, FileSearchConfig, MatchType, run, run_bounded_no_follow, run_bounded_no_follow_with_visit,
    run_with_index,
};
use std::num::NonZero;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use tempfile::TempDir;

#[tokio::test(flavor = "current_thread")]
async fn concurrent_index_builds_share_async_cache_entry() {
    let workspace = TempDir::new().expect("workspace");
    std::fs::write(workspace.path().join("widget.rs"), "fn widget() {}\n").expect("fixture source");

    let cache = FileIndexCache::new(workspace.path().to_path_buf(), Vec::new(), true, 1);
    let (first, second) = tokio::join!(cache.get_or_build(), cache.get_or_build());
    let first = first.expect("build file index");
    let second = second.expect("reuse file index");

    assert!(Arc::ptr_eq(&first, &second));
}

#[tokio::test(flavor = "current_thread")]
async fn background_refresh_is_safe_when_called_from_tokio() {
    let workspace = TempDir::new().expect("workspace");
    std::fs::write(workspace.path().join("widget.rs"), "fn widget() {}\n").expect("fixture source");

    let cache = FileIndexCache::new(workspace.path().to_path_buf(), Vec::new(), false, 1);
    cache.get_or_build().await.expect("initial index");

    assert!(cache.refresh_background().is_some());
}

#[test]
fn background_refresh_returns_snapshot_without_runtime() {
    let workspace = TempDir::new().expect("workspace");
    std::fs::write(workspace.path().join("widget.rs"), "fn widget() {}\n").expect("fixture source");

    let runtime = tokio::runtime::Runtime::new().expect("Tokio runtime");
    let cache = FileIndexCache::new(workspace.path().to_path_buf(), Vec::new(), false, 1);
    runtime.block_on(cache.get_or_build()).expect("initial index");
    drop(runtime);

    assert!(cache.refresh_background().is_some());
}

#[tokio::test(flavor = "current_thread")]
async fn incremental_directory_updates_use_the_cache_root() {
    let workspace = TempDir::new().expect("workspace");
    std::fs::write(workspace.path().join("widget.rs"), "fn widget() {}\n").expect("fixture source");
    std::fs::create_dir(workspace.path().join("new_directory")).expect("fixture directory");

    let cache = Arc::new(FileIndexCache::new(workspace.path().to_path_buf(), Vec::new(), false, 1));
    cache.get_or_build().await.expect("initial index");

    tokio::task::spawn_blocking({
        let cache = Arc::clone(&cache);
        move || cache.update_file("new_directory", true)
    })
    .await
    .expect("incremental directory update task");

    let index = cache.get_or_build().await.expect("updated index");
    let matches = index.query("new_directory", 16, None);
    assert!(matches.iter().any(|(_, _, match_type)| *match_type == MatchType::Directory));
}

fn indexed_search_config(workspace: &std::path::Path, pattern: &str, cancel_flag: Arc<AtomicBool>) -> FileSearchConfig {
    FileSearchConfig {
        pattern_text: pattern.to_string(),
        limit: NonZero::new(16).expect("non-zero limit"),
        search_directory: workspace.to_path_buf(),
        exclude: Vec::new(),
        threads: NonZero::new(1).expect("non-zero threads"),
        cancel_flag,
        compute_indices: false,
        respect_gitignore: false,
    }
}

fn result_signature(results: &super::FileSearchResults) -> Vec<(u32, String, MatchType)> {
    results
        .matches
        .iter()
        .map(|candidate| (candidate.score, candidate.path.clone(), candidate.match_type))
        .collect()
}

#[tokio::test(flavor = "current_thread")]
async fn indexed_search_preserves_scores_order_and_match_types() {
    let workspace = TempDir::new().expect("workspace");
    std::fs::create_dir_all(workspace.path().join("src/widget_dir")).expect("fixture directory");
    std::fs::write(workspace.path().join("src/widget.rs"), "fn widget() {}\n").expect("fixture source");
    std::fs::write(workspace.path().join("src/widget_test.rs"), "fn widget_test() {}\n").expect("fixture source");
    std::fs::write(workspace.path().join("README.md"), "widget documentation\n").expect("fixture docs");

    let cache = FileIndexCache::new(workspace.path().to_path_buf(), Vec::new(), false, 1);
    let first =
        run_with_index(indexed_search_config(workspace.path(), "widget", Arc::new(AtomicBool::new(false))), &cache)
            .await
            .expect("indexed search");
    let second =
        run_with_index(indexed_search_config(workspace.path(), "widget", Arc::new(AtomicBool::new(false))), &cache)
            .await
            .expect("repeat indexed search");

    assert_eq!(result_signature(&first), result_signature(&second));
    assert!(
        first
            .matches
            .iter()
            .any(|candidate| candidate.match_type == MatchType::Directory)
    );
    assert!(first.matches.iter().any(|candidate| candidate.match_type == MatchType::File));
    assert!(first.matches.windows(2).all(|window| window[0].score >= window[1].score));
}

#[tokio::test(flavor = "current_thread")]
async fn indexed_search_honors_cancellation_before_scoring() {
    let workspace = TempDir::new().expect("workspace");
    std::fs::write(workspace.path().join("widget.rs"), "fn widget() {}\n").expect("fixture source");
    let cache = FileIndexCache::new(workspace.path().to_path_buf(), Vec::new(), false, 1);
    let cancel_flag = Arc::new(AtomicBool::new(true));

    let results = run_with_index(indexed_search_config(workspace.path(), "widget", cancel_flag), &cache)
        .await
        .expect("cancelled indexed search");
    assert!(results.matches.is_empty());
    assert_eq!(results.total_match_count, 0);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn incremental_updates_do_not_mutate_an_old_search_index() {
    let workspace = TempDir::new().expect("workspace");
    std::fs::write(workspace.path().join("old_widget.rs"), "fn old_widget() {}\n").expect("fixture source");
    let cache = Arc::new(FileIndexCache::new(workspace.path().to_path_buf(), Vec::new(), false, 1));
    let old_index = cache.get_or_build().await.expect("initial index");

    tokio::task::spawn_blocking({
        let cache = Arc::clone(&cache);
        move || cache.update_file("new_widget.rs", true)
    })
    .await
    .expect("incremental update task");

    let old_matches = old_index.query("widget", 16, None);
    let new_index = cache.get_or_build().await.expect("updated index");
    let new_matches = new_index.query("widget", 16, None);
    let old_paths = old_matches
        .iter()
        .filter_map(|(_, path_id, _)| old_index.path_texts_by_id.get(path_id.as_u32() as usize))
        .map(AsRef::as_ref)
        .collect::<Vec<&str>>();
    let new_paths = new_matches
        .iter()
        .filter_map(|(_, path_id, _)| new_index.path_texts_by_id.get(path_id.as_u32() as usize))
        .map(AsRef::as_ref)
        .collect::<Vec<&str>>();

    assert!(!old_paths.contains(&"new_widget.rs"));
    assert!(new_paths.contains(&"new_widget.rs"));
}

fn uncached_search_config(
    workspace: &std::path::Path,
    pattern: &str,
    limit: usize,
    cancel_flag: Arc<AtomicBool>,
) -> FileSearchConfig {
    FileSearchConfig {
        pattern_text: pattern.to_string(),
        limit: NonZero::new(limit).expect("non-zero limit"),
        search_directory: workspace.to_path_buf(),
        exclude: Vec::new(),
        threads: NonZero::new(4).expect("non-zero threads"),
        cancel_flag,
        compute_indices: false,
        respect_gitignore: false,
    }
}

#[test]
fn uncached_parallel_search_aggregates_worker_paths_and_counts() {
    let workspace = TempDir::new().expect("workspace");
    for index in 0..64 {
        std::fs::create_dir(workspace.path().join(format!("widget_dir_{index:03}"))).expect("fixture directory");
        std::fs::write(workspace.path().join(format!("widget_file_{index:03}.rs")), "fn widget() {}\n")
            .expect("fixture source");
    }

    let results = run(uncached_search_config(workspace.path(), "widget", 32, Arc::new(AtomicBool::new(false))))
        .expect("uncached parallel search");

    assert_eq!(results.total_match_count, 128);
    assert_eq!(results.matches.len(), 32);
    let unique_paths = results
        .matches
        .iter()
        .map(|candidate| candidate.path.as_str())
        .collect::<std::collections::HashSet<_>>();
    assert_eq!(unique_paths.len(), results.matches.len());
    assert!(results.matches.iter().all(|candidate| candidate.path.contains("widget")));
}

#[test]
fn uncached_parallel_search_honors_cancellation() {
    let workspace = TempDir::new().expect("workspace");
    for index in 0..32 {
        std::fs::write(workspace.path().join(format!("widget_{index:03}.rs")), "fn widget() {}\n")
            .expect("fixture source");
    }
    let cancel_flag = Arc::new(AtomicBool::new(true));

    let results =
        run(uncached_search_config(workspace.path(), "widget", 16, cancel_flag)).expect("cancelled uncached search");

    assert!(results.matches.is_empty());
    assert_eq!(results.total_match_count, 0);
}

#[test]
fn uncached_equal_score_results_are_lexically_ordered_and_repeatable() {
    let workspace = TempDir::new().expect("workspace");
    for name in ["a_x.rs", "b_x.rs", "c_x.rs", "d_x.rs", "e_x.rs"] {
        std::fs::write(workspace.path().join(name), "fn x() {}\n").expect("fixture source");
    }

    let search = || {
        run(uncached_search_config(workspace.path(), "x", 3, Arc::new(AtomicBool::new(false))))
            .expect("uncached equal-score search")
            .matches
            .into_iter()
            .map(|candidate| (candidate.score, candidate.path))
            .collect::<Vec<_>>()
    };

    let expected = search();
    for _ in 0..20 {
        assert_eq!(search(), expected);
    }
    assert!(
        expected
            .windows(2)
            .all(|window| { window[0].0 > window[1].0 || (window[0].0 == window[1].0 && window[0].1 <= window[1].1) })
    );
}

fn bounded_paths(workspace: &std::path::Path) -> Vec<String> {
    run_bounded_no_follow(FileSearchConfig {
        pattern_text: "widget".to_string(),
        limit: NonZero::new(2).expect("non-zero limit"),
        search_directory: workspace.to_path_buf(),
        exclude: Vec::new(),
        threads: NonZero::new(4).expect("non-zero threads"),
        cancel_flag: Arc::new(AtomicBool::new(false)),
        compute_indices: false,
        respect_gitignore: true,
    })
    .expect("bounded path search")
    .matches
    .into_iter()
    .map(|candidate| candidate.path)
    .collect()
}

#[test]
fn bounded_path_selection_is_stable_across_repeated_walks() {
    let workspace = TempDir::new().expect("workspace");
    for directory in ["z", "a", "m", "b", "y"] {
        let directory = workspace.path().join(directory);
        std::fs::create_dir(&directory).expect("fixture directory");
        std::fs::write(directory.join("widget.rs"), "fn widget() {}\n").expect("fixture source");
    }

    let expected = bounded_paths(workspace.path());
    assert_eq!(expected.len(), 2);
    for _ in 0..20 {
        assert_eq!(bounded_paths(workspace.path()), expected);
    }
}

#[test]
fn bounded_path_selection_is_the_sorted_prefix_and_stops_early() {
    let workspace = TempDir::new().expect("workspace");
    for directory in ["z", "a", "m", "b", "y"] {
        let directory = workspace.path().join(directory);
        std::fs::create_dir(&directory).expect("fixture directory");
        std::fs::write(directory.join("widget.rs"), "fn widget() {}\n").expect("fixture source");
    }
    let mut visited = Vec::new();

    let results = run_bounded_no_follow_with_visit(
        FileSearchConfig {
            pattern_text: "widget".to_string(),
            limit: NonZero::new(2).expect("non-zero limit"),
            search_directory: workspace.path().to_path_buf(),
            exclude: Vec::new(),
            threads: NonZero::new(4).expect("non-zero threads"),
            cancel_flag: Arc::new(AtomicBool::new(false)),
            compute_indices: false,
            respect_gitignore: true,
        },
        |path| visited.push(path.to_path_buf()),
    )
    .expect("bounded path search");
    let mut paths = results.matches.into_iter().map(|candidate| candidate.path).collect::<Vec<_>>();
    paths.sort();

    assert_eq!(paths, vec!["a/widget.rs", "b/widget.rs"]);
    assert!(visited.len() < 11, "the bounded route must stop before traversing the complete fixture tree");
    assert_eq!(results.total_match_count, 3);
}