use basemyai::{Fused, RRF_K, Ranking, rrf_fuse};
fn ranking(signal: &str, ids: &[&str]) -> Ranking {
Ranking {
signal: signal.to_string(),
ids: ids.iter().map(|s| (*s).to_string()).collect(),
}
}
const EPS: f64 = 1e-12;
fn find<'a>(fused: &'a [Fused], id: &str) -> &'a Fused {
fused
.iter()
.find(|f| f.id == id)
.expect("l'id attendu doit être présent dans le résultat fusionné")
}
#[test]
fn rrf_favorise_les_ids_presents_dans_plusieurs_signaux() {
let rankings = [ranking("vector", &["x", "y"]), ranking("graph", &["z", "y"])];
let fused = rrf_fuse(&rankings, RRF_K);
let x = find(&fused, "x").score; let y = find(&fused, "y").score; assert!(y > x, "y (consensus 2 signaux) doit battre x (pic isolé)");
assert_eq!(fused[0].id, "y", "y doit être premier");
}
#[test]
fn contributions_liste_les_signaux_sans_doublon_dans_l_ordre_de_premiere_apparition() {
let rankings = [
ranking("vector", &["m", "n"]),
ranking("recency", &["m"]),
ranking("graph", &["n", "m"]),
];
let fused = rrf_fuse(&rankings, RRF_K);
let m = find(&fused, "m");
assert_eq!(
m.contributions,
vec!["vector".to_string(), "recency".to_string(), "graph".to_string()],
"contributions ordonnées par première apparition, sans doublon"
);
let n = find(&fused, "n");
assert_eq!(n.contributions, vec!["vector".to_string(), "graph".to_string()],);
}
#[test]
fn score_exact_sur_un_petit_cas_connu() {
let k = 60.0;
let rankings = [ranking("s1", &["a", "b"]), ranking("s2", &["a", "c"])];
let fused = rrf_fuse(&rankings, k);
let a = find(&fused, "a");
assert!(
(a.score - 2.0 / 60.0).abs() < EPS,
"a doit valoir 2/60, obtenu {}",
a.score
);
let b = find(&fused, "b");
assert!((b.score - 1.0 / 61.0).abs() < EPS, "b doit valoir 1/61");
let c = find(&fused, "c");
assert!((c.score - 1.0 / 61.0).abs() < EPS, "c doit valoir 1/61");
assert_eq!(fused[0].id, "a");
}
#[test]
fn departage_deterministe_par_id_croissant_a_score_egal() {
let rankings = [
ranking("s1", &["gamma"]),
ranking("s2", &["alpha"]),
ranking("s3", &["beta"]),
];
let fused = rrf_fuse(&rankings, 60.0);
let ids: Vec<&str> = fused.iter().map(|f| f.id.as_str()).collect();
assert_eq!(
ids,
vec!["alpha", "beta", "gamma"],
"à score égal, tri par id croissant (lexicographique)"
);
for f in &fused {
assert!((f.score - 1.0 / 60.0).abs() < EPS);
}
}
#[test]
fn cas_limite_rankings_globalement_vide() {
let fused = rrf_fuse(&[], RRF_K);
assert!(fused.is_empty(), "entrée vide → résultat vide");
}
#[test]
fn cas_limite_ranking_aux_ids_vides_est_ignore() {
let rankings = [ranking("vide", &[]), ranking("plein", &["a"]), ranking("vide2", &[])];
let fused = rrf_fuse(&rankings, RRF_K);
assert_eq!(fused.len(), 1, "seul l'id réel doit apparaître");
let a = find(&fused, "a");
assert!((a.score - 1.0 / 60.0).abs() < EPS);
assert_eq!(a.contributions, vec!["plein".to_string()]);
}