use alpha_id::fusion::{rrf_merge, cross_segment_dedupe}; use alpha_id::model::Candidate; fn cand(code: &str, seg: usize, rerank: Option) -> Candidate { Candidate { icd_code: code.into(), alpha_text: code.into(), segment_idx: seg, lexical: None, semantic: None, rerank } } #[test] fn rrf_merges_two_rankings_by_reciprocal_rank() { let lex = vec![0usize, 1, 2]; // entry indices ranked let sem = vec![2usize, 0, 3]; let merged = rrf_merge(&lex, &sem, 60); // entry 0 appears rank0 in lex and rank1 in sem -> highest assert_eq!(merged[0], 0); assert!(merged.contains(&3)); } #[test] fn cross_segment_keeps_max_score_and_all_sources() { let cands = vec![ cand("E11.9", 0, Some(0.4)), cand("E11.9", 2, Some(0.9)), cand("I10.90", 1, Some(0.5)), ]; let mut merged = cross_segment_dedupe(cands); merged.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); assert_eq!(merged[0].0.icd_code, "E11.9"); assert_eq!(merged[0].1, 0.9); // max assert_eq!(merged[0].0.source_segments, vec![0, 2]); // all sources } #[test] fn fusion_is_deterministic_across_runs() { // Build inputs with deliberate score ties so HashMap iteration order would // matter without the deterministic tiebreaker. // rrf_merge: entries 5 and 10 appear at symmetric positions in both lists, // so their RRF scores are identical. The tiebreaker must put 5 before 10. let lex = vec![5usize, 10, 20]; let sem = vec![10usize, 5, 30]; let first = rrf_merge(&lex, &sem, 60); for _ in 0..4 { let run = rrf_merge(&lex, &sem, 60); assert_eq!(first, run, "rrf_merge output changed between runs"); } // Verify the tiebreaker: 5 and 10 have equal scores (symmetric positions); // ascending id means 5 should come before 10. let pos5 = first.iter().position(|&x| x == 5).expect("5 missing"); let pos10 = first.iter().position(|&x| x == 10).expect("10 missing"); assert!(pos5 < pos10, "tiebreaker should put smaller id first: got pos5={pos5} pos10={pos10}"); // cross_segment_dedupe: two codes with equal score — lexicographic tiebreaker. let cands = vec![ cand("Z99.89", 0, Some(0.7)), cand("A00.0", 1, Some(0.7)), cand("M50.00", 2, Some(0.5)), ]; let first_dedup = cross_segment_dedupe(cands.clone()); for _ in 0..4 { let run = cross_segment_dedupe(cands.clone()); let ids: Vec<&str> = run.iter().map(|(d, _)| d.icd_code.as_str()).collect(); let first_ids: Vec<&str> = first_dedup.iter().map(|(d, _)| d.icd_code.as_str()).collect(); assert_eq!(first_ids, ids, "cross_segment_dedupe output changed between runs"); } // Verify tiebreaker: A00.0 < Z99.89 lexicographically, so A00.0 first among ties. assert_eq!(first_dedup[0].0.icd_code, "A00.0", "tiebreaker should put A00.0 before Z99.89 (both score 0.7)"); assert_eq!(first_dedup[1].0.icd_code, "Z99.89"); assert_eq!(first_dedup[2].0.icd_code, "M50.00"); }