Files
yvgude--lean-ctx/rust/tests/reranking_scenarios.rs
T
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

455 lines
15 KiB
Rust

//! End-to-end scenario tests for the Post-RRF Reranking Pipeline.
//!
//! Tests validate that the reranking pipeline produces correct behavior
//! across realistic code search scenarios.
use lean_ctx::core::bm25_index::ChunkKind;
use lean_ctx::core::hybrid_search::HybridResult;
use lean_ctx::core::search_reranking::{
QueryType, classify_query, rerank_pipeline, resolve_weights,
};
fn make_result(file: &str, symbol: &str, kind: ChunkKind, score: f64) -> HybridResult {
HybridResult {
file_path: file.to_string(),
symbol_name: symbol.to_string(),
kind,
start_line: 1,
end_line: 20,
snippet: format!("fn {symbol}() {{ /* ... */ }}"),
rrf_score: score,
bm25_score: Some(score),
dense_score: None,
bm25_rank: Some(1),
dense_rank: None,
}
}
// ==========================================================================
// Scenario 1: Symbol query should boost definitions to top
// ==========================================================================
#[test]
fn scenario_symbol_query_boosts_definition() {
let mut results = vec![
make_result(
"src/handlers/auth.rs",
"handle_login",
ChunkKind::Function,
0.9,
),
make_result("src/main.rs", "main", ChunkKind::Function, 0.85),
make_result("src/models/user.rs", "UserService", ChunkKind::Struct, 0.4),
make_result(
"src/services/auth.rs",
"UserService",
ChunkKind::Struct,
0.3,
),
make_result(
"tests/test_user.rs",
"test_user_service",
ChunkKind::Function,
0.7,
),
];
rerank_pipeline(&mut results, "UserService", 5);
// UserService struct definitions should be at top despite lower initial scores
assert_eq!(results[0].symbol_name, "UserService");
assert_eq!(results[0].kind, ChunkKind::Struct);
}
// ==========================================================================
// Scenario 2: NL query should not apply definition boost
// ==========================================================================
#[test]
fn scenario_nl_query_no_definition_boost() {
let mut results = vec![
make_result("src/auth.rs", "authenticate", ChunkKind::Function, 0.9),
make_result("src/config.rs", "load_config", ChunkKind::Function, 0.8),
make_result("src/auth.rs", "verify_token", ChunkKind::Function, 0.7),
];
let scores_before: Vec<f64> = results.iter().map(|r| r.rrf_score).collect();
rerank_pipeline(&mut results, "how to handle authentication", 3);
// No symbol definition boost for NL queries; order based on coherence + diversity
// The first result should still be from auth.rs (coherence boost)
assert!(results[0].file_path.contains("auth"));
// Scores should change (coherence/noise/diversity) but no massive boost
assert!(results[0].rrf_score - scores_before[0] < scores_before[0] * 4.0);
}
// ==========================================================================
// Scenario 3: Test files get penalized
// ==========================================================================
#[test]
fn scenario_test_files_penalized() {
let mut results = vec![
make_result(
"tests/integration/test_auth.rs",
"test_login",
ChunkKind::Function,
1.0,
),
make_result(
"src/__tests__/auth.spec.ts",
"auth_spec",
ChunkKind::Function,
1.0,
),
make_result("src/auth.rs", "login", ChunkKind::Function, 0.7),
make_result(
"test/unit/auth_test.go",
"TestAuth",
ChunkKind::Function,
1.0,
),
];
rerank_pipeline(&mut results, "authentication logic", 4);
// src/auth.rs should rank higher than test files despite lower initial score
assert_eq!(results[0].file_path, "src/auth.rs");
}
// ==========================================================================
// Scenario 4: Diversity prevents single-file dominance
// ==========================================================================
#[test]
fn scenario_diversity_prevents_single_file_dominance() {
let mut results = vec![
make_result("src/giant_module.rs", "fn_a", ChunkKind::Function, 1.0),
make_result("src/giant_module.rs", "fn_b", ChunkKind::Function, 0.95),
make_result("src/giant_module.rs", "fn_c", ChunkKind::Function, 0.90),
make_result("src/giant_module.rs", "fn_d", ChunkKind::Function, 0.85),
make_result("src/giant_module.rs", "fn_e", ChunkKind::Function, 0.80),
make_result("src/helper.rs", "helper_fn", ChunkKind::Function, 0.75),
make_result("src/utils.rs", "util_fn", ChunkKind::Function, 0.70),
];
rerank_pipeline(&mut results, "module functions", 5);
// Within top 5, we should see helper.rs and utils.rs (not just giant_module.rs)
let files_in_top5: Vec<&str> = results.iter().map(|r| r.file_path.as_str()).collect();
assert!(
files_in_top5.contains(&"src/helper.rs"),
"helper.rs should be in top 5 due to diversity, got: {files_in_top5:?}",
);
assert!(
files_in_top5.contains(&"src/utils.rs"),
"utils.rs should be in top 5 due to diversity, got: {files_in_top5:?}",
);
}
// ==========================================================================
// Scenario 5: File coherence boosts multi-hit files
// ==========================================================================
#[test]
fn scenario_file_coherence_boosts_multi_hit() {
let mut results = vec![
make_result("src/isolated.rs", "isolated_fn", ChunkKind::Function, 0.8),
make_result("src/coherent.rs", "fn_one", ChunkKind::Function, 0.6),
make_result("src/coherent.rs", "fn_two", ChunkKind::Function, 0.5),
make_result("src/coherent.rs", "fn_three", ChunkKind::Function, 0.4),
];
rerank_pipeline(&mut results, "coherent operations", 4);
// coherent.rs should get a file coherence boost since multiple chunks match
// The top result from coherent.rs should be boosted above isolated.rs
let coherent_top = results
.iter()
.find(|r| r.file_path == "src/coherent.rs")
.unwrap();
assert!(
coherent_top.rrf_score > 0.6,
"coherent.rs top chunk should be boosted above its initial 0.6, got {}",
coherent_top.rrf_score
);
}
// ==========================================================================
// Scenario 6: Legacy/compat paths get penalized
// ==========================================================================
#[test]
fn scenario_legacy_compat_penalized() {
let mut results = vec![
make_result(
"src/compat/old_api.rs",
"handle_request",
ChunkKind::Function,
1.0,
),
make_result(
"src/legacy/v1_handler.rs",
"process",
ChunkKind::Function,
0.95,
),
make_result(
"src/deprecated/auth.rs",
"authenticate",
ChunkKind::Function,
0.90,
),
make_result(
"src/api/handler.rs",
"handle_request",
ChunkKind::Function,
0.6,
),
];
rerank_pipeline(&mut results, "handle request", 4);
// Production code should rank above legacy/compat despite lower initial score
assert_eq!(results[0].file_path, "src/api/handler.rs");
}
// ==========================================================================
// Scenario 7: .d.ts type stubs get mild penalty
// ==========================================================================
#[test]
fn scenario_type_stubs_mild_penalty() {
let mut results = vec![
make_result("src/types.d.ts", "AuthConfig", ChunkKind::Struct, 0.9),
make_result("src/auth.ts", "AuthConfig", ChunkKind::Struct, 0.8),
];
rerank_pipeline(&mut results, "AuthConfig", 2);
// Both should still be present but auth.ts should rank above .d.ts
assert_eq!(results[0].file_path, "src/auth.ts");
}
// ==========================================================================
// Scenario 8: Architecture query classification
// ==========================================================================
#[test]
fn scenario_architecture_query_weights() {
assert_eq!(
classify_query("how does authentication work"),
QueryType::Architecture
);
assert_eq!(
classify_query("where is the data flow"),
QueryType::Architecture
);
let (bm25_w, dense_w) = resolve_weights(QueryType::Architecture);
// Architecture queries should favor dense (semantic) over BM25 (lexical)
assert!(dense_w > bm25_w);
}
// ==========================================================================
// Scenario 9: Mixed scenario - symbol in tests should still lose to prod definition
// ==========================================================================
#[test]
fn scenario_symbol_in_test_vs_prod() {
let mut results = vec![
make_result("tests/test_parser.rs", "Parser", ChunkKind::Struct, 0.9),
make_result("src/parser/mod.rs", "Parser", ChunkKind::Struct, 0.5),
make_result("src/parser/mod.rs", "parse", ChunkKind::Function, 0.4),
make_result("examples/demo.rs", "use_parser", ChunkKind::Function, 0.8),
];
rerank_pipeline(&mut results, "Parser", 4);
// Production definition should win despite lower initial score
// (definition boost + test penalty + example penalty on non-defining chunks)
assert_eq!(results[0].file_path, "src/parser/mod.rs");
assert_eq!(results[0].symbol_name, "Parser");
}
// ==========================================================================
// Scenario 10: Barrel/index files get moderate penalty
// ==========================================================================
#[test]
fn scenario_barrel_files_penalized() {
let mut results = vec![
make_result(
"src/components/index.ts",
"export_all",
ChunkKind::Other,
0.9,
),
make_result("src/components/Button.ts", "Button", ChunkKind::Struct, 0.7),
make_result(
"src/utils/__init__.py",
"module_init",
ChunkKind::Other,
0.85,
),
make_result(
"src/utils/helpers.py",
"format_date",
ChunkKind::Function,
0.6,
),
];
rerank_pipeline(&mut results, "button component", 4);
// Actual implementation files should rank above barrel/index files
let top_2_files: Vec<&str> = results
.iter()
.take(2)
.map(|r| r.file_path.as_str())
.collect();
assert!(
top_2_files.contains(&"src/components/Button.ts")
|| top_2_files.contains(&"src/utils/helpers.py"),
"Implementation files should be in top 2, got: {top_2_files:?}",
);
}
// ==========================================================================
// Scenario 11: Qualified symbol extraction
// ==========================================================================
#[test]
fn scenario_qualified_symbol_search() {
let mut results = vec![
make_result("src/auth/mod.rs", "authenticate", ChunkKind::Function, 0.8),
make_result("src/auth/service.rs", "verify", ChunkKind::Function, 0.9),
make_result("src/auth/service.rs", "AuthService", ChunkKind::Struct, 0.3),
];
rerank_pipeline(&mut results, "auth::AuthService", 3);
// AuthService definition should be boosted to top
assert_eq!(results[0].symbol_name, "AuthService");
}
// ==========================================================================
// Scenario 12: Empty results don't panic
// ==========================================================================
#[test]
fn scenario_empty_results() {
let mut results: Vec<HybridResult> = vec![];
rerank_pipeline(&mut results, "anything", 10);
assert!(results.is_empty());
}
// ==========================================================================
// Scenario 13: Single result passes through
// ==========================================================================
#[test]
fn scenario_single_result() {
let mut results = vec![make_result("src/main.rs", "main", ChunkKind::Function, 1.0)];
rerank_pipeline(&mut results, "main", 1);
assert_eq!(results.len(), 1);
assert_eq!(results[0].file_path, "src/main.rs");
}
// ==========================================================================
// Scenario 14: snake_case symbol recognition
// ==========================================================================
#[test]
fn scenario_snake_case_symbol() {
assert_eq!(classify_query("get_user_by_id"), QueryType::Symbol);
assert_eq!(classify_query("_private_field"), QueryType::Symbol);
assert_eq!(classify_query("MAX_RETRY_COUNT"), QueryType::Symbol);
}
// ==========================================================================
// Scenario 15: Real-world multi-signal interaction
// ==========================================================================
#[test]
fn scenario_multi_signal_interaction() {
// Simulates: searching for "BM25Index" in a codebase with:
// - Definition in src/core/bm25_index.rs (low initial score)
// - Usage in tests (high initial score)
// - Usage in examples (medium score)
// - Usage in production code (medium score)
let mut results = vec![
make_result(
"tests/test_search.rs",
"test_bm25",
ChunkKind::Function,
1.0,
),
make_result(
"examples/search_demo.rs",
"demo_search",
ChunkKind::Function,
0.9,
),
make_result(
"src/tools/search.rs",
"run_search",
ChunkKind::Function,
0.8,
),
make_result(
"src/tools/search.rs",
"format_results",
ChunkKind::Function,
0.7,
),
make_result(
"src/core/bm25_index.rs",
"BM25Index",
ChunkKind::Struct,
0.4,
),
make_result(
"src/core/bm25_index.rs",
"search",
ChunkKind::Function,
0.35,
),
];
rerank_pipeline(&mut results, "BM25Index", 4);
// Expected: BM25Index definition rises to top (definition boost)
// Test + Example files get noise penalty
// Production usage stays in middle
assert_eq!(
results[0].file_path, "src/core/bm25_index.rs",
"BM25Index definition should be #1"
);
assert_eq!(results[0].symbol_name, "BM25Index");
// Production code (src/tools/search.rs) should be in top 2
assert_eq!(
results[1].file_path, "src/tools/search.rs",
"Production usage should be #2"
);
// Test file should rank below example-free production code
let test_pos = results
.iter()
.position(|r| r.file_path == "tests/test_search.rs");
let example_pos = results
.iter()
.position(|r| r.file_path == "examples/search_demo.rs");
// Both penalized paths should be below position 1
assert!(
test_pos.unwrap_or(99) > 1,
"Test file should be below production code"
);
assert!(
example_pos.unwrap_or(99) > 1,
"Example file should be below production code"
);
}