use super::*;
use crate::search::context::{
BudgetInfo, ContextBundle, ContextChunk, ContextFile, ContextSummary, FileRelevance,
};
use crate::types::{ChunkType, FileCategory};
fn bundle_of(files: &[(&str, f32)]) -> ContextBundle {
ContextBundle {
capture: None,
query: "q".to_string(),
files: files
.iter()
.map(|(path, score)| ContextFile {
path: (*path).to_string(),
language: "rust".to_string(),
relevance: FileRelevance::Direct,
category: FileCategory::Source,
score: *score,
coupled_to: vec![],
repo: None,
chunks: vec![ContextChunk {
id: String::new(),
name: None,
chunk_type: ChunkType::Function,
start_line: 1,
end_line: 2,
score: *score,
match_type: None,
content: None,
}],
})
.collect(),
budget: BudgetInfo {
max_lines: 150,
used_lines: 0,
pinned_lines: 0,
},
summary: ContextSummary {
structural_additions: 0,
total_files: files.len(),
total_chunks: files.len(),
direct_hits: files.len(),
coupled_additions: 0,
bridged_additions: 0,
source_files: files.len(),
doc_files: 0,
top_semantic_score: 0.0,
pinned_chunks: 0,
knowledge_additions: 0,
},
}
}
#[test]
fn test_selection_is_by_score_not_by_path_lexicography() {
let mut strong: Vec<(&str, f32)> = vec![
("src/z01.rs", 0.90),
("src/z02.rs", 0.91),
("src/z03.rs", 0.92),
("src/z04.rs", 0.93),
("src/z05.rs", 0.94),
("src/z06.rs", 0.95),
("src/z07.rs", 0.96),
("src/z08.rs", 0.97),
("src/z09.rs", 0.98),
("src/z10.rs", 0.99),
];
let only_strong = compute_session_id(&bundle_of(&strong), 0.0);
strong.extend_from_slice(&[
("src/a01.rs", 0.10),
("src/a02.rs", 0.11),
("src/a03.rs", 0.12),
]);
let with_weak = compute_session_id(&bundle_of(&strong), 0.0);
assert_eq!(
only_strong, with_weak,
"adding low-scoring chunks that sort early changed the fingerprint — \
selection is still lexicographic",
);
}
#[test]
fn test_fingerprint_is_stable_when_scores_reorder_without_changing_the_set() {
let ascending = bundle_of(&[("src/a.rs", 0.10), ("src/b.rs", 0.20), ("src/c.rs", 0.30)]);
let descending = bundle_of(&[("src/a.rs", 0.30), ("src/b.rs", 0.20), ("src/c.rs", 0.10)]);
assert_eq!(
compute_session_id(&ascending, 0.0),
compute_session_id(&descending, 0.0),
"the fingerprint moved when only the score ORDER changed; the final \
sort must happen after the truncate",
);
}
#[test]
fn test_a_different_selected_set_still_changes_the_fingerprint() {
let a = bundle_of(&[("src/a.rs", 0.5), ("src/b.rs", 0.5)]);
let b = bundle_of(&[("src/a.rs", 0.5), ("src/c.rs", 0.5)]);
assert_ne!(compute_session_id(&a, 0.0), compute_session_id(&b, 0.0));
}
#[test]
fn test_ties_break_deterministically_by_key_not_by_bundle_order() {
let forward = bundle_of(&[
("src/a.rs", 0.5),
("src/b.rs", 0.5),
("src/c.rs", 0.5),
("src/d.rs", 0.5),
("src/e.rs", 0.5),
("src/f.rs", 0.5),
("src/g.rs", 0.5),
("src/h.rs", 0.5),
("src/i.rs", 0.5),
("src/j.rs", 0.5),
("src/k.rs", 0.5),
]);
let reversed_files = [
("src/k.rs", 0.5),
("src/j.rs", 0.5),
("src/i.rs", 0.5),
("src/h.rs", 0.5),
("src/g.rs", 0.5),
("src/f.rs", 0.5),
("src/e.rs", 0.5),
("src/d.rs", 0.5),
("src/c.rs", 0.5),
("src/b.rs", 0.5),
("src/a.rs", 0.5),
];
assert_eq!(
compute_session_id(&forward, 0.0),
compute_session_id(&bundle_of(&reversed_files), 0.0),
"an all-ties bundle fingerprinted differently depending on file order",
);
}
#[test]
fn test_threshold_filters_before_selection() {
let b = bundle_of(&[("src/a.rs", 0.9), ("src/b.rs", 0.1)]);
let above_only = bundle_of(&[("src/a.rs", 0.9)]);
assert_eq!(
compute_session_id(&b, 0.5),
compute_session_id(&above_only, 0.5),
);
}
#[test]
fn test_selection_caps_at_ten() {
let ten: Vec<(&str, f32)> = vec![
("src/a.rs", 0.99),
("src/b.rs", 0.98),
("src/c.rs", 0.97),
("src/d.rs", 0.96),
("src/e.rs", 0.95),
("src/f.rs", 0.94),
("src/g.rs", 0.93),
("src/h.rs", 0.92),
("src/i.rs", 0.91),
("src/j.rs", 0.90),
];
let mut eleven = ten.clone();
eleven.push(("src/k.rs", 0.01));
assert_eq!(
compute_session_id(&bundle_of(&ten), 0.0),
compute_session_id(&bundle_of(&eleven), 0.0),
"an 11th chunk below the cut changed the fingerprint",
);
}
#[test]
fn test_empty_bundle_hashes() {
let id = compute_session_id(&bundle_of(&[]), 0.0);
assert_eq!(id.len(), 16);
}