use super::*;
use crate::core::embed::MockEmbedder;
use crate::core::store::UsearchStore;
use std::sync::atomic::Ordering;
fn raw(id: &str, file: &str, content: &str) -> RawChunk {
RawChunk {
id: id.to_string(),
file: file.to_string(),
start_line: 1,
end_line: 1 + content.lines().count(),
content: content.to_string(),
function_name: None,
language: Some("rust".to_string()),
chunk_type: crate::core::chunker::ChunkType::Code,
calls: Vec::new(),
inherits_from: Vec::new(),
chunk_depth: 0,
parent_chunk_id: None,
child_chunk_ids: Vec::new(),
nlp_keywords: Vec::new(),
nlp_code_refs: Vec::new(),
virtual_terms: Vec::new(),
}
}
fn make_indexer() -> CodeIndexer {
let dim = 32;
let embedder: Arc<dyn Embedder> = Arc::new(MockEmbedder::new(dim));
let store: Arc<dyn VectorStore> = Arc::new(UsearchStore::new(dim).expect("usearch new"));
CodeIndexer::new("test", "/tmp/test").with_components(embedder, store)
}
#[tokio::test]
async fn test_save_chunks_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("chunks.json");
let idx = make_indexer();
idx.add_chunk(raw("a", "src/a.rs", "fn authenticate() {}"))
.await
.unwrap();
idx.add_chunk(raw("b", "src/b.rs", "fn verify_token() {}"))
.await
.unwrap();
idx.save_chunks_to_disk(&path).await.expect("save chunks");
assert!(path.exists());
let restored = make_indexer();
let n = restored
.load_chunks_from_disk(&path)
.await
.expect("load chunks");
assert_eq!(n, 2);
assert_eq!(restored.chunk_count(), 2);
let bm25 = restored.bm25.read().await;
let hits = bm25.score_query_all("authenticate", 5);
drop(bm25);
assert!(
hits.iter().any(|(id, _)| id == "a"),
"BM25 not rebuilt from restored chunks: {:?}",
hits
);
}
#[tokio::test]
async fn test_load_chunks_missing_file_returns_zero() {
let idx = make_indexer();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nope.json");
let n = idx.load_chunks_from_disk(&path).await.unwrap();
assert_eq!(n, 0);
}
#[tokio::test]
async fn test_persist_coalesces_concurrent_calls() {
let idx = make_indexer();
idx.add_chunk(raw("a", "a.rs", "fn a() {}")).await.unwrap();
for _ in 0..64 {
idx.spawn_incremental_persist();
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
loop {
let in_flight = idx.persist_state.in_flight.load(Ordering::Acquire);
let dirty = idx.persist_state.dirty.load(Ordering::Acquire);
if !in_flight && !dirty {
break;
}
if std::time::Instant::now() >= deadline {
panic!(
"persist coalescing loop did not drain within 15s: \
in_flight={in_flight}, dirty={dirty}"
);
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
idx.persist_state.dirty.store(false, Ordering::Release);
idx.spawn_incremental_persist();
let _ = idx.persist_state.in_flight.load(Ordering::Acquire);
}
#[tokio::test]
async fn test_search_integration_returns_relevant_chunk_first() {
let idx = make_indexer();
idx.add_chunk(raw(
"src/auth.rs:1:5",
"src/auth.rs",
"fn authenticate(user: &str, password: &str) -> bool { true }",
))
.await
.unwrap();
idx.add_chunk(raw(
"src/render.rs:1:3",
"src/render.rs",
"fn render_ui_components() { /* svelte */ }",
))
.await
.unwrap();
idx.add_chunk(raw(
"src/db.rs:1:4",
"src/db.rs",
"struct Database { conn: String }",
))
.await
.unwrap();
let q = SearchQuery {
text: "fn authenticate".to_string(),
top_k: 3,
expand_graph: false,
compact: true,
..Default::default()
};
let results = idx.search(&q).await.expect("search");
assert!(!results.is_empty(), "search should return at least one hit");
assert_eq!(
results[0].id,
"src/auth.rs:1:5",
"auth chunk must rank first; got {:?}",
results.iter().map(|r| &r.id).collect::<Vec<_>>()
);
assert!(
results[0].compact_snippet.is_some(),
"compact_snippet should be populated when compact=true"
);
assert!(
results[0].match_reason == "hybrid" || results[0].match_reason == "bm25",
"expected hybrid or bm25 match_reason, got {}",
results[0].match_reason
);
}
#[tokio::test]
async fn test_query_cache_skips_embedder_on_repeat() {
let idx = make_indexer();
let q = "find user authentication logic";
let v1 = idx.embed_query(q).await.unwrap().unwrap();
let key = hash_query(q);
let cached = {
let mut g = idx.query_cache.lock().unwrap();
g.get(&key).cloned()
};
assert_eq!(cached.as_ref(), Some(&v1), "cache must be populated");
let v2 = idx.embed_query(q).await.unwrap().unwrap();
assert_eq!(v1, v2, "second call must return identical vector via cache");
}
#[tokio::test]
async fn test_search_with_no_embedder_falls_back_to_bm25() {
let idx = CodeIndexer::new("bm25-only", "/tmp/test");
idx.add_chunk(raw("f.rs:1:1", "f.rs", "fn authenticate() {}"))
.await
.unwrap();
idx.add_chunk(raw("g.rs:1:1", "g.rs", "fn unrelated() {}"))
.await
.unwrap();
let q = SearchQuery {
text: "authenticate".to_string(),
top_k: 5,
expand_graph: false,
compact: false,
..Default::default()
};
let r = idx.search(&q).await.unwrap();
assert_eq!(r[0].id, "f.rs:1:1");
assert_eq!(r[0].match_reason, "bm25");
}
#[tokio::test]
async fn test_remove_chunk_removes_from_results() {
let idx = make_indexer();
idx.add_chunk(raw("a:1:1", "a.rs", "fn authenticate() {}"))
.await
.unwrap();
idx.add_chunk(raw("b:1:1", "b.rs", "fn other_thing() {}"))
.await
.unwrap();
idx.remove_chunk("a:1:1").await.unwrap();
let q = SearchQuery {
text: "authenticate".to_string(),
top_k: 5,
expand_graph: false,
compact: false,
..Default::default()
};
let r = idx.search(&q).await.unwrap();
assert!(!r.iter().any(|c| c.id == "a:1:1"));
}
#[tokio::test]
async fn test_kg_expansion_marks_neighbours_with_hybrid_kg() {
let idx = CodeIndexer::new("kg-test", "/tmp/test");
idx.add_chunk(RawChunk {
id: "h:1".to_string(),
file: "h.rs".to_string(),
start_line: 1,
end_line: 3,
content: "fn login_handler() { /* dispatch to verifier */ }".to_string(),
function_name: Some("login_handler".to_string()),
language: Some("rust".to_string()),
chunk_type: crate::core::chunker::ChunkType::Function,
calls: vec!["authenticate".to_string()],
inherits_from: Vec::new(),
chunk_depth: 0,
parent_chunk_id: None,
child_chunk_ids: Vec::new(),
nlp_keywords: Vec::new(),
nlp_code_refs: Vec::new(),
virtual_terms: Vec::new(),
})
.await
.unwrap();
idx.add_chunk(RawChunk {
id: "a:1".to_string(),
file: "a.rs".to_string(),
start_line: 1,
end_line: 1,
content: "fn authenticate() {}".to_string(),
function_name: Some("authenticate".to_string()),
language: Some("rust".to_string()),
chunk_type: crate::core::chunker::ChunkType::Function,
calls: Vec::new(),
inherits_from: Vec::new(),
chunk_depth: 0,
parent_chunk_id: None,
child_chunk_ids: Vec::new(),
nlp_keywords: Vec::new(),
nlp_code_refs: Vec::new(),
virtual_terms: Vec::new(),
})
.await
.unwrap();
let q = SearchQuery {
text: "callers of authenticate".to_string(),
top_k: 10,
expand_graph: true,
compact: false,
..Default::default()
};
let results = idx.search(&q).await.unwrap();
let login = results
.iter()
.find(|c| c.id == "h:1")
.expect("login_handler should surface via KG expansion");
assert_eq!(
login.match_reason, "hybrid+kg",
"KG-expanded chunks must carry hybrid+kg marker, got {}",
login.match_reason
);
let trigger = results
.iter()
.find(|c| c.id == "a:1")
.expect("authenticate must appear directly");
let expected = trigger.score * KG_EXPAND_SCORE_FACTOR;
assert!(
(login.score - expected).abs() < 1e-5,
"expected KG score = 0.7 * {} = {}, got {}",
trigger.score,
expected,
login.score
);
}
#[tokio::test]
async fn test_kg_expansion_disabled_by_expand_graph_false() {
let idx = make_indexer();
idx.add_chunk(RawChunk {
id: "h:1".to_string(),
file: "h.rs".to_string(),
start_line: 1,
end_line: 1,
content: "fn caller() { target(); }".to_string(),
function_name: Some("caller".to_string()),
language: Some("rust".to_string()),
chunk_type: crate::core::chunker::ChunkType::Function,
calls: vec!["target".to_string()],
inherits_from: Vec::new(),
chunk_depth: 0,
parent_chunk_id: None,
child_chunk_ids: Vec::new(),
nlp_keywords: Vec::new(),
nlp_code_refs: Vec::new(),
virtual_terms: Vec::new(),
})
.await
.unwrap();
idx.add_chunk(RawChunk {
id: "t:1".to_string(),
file: "t.rs".to_string(),
start_line: 1,
end_line: 1,
content: "fn target() {}".to_string(),
function_name: Some("target".to_string()),
language: Some("rust".to_string()),
chunk_type: crate::core::chunker::ChunkType::Function,
calls: Vec::new(),
inherits_from: Vec::new(),
chunk_depth: 0,
parent_chunk_id: None,
child_chunk_ids: Vec::new(),
nlp_keywords: Vec::new(),
nlp_code_refs: Vec::new(),
virtual_terms: Vec::new(),
})
.await
.unwrap();
let q = SearchQuery {
text: "callers of target".to_string(),
top_k: 10,
expand_graph: false,
compact: false,
..Default::default()
};
let results = idx.search(&q).await.unwrap();
assert!(
!results.iter().any(|c| c.match_reason.contains("kg")),
"expand_graph=false must suppress KG expansion, got {results:#?}"
);
}
#[tokio::test]
async fn test_symbol_graph_rebuilds_after_indexing() {
let idx = make_indexer();
assert_eq!(idx.symbol_graph().await.node_count(), 0);
idx.index_file("a.rs", "fn alpha() { beta(); }\nfn beta() {}\n")
.await
.unwrap();
let g = idx.symbol_graph().await;
assert!(g.node_count() >= 2, "graph should hold alpha + beta");
assert!(
!g.callees_of("alpha", 1).is_empty(),
"alpha should have a callee edge to beta"
);
}
#[tokio::test]
async fn test_entity_exact_match_finds_chunk() {
let idx = make_indexer();
idx.index_file("e.rs", "pub struct MyType { x: u32 }\nfn f() {}\n")
.await
.unwrap();
let hit = idx.entity_exact_match("MyType").await;
assert!(hit.is_some(), "expected entity_exact_match to find MyType");
let hit_id = hit.unwrap();
let chunks = idx.chunks.read().await;
assert!(
chunks
.get(&hit_id)
.map(|c| c.file == "e.rs")
.unwrap_or(false),
"matched chunk should live in e.rs",
);
}
#[tokio::test]
async fn test_entity_exact_match_struct_ranks_first() {
let idx = CodeIndexer::new("ent-rank-1", "/tmp/test");
idx.index_file(
"src/types.rs",
"pub struct FooBar { pub x: u32 }\n\nfn unrelated() { let _ = 1; }\n",
)
.await
.unwrap();
idx.index_file("src/other.rs", "fn other_thing() {}\n")
.await
.unwrap();
let q = SearchQuery {
text: "FooBar".to_string(),
top_k: 5,
expand_graph: false,
compact: false,
..Default::default()
};
let results = idx.search(&q).await.expect("search");
assert!(!results.is_empty(), "search must return at least one hit");
assert_eq!(
results[0].file,
"src/types.rs",
"FooBar's defining file must rank first; got {:?}",
results.iter().map(|r| &r.file).collect::<Vec<_>>(),
);
assert!(
results[0].content.contains("FooBar"),
"rank-1 chunk must contain the FooBar definition; got {:?}",
results[0].content,
);
}
#[tokio::test]
async fn test_entity_exact_match_skips_non_symbol_entities() {
let idx = make_indexer();
idx.index_file("lit.rs", "fn f() { let _ = \"this is a long literal\"; }\n")
.await
.unwrap();
assert!(
idx.entity_exact_match("literal").await.is_none(),
"non-symbol entity types must not satisfy entity_exact_match"
);
}
#[tokio::test]
async fn test_entity_exact_match_skips_multiword_query() {
let idx = make_indexer();
idx.index_file("e.rs", "use std::sync::Arc;\nfn f() {}\n")
.await
.unwrap();
assert!(idx.entity_exact_match("Arc thing").await.is_none());
}
#[tokio::test]
async fn test_virtual_terms_populated_from_entities() {
let idx = make_indexer();
idx.index_file(
"v.rs",
"use std::sync::Arc;\nfn f() { let _x: Arc<String> = Arc::new(String::new()); }\n",
)
.await
.unwrap();
let chunks = idx.chunks.read().await;
let f_chunk = chunks
.values()
.find(|c| c.function_name.as_deref() == Some("f"))
.expect("f chunk");
assert!(
f_chunk.virtual_terms.iter().any(|t| t == "Arc"),
"expected 'Arc' in virtual_terms, got {:?}",
f_chunk.virtual_terms
);
}
#[tokio::test]
async fn test_get_embedding_returns_some_after_indexing() {
let idx = make_indexer();
idx.add_chunk(raw("a:1:1", "a.rs", "fn alpha() {}"))
.await
.unwrap();
let emb = idx.get_embedding("a:1:1");
assert!(emb.is_some(), "expected embedding cached after add_chunk");
assert!(idx.get_embedding("nope").is_none());
}
#[tokio::test]
async fn test_similar_by_embedding_excludes_seed() {
let idx = make_indexer();
idx.add_chunk(raw("a:1:1", "a.rs", "fn alpha() {}"))
.await
.unwrap();
idx.add_chunk(raw("b:1:1", "b.rs", "fn beta() {}"))
.await
.unwrap();
let emb = idx.get_embedding("a:1:1").unwrap();
let results = idx
.similar_by_embedding(&emb, 5, Some("a:1:1"))
.await
.unwrap();
assert!(results.iter().all(|c| c.id != "a:1:1"));
assert!(results.iter().all(|c| c.match_reason == "vector"));
}
#[tokio::test]
async fn test_index_files_batch_indexes_all_chunks_once() {
let idx = make_indexer();
let files = vec![
(
"src/a.rs".to_string(),
"fn alpha() { beta(); }\nfn beta() {}\n".to_string(),
),
(
"src/b.rs".to_string(),
"fn gamma() {}\nfn delta() { gamma(); }\n".to_string(),
),
];
let added = idx.index_files_batch(&files).await.unwrap();
assert!(added >= 4, "expected at least 4 chunks, got {added}");
let g = idx.symbol_graph().await;
assert!(g.node_count() >= 4);
let q = SearchQuery {
text: "fn alpha".to_string(),
top_k: 5,
expand_graph: false,
compact: false,
..Default::default()
};
let r = idx.search(&q).await.unwrap();
assert!(r.iter().any(|c| c.file == "src/a.rs"));
}
#[tokio::test]
async fn test_index_files_batch_empty_input_is_noop() {
let idx = make_indexer();
let added = idx.index_files_batch(&[]).await.unwrap();
assert_eq!(added, 0);
assert_eq!(idx.chunk_count(), 0);
}
#[tokio::test]
async fn test_index_files_batch_bm25_only_mode() {
let idx = CodeIndexer::new("bm25-batch", "/tmp/test");
let files = vec![(
"x.rs".to_string(),
"fn authenticate() {}\nfn other() {}\n".to_string(),
)];
let added = idx.index_files_batch(&files).await.unwrap();
assert!(added >= 2);
let r = idx
.search(&SearchQuery {
text: "authenticate".to_string(),
top_k: 5,
expand_graph: false,
compact: false,
..Default::default()
})
.await
.unwrap();
assert!(r.iter().any(|c| c.content.contains("authenticate")));
}
#[tokio::test]
async fn search_uses_domain_terms_when_provided() {
use crate::core::classifier::{QueryClassifier, QueryIntent};
let plain = QueryClassifier::classify("PMS integration query");
assert_eq!(
plain,
QueryIntent::Unknown,
"baseline: plain classifier must treat the PMS phrase as Unknown"
);
let idx =
CodeIndexer::new("domain-test", "/tmp/domain").with_domain_terms(vec!["PMS".to_string()]);
idx.index_file("api.rs", "fn pms_handler() {}\nfn other() {}\n")
.await
.expect("index_file ok");
let r = idx
.search(&SearchQuery {
text: "PMS integration query".into(),
top_k: 5,
expand_graph: false,
compact: false,
..Default::default()
})
.await
.expect("search ok");
assert!(
r.iter().any(|c| c.content.contains("pms_handler")),
"expected pms_handler chunk to appear in results: {:?}",
r.iter().map(|c| &c.content).collect::<Vec<_>>()
);
}
#[test]
fn test_file_type_multiplier_demotes_docs() {
assert_eq!(file_type_score_multiplier("src/auth.rs"), 1.0);
assert_eq!(file_type_score_multiplier("src/auth.py"), 1.0);
assert_eq!(file_type_score_multiplier("src/auth.go"), 1.0);
assert_eq!(file_type_score_multiplier("CHANGELOG.md"), 0.5);
assert_eq!(file_type_score_multiplier("docs/CLAUDE.md"), 0.5);
assert_eq!(file_type_score_multiplier("Cargo.toml"), 0.5);
assert_eq!(file_type_score_multiplier("config.yaml"), 0.5);
assert_eq!(file_type_score_multiplier("data.json"), 0.5);
assert_eq!(file_type_score_multiplier("README.MD"), 0.5);
}
#[tokio::test]
async fn test_definition_demotes_markdown_below_source() {
let idx = make_indexer();
idx.add_chunk(raw(
"doc:1",
"CHANGELOG.md",
"## CodeChunk struct\nCodeChunk struct fields: id, file. CodeChunk struct fields are stable.",
))
.await
.unwrap();
idx.add_chunk(raw(
"src:1",
"src/indexer.rs",
"pub struct CodeChunk { pub id: String, pub file: String }",
))
.await
.unwrap();
let q = SearchQuery {
text: "struct CodeChunk fields".to_string(),
top_k: 10,
expand_graph: false,
compact: false,
..Default::default()
};
let results = idx.search(&q).await.unwrap();
assert!(!results.is_empty(), "search must return results");
assert!(
results[0].file.ends_with(".rs"),
"Definition intent must rank source over docs, top result file = {}",
results[0].file
);
}
#[tokio::test]
async fn test_conceptual_does_not_demote_docs() {
let idx = make_indexer();
idx.add_chunk(raw(
"doc:1",
"ARCHITECTURE.md",
"How does the CodeChunk pipeline work in trusty-search.",
))
.await
.unwrap();
idx.add_chunk(raw(
"src:1",
"src/indexer.rs",
"pub struct CodeChunk { pub id: String }",
))
.await
.unwrap();
let q = SearchQuery {
text: "how does the CodeChunk pipeline work".to_string(),
top_k: 10,
expand_graph: false,
compact: false,
..Default::default()
};
let results = idx.search(&q).await.unwrap();
assert!(
results.iter().any(|c| c.file.ends_with(".md")),
"Conceptual queries must still surface .md docs"
);
}
#[tokio::test]
async fn test_kg_results_survive_top_k_truncation() {
let idx = CodeIndexer::new("kg-trunc", "/tmp/test");
idx.add_chunk(RawChunk {
id: "src:caller".to_string(),
file: "caller.rs".to_string(),
start_line: 1,
end_line: 3,
content: "fn caller() { /* dispatches */ }".to_string(),
function_name: Some("caller".to_string()),
language: Some("rust".to_string()),
chunk_type: crate::core::chunker::ChunkType::Function,
calls: vec!["authenticate".to_string()],
inherits_from: Vec::new(),
chunk_depth: 0,
parent_chunk_id: None,
child_chunk_ids: Vec::new(),
nlp_keywords: Vec::new(),
nlp_code_refs: Vec::new(),
virtual_terms: Vec::new(),
})
.await
.unwrap();
idx.add_chunk(RawChunk {
id: "src:authenticate".to_string(),
file: "auth.rs".to_string(),
start_line: 1,
end_line: 1,
content: "fn authenticate() {}".to_string(),
function_name: Some("authenticate".to_string()),
language: Some("rust".to_string()),
chunk_type: crate::core::chunker::ChunkType::Function,
calls: Vec::new(),
inherits_from: Vec::new(),
chunk_depth: 0,
parent_chunk_id: None,
child_chunk_ids: Vec::new(),
nlp_keywords: Vec::new(),
nlp_code_refs: Vec::new(),
virtual_terms: Vec::new(),
})
.await
.unwrap();
let q = SearchQuery {
text: "callers of authenticate".to_string(),
top_k: 10,
expand_graph: true,
compact: false,
..Default::default()
};
let results = idx.search(&q).await.unwrap();
assert!(
results.iter().any(|c| c.match_reason == "hybrid+kg"),
"at least one result must carry 'hybrid+kg' match_reason, got: {:#?}",
results
.iter()
.map(|c| (&c.id, &c.match_reason))
.collect::<Vec<_>>()
);
}
#[test]
fn test_intent_routing_definitions() {
use crate::core::classifier::QueryIntent;
let (a, b, kg) = QueryIntent::Definition.weights();
assert!((a - 0.3).abs() < 1e-6 && (b - 0.7).abs() < 1e-6 && !kg);
let (a, b, kg) = QueryIntent::Usage.weights();
assert!((a - 0.5).abs() < 1e-6 && (b - 0.5).abs() < 1e-6 && kg);
}
#[tokio::test]
async fn test_enumerate_chunks_paginates_stable_order() {
let idx = make_indexer();
fn raw_lines(id: &str, file: &str, start: usize, end: usize, content: &str) -> RawChunk {
let mut r = raw(id, file, content);
r.start_line = start;
r.end_line = end;
r
}
idx.add_chunk(raw_lines("b.rs:10:20", "b.rs", 10, 20, "fn b_two() {}"))
.await
.unwrap();
idx.add_chunk(raw_lines("a.rs:1:5", "a.rs", 1, 5, "fn a_one() {}"))
.await
.unwrap();
idx.add_chunk(raw_lines("b.rs:1:5", "b.rs", 1, 5, "fn b_one() {}"))
.await
.unwrap();
idx.add_chunk(raw_lines("a.rs:30:40", "a.rs", 30, 40, "fn a_two() {}"))
.await
.unwrap();
let (total_all, all) = idx.enumerate_chunks(0, 100).await;
assert_eq!(total_all, 4);
let ids: Vec<_> = all.iter().map(|c| c.id.as_str()).collect();
assert_eq!(
ids,
vec!["a.rs:1:5", "a.rs:30:40", "b.rs:1:5", "b.rs:10:20"]
);
let (total_p1, page1) = idx.enumerate_chunks(0, 2).await;
let (total_p2, page2) = idx.enumerate_chunks(2, 2).await;
assert_eq!(total_p1, 4);
assert_eq!(total_p2, 4);
assert_eq!(page1.len(), 2);
assert_eq!(page2.len(), 2);
let combined: Vec<_> = page1
.iter()
.chain(page2.iter())
.map(|c| c.id.as_str())
.collect();
assert_eq!(combined, ids);
let (total_end, end) = idx.enumerate_chunks(10, 5).await;
assert_eq!(total_end, 4);
assert!(end.is_empty());
let (total_z, z) = idx.enumerate_chunks(0, 0).await;
assert_eq!(total_z, 4);
assert!(z.is_empty());
}
fn make_branch_query(text: &str, files: Vec<String>, boost: f32) -> SearchQuery {
SearchQuery {
text: text.to_string(),
top_k: 10,
expand_graph: false,
compact: false,
branch_files: Some(files),
branch_boost: boost,
branch: None,
}
}
#[tokio::test]
async fn test_branch_boost_applied_to_matching_chunks() {
let idx = make_indexer();
idx.add_chunk(raw(
"src/on.rs:1:1",
"src/on.rs",
"fn authenticate(user: &str) -> bool { true }",
))
.await
.unwrap();
idx.add_chunk(raw(
"src/off.rs:1:1",
"src/off.rs",
"fn authenticate(user: &str) -> bool { true }",
))
.await
.unwrap();
let q = make_branch_query("fn authenticate", vec!["src/on.rs".to_string()], 1.5);
let results = idx.search(&q).await.unwrap();
assert!(!results.is_empty(), "branch-aware search must return hits");
let on_branch = results
.iter()
.find(|c| c.file == "src/on.rs")
.expect("on-branch chunk in results");
let off_branch = results.iter().find(|c| c.file == "src/off.rs");
assert!(on_branch.on_branch, "on_branch must be true for on.rs");
if let Some(off) = off_branch {
assert!(!off.on_branch, "on_branch must be false for off.rs");
assert!(
on_branch.score >= off.score,
"branch boost must make on.rs >= off.rs (got {} vs {})",
on_branch.score,
off.score
);
}
assert_eq!(
results[0].file,
"src/on.rs",
"on-branch chunk must rank first; got {:?}",
results.iter().map(|c| &c.file).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn test_branch_boost_clamped_to_3x() {
let q = make_branch_query("foo", vec!["src/on.rs".to_string()], 10.0);
let root = std::path::PathBuf::from("/tmp/test");
let (set, boost) = super::search::resolve_branch_set(&q, &root);
assert!(set.is_some(), "branch set must be present");
assert!(
(boost - 3.0).abs() < f32::EPSILON,
"branch_boost=10.0 must clamp to 3.0, got {boost}"
);
let q_low = make_branch_query("foo", vec!["src/on.rs".to_string()], 0.0);
let (set_low, boost_low) = super::search::resolve_branch_set(&q_low, &root);
assert!(
(boost_low - 1.0).abs() < f32::EPSILON,
"branch_boost=0.0 must clamp to 1.0, got {boost_low}"
);
assert!(
set_low.is_none(),
"branch_boost=1.0 must drop the set (no-op)"
);
}
#[tokio::test]
async fn test_on_branch_set_correctly() {
let idx = make_indexer();
idx.add_chunk(raw(
"src/on.rs:1:1",
"src/on.rs",
"fn authenticate() -> bool { true }",
))
.await
.unwrap();
idx.add_chunk(raw(
"src/off.rs:1:1",
"src/off.rs",
"fn authenticate() -> bool { true }",
))
.await
.unwrap();
let q = make_branch_query("fn authenticate", vec!["src/on.rs".to_string()], 1.5);
let results = idx.search(&q).await.unwrap();
for c in &results {
if c.file == "src/on.rs" {
assert!(c.on_branch, "on.rs must be flagged on_branch=true");
} else if c.file == "src/off.rs" {
assert!(!c.on_branch, "off.rs must be flagged on_branch=false");
}
}
let q2 = make_branch_query("fn authenticate", vec!["./src/on.rs".to_string()], 1.5);
let results2 = idx.search(&q2).await.unwrap();
let on2 = results2
.iter()
.find(|c| c.file == "src/on.rs")
.expect("on-branch chunk in results");
assert!(on2.on_branch, "leading './' must be normalized away");
}
#[tokio::test]
async fn test_no_boost_when_branch_files_absent() {
let idx = make_indexer();
idx.add_chunk(raw(
"src/auth.rs:1:5",
"src/auth.rs",
"fn authenticate(user: &str, password: &str) -> bool { true }",
))
.await
.unwrap();
idx.add_chunk(raw(
"src/render.rs:1:3",
"src/render.rs",
"fn render_ui_components() { /* svelte */ }",
))
.await
.unwrap();
let q = SearchQuery {
text: "fn authenticate".to_string(),
top_k: 5,
expand_graph: false,
compact: false,
branch_files: None,
branch_boost: SearchQuery::default_branch_boost(),
branch: None,
};
let results = idx.search(&q).await.unwrap();
assert!(!results.is_empty());
for c in &results {
assert!(
!c.on_branch,
"on_branch must default to false when no branch context provided"
);
}
}