use std::sync::Arc;
use std::time::{Duration, Instant};
use fathomdb_embedder_api::{Embedder, EmbedderError, EmbedderIdentity, Vector};
use fathomdb_engine::{
apply_recency_reweight, Engine, IdSpace, PreparedWrite, SearchHit, SoftFallbackBranch,
};
use fathomdb_schema::SQLITE_SUFFIX;
use tempfile::TempDir;
fn hit(id: u64, body: &str, score: f64) -> SearchHit {
SearchHit {
id: IdSpace::content(id.to_string()),
write_cursor: id,
kind: "doc".to_string(),
body: body.to_string(),
score,
branch: SoftFallbackBranch::Vector,
source_id: None,
ce_score: None,
}
}
#[test]
fn recency_disabled_is_a_no_op() {
let hits = vec![hit(1, "older", 0.01), hit(2, "newer", 0.01)];
let out = apply_recency_reweight(hits.clone(), false);
assert_eq!(out, hits, "flag off => order + scores unchanged (pure RRF)");
}
#[test]
fn recency_enabled_breaks_equal_rrf_tie_toward_recent() {
let hits = vec![hit(1, "older", 0.01), hit(2, "newer", 0.01)];
let out = apply_recency_reweight(hits, true);
assert_eq!(out[0].body, "newer", "more-recent (higher write_cursor) wins the tie");
assert_eq!(out[1].body, "older");
assert!(out[0].score >= out[1].score, "reweighted list stays sorted by score desc");
}
#[test]
fn recency_does_not_override_a_clear_rrf_signal() {
let hits = vec![hit(9, "recent", 0.01), hit(1, "strong", 0.02)];
let out = apply_recency_reweight(hits, true);
assert_eq!(out[0].body, "strong", "recency is a near-tie nudge, not an override");
}
#[test]
fn recency_reweight_latency_gate() {
let hits: Vec<SearchHit> =
(0..10).map(|i| hit(i as u64, &format!("body-{i}"), 0.02 - (i as f64) * 0.001)).collect();
let started = Instant::now();
for _ in 0..1000 {
let _ = apply_recency_reweight(hits.clone(), true);
}
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_millis(200),
"reweighting a top-10 set 1000x must be cheap, took {elapsed:?}"
);
}
#[derive(Clone, Debug)]
struct FixedEmbedder;
impl Embedder for FixedEmbedder {
fn identity(&self) -> EmbedderIdentity {
EmbedderIdentity::new("deterministic", "rev-a", 8)
}
fn embed(&self, _text: &str) -> Result<Vector, EmbedderError> {
let mut v = vec![0.0_f32; 8];
v[0] = 1.0;
Ok(v)
}
}
fn fixture(name: &str) -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().unwrap();
let path = dir.path().join(format!("{name}{SQLITE_SUFFIX}"));
(dir, path)
}
#[test]
fn recency_flag_off_by_default_e2e() {
let (_dir, path) = fixture("g12_default_off");
let opened = Engine::open_with_embedder_for_test(&path, Arc::new(FixedEmbedder)).expect("open");
opened.engine.configure_vector_kind_for_test("doc").expect("vector kind");
for body in ["recency alpha", "recency beta"] {
opened
.engine
.write(&[PreparedWrite::Node {
kind: "doc".to_string(),
body: body.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: None,
state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
}])
.expect("write");
}
opened.engine.drain(10_000).expect("drain");
let baseline = opened.engine.search("recency").expect("search");
assert!(!baseline.results.is_empty());
opened.engine.set_recency_reweight_enabled_for_test(true);
let reweighted = opened.engine.search("recency").expect("search");
let base_bodies: std::collections::BTreeSet<&str> =
baseline.results.iter().map(|h| h.body.as_str()).collect();
let rw_bodies: std::collections::BTreeSet<&str> =
reweighted.results.iter().map(|h| h.body.as_str()).collect();
assert_eq!(base_bodies, rw_bodies, "recency reweight preserves the result set");
for w in reweighted.results.windows(2) {
assert!(w[0].score >= w[1].score, "reweighted list stays sorted by score desc");
}
opened.engine.close().unwrap();
}