use super::*;
#[test]
fn test_merge_pdgs_merges_nodes_edges_and_indexes() {
use crate::graph::pdg::{Edge, EdgeMetadata, EdgeType, Node, NodeType};
use std::sync::Arc;
let mut target = ProgramDependenceGraph::new();
target.add_node(Node {
id: "keep.rs:keeper".to_string(),
node_type: NodeType::Function,
name: "keeper".to_string(),
file_path: Arc::from("keep.rs"),
byte_range: (0, 5),
complexity: 1,
language: "rust".to_string(),
});
let mut source = ProgramDependenceGraph::new();
let a = source.add_node(Node {
id: "a.rs:foo".to_string(),
node_type: NodeType::Function,
name: "foo".to_string(),
file_path: Arc::from("a.rs"),
byte_range: (0, 10),
complexity: 3,
language: "rust".to_string(),
});
let b = source.add_node(Node {
id: "a.rs:bar".to_string(),
node_type: NodeType::Class,
name: "bar".to_string(),
file_path: Arc::from("a.rs"),
byte_range: (11, 20),
complexity: 0,
language: "rust".to_string(),
});
source.add_edge(
a,
b,
Edge {
edge_type: EdgeType::Call,
metadata: EdgeMetadata {
call_count: Some(2),
variable_name: None,
confidence: Some(0.9),
channel: None,
position: None,
},
},
);
merge_pdgs(&mut target, source);
assert_eq!(target.node_count(), 3);
assert_eq!(target.edge_count(), 1);
assert!(target.find_by_symbol("keep.rs:keeper").is_some());
let foo = target.find_by_symbol("a.rs:foo").expect("foo should merge");
let bar = target.find_by_symbol("a.rs:bar").expect("bar should merge");
assert_eq!(target.get_node(foo).unwrap().complexity, 3);
let call_edges: Vec<_> = target
.edge_indices()
.filter_map(|idx| {
let edge = target.get_edge(idx)?;
if edge.edge_type == EdgeType::Call {
Some((target.edge_endpoints(idx).unwrap(), edge.metadata.clone()))
} else {
None
}
})
.collect();
assert_eq!(call_edges.len(), 1);
let ((caller, callee), metadata) = &call_edges[0];
assert_eq!(*caller, foo);
assert_eq!(*callee, bar);
assert_eq!(metadata.call_count, Some(2));
}
#[test]
fn test_merge_pdgs_empty_source_is_noop() {
let mut target = ProgramDependenceGraph::new();
assert_eq!(target.node_count(), 0);
merge_pdgs(&mut target, ProgramDependenceGraph::new());
assert_eq!(target.node_count(), 0);
assert_eq!(target.edge_count(), 0);
}
#[test]
fn test_merge_pdgs_dedupes_duplicate_node_ids_and_remaps_edges() {
use crate::graph::pdg::{Edge, EdgeMetadata, EdgeType, Node, NodeType};
use std::sync::Arc;
fn caller(id: &str, file: &str) -> Node {
Node {
id: id.to_string(),
node_type: NodeType::Function,
name: "main".to_string(),
file_path: Arc::from(file),
byte_range: (0, 10),
complexity: 1,
language: "rust".to_string(),
}
}
fn external_node(file: &str) -> Node {
Node {
id: "external::String".to_string(),
node_type: NodeType::External,
name: "String".to_string(),
file_path: Arc::from(file),
byte_range: (0, 0),
complexity: 0,
language: "external".to_string(),
}
}
fn call_edge() -> crate::graph::pdg::Edge {
Edge {
edge_type: EdgeType::Call,
metadata: EdgeMetadata {
call_count: None,
variable_name: None,
confidence: None,
channel: None,
position: None,
},
}
}
let mut target = ProgramDependenceGraph::new();
for file in ["a.rs", "b.rs"] {
let mut source = ProgramDependenceGraph::new();
let caller_idx = source.add_node(caller(&format!("{file}:main"), file));
let external_idx = source.add_node(external_node(file));
source.add_edge(caller_idx, external_idx, call_edge());
merge_pdgs(&mut target, source);
}
assert_eq!(target.node_count(), 3, "two callers + one shared external");
let external = target
.find_by_id("external::String")
.expect("shared external survives the merges");
assert_eq!(
target.get_node(external).unwrap().file_path.as_ref(),
"<external>",
"shared placeholder is not owned by any file"
);
let inbound_calls = target
.edge_indices()
.filter(|idx| {
target
.get_edge(*idx)
.is_some_and(|edge| edge.edge_type == EdgeType::Call)
&& target
.edge_endpoints(*idx)
.is_some_and(|(_, to)| to == external)
})
.count();
assert_eq!(
inbound_calls, 2,
"both callers' edges point at the shared node"
);
}
#[test]
fn test_remove_file_keeps_shared_external_placeholders() {
use crate::graph::pdg::{Edge, EdgeMetadata, EdgeType, Node, NodeType};
use std::sync::Arc;
let mut target = ProgramDependenceGraph::new();
let mut source = ProgramDependenceGraph::new();
let a_main = source.add_node(Node {
id: "a.rs:main".to_string(),
node_type: NodeType::Function,
name: "main".to_string(),
file_path: Arc::from("a.rs"),
byte_range: (0, 10),
complexity: 1,
language: "rust".to_string(),
});
let external_idx = source.add_node(Node {
id: "external::String".to_string(),
node_type: NodeType::External,
name: "String".to_string(),
file_path: Arc::from("a.rs"),
byte_range: (0, 0),
complexity: 0,
language: "external".to_string(),
});
source.add_edge(
a_main,
external_idx,
Edge {
edge_type: EdgeType::Call,
metadata: EdgeMetadata {
call_count: None,
variable_name: None,
confidence: None,
channel: None,
position: None,
},
},
);
merge_pdgs(&mut target, source);
let mut later = ProgramDependenceGraph::new();
later.add_node(Node {
id: "b.rs:main".to_string(),
node_type: NodeType::Function,
name: "main".to_string(),
file_path: Arc::from("b.rs"),
byte_range: (0, 10),
complexity: 1,
language: "rust".to_string(),
});
merge_pdgs(&mut target, later);
let external = target
.find_by_id("external::String")
.expect("shared external exists");
let b_idx = target.find_by_id("b.rs:main").expect("b.rs:main exists");
target.add_edge(
b_idx,
external,
Edge {
edge_type: EdgeType::Call,
metadata: EdgeMetadata {
call_count: None,
variable_name: None,
confidence: None,
channel: None,
position: None,
},
},
);
target.remove_file("a.rs");
let external_after = target
.find_by_id("external::String")
.expect("shared external survives a.rs removal");
assert_eq!(external_after, external);
let b_call_survives = target.edge_indices().any(|idx| {
target.edge_endpoints(idx) == Some((b_idx, external_after))
&& target
.get_edge(idx)
.is_some_and(|edge| edge.edge_type == EdgeType::Call)
});
assert!(
b_call_survives,
"b.rs's edge to the shared external survives"
);
assert_eq!(target.node_count(), 2, "b.rs:main + the shared external");
}
#[test]
fn test_tokenize_code_camel_case() {
let toks = tokenize_code("getUserName");
assert!(
toks.contains(&"get".to_string()),
"expected 'get', got {:?}",
toks
);
assert!(
toks.contains(&"user".to_string()),
"expected 'user', got {:?}",
toks
);
assert!(
toks.contains(&"name".to_string()),
"expected 'name', got {:?}",
toks
);
}
#[test]
fn test_tokenize_code_acronyms_and_digits() {
let toks = tokenize_code("HTTPConnection");
assert!(
toks.contains(&"http".to_string()),
"expected 'http', got {:?}",
toks
);
assert!(
toks.contains(&"connection".to_string()),
"expected 'connection', got {:?}",
toks
);
let toks2 = tokenize_code("HTTP2Connection");
assert!(
toks2.contains(&"http".to_string()),
"expected 'http', got {:?}",
toks2
);
assert!(
toks2.contains(&"2".to_string()),
"expected '2', got {:?}",
toks2
);
assert!(
toks2.contains(&"connection".to_string()),
"expected 'connection', got {:?}",
toks2
);
}
#[test]
fn test_tokenize_code_snake_case() {
let toks = tokenize_code("get_user_name");
assert!(
toks.contains(&"get".to_string()),
"expected 'get', got {:?}",
toks
);
assert!(
toks.contains(&"user".to_string()),
"expected 'user', got {:?}",
toks
);
assert!(
toks.contains(&"name".to_string()),
"expected 'name', got {:?}",
toks
);
}
#[test]
fn test_tokenize_code_filters_short_tokens() {
let toks = tokenize_code("a b c xyz");
assert!(!toks.contains(&"a".to_string()));
assert!(!toks.contains(&"b".to_string()));
assert!(!toks.contains(&"c".to_string()));
assert!(toks.contains(&"xyz".to_string()));
}
#[test]
fn test_tokenize_code_empty() {
let toks = tokenize_code("");
assert!(toks.is_empty());
}
#[test]
fn test_preceding_doc_context_is_bounded_and_ordered() {
let source = b"/// first\n/// second\nfn demo() {}\n";
let start = source
.windows(2)
.position(|window| window == b"fn")
.unwrap();
assert_eq!(preceding_doc_context(source, start), "first\nsecond");
}
#[test]
fn test_tfidf_embedder_empty_corpus() {
let embedder = TfIdfEmbedder::build(&[]);
let vec = embedder.embed("test query");
assert_eq!(
vec.len(),
768,
"must produce 768-dim vector even for empty corpus"
);
assert!(vec.iter().all(|&v| v == 0.0), "empty corpus → zero vector");
}
#[test]
fn test_tfidf_embedding_dimension() {
let docs: Vec<(String, String)> = (0..10)
.map(|i| {
(
format!("doc_{}", i),
format!(
"fn handle_request_{} {{ let result = process(); result }}",
i
),
)
})
.collect();
let embedder = TfIdfEmbedder::build(&docs);
let vec = embedder.embed("handle request process");
assert_eq!(vec.len(), 768, "embedding dimension must be 768");
}
#[test]
fn test_tfidf_embedding_normalized() {
let docs: Vec<(String, String)> = vec![
(
"auth".to_string(),
"fn authenticate_user(token: &str) -> bool { verify_token(token) }".to_string(),
),
(
"db".to_string(),
"fn connect_database(url: &str) -> Connection { open_connection(url) }".to_string(),
),
(
"http".to_string(),
"fn send_request(endpoint: &str) -> Response { http_get(endpoint) }".to_string(),
),
];
let embedder = TfIdfEmbedder::build(&docs);
let vec = embedder.embed("authenticate token verify");
let magnitude: f32 = vec.iter().map(|v| v * v).sum::<f32>().sqrt();
if magnitude > 1e-9 {
assert!(
(magnitude - 1.0).abs() < 1e-4,
"embedding should be L2-normalized, got magnitude {}",
magnitude
);
}
}
#[test]
fn test_sanitize_for_prefix_keeps_full_project_id() {
let a = "aaaaaaaaaaaaaaaaaaaa_project_one";
let b = "aaaaaaaaaaaaaaaaaaaa_project_two";
assert_ne!(sanitize_for_prefix(a), sanitize_for_prefix(b));
assert!(sanitize_for_prefix(a).ends_with("project_one"));
assert!(sanitize_for_prefix(b).ends_with("project_two"));
}
#[test]
fn test_tfidf_related_content_higher_similarity() {
let docs: Vec<(String, String)> = vec![
(
"a1".into(),
"fn authenticate_user(token: &str) -> bool { verify_token(token) }".into(),
),
(
"a2".into(),
"fn check_user_credentials(password: &str) -> bool { hash_check(password) }".into(),
),
(
"b1".into(),
"fn connect_database(url: &str) -> Connection { open_connection(url) }".into(),
),
(
"b2".into(),
"fn execute_sql_query(query: &str) -> Vec<Row> { db_execute(query) }".into(),
),
(
"c1".into(),
"fn parse_json_payload(data: &str) -> Value { serde_parse(data) }".into(),
),
];
let embedder = TfIdfEmbedder::build(&docs);
let auth1 = embedder.embed("fn authenticate_user token verify");
let auth2 = embedder.embed("fn check_user credentials password hash");
let db1 = embedder.embed("fn connect database execute sql query");
let cosine = |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b.iter()).map(|(x, y)| x * y).sum() };
let sim_related = cosine(&auth1, &auth2);
let sim_unrelated = cosine(&auth1, &db1);
assert!(
sim_related >= sim_unrelated - 0.1,
"related similarity ({}) should not be much lower than unrelated similarity ({})",
sim_related,
sim_unrelated
);
}
#[test]
fn test_tfidf_zero_vector_for_unseen_terms() {
let docs: Vec<(String, String)> = vec![("a".into(), "fn foo_bar() -> bool { true }".into())];
let embedder = TfIdfEmbedder::build(&docs);
let vec = embedder.embed("zzzzzz aaaaaaa bbbbbbb cccccccc");
let magnitude: f32 = vec.iter().map(|v| v * v).sum::<f32>().sqrt();
assert!(magnitude < 1.1, "magnitude out of range: {}", magnitude);
}
#[test]
fn test_tfidf_partition_matches_sort_selection() {
use std::collections::HashMap;
let mut docs: Vec<(String, String)> = Vec::with_capacity(200);
let token_names: Vec<String> = (0usize..1200)
.map(|i| {
let first = (b'a' + (i % 26) as u8) as char;
let second = (b'a' + ((i / 26) % 26) as u8) as char;
let third = (b'a' + ((i / 676) % 26) as u8) as char;
format!("{}{}{}", first, second, third)
})
.collect();
let mut token_doc_assignments: Vec<(String, Vec<usize>)> = Vec::new();
for (token_id, token) in token_names.iter().enumerate() {
let df = 3 + (token_id % 6);
let docs_with_token: Vec<usize> = (0..df)
.map(|j| (token_id * 7 + j * 13) % 200) .collect();
token_doc_assignments.push((token.clone(), docs_with_token));
}
for doc_id in 0..200 {
let mut tokens = Vec::new();
for (token, doc_ids) in &token_doc_assignments {
if doc_ids.contains(&doc_id) {
tokens.push(token.clone());
}
}
let content = tokens.join(" ");
docs.push((format!("doc_{}", doc_id), content));
}
let embedder = TfIdfEmbedder::build(&docs);
let tokenized: Vec<(String, Vec<String>)> = docs
.iter()
.map(|(id, content)| (id.clone(), tokenize_code(content)))
.collect();
let n = tokenized.len();
let n_f = n as f32;
let min_df: usize = if n < 50 { 1 } else { (n / 1000).max(3) };
let max_df: usize = if n < 50 { n } else { (n / 4).max(min_df + 1) };
let mut df: HashMap<String, usize> = HashMap::new();
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
for (_, tokens) in &tokenized {
seen.clear();
for tok in tokens {
if seen.insert(tok.as_str()) {
*df.entry(tok.to_string()).or_insert(0) += 1;
}
}
}
let mut ref_scores: Vec<(String, f32)> = df
.into_iter()
.filter(|(_, c)| *c >= min_df && *c <= max_df)
.map(|(tok, c)| (tok, (n_f / c as f32).ln()))
.collect();
ref_scores.sort_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
let target_dim = crate::search::search::DEFAULT_EMBEDDING_DIMENSION;
let expected_vocab: Vec<String> = if ref_scores.len() <= target_dim {
ref_scores.iter().map(|(t, _)| t.clone()).collect()
} else {
let total = ref_scores.len();
let stride = total as f64 / target_dim as f64;
(0..target_dim)
.map(|i| {
ref_scores[((i as f64 * stride) as usize).min(total - 1)]
.0
.clone()
})
.collect()
};
assert_eq!(
embedder.vocab.len(),
expected_vocab.len(),
"vocab length mismatch: got {} expected {}",
embedder.vocab.len(),
expected_vocab.len()
);
for (i, (got, expected)) in embedder.vocab.iter().zip(expected_vocab.iter()).enumerate() {
assert_eq!(
got, expected,
"vocab mismatch at position {i}: got '{got}' expected '{expected}'"
);
}
}
#[test]
fn test_detect_changed_manifests_cold_start_no_false_positive() {
use crate::cli::memory::{CacheSpiller, MemoryConfig};
use std::path::PathBuf;
let temp_dir = tempfile::tempdir().unwrap();
let config = MemoryConfig {
cache_dir: temp_dir.path().join("cache"),
max_cache_bytes: 10_000_000,
..Default::default()
};
let mut spiller = CacheSpiller::new(config).unwrap();
let manifest_path = PathBuf::from("/project/Cargo.toml");
let mut old_hashes = std::collections::HashMap::new();
old_hashes.insert(manifest_path.display().to_string(), "abc123".to_string());
let old_scan = ProjectFileScan {
source_paths: vec![PathBuf::from("/project/src/main.rs")],
manifest_paths: vec![manifest_path.clone()],
manifest_paths_canonical: Vec::new(),
source_directories: vec![PathBuf::from("/project/src")],
manifest_hashes: old_hashes,
};
let cache_key = crate::cli::memory::project_scan_cache_key("test_project");
let serialized = bincode::serialize(&old_scan).unwrap();
let entry = crate::cli::memory::CacheEntry::Binary {
metadata: std::collections::HashMap::new(),
serialized_data: serialized,
};
spiller
.store_mut()
.insert(cache_key.clone(), entry)
.unwrap();
spiller.store_mut().persist_key(&cache_key).unwrap();
let _ = spiller.store_mut().remove(&cache_key);
assert!(
spiller.store().peek(&cache_key).is_none(),
"peek should return None after removal"
);
let mut current_hashes = std::collections::HashMap::new();
current_hashes.insert(manifest_path.display().to_string(), "abc123".to_string());
let current_scan = ProjectFileScan {
source_paths: vec![],
manifest_paths: vec![manifest_path.clone()],
manifest_paths_canonical: Vec::new(),
source_directories: vec![],
manifest_hashes: current_hashes,
};
let changed = detect_changed_manifests(¤t_scan, "test_project", &spiller);
assert!(
changed.is_empty(),
"cold start should NOT produce false-positive manifest changes, got: {:?}",
changed
);
}
#[test]
fn test_clear_query_caches_keeps_project_id_prefix_siblings() {
use crate::cli::memory::{CacheEntry, CacheSpiller, MemoryConfig};
let temp_dir = tempfile::tempdir().unwrap();
let config = MemoryConfig {
cache_dir: temp_dir.path().join("cache"),
max_cache_bytes: 10_000_000,
..Default::default()
};
let mut spiller = CacheSpiller::new(config).unwrap();
let project_1_key = "search:query:project_1:fingerprint:10:auth".to_string();
let project_10_key = "search:query:project_10:fingerprint:10:auth".to_string();
let analysis_1_key = "analysis:analyze:project_1:fingerprint:10:auth".to_string();
let analysis_10_key = "analysis:analyze:project_10:fingerprint:10:auth".to_string();
let entry = CacheEntry::Binary {
metadata: std::collections::HashMap::new(),
serialized_data: b"cached".to_vec(),
};
for key in [
&project_1_key,
&project_10_key,
&analysis_1_key,
&analysis_10_key,
] {
spiller
.store_mut()
.insert(key.clone(), entry.clone())
.unwrap();
spiller.store_mut().persist_key(key).unwrap();
}
clear_query_caches(&mut spiller, "project_1");
assert!(spiller.store().peek(&project_1_key).is_none());
assert!(spiller.store().peek(&analysis_1_key).is_none());
assert!(spiller.store().peek(&project_10_key).is_some());
assert!(spiller.store().peek(&analysis_10_key).is_some());
assert!(
spiller
.store_mut()
.get_or_load(&project_10_key)
.unwrap()
.is_some(),
"disk cache for project_10 must not be deleted by project_1 prefix cleanup"
);
assert!(
spiller
.store_mut()
.get_or_load(&analysis_10_key)
.unwrap()
.is_some(),
"analysis disk cache for project_10 must not be deleted by project_1 prefix cleanup"
);
}
#[test]
fn test_detect_changed_manifests_detects_real_change() {
use crate::cli::memory::{CacheSpiller, MemoryConfig};
use std::path::PathBuf;
let temp_dir = tempfile::tempdir().unwrap();
let config = MemoryConfig {
cache_dir: temp_dir.path().join("cache"),
max_cache_bytes: 10_000_000,
..Default::default()
};
let mut spiller = CacheSpiller::new(config).unwrap();
let manifest_path = PathBuf::from("/project/Cargo.toml");
let mut old_hashes = std::collections::HashMap::new();
old_hashes.insert(manifest_path.display().to_string(), "old_hash".to_string());
let old_scan = ProjectFileScan {
source_paths: vec![PathBuf::from("/project/src/main.rs")],
manifest_paths: vec![manifest_path.clone()],
manifest_paths_canonical: Vec::new(),
source_directories: vec![PathBuf::from("/project/src")],
manifest_hashes: old_hashes,
};
let cache_key = crate::cli::memory::project_scan_cache_key("test_project2");
let serialized = bincode::serialize(&old_scan).unwrap();
let entry = crate::cli::memory::CacheEntry::Binary {
metadata: std::collections::HashMap::new(),
serialized_data: serialized,
};
spiller
.store_mut()
.insert(cache_key.clone(), entry)
.unwrap();
spiller.store_mut().persist_key(&cache_key).unwrap();
let _ = spiller.store_mut().remove(&cache_key);
let mut current_hashes = std::collections::HashMap::new();
current_hashes.insert(manifest_path.display().to_string(), "new_hash".to_string());
let current_scan = ProjectFileScan {
source_paths: vec![PathBuf::from("/project/src/main.rs")],
manifest_paths: vec![manifest_path.clone()],
manifest_paths_canonical: Vec::new(),
source_directories: vec![PathBuf::from("/project/src")],
manifest_hashes: current_hashes,
};
let changed = detect_changed_manifests(¤t_scan, "test_project2", &spiller);
assert_eq!(
changed.len(),
1,
"should detect exactly one changed manifest"
);
assert_eq!(
changed[0], manifest_path,
"should detect the correct manifest as changed"
);
}
#[test]
fn test_detect_changed_manifests_uses_in_memory_cache_first() {
use crate::cli::memory::{CacheSpiller, MemoryConfig};
use std::path::PathBuf;
let temp_dir = tempfile::tempdir().unwrap();
let config = MemoryConfig {
cache_dir: temp_dir.path().join("cache"),
max_cache_bytes: 10_000_000,
..Default::default()
};
let mut spiller = CacheSpiller::new(config).unwrap();
let manifest_path = PathBuf::from("/project/Cargo.toml");
let mut disk_hashes = std::collections::HashMap::new();
disk_hashes.insert(
manifest_path.display().to_string(),
"stale_hash".to_string(),
);
let disk_scan = ProjectFileScan {
source_paths: vec![],
manifest_paths: vec![manifest_path.clone()],
manifest_paths_canonical: Vec::new(),
source_directories: vec![],
manifest_hashes: disk_hashes,
};
let cache_key = crate::cli::memory::project_scan_cache_key("test_project3");
let serialized = bincode::serialize(&disk_scan).unwrap();
let disk_entry = crate::cli::memory::CacheEntry::Binary {
metadata: std::collections::HashMap::new(),
serialized_data: serialized,
};
spiller
.store_mut()
.insert(cache_key.clone(), disk_entry)
.unwrap();
spiller.store_mut().persist_key(&cache_key).unwrap();
let mut mem_hashes = std::collections::HashMap::new();
mem_hashes.insert(
manifest_path.display().to_string(),
"current_hash".to_string(),
);
let mem_scan = ProjectFileScan {
source_paths: vec![],
manifest_paths: vec![manifest_path.clone()],
manifest_paths_canonical: Vec::new(),
source_directories: vec![],
manifest_hashes: mem_hashes,
};
let mem_serialized = bincode::serialize(&mem_scan).unwrap();
let mem_entry = crate::cli::memory::CacheEntry::Binary {
metadata: std::collections::HashMap::new(),
serialized_data: mem_serialized,
};
spiller
.store_mut()
.insert(cache_key.clone(), mem_entry)
.unwrap();
let mut current_hashes = std::collections::HashMap::new();
current_hashes.insert(
manifest_path.display().to_string(),
"current_hash".to_string(),
);
let current_scan = ProjectFileScan {
source_paths: vec![PathBuf::from("/project/src/main.rs")],
manifest_paths: vec![manifest_path.clone()],
manifest_paths_canonical: Vec::new(),
source_directories: vec![PathBuf::from("/project/src")],
manifest_hashes: current_hashes,
};
let changed = detect_changed_manifests(¤t_scan, "test_project3", &spiller);
assert!(
changed.is_empty(),
"in-memory cache should be preferred over disk; should see no changes"
);
}
#[test]
fn test_tfidf_embedder_clone_roundtrip() {
let docs = vec![
("a".to_string(), "fn alpha beta gamma".to_string()),
("b".to_string(), "fn delta epsilon zeta".to_string()),
];
let embedder = TfIdfEmbedder::build(&docs);
let cloned = embedder.clone();
assert_eq!(embedder.vocab, cloned.vocab);
assert_eq!(embedder.idf, cloned.idf);
assert_eq!(embedder.dimension, cloned.dimension);
}
#[test]
fn test_tfidf_incremental_batches_match_full_build() {
let docs: Vec<(String, String)> = (0..120)
.map(|i| {
let body = format!(
"fn item_{}() {{ let value = {}; let shared = value + {}; }}",
i,
i,
i % 7
);
(format!("doc_{i}"), body)
})
.collect();
let full = TfIdfEmbedder::build(&docs);
let tokenized: Vec<(String, Vec<String>)> = docs
.iter()
.map(|(id, content)| (id.clone(), tokenize_code(content)))
.collect();
let chunked = TfIdfEmbedder::build_from_tokens(&tokenized);
assert_eq!(full.vocab, chunked.vocab);
assert_eq!(full.idf, chunked.idf);
assert_eq!(full.dimension, chunked.dimension);
}
#[test]
fn test_index_nodes_respects_batch_size_and_matches_results() {
use crate::graph::pdg::{Node, NodeType, ProgramDependenceGraph};
use crate::search::search::SearchEngine;
let mut pdg = ProgramDependenceGraph::new();
for i in 0..8 {
pdg.add_node(Node {
id: format!("node_{i}"),
name: format!("symbol_{i}"),
file_path: format!("/tmp/file_{i}.rs").into(),
language: format!("rust batch {i}"),
node_type: NodeType::Function,
byte_range: (0, 100),
complexity: i as u32 + 1,
});
}
let mut file_stats_cache = None;
let mut engine_small = SearchEngine::new();
let embedder_small = index_nodes(&pdg, &mut engine_small, &mut file_stats_cache, 2).unwrap();
let mut file_stats_cache = None;
let mut engine_large = SearchEngine::new();
let embedder_large = index_nodes(&pdg, &mut engine_large, &mut file_stats_cache, 64).unwrap();
let tfidf_small = match embedder_small {
HybridEmbedder::TfIdfOnly(emb) => emb,
#[cfg(feature = "onnx")]
HybridEmbedder::HybridLocal { tfidf, .. } => tfidf,
#[cfg(feature = "remote-embeddings")]
HybridEmbedder::HybridRemote { tfidf, .. } => tfidf,
};
let tfidf_large = match embedder_large {
HybridEmbedder::TfIdfOnly(emb) => emb,
#[cfg(feature = "onnx")]
HybridEmbedder::HybridLocal { tfidf, .. } => tfidf,
#[cfg(feature = "remote-embeddings")]
HybridEmbedder::HybridRemote { tfidf, .. } => tfidf,
};
assert_eq!(tfidf_small.vocab, tfidf_large.vocab);
assert_eq!(tfidf_small.idf, tfidf_large.idf);
assert_eq!(tfidf_small.dimension, tfidf_large.dimension);
}
#[test]
fn pdg_search_fingerprint_is_order_independent_and_content_sensitive() {
use crate::graph::pdg::{Node, NodeType, ProgramDependenceGraph};
fn node(id: &str, complexity: u32) -> Node {
Node {
id: id.to_string(),
name: format!("symbol_{id}"),
file_path: format!("/tmp/{id}.rs").into(),
language: "rust".to_string(),
node_type: NodeType::Function,
byte_range: (0, 10),
complexity,
}
}
let mut forward = ProgramDependenceGraph::new();
forward.add_node(node("a", 1));
forward.add_node(node("b", 2));
let mut reverse = ProgramDependenceGraph::new();
reverse.add_node(node("b", 2));
reverse.add_node(node("a", 1));
let mut changed = ProgramDependenceGraph::new();
changed.add_node(node("a", 9));
changed.add_node(node("b", 2));
assert_eq!(
pdg_search_fingerprint(&forward),
pdg_search_fingerprint(&reverse)
);
assert_ne!(
pdg_search_fingerprint(&forward),
pdg_search_fingerprint(&changed)
);
}
#[cfg(any(feature = "onnx", feature = "remote-embeddings"))]
#[test]
fn test_empty_neural_persist_removes_stale_mmap() {
use crate::search::search::SearchEngine;
let temp = tempfile::TempDir::new().unwrap();
let stale_path = neural_mmap_embeddings_path(temp.path());
std::fs::create_dir_all(stale_path.parent().unwrap()).unwrap();
std::fs::write(&stale_path, b"stale neural data").unwrap();
let engine = SearchEngine::new();
persist_neural_embeddings_to_mmap(
&engine,
temp.path(),
None,
crate::config::MatryoshkaSetting::Off,
)
.unwrap();
assert!(
!stale_path.exists(),
"empty neural persistence should remove stale neural mmap file"
);
}
#[test]
fn test_empty_fragment_persist_removes_stale_mmap() {
let temp = tempfile::TempDir::new().unwrap();
let stale_path = fragment_mmap_embeddings_path(temp.path());
std::fs::create_dir_all(stale_path.parent().unwrap()).unwrap();
std::fs::write(&stale_path, b"stale fragment data").unwrap();
persist_fragment_embeddings_to_mmap(temp.path(), &[]).unwrap();
assert!(
!stale_path.exists(),
"empty fragment persistence should remove stale fragment mmap file"
);
}
#[test]
fn test_fragment_mmap_roundtrip() {
let temp = tempfile::TempDir::new().unwrap();
let embeddings = vec![
("hash_abc".to_string(), vec![0.1, 0.2, 0.3]),
("hash_def".to_string(), vec![0.4, 0.5, 0.6]),
];
persist_fragment_embeddings_to_mmap(temp.path(), &embeddings).unwrap();
let index = try_load_fragment_mmap_embeddings_from_storage(&temp.path().join(".leindex"))
.expect("fragment mmap loads");
assert_eq!(index.len(), 2);
assert_eq!(index.dimension(), 3);
assert!(index.get_embedding("hash_abc").is_some());
assert!(index.get_embedding("hash_def").is_some());
}
#[test]
fn test_fragment_mmap_missing_is_none() {
let temp = tempfile::TempDir::new().unwrap();
assert!(
try_load_fragment_mmap_embeddings_from_storage(&temp.path().join(".leindex")).is_none()
);
}
#[test]
fn test_index_nodes_accumulates_df_across_passes() {
use crate::graph::pdg::{Node, NodeType, ProgramDependenceGraph};
use crate::search::search::SearchEngine;
let mut pdg = ProgramDependenceGraph::new();
for i in 0..6 {
let content_tag = if i < 3 { "shared_token" } else { "other_token" };
pdg.add_node(Node {
id: format!("node_{i}"),
name: format!("symbol_{i}"),
file_path: format!("/tmp/file_{i}.rs").into(),
language: "rust".to_string(),
node_type: NodeType::Function,
byte_range: (0, 100),
complexity: 1,
});
let _ = content_tag;
}
let mut cache = None;
let mut engine = SearchEngine::new();
let embedder = index_nodes(&pdg, &mut engine, &mut cache, 3).unwrap();
let tfidf_embedder = match embedder {
HybridEmbedder::TfIdfOnly(emb) => emb,
#[cfg(feature = "onnx")]
HybridEmbedder::HybridLocal { tfidf, .. } => tfidf,
#[cfg(feature = "remote-embeddings")]
HybridEmbedder::HybridRemote { tfidf, .. } => tfidf,
};
assert_eq!(
tfidf_embedder.dimension,
crate::search::search::DEFAULT_EMBEDDING_DIMENSION
);
assert_eq!(engine.node_count(), 6);
}
#[test]
fn test_tfidf_embedder_persist_roundtrip() {
let temp = tempfile::tempdir().unwrap();
let docs = vec![("a".to_string(), "fn alpha beta gamma".to_string())];
let embedder = TfIdfEmbedder::build(&docs);
let pdg = { crate::graph::pdg::ProgramDependenceGraph::new() };
embedder
.persist_to_storage(temp.path(), &pdg, None)
.unwrap();
let loaded = TfIdfEmbedder::load_from_storage(temp.path())
.unwrap()
.unwrap();
assert_eq!(embedder.vocab, loaded.vocab);
assert_eq!(embedder.idf, loaded.idf);
assert_eq!(embedder.dimension, loaded.dimension);
}
#[test]
fn test_tfidf_embedder_missing_file_returns_none() {
let temp = tempfile::tempdir().unwrap();
assert!(
TfIdfEmbedder::load_from_storage(temp.path())
.unwrap()
.is_none()
);
}
#[test]
fn test_tfidf_embedder_freshness_checks_pdg_counts() {
let mut embedder = TfIdfEmbedder::build_from_tokens(&[]);
embedder.pdg_nodes = 3;
embedder.pdg_edges = 7;
embedder.pdg_fingerprint = "test-fingerprint".to_string();
let fp = "test-fingerprint";
assert!(embedder.is_fresh(3, 7, fp));
assert!(!embedder.is_fresh(4, 7, fp));
assert!(!embedder.is_fresh(3, 8, fp));
}
#[test]
fn test_tfidf_build_from_tokens_is_deterministic_across_batch_sizes() {
let docs: Vec<(String, String)> = (0..90)
.map(|i| {
let body = format!(
"pub fn symbol_{}() -> usize {{ {} + {} + {} }}",
i,
i,
i % 5,
i % 11
);
(format!("doc_{i}"), body)
})
.collect();
let tokenized: Vec<(String, Vec<String>)> = docs
.iter()
.map(|(id, content)| (id.clone(), tokenize_code(content)))
.collect();
let embedder_a = TfIdfEmbedder::build_from_tokens(&tokenized);
let embedder_b = TfIdfEmbedder::build_from_tokens(&tokenized);
assert_eq!(embedder_a.vocab, embedder_b.vocab);
assert_eq!(embedder_a.idf, embedder_b.idf);
}
#[test]
fn test_read_file_once_hash_and_content() {
let temp = tempfile::tempdir().unwrap();
let file_path = temp.path().join("test_file.txt");
let content = b"Hello, streaming BLAKE3 world!";
std::fs::write(&file_path, content).unwrap();
let (hash, bytes) = read_file_once(&file_path).unwrap();
let expected_hash = blake3::hash(content).to_hex().to_string();
assert_eq!(
hash, expected_hash,
"streaming BLAKE3 hash must match independent computation"
);
assert_eq!(
bytes.as_slice(),
content,
"file bytes must match original content"
);
}
#[test]
fn test_read_file_once_empty_file() {
let temp = tempfile::tempdir().unwrap();
let file_path = temp.path().join("empty.txt");
std::fs::write(&file_path, b"").unwrap();
let (hash, bytes) = read_file_once(&file_path).unwrap();
let expected_hash = blake3::hash(b"").to_hex().to_string();
assert_eq!(
hash, expected_hash,
"empty file hash must match blake3 of empty input"
);
assert!(bytes.is_empty(), "empty file bytes should be empty");
}
#[test]
fn test_read_file_once_error() {
let result = read_file_once(Path::new("/nonexistent/path/to/file.txt"));
assert!(
result.is_err(),
"reading a nonexistent file should return an error"
);
}
fn cache_key_stats() -> IndexStats {
IndexStats {
total_files: 3,
files_parsed: 3,
successful_parses: 3,
failed_parses: 0,
total_signatures: 7,
signature_scope: "full".to_string(),
pdg_nodes: 7,
pdg_edges: 6,
indexed_nodes: 7,
indexing_time_ms: 100,
external_deps_in_lockfile: 0,
external_deps_resolved: 0,
external_deps_unresolved: 0,
external_deps_total: 0,
external_deps_builtin: 0,
}
}
fn cache_key_with_fragments(
fragment_enabled: bool,
fragment_weight: f64,
fragment_root_hash: &str,
) -> String {
cache_key_with_rerank_backend(
fragment_enabled,
fragment_weight,
fragment_root_hash,
"onnx",
)
}
fn cache_key_with_rerank_backend(
fragment_enabled: bool,
fragment_weight: f64,
fragment_root_hash: &str,
rerank_backend: &str,
) -> String {
cache_key_with_rerank_activation(
fragment_enabled,
fragment_weight,
fragment_root_hash,
rerank_backend,
true,
)
}
fn cache_key_with_rerank_activation(
fragment_enabled: bool,
fragment_weight: f64,
fragment_root_hash: &str,
rerank_backend: &str,
rerank_backend_active: bool,
) -> String {
search_cache_key_for(&SearchCacheKeyInputs {
project_id: "proj-a",
project_path: Path::new("/tmp/proj-a"),
stats: &cache_key_stats(),
query: "auth token verify",
top_k: 10,
query_type: None,
neural_available: true,
search_mode: "hybrid",
neural_weight: 0.4,
rerank_enabled: true,
rerank_top_n: 80,
rerank_model: "qwen3-reranker-0.6b-seq-cls",
rerank_backend,
rerank_backend_active,
fragment_index_enabled: fragment_enabled,
fragment_weight,
fragment_root_hash,
embed_model: "qwen3-embed-0.6b",
})
}
#[test]
fn test_cache_key_rerank_backend_change_invalidates() {
let onnx = cache_key_with_rerank_backend(false, 0.0, "", "onnx");
let system_one = cache_key_with_rerank_backend(false, 0.0, "", "system-one");
let off = cache_key_with_rerank_backend(false, 0.0, "", "off");
assert_ne!(
onnx, system_one,
"onnx vs system-one must invalidate the key"
);
assert_ne!(onnx, off, "onnx vs off must invalidate the key");
assert_ne!(system_one, off, "system-one vs off must invalidate the key");
}
#[test]
fn test_cache_key_rerank_activation_change_invalidates() {
let disabled = cache_key_with_rerank_activation(false, 0.0, "", "system-one", false);
let enabled = cache_key_with_rerank_activation(false, 0.0, "", "system-one", true);
assert_ne!(
disabled, enabled,
"arming the system-one flag must invalidate the key"
);
}
#[test]
fn test_cache_key_rerank_backend_deterministic() {
let a = cache_key_with_rerank_backend(true, 0.10, "root-hash-A", "system-one");
let b = cache_key_with_rerank_backend(true, 0.10, "root-hash-A", "system-one");
assert_eq!(a, b, "same rerank backend must produce the same key");
}
#[test]
fn test_cache_key_fragment_weight_change_invalidates() {
let base = cache_key_with_fragments(true, 0.10, "root-hash-A");
let changed = cache_key_with_fragments(true, 0.25, "root-hash-A");
assert_ne!(
base, changed,
"fragment_weight change must invalidate the key"
);
}
#[test]
fn test_cache_key_fragment_root_hash_change_invalidates() {
let base = cache_key_with_fragments(true, 0.10, "root-hash-A");
let changed = cache_key_with_fragments(true, 0.10, "root-hash-B");
assert_ne!(
base, changed,
"fragment root hash change must invalidate the key"
);
}
#[test]
fn test_cache_key_fragment_enabled_change_invalidates() {
let base = cache_key_with_fragments(true, 0.10, "root-hash-A");
let off = cache_key_with_fragments(false, 0.10, "root-hash-A");
assert_ne!(base, off, "fragment master switch must invalidate the key");
}
#[test]
fn test_cache_key_fragment_knobs_deterministic() {
let a = cache_key_with_fragments(true, 0.10, "root-hash-A");
let b = cache_key_with_fragments(true, 0.10, "root-hash-A");
assert_eq!(a, b, "same fragment knobs must produce the same key");
}
#[test]
fn test_cache_key_legacy_v2_without_fragment_fields_parses() {
let legacy = search_cache_key(&format!(
"v2:query:{}:{}:{}:{}:{:?}:neural={}:mode={}:nw={}:rr={}|{}|{}:embed={}",
stable_project_cache_id("proj-a", Path::new("/tmp/proj-a")),
index_fingerprint(&cache_key_stats()),
10,
"auth token verify",
None::<crate::search::ranking::QueryType>,
true,
"hybrid",
0.4,
true,
80,
"qwen3-reranker-0.6b-seq-cls",
"qwen3-embed-0.6b",
));
let modern = cache_key_with_fragments(false, 0.0, "");
assert!(!legacy.is_empty(), "legacy key format still produces a key");
assert_ne!(
legacy, modern,
"legacy key must not collide with the fragment-extended key"
);
}