use super::*;
#[test]
fn historical_reuse_budget_counts_both_rename_and_copy_paths() {
let changes = vec![
source::change_status::GitChange::Renamed {
old_path: "src/old.rs".to_owned(),
new_path: "src/new.rs".to_owned(),
},
source::change_status::GitChange::Copied {
old_path: "src/source.rs".to_owned(),
new_path: "src/copy.rs".to_owned(),
},
source::change_status::GitChange::Deleted {
path: "src/deleted.rs".to_owned(),
},
];
assert_eq!(index::impacted_path_count(&changes), 5);
}
#[test]
fn historical_reuse_budget_counts_files_expanded_from_gitlinks() {
let child = test_fixtures::TempGitRepo::create("historical-reuse-gitlink-child");
for index in 0..=index::MAX_HISTORICAL_REUSE_CHANGED_PATHS {
child.write(
&format!("src/file_{index:03}.rs"),
&format!("pub fn value_{index:03}() -> usize {{ {index} }}\n"),
);
}
child.git(["add", "."]);
child.git(["commit", "-m", "child files"]);
let parent = test_fixtures::TempGitRepo::create("historical-reuse-gitlink-parent");
parent.write("src/lib.rs", "pub fn parent() {}\n");
parent.git(["add", "."]);
parent.git(["commit", "-m", "base"]);
let base = parent.git_text(["rev-parse", "HEAD"]);
let child_path = child.path.to_str().expect("child path should be unicode");
parent.git([
"-c",
"protocol.file.allow=always",
"submodule",
"add",
child_path,
"external_deps/rust_sdk",
]);
parent.git(["commit", "-m", "add large submodule"]);
let fits = index::historical_reuse_diff_fits_budget(
&parent.path,
&base,
"HEAD",
&[],
&["rust".to_owned()],
)
.expect("gitlink expansion should be measured");
assert!(!fits);
}
use crate::domain::CodeIndexResourceBudget;
use std::fs;
use super::changes::{GitChange, parse_name_status_z, tracked_entries};
use super::git::git_batch_blobs;
use super::source::{
reset_source_read_counts_for_root, source_language_filter_allows, source_read_counts_for_root,
};
use super::test_fixtures::{TempGitRepo, TempSourceDir, reference, symbol};
#[test]
fn detects_supported_languages_and_filters_paths() {
let registration = CodeRepositoryRegistration::new(
"repo",
"alias",
"/tmp/repo",
vec!["src".to_owned()],
Vec::new(),
)
.expect("registration should validate");
let selector =
CodeRepositorySelector::new("alias", "HEAD", Vec::new(), vec!["rust".to_owned()])
.expect("selector should validate");
let trailing_slash_selector = CodeRepositorySelector::new(
"alias",
"HEAD",
vec!["src/".to_owned()],
vec!["rust".to_owned()],
)
.expect("selector should validate");
assert_eq!(language_id("src/lib.rs"), Some("rust"));
assert_eq!(language_id("src/app.py"), Some("python"));
assert_eq!(language_id("src/app.js"), Some("javascript"));
assert_eq!(language_id("src/app.jsx"), Some("jsx"));
assert_eq!(language_id("src/app.ts"), Some("typescript"));
assert_eq!(language_id("src/app.tsx"), Some("tsx"));
assert_eq!(language_id("src/app.go"), Some("go"));
assert_eq!(language_id("src/App.java"), Some("java"));
assert_eq!(language_id("src/App.kt"), Some("kotlin"));
assert_eq!(language_id("src/App.scala"), Some("scala"));
assert_eq!(language_id("src/app.c"), Some("c"));
assert_eq!(language_id("include/app.h"), Some("c"));
assert_eq!(language_id("src/app.cpp"), Some("cpp"));
assert_eq!(language_id("include/app.hpp"), Some("cpp"));
assert_eq!(language_id("src/App.cs"), Some("csharp"));
assert_eq!(language_id("src/app.rb"), Some("ruby"));
assert_eq!(language_id("Gemfile"), Some("ruby"));
assert_eq!(language_id("src/app.php"), Some("php"));
assert_eq!(language_id("src/App.swift"), Some("swift"));
assert_eq!(language_id("schema/main.sql"), Some("sql"));
assert_eq!(language_id("scripts/app.sh"), Some("bash"));
assert_eq!(language_id(".bashrc"), Some("bash"));
assert!(path_is_selected("src/lib.rs", ®istration, &selector));
assert!(path_is_selected(
"src/lib.rs",
®istration,
&trailing_slash_selector
));
assert!(!path_is_selected("tests/lib.rs", ®istration, &selector));
assert!(!path_is_selected("src/app.py", ®istration, &selector));
assert!(source_language_filter_allows(
"include/app.h",
&["cpp".to_owned()]
));
assert!(source_language_filter_allows(
"docs/operations.md",
&["unknown".to_owned()]
));
assert!(!source_language_filter_allows(
"src/app.py",
&["unknown".to_owned()]
));
assert!(!source_language_filter_allows(
"src/app.c",
&["cpp".to_owned()]
));
let file_filter_selector = CodeRepositorySelector::new(
"alias",
"HEAD",
vec!["src/generated/temp.rs".to_owned()],
vec!["rust".to_owned()],
)
.expect("selector should validate");
assert!(!path_scope_allows(
"src/generated",
®istration,
&file_filter_selector
));
assert!(path_scope_overlaps(
"src/generated",
®istration,
&file_filter_selector
));
}
#[test]
fn selector_filters_cannot_widen_registered_scope() {
let registration = CodeRepositoryRegistration::new(
"repo",
"alias",
"/tmp/repo",
vec!["src".to_owned()],
vec!["rust".to_owned()],
)
.expect("registration should validate");
let wider_path_selector =
CodeRepositorySelector::new("alias", "HEAD", vec!["tests".to_owned()], Vec::new())
.expect("selector should validate");
let wider_language_selector =
CodeRepositorySelector::new("alias", "HEAD", Vec::new(), vec!["python".to_owned()])
.expect("selector should validate");
assert!(!path_is_selected(
"tests/lib.rs",
®istration,
&wider_path_selector
));
assert!(!path_is_selected(
"src/app.py",
®istration,
&wider_language_selector
));
}
#[test]
fn language_scoped_selection_keeps_dependency_manifests() {
let registration =
CodeRepositoryRegistration::new("repo", "alias", "/tmp/repo", Vec::new(), Vec::new())
.expect("registration should validate");
let rust_selector =
CodeRepositorySelector::new("alias", "HEAD", Vec::new(), vec!["rust".to_owned()])
.expect("selector should validate");
let javascript_selector =
CodeRepositorySelector::new("alias", "HEAD", Vec::new(), vec!["javascript".to_owned()])
.expect("selector should validate");
let python_selector =
CodeRepositorySelector::new("alias", "HEAD", Vec::new(), vec!["python".to_owned()])
.expect("selector should validate");
let java_selector =
CodeRepositorySelector::new("alias", "HEAD", Vec::new(), vec!["java".to_owned()])
.expect("selector should validate");
let cpp_selector =
CodeRepositorySelector::new("alias", "HEAD", Vec::new(), vec!["cpp".to_owned()])
.expect("selector should validate");
assert!(path_is_selected(
"Cargo.toml",
®istration,
&rust_selector
));
assert!(path_is_selected(
"Cargo.lock",
®istration,
&rust_selector
));
assert!(!path_is_selected(
"package.json",
®istration,
&rust_selector
));
assert!(path_is_selected(
"package-lock.json",
®istration,
&javascript_selector
));
assert!(path_is_selected(
"requirements/base.txt",
®istration,
&python_selector
));
assert!(path_is_selected(
"constraints.txt",
®istration,
&python_selector
));
assert!(path_is_selected("pom.xml", ®istration, &java_selector));
assert!(path_is_selected(
"build.gradle",
®istration,
&java_selector
));
assert!(path_is_selected(
"conanfile.py",
®istration,
&cpp_selector
));
}
#[test]
fn dot_path_filter_selects_repository_root() {
let registration = CodeRepositoryRegistration::new(
"repo",
"alias",
"/tmp/repo",
vec![".".to_owned()],
Vec::new(),
)
.expect("registration should validate");
let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
.expect("selector should validate");
let selector_dot =
CodeRepositorySelector::new("alias", "HEAD", vec!["./".to_owned()], Vec::new())
.expect("selector should validate");
let selector_relative =
CodeRepositorySelector::new("alias", "HEAD", vec!["./src".to_owned()], Vec::new())
.expect("selector should validate");
assert!(path_is_selected("src/lib.rs", ®istration, &selector));
assert!(path_is_selected("README.md", ®istration, &selector));
assert!(path_is_selected("src/lib.rs", ®istration, &selector_dot));
assert!(path_is_selected(
"src/lib.rs",
®istration,
&selector_relative
));
}
#[test]
fn explicit_file_preset_opt_in_stays_path_scoped() {
let registration = CodeRepositoryRegistration::new(
"repo",
"alias",
"/tmp/repo",
vec![".".to_owned(), "manual.pdf".to_owned()],
Vec::new(),
)
.expect("registration should validate");
let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
.expect("selector should validate");
assert!(path_is_selected("manual.pdf", ®istration, &selector));
assert!(!path_is_selected("other.pdf", ®istration, &selector));
}
#[test]
fn git_batch_blobs_reads_multiple_commit_files() {
let repo = TempGitRepo::create("batch-blobs");
repo.write("src/alpha.rs", "pub fn alpha() {}\n");
repo.write("src/beta.rs", "pub fn beta() {\n alpha();\n}\n");
repo.git(["add", "."]);
repo.git(["commit", "-m", "base"]);
let commit = repo.git_text(["rev-parse", "HEAD"]);
let blobs = git_batch_blobs(
&repo.path,
&commit,
&["src/alpha.rs".to_owned(), "src/beta.rs".to_owned()],
)
.expect("batch blobs should load");
assert_eq!(blobs[0], b"pub fn alpha() {}\n");
assert_eq!(blobs[1], b"pub fn beta() {\n alpha();\n}\n");
}
#[test]
fn tracked_entries_include_blob_sizes_for_batch_planning() {
let repo = TempGitRepo::create("tracked-entry-sizes");
repo.write("src/alpha.rs", "fn alpha() {}\n");
repo.write("src/beta.rs", "fn beta() {}\n");
repo.git(["add", "."]);
repo.git(["commit", "-m", "base"]);
let commit = repo.git_text(["rev-parse", "HEAD"]);
let entries = tracked_entries(&repo.path, &commit).expect("entries should load");
assert!(entries.iter().any(|entry| {
entry.path == "src/alpha.rs" && entry.byte_count == "fn alpha() {}\n".len()
}));
assert!(entries.iter().any(|entry| {
entry.path == "src/beta.rs" && entry.byte_count == "fn beta() {}\n".len()
}));
}
#[test]
fn tracked_entries_skip_gitlink_submodules() {
let repo = TempGitRepo::create("tracked-entry-gitlinks");
repo.write("src/lib.rs", "fn alpha() {}\n");
repo.git(["add", "."]);
repo.git(["commit", "-m", "base"]);
let commit = repo.git_text(["rev-parse", "HEAD"]);
repo.git([
"update-index",
"--add",
"--cacheinfo",
"160000",
commit.as_str(),
"vendor/module",
]);
repo.git(["commit", "-m", "add gitlink"]);
let head = repo.git_text(["rev-parse", "HEAD"]);
let entries = tracked_entries(&repo.path, &head).expect("entries should load");
assert!(entries.iter().any(|entry| entry.path == "src/lib.rs"));
assert!(!entries.iter().any(|entry| entry.path == "vendor/module"));
}
#[test]
fn full_index_plan_stops_batch_before_next_blob_exceeds_byte_budget() {
let repo = TempGitRepo::create("byte-budget-fetch");
repo.write("src/a.rs", "fn a() {}\n");
repo.write("src/b.rs", "fn b() {}\n");
repo.write("src/c.rs", "fn c() {}\n");
repo.git(["add", "."]);
repo.git(["commit", "-m", "base"]);
let budget = CodeIndexResourceBudget::new(128, "fn a() {}\nfn b() {}\n".len(), 50_000)
.expect("budget should validate");
let plan = prepare_full_index_plan(repo.registration(), repo.selector(), budget)
.expect("plan should prepare");
let (plan, first_batch) = plan.parse_next_batch().expect("first batch should parse");
let (plan, second_batch) = plan.parse_next_batch().expect("second batch should parse");
let (_, third_batch) = plan.parse_next_batch().expect("third batch should parse");
let first_batch = first_batch.expect("first batch should exist");
let second_batch = second_batch.expect("second batch should exist");
assert!(third_batch.is_none());
assert_eq!(first_batch.files.len(), 2);
assert_eq!(first_batch.files[0].path, "src/a.rs");
assert_eq!(first_batch.files[1].path, "src/b.rs");
assert_eq!(second_batch.files.len(), 1);
assert_eq!(second_batch.files[0].path, "src/c.rs");
}
#[test]
fn full_index_plan_preserves_order_across_bounded_parallel_parse_chunks() {
let repo = TempGitRepo::create("parallel-fetch-order");
for index in 0..40 {
repo.write(
&format!("src/file_{index:02}.rs"),
&format!("fn f_{index}() {{}}\n"),
);
}
repo.git(["add", "."]);
repo.git(["commit", "-m", "base"]);
let budget =
CodeIndexResourceBudget::new(40, 1024 * 1024, 50_000).expect("budget should validate");
let plan = prepare_full_index_plan(repo.registration(), repo.selector(), budget)
.expect("plan should prepare");
let (_, batch) = plan.parse_next_batch().expect("batch should parse");
let batch = batch.expect("batch should exist");
assert_eq!(batch.files.len(), 40);
for (index, file) in batch.files.iter().enumerate() {
assert_eq!(file.path, format!("src/file_{index:02}.rs"));
}
}
#[test]
fn explicit_default_exclusion_opt_in_supports_dataset_paths() {
let registration = CodeRepositoryRegistration::new(
"repo",
"alias",
"/tmp/repo",
vec!["data/events.jsonl".to_owned()],
Vec::new(),
)
.expect("registration should validate");
let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
.expect("selector should validate");
assert!(path_is_selected(
"data/events.jsonl",
®istration,
&selector
));
assert!(!path_is_selected(
"other/events.jsonl",
®istration,
&selector
));
}
#[test]
fn git_tracked_build_directories_are_selected_without_directory_opt_in() {
let registration = CodeRepositoryRegistration::new(
"repo",
"alias",
"/tmp/repo",
vec![".".to_owned()],
Vec::new(),
)
.expect("registration should validate");
let selector = CodeRepositorySelector::new("alias", "HEAD", Vec::new(), Vec::new())
.expect("selector should validate");
for path in [
"build/workflow.yaml",
".cloudbuild/cloudbuild.yaml",
".cid/pipeline.yml",
".build_config/settings.toml",
"target/generated.rs",
"vendor/pkg/lib.rs",
"third_party/pkg/lib.rs",
] {
assert!(path_is_selected(path, ®istration, &selector), "{path}");
}
}
#[test]
fn incremental_deletions_survive_new_gitignore_rules() {
let repo = TempGitRepo::create("incremental-tightened-gitignore");
repo.write("src/lib.rs", "fn kept() {}\n");
repo.git(["add", "."]);
repo.git(["commit", "-m", "initial"]);
let base = repo.git_text(["rev-parse", "HEAD"]);
repo.write(".gitignore", "src\n");
fs::remove_file(repo.path.join("src/lib.rs")).expect("source file should delete");
repo.git(["add", "."]);
repo.git(["commit", "-m", "tighten gitignore and delete"]);
let snapshot = build_index_snapshot(
&repo.registration(),
&repo.selector(),
CodeIndexMode::incremental(base, "HEAD").expect("incremental mode should validate"),
Vec::new(),
)
.expect("incremental delete should index");
assert_eq!(snapshot.deleted_paths, ["src/lib.rs"]);
}
#[test]
fn incremental_regular_changes_use_a_bounded_batch_blob_read() {
let repo = TempGitRepo::create("incremental-batch-blob-read");
for index in 0..48 {
repo.write(
&format!("src/file_{index:02}.rs"),
&format!("pub fn value_{index:02}() -> u64 {{ {index} }}\n"),
);
}
repo.git(["add", "."]);
repo.git(["commit", "-m", "initial"]);
let base = repo.git_text(["rev-parse", "HEAD"]);
for index in 0..48 {
repo.write(
&format!("src/file_{index:02}.rs"),
&format!("pub fn value_{index:02}() -> u64 {{ {} }}\n", index + 100),
);
}
repo.git(["add", "."]);
repo.git(["commit", "-m", "update files"]);
reset_source_read_counts_for_root(repo.path.clone());
let snapshot = build_index_snapshot(
&repo.registration(),
&repo.selector(),
CodeIndexMode::incremental(base, "HEAD").expect("incremental mode should validate"),
Vec::new(),
)
.expect("incremental changes should index");
assert_eq!(snapshot.files.len(), 48);
assert_eq!(source_read_counts_for_root(&repo.path), (0, 1));
}
#[test]
fn repository_id_includes_local_root_with_remote_origin() {
let first = TempGitRepo::create("repo-id-first");
let second = TempGitRepo::create("repo-id-second");
first.git([
"remote",
"add",
"origin",
"https://example.invalid/repo.git",
]);
second.git([
"remote",
"add",
"origin",
"https://example.invalid/repo.git",
]);
let first_registration =
register_repository(&first.path, "first", Vec::new(), Vec::new()).expect("first repo");
let second_registration =
register_repository(&second.path, "second", Vec::new(), Vec::new()).expect("second repo");
assert_ne!(
first_registration.repository_id,
second_registration.repository_id
);
}
#[test]
fn blank_repository_alias_defaults_to_git_root_directory_name() {
let repo = TempGitRepo::create("project-default-alias");
let nested = repo.path.join("src");
let expected_alias = repo
.path
.file_name()
.and_then(|name| name.to_str())
.expect("fixture root should have a directory name")
.to_owned();
let registration =
register_repository(nested, " ", Vec::new(), Vec::new()).expect("repo should register");
assert_eq!(registration.alias, expected_alias);
assert_eq!(registration.root_path, repo.path.display().to_string());
}
#[test]
fn register_repository_rejects_language_filters() {
let repo = TempGitRepo::create("register-language-filter");
repo.write("src/lib.rs", "fn value() {}\n");
repo.git(["add", "."]);
repo.git(["commit", "-m", "initial"]);
let error = register_repository(&repo.path, "fixture", Vec::new(), vec!["rust".to_owned()])
.expect_err("registration language filters should be rejected");
assert!(
error
.to_string()
.contains(REGISTRATION_LANGUAGE_FILTER_ERROR)
);
}
#[test]
fn register_repository_accepts_non_git_source_directory() {
let source = TempSourceDir::create("non-git-register");
source.write("src/lib.rs", "fn value() {}\n");
let expected_alias = source
.path
.file_name()
.and_then(|name| name.to_str())
.expect("fixture root should have a directory name")
.to_owned();
let registration =
register_repository(&source.path, " ", Vec::new(), Vec::new()).expect("source dir");
assert_eq!(registration.alias, expected_alias);
assert_eq!(
registration.root_path,
source.path.canonicalize().unwrap().display().to_string()
);
}
#[test]
fn non_git_full_index_uses_default_source_whitelist() {
let source = TempSourceDir::create("non-git-default-whitelist");
source.write("src/lib.rs", "pub fn indexed_src() {}\n");
source.write("include/public.h", "int indexed_header(void);\n");
source.write("README.md", "# indexed docs\n");
source.write("build/generated.rs", "pub fn build_output() {}\n");
source.write("dist/bundle.js", "export function bundled() {}\n");
source.write("target/generated.rs", "pub fn target_output() {}\n");
source.write(
"node_modules/pkg/index.js",
"export function dependency() {}\n",
);
let mut plan = prepare_full_index_plan(
source.registration(),
source.selector(),
CodeIndexResourceBudget::default(),
)
.expect("filesystem plan should prepare");
let mut paths = Vec::new();
let mut symbol_names = Vec::new();
loop {
let (next_plan, batch) = plan.parse_next_batch().expect("batch should parse");
plan = next_plan;
let Some(batch) = batch else {
break;
};
paths.extend(batch.files.into_iter().map(|file| file.path));
symbol_names.extend(batch.symbols.into_iter().map(|symbol| symbol.name));
}
assert!(paths.iter().any(|path| path == "src/lib.rs"));
assert!(paths.iter().any(|path| path == "include/public.h"));
assert!(paths.iter().any(|path| path == "README.md"));
for skipped in [
"build/generated.rs",
"dist/bundle.js",
"target/generated.rs",
"node_modules/pkg/index.js",
] {
assert!(paths.iter().all(|path| path != skipped), "{skipped}");
}
assert!(symbol_names.iter().any(|name| name == "indexed_src"));
}
#[test]
fn non_git_explicit_path_filter_opts_into_broad_directory() {
let source = TempSourceDir::create("non-git-build-opt-in");
source.write("src/lib.rs", "pub fn indexed_src() {}\n");
source.write("build/generated.rs", "pub fn build_output() {}\n");
let registration = CodeRepositoryRegistration::new(
"repo",
"alias",
source.path.display().to_string(),
vec!["build".to_owned()],
Vec::new(),
)
.expect("registration should validate");
let plan = prepare_full_index_plan(
registration,
source.selector(),
CodeIndexResourceBudget::default(),
)
.expect("filesystem plan should prepare");
let (_, batch) = plan.parse_next_batch().expect("batch should parse");
let paths = batch
.expect("batch should exist")
.files
.into_iter()
.map(|file| file.path)
.collect::<Vec<_>>();
assert_eq!(paths, ["build/generated.rs"]);
}
#[test]
fn non_git_incremental_uses_synthetic_file_fingerprints() {
let source = TempSourceDir::create("non-git-incremental");
source.write("src/lib.rs", "pub fn value() -> u32 { 0 }\n");
source.write("include/old.h", "int old_value(void);\n");
let registration = source.registration();
let selector = source.selector();
let base_snapshot =
build_index_snapshot(®istration, &selector, CodeIndexMode::Full, Vec::new())
.expect("base filesystem index should build");
let previous_hashes = base_snapshot
.files
.iter()
.map(|file| CodeFileFingerprint {
path: file.path.clone(),
blob_hash: file.blob_hash.clone(),
})
.collect::<Vec<_>>();
source.write("src/lib.rs", "pub fn value() -> u32 { 1 }\n");
source.write("src/new.rs", "pub fn new_value() {}\n");
fs::remove_file(source.path.join("include/old.h")).expect("old header should delete");
let snapshot = build_index_snapshot_with_base_commit(
®istration,
&selector,
CodeIndexMode::incremental("previous", "HEAD").expect("incremental mode should validate"),
previous_hashes,
Some(base_snapshot.resolved_commit_sha.clone()),
)
.expect("incremental filesystem index should build");
assert!(snapshot.resolved_commit_sha.starts_with("filesystem:"));
assert_eq!(
snapshot.base_resolved_commit_sha.as_deref(),
Some(base_snapshot.resolved_commit_sha.as_str())
);
assert_eq!(snapshot.deleted_paths, ["include/old.h"]);
assert!(snapshot.files.iter().any(|file| file.path == "src/lib.rs"));
assert!(snapshot.files.iter().any(|file| file.path == "src/new.rs"));
}
#[test]
fn non_git_source_fallback_reads_filesystem_snapshot_paths() {
let source = TempSourceDir::create("non-git-source-fallback");
source.write("src/lib.rs", "pub fn fallback_target() {}\n");
let registration = source.registration();
let commit =
resolve_repository_ref(&source.path, "HEAD").expect("filesystem ref should resolve");
let outcome = source_grep_matches(
®istration,
&commit,
SourceGrepRequest {
query: "fallback_target".to_owned(),
paths: vec!["src/lib.rs".to_owned()],
path_filters: Vec::new(),
language_filters: Vec::new(),
limit: 5,
kind: SourceGrepKind::Definition,
exclude_generated: false,
},
)
.expect("source fallback should read filesystem source");
assert_eq!(outcome.matches.len(), 1);
assert_eq!(outcome.matches[0].path, "src/lib.rs");
assert!(outcome.degraded_reason.is_none());
}
#[test]
fn diff_refs_reject_dash_prefixed_values() {
let repo = TempGitRepo::create("dash-ref");
repo.write("src/lib.rs", "fn value() {}\n");
repo.git(["add", "."]);
repo.git(["commit", "-m", "initial"]);
let error = changed_paths_for_diff(&repo.path, "--cached", "HEAD")
.expect_err("dash-prefixed refs should be rejected");
assert!(error.to_string().contains("must not start"));
}
#[test]
fn parses_git_name_status_for_rename_copy_and_delete() {
let changes =
parse_name_status_z(b"M\0src/lib.rs\0R100\0old.rs\0new.rs\0C100\0a.py\0b.py\0D\0gone.ts\0")
.expect("name-status should parse");
assert_eq!(
changes,
vec![
GitChange::AddedOrModified {
path: "src/lib.rs".to_owned()
},
GitChange::Renamed {
old_path: "old.rs".to_owned(),
new_path: "new.rs".to_owned()
},
GitChange::Copied {
old_path: "a.py".to_owned(),
new_path: "b.py".to_owned()
},
GitChange::Deleted {
path: "gone.ts".to_owned()
}
]
);
}
#[test]
fn worktree_status_uses_destination_path_for_renames_and_copies() {
let paths = worktree_changed_paths(
b"R src/new.rs\0src/old.rs\0C src/copied.rs\0src/source.rs\0 M src/lib.rs\0",
);
assert_eq!(paths[0].path, "src/new.rs");
assert_eq!(paths[0].deleted_source.as_deref(), Some("src/old.rs"));
assert_eq!(paths[1].path, "src/copied.rs");
assert_eq!(paths[1].deleted_source, None);
assert_eq!(paths[2].path, "src/lib.rs");
assert_eq!(paths[2].deleted_source, None);
}
#[test]
fn repository_ids_include_local_checkout_identity() {
let first = TempGitRepo::create("repo-id-first");
let second = TempGitRepo::create("repo-id-second");
first.git([
"config",
"remote.origin.url",
"https://example.invalid/repo.git",
]);
second.git([
"config",
"remote.origin.url",
"https://example.invalid/repo.git",
]);
let first = register_repository(&first.path, "first", Vec::new(), Vec::new())
.expect("first repository should register");
let second = register_repository(&second.path, "second", Vec::new(), Vec::new())
.expect("second repository should register");
assert_ne!(first.repository_id, second.repository_id);
}
#[test]
fn rejects_dash_prefixed_git_refs_before_diff_execution() {
let repo = TempGitRepo::create("dash-ref");
repo.write("src/lib.rs", "fn value() {}\n");
repo.git(["add", "."]);
repo.git(["commit", "-m", "initial"]);
let error = changed_paths_for_diff(&repo.path, "--cached", "HEAD")
.expect_err("dash-prefixed refs should be rejected");
assert!(error.to_string().contains("base_ref"));
}
#[test]
fn impact_paths_for_copies_only_include_destination() {
let paths = impact_paths_from_changes(vec![GitChange::Copied {
old_path: "src/source.rs".to_owned(),
new_path: "src/copied.rs".to_owned(),
}]);
assert_eq!(paths, ["src/copied.rs"]);
}
#[test]
fn incremental_deletions_are_limited_to_selected_scope() {
let repo = TempGitRepo::create("incremental-delete-scope");
repo.write("src/lib.rs", "fn kept() {}\n");
repo.write("docs/out.rs", "fn out_of_scope() {}\n");
repo.git(["add", "."]);
repo.git(["commit", "-m", "initial"]);
let base = repo.git_text(["rev-parse", "HEAD"]);
fs::remove_file(repo.path.join("docs/out.rs")).expect("out-of-scope file should delete");
repo.git(["add", "."]);
repo.git(["commit", "-m", "delete docs"]);
let snapshot = build_index_snapshot(
&repo.registration(),
&repo.selector(),
CodeIndexMode::incremental(base, "HEAD").expect("incremental mode should validate"),
Vec::new(),
)
.expect("incremental delete should index");
assert!(snapshot.deleted_paths.is_empty());
}
#[test]
fn deleted_symbol_names_are_extracted_from_base_diff() {
let repo = TempGitRepo::create("deleted-symbol-seeds");
repo.write("src/lib.rs", "fn removed_api() {}\n");
repo.git(["add", "."]);
repo.git(["commit", "-m", "initial"]);
let base = repo.git_text(["rev-parse", "HEAD"]);
fs::remove_file(repo.path.join("src/lib.rs")).expect("source file should delete");
repo.git(["add", "."]);
repo.git(["commit", "-m", "delete api"]);
let names =
deleted_symbol_names_for_diff(&repo.registration(), &repo.selector(), &base, "HEAD")
.expect("deleted symbols should parse");
assert_eq!(names, ["removed_api"]);
}
#[test]
fn reference_resolution_prefers_same_path_and_leaves_ambiguous_names_unresolved() {
let symbols = vec![
symbol("sym-a", "src/a.rs", "run"),
symbol("sym-b", "src/b.rs", "run"),
];
let mut references = vec![
reference("ref-a", "src/a.rs", "run"),
reference("ref-c", "src/c.rs", "run"),
];
resolve_reference_targets(&symbols, &mut references);
assert_eq!(
references[0].target_symbol_snapshot_id.as_deref(),
Some("sym-a")
);
assert_eq!(references[1].target_symbol_snapshot_id, None);
}