#[cfg(test)]
mod filter_tests {
#[allow(clippy::wildcard_imports)]
use super::super::*;
#[test]
fn filter_language_rust() {
let f = SearchFilter::new(Some(&["rust".into()]), None).unwrap();
assert!(f.matches("src/main.rs"));
assert!(!f.matches("src/main.ts"));
}
#[test]
fn filter_path_glob() {
let f = SearchFilter::new(None, Some("rust/src/**")).unwrap();
assert!(f.matches("rust/src/core/mod.rs"));
assert!(!f.matches("website/src/pages/index.astro"));
}
}
#[cfg(test)]
mod root_resolution_tests {
#[allow(clippy::wildcard_imports)]
use super::super::*;
#[test]
fn subdir_filter_scopes_results_to_subtree() {
let f = SearchFilter::new(None, None)
.unwrap()
.with_subdir(Some("crate_a/src".to_string()));
assert!(f.is_active());
assert!(f.matches("crate_a/src/auth.rs"));
assert!(f.matches("crate_a/src/nested/db.rs"));
assert!(!f.matches("crate_b/src/auth.rs"));
assert!(!f.matches("crate_a/src_extra/x.rs"));
assert!(f.matches(r"crate_a\src\win.rs"));
}
#[test]
fn subdir_filter_combines_with_extension_filter() {
let f = SearchFilter::new(Some(&["rust".to_string()]), None)
.unwrap()
.with_subdir(Some("src".to_string()));
assert!(f.matches("src/main.rs"));
assert!(!f.matches("src/readme.md"), "wrong extension");
assert!(!f.matches("docs/main.rs"), "outside subdir");
}
#[test]
fn empty_subdir_is_no_scope() {
let f = SearchFilter::new(None, None)
.unwrap()
.with_subdir(Some(String::new()));
assert!(!f.is_active());
assert!(f.matches("anything/here.rs"));
}
#[test]
fn search_subdir_filter_derives_relative_prefix() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let sub = root.join("a").join("b");
std::fs::create_dir_all(&sub).unwrap();
assert_eq!(search_subdir_filter(root, &sub).as_deref(), Some("a/b"));
assert_eq!(search_subdir_filter(root, root), None);
assert_eq!(search_subdir_filter(&sub, root), None);
}
#[test]
fn resolve_search_root_promotes_subdir_to_project_root() {
let _lock = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
let root = crate::core::pathutil::safe_canonicalize_or_self(tmp.path());
let sub = root.join("crate_a").join("src");
std::fs::create_dir_all(&sub).unwrap();
crate::test_env::set_var("LEAN_CTX_PROJECT_ROOT", root.to_string_lossy().as_ref());
let from_sub = resolve_search_root(&sub.to_string_lossy());
let from_root = resolve_search_root(&root.to_string_lossy());
crate::test_env::remove_var("LEAN_CTX_PROJECT_ROOT");
let (resolved, subdir) = from_sub.unwrap();
assert_eq!(resolved, root, "subdir must promote to the project root");
assert_eq!(subdir.as_deref(), Some("crate_a/src"));
let (resolved_root, subdir_root) = from_root.unwrap();
assert_eq!(resolved_root, root);
assert_eq!(subdir_root, None, "the root itself carries no subdir scope");
}
#[test]
fn resolve_search_root_errors_on_missing_path() {
assert!(resolve_search_root("/definitely/not/here/xyzzy-7f3a91").is_err());
}
}
#[cfg(all(test, feature = "embeddings"))]
mod cold_start_guard_tests {
#[allow(clippy::wildcard_imports)]
use super::super::*;
#[test]
fn budget_zero_disables_guard() {
assert!(!exceeds_inline_embed_budget(1_000_000, 0));
}
#[test]
fn budget_is_inclusive_and_triggers_above_threshold() {
assert!(!exceeds_inline_embed_budget(0, 2000), "warm index: inline");
assert!(
!exceeds_inline_embed_budget(2000, 2000),
"at the budget: still inline"
);
assert!(
exceeds_inline_embed_budget(2001, 2000),
"over the budget: degrade"
);
}
#[test]
fn default_threshold_positive_when_env_unset() {
if std::env::var_os("LEAN_CTX_HYBRID_INLINE_EMBED_MAX").is_none() {
assert!(inline_embed_max_chunks() >= 1);
}
}
#[test]
fn dense_build_hint_always_points_at_the_cli_build() {
let full = dense_build_hint(22_741, false);
assert!(full.contains("lean-ctx index build-semantic"));
assert!(full.contains("22741"));
let compact = dense_build_hint(22_741, true);
assert!(compact.contains("lean-ctx index build-semantic"));
assert!(compact.contains("22741"));
}
}
#[cfg(test)]
mod determinism_tests {
#[allow(clippy::wildcard_imports)]
use super::super::*;
#[test]
fn rrf_merge_hybrid_is_deterministic_on_ties() {
let a = HybridResult {
file_path: "a.rs".to_string(),
symbol_name: "foo".to_string(),
kind: crate::core::bm25_index::ChunkKind::Function,
start_line: 1,
end_line: 1,
snippet: "a".to_string(),
rrf_score: 0.0,
bm25_score: None,
dense_score: None,
bm25_rank: None,
dense_rank: None,
};
let b = HybridResult {
file_path: "b.rs".to_string(),
symbol_name: "foo".to_string(),
kind: crate::core::bm25_index::ChunkKind::Function,
start_line: 1,
end_line: 1,
snippet: "b".to_string(),
rrf_score: 0.0,
bm25_score: None,
dense_score: None,
bm25_rank: None,
dense_rank: None,
};
let fused = rrf_merge_hybrid(
vec![
("root".to_string(), vec![a.clone(), b.clone()]),
("root".to_string(), vec![b.clone(), a.clone()]),
],
10,
);
assert_eq!(fused.len(), 2);
assert_eq!(fused[0].file_path, "a.rs");
assert_eq!(fused[1].file_path, "b.rs");
}
}
#[cfg(test)]
mod dense_config_tests {
use crate::core::hybrid_search::HybridConfig;
#[test]
fn dense_enabled_defaults_true() {
assert!(HybridConfig::default().dense_enabled);
}
#[test]
fn dense_enabled_deserializes_false() {
let cfg: HybridConfig = toml::from_str("dense_enabled = false").unwrap();
assert!(!cfg.dense_enabled);
assert_eq!(cfg.bm25_candidates, 75);
assert_eq!(cfg.splade_weight, 0.5);
}
}
#[cfg(all(test, feature = "embeddings"))]
mod dense_toggle_tests {
#[allow(clippy::wildcard_imports)]
use super::super::*;
use crate::core::bm25_index::{BM25Index, ChunkKind, CodeChunk, tokenize};
use crate::core::hybrid_search::HybridConfig;
fn small_index() -> BM25Index {
BM25Index::from_chunks_for_test(vec![
CodeChunk {
file_path: "auth.rs".into(),
symbol_name: "validate_token".into(),
kind: ChunkKind::Function,
start_line: 1,
end_line: 10,
content: "fn validate_token(token: &str) -> bool { check_jwt_expiry(token) }"
.into(),
tokens: tokenize("fn validate_token token str bool check_jwt_expiry token"),
token_count: 0,
},
CodeChunk {
file_path: "db.rs".into(),
symbol_name: "connect_database".into(),
kind: ChunkKind::Function,
start_line: 1,
end_line: 5,
content: "fn connect_database(url: &str) -> Pool { create_pool(url) }".into(),
tokens: tokenize("fn connect_database url str Pool create_pool url"),
token_count: 0,
},
])
}
#[test]
fn bm25_graph_search_ranks_without_embeddings() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let index = small_index();
let cfg = HybridConfig {
dense_enabled: false,
..Default::default()
};
let filter = SearchFilter::new(None, None).unwrap();
let out = bm25_graph_search(
"jwt token validation",
root,
&index,
5,
false,
&filter,
&cfg,
);
assert!(
out.contains("Semantic search (BM25"),
"expected BM25 header, got: {out}"
);
assert!(
out.contains("validate_token"),
"expected lexical match, got: {out}"
);
assert!(
!root.join("embeddings.json").exists(),
"dense-disabled path must not persist embeddings.json"
);
}
}