86 lines
3.1 KiB
Python
86 lines
3.1 KiB
Python
"""llm.rerank — similarity × recency blend, pure."""
|
||
|
||
from datetime import date
|
||
|
||
import pytest
|
||
|
||
from llm.rerank import Hit, blend, filter_since, recency
|
||
|
||
NOW = date(2026, 9, 3)
|
||
|
||
|
||
def _hit(key, distance, when, kind="comment"):
|
||
return Hit(
|
||
text=f"text {key}",
|
||
metadata={"item_key": key, "date": when, "kind": kind},
|
||
distance=distance,
|
||
)
|
||
|
||
|
||
class TestRecency:
|
||
def test_today_is_one(self):
|
||
assert recency("2026-09-03", now=NOW, half_life_days=365) == pytest.approx(1.0)
|
||
|
||
def test_one_half_life_is_half(self):
|
||
assert recency("2025-09-03", now=NOW, half_life_days=365) == pytest.approx(0.5)
|
||
|
||
def test_future_dates_clamp_to_one(self):
|
||
assert recency("2027-01-01", now=NOW, half_life_days=365) == 1.0
|
||
|
||
def test_undated_or_garbage_is_zero(self):
|
||
assert recency("", now=NOW, half_life_days=365) == 0.0
|
||
assert recency("not a date", now=NOW, half_life_days=365) == 0.0
|
||
|
||
def test_datetime_prefix_accepted(self):
|
||
assert recency("2026-09-03T12:00:00Z", now=NOW, half_life_days=365) == 1.0
|
||
|
||
|
||
class TestBlend:
|
||
def test_recent_beats_slightly_closer_old_hit(self):
|
||
old = _hit("OLD", distance=0.20, when="2019-01-01")
|
||
new = _hit("NEW", distance=0.25, when="2026-08-19")
|
||
out = blend([old, new], weight=0.3, half_life_days=365, now=NOW, top_n=8)
|
||
assert [h.metadata["item_key"] for h in out] == ["NEW", "OLD"]
|
||
assert out[0].score > out[1].score
|
||
|
||
def test_weight_zero_is_pure_similarity(self):
|
||
old = _hit("OLD", distance=0.20, when="2019-01-01")
|
||
new = _hit("NEW", distance=0.25, when="2026-08-19")
|
||
out = blend([old, new], weight=0.0, half_life_days=365, now=NOW, top_n=8)
|
||
assert [h.metadata["item_key"] for h in out] == ["OLD", "NEW"]
|
||
|
||
def test_dedupes_per_item_keeping_best_chunk(self):
|
||
a1 = _hit("A", distance=0.30, when="2026-01-01")
|
||
a2 = _hit("A", distance=0.10, when="2026-01-01")
|
||
b = _hit("B", distance=0.20, when="2026-01-01")
|
||
out = blend([a1, a2, b], weight=0.3, half_life_days=365, now=NOW, top_n=8)
|
||
assert [(h.metadata["item_key"], h.distance) for h in out] == [
|
||
("A", 0.10),
|
||
("B", 0.20),
|
||
]
|
||
|
||
def test_top_n_truncates(self):
|
||
hits = [_hit(f"K{i}", 0.1 * i, "2026-01-01") for i in range(5)]
|
||
assert len(blend(hits, weight=0.3, half_life_days=365, now=NOW, top_n=2)) == 2
|
||
|
||
def test_distance_clamped_into_unit_range(self):
|
||
far = _hit("F", distance=1.7, when="")
|
||
(out,) = blend([far], weight=0.0, half_life_days=365, now=NOW, top_n=1)
|
||
assert out.score == 0.0
|
||
|
||
|
||
class TestFilterSince:
|
||
def test_keeps_on_or_after_and_drops_undated(self):
|
||
hits = [
|
||
_hit("A", 0.1, "2025-12-31"),
|
||
_hit("B", 0.1, "2026-01-01"),
|
||
_hit("C", 0.1, ""),
|
||
]
|
||
assert [h.metadata["item_key"] for h in filter_since(hits, "2026-01-01")] == [
|
||
"B"
|
||
]
|
||
|
||
def test_empty_since_is_noop(self):
|
||
hits = [_hit("A", 0.1, ""), _hit("B", 0.1, "2020-01-01")]
|
||
assert filter_since(hits, "") == hits
|