"""Unit: the D6 recency boost on the fused score (phase 106, task 07). Pure-function pins for :func:`app.rag.retriever.apply_recency_boost` — the decay magnitude (zero age → full weight, one half-life → ``weight/e``, ten half-lives → negligible), the future-date clamp, the ``weight=0`` kill switch (byte-identical scores AND order), the tie-breaks (a raw-score tie breaks toward the newer document; the ``(path, position)`` key still applies when scores AND cosines AND ages are equal), and the no-mutation contract (the ``fuse`` convention). Fake rows, no DB — the real-Postgres fine-line battery (the owner's scenario) lives in ``tests/integration/test_recency_boost.py``. """ from __future__ import annotations import math import uuid from datetime import UTC, datetime, timedelta import pytest from app.config import get_settings from app.models import Document from app.rag.retriever import RetrievedChunk, apply_recency_boost #: A fixed "now" — the pins must not depend on the wall clock. NOW = datetime(2026, 9, 13, 12, 0, tzinfo=UTC) def _doc(path: str, created_at: datetime, source: str = "Homelab") -> Document: return Document( id=uuid.uuid4(), source=source, path=path, full_path=f"/tmp/{path}", title=path, content="x", content_hash="0" * 64, indexed_at=created_at, created_at=created_at, ) def _chunk( doc: Document, score: float, position: int = 0, cosine: float = 0.9 ) -> RetrievedChunk: return RetrievedChunk( chunk_id=uuid.uuid4(), position=position, content="x", score=score, document=doc, cosine=cosine, ) def test_zero_age_gets_the_full_weight_exactly() -> None: """Age 0 → ``+weight`` with no float drift (``exp(0) == 1.0``).""" c = _chunk(_doc("a.md", NOW), 0.032787) out = apply_recency_boost([c], now=NOW, weight=0.0007, half_life_days=365) assert out[0].score == 0.032787 + 0.0007 def test_one_half_life_of_age_decays_to_weight_over_e() -> None: """Age = half-life → ``+weight·e⁻¹`` (±1e-9).""" c = _chunk(_doc("a.md", NOW - timedelta(days=365)), 0.032787) out = apply_recency_boost([c], now=NOW, weight=0.0007, half_life_days=365) assert out[0].score == pytest.approx( 0.032787 + 0.0007 * math.exp(-1.0), abs=1e-9 ) def test_ten_half_lives_of_age_is_negligible() -> None: """Age 10× the half-life → ``+weight·e⁻¹⁰`` < ``weight·1e-3`` — the boost has faded to nothing (recency is an age signal, not a binary).""" c = _chunk(_doc("a.md", NOW - timedelta(days=3650)), 0.032787) out = apply_recency_boost([c], now=NOW, weight=0.0007, half_life_days=365) assert out[0].score - 0.032787 < 0.0007 * 1e-3 def test_future_created_at_clamps_to_zero_age() -> None: """A future ``created_at`` clamps to age 0 — the full weight (the D3 today-folding consistency: a future-sourced doc reads as brand-new).""" c = _chunk(_doc("a.md", NOW + timedelta(days=30)), 0.032787) out = apply_recency_boost([c], now=NOW, weight=0.0007, half_life_days=365) assert out[0].score == 0.032787 + 0.0007 def test_weight_zero_is_byte_identical_scores_and_order() -> None: """The kill switch: ``weight=0`` leaves every score untouched and the order byte-identical for an already-fused (already 4-key-sorted) input.""" a0 = _chunk(_doc("a.md", NOW - timedelta(days=10)), 0.03, cosine=0.9, position=0) b0 = _chunk(_doc("b.md", NOW - timedelta(days=20)), 0.03, cosine=0.8, position=0) a1 = _chunk(_doc("a.md", NOW - timedelta(days=10)), 0.02, cosine=0.95, position=0) c1 = _chunk(_doc("c.md", NOW - timedelta(days=30)), 0.02, cosine=0.5, position=1) chunks = [a0, b0, a1, c1] # already sorted by the 4-key order out = apply_recency_boost(chunks, now=NOW, weight=0.0, half_life_days=365) assert [ (rc.score, rc.cosine, rc.document.path, rc.position) for rc in out ] == [ (rc.score, rc.cosine, rc.document.path, rc.position) for rc in chunks ] def test_raw_score_tie_breaks_toward_the_newer_document() -> None: """An EXACT raw-score + cosine tie: the boost moves only the newer document (the older one's boost has decayed to ~0), so the newer rank rises above it — and with ``weight=0`` the pre-phase (path-ordered) ranking stands.""" old = _chunk(_doc("a-old.md", NOW - timedelta(days=2500)), 0.016129) new = _chunk(_doc("b-new.md", NOW), 0.016129) out = apply_recency_boost([old, new], now=NOW, weight=0.0007, half_life_days=365) assert [rc.document.path for rc in out] == ["b-new.md", "a-old.md"] out_off = apply_recency_boost([old, new], now=NOW, weight=0.0, half_life_days=365) assert [rc.document.path for rc in out_off] == ["a-old.md", "b-new.md"] def test_path_position_tiebreak_when_scores_cosines_and_ages_equal() -> None: """When the boosted scores AND cosines are equal (same age → same boost), the EXISTING ``(path, position)`` tie-break still decides — first by path, then by position within one path.""" a = _chunk(_doc("a.md", NOW - timedelta(days=100)), 0.02, cosine=0.5, position=1) b = _chunk(_doc("b.md", NOW - timedelta(days=100)), 0.02, cosine=0.5, position=0) out = apply_recency_boost([b, a], now=NOW, weight=0.001, half_life_days=365) assert [rc.document.path for rc in out] == ["a.md", "b.md"] # Same path, different positions (same doc, same age, same score): p1 = _chunk(_doc("a.md", NOW - timedelta(days=100)), 0.02, cosine=0.5, position=1) p0 = _chunk(_doc("a.md", NOW - timedelta(days=100)), 0.02, cosine=0.5, position=0) out2 = apply_recency_boost([p1, p0], now=NOW, weight=0.001, half_life_days=365) assert [rc.position for rc in out2] == [0, 1] def test_defaults_come_from_settings_when_omitted() -> None: """Omitted *weight* / *half_life_days* fall back to the settings (``recency_boost`` / ``recency_half_life_days``) — the explicit settings values must reproduce the default call exactly.""" chunks = [ _chunk(_doc("a.md", NOW - timedelta(days=30)), 0.02), _chunk(_doc("b.md", NOW - timedelta(days=700)), 0.02), ] s = get_settings() out_default = apply_recency_boost(chunks, now=NOW) out_explicit = apply_recency_boost( chunks, now=NOW, weight=s.recency_boost, half_life_days=s.recency_half_life_days, ) assert [rc.score for rc in out_default] == [rc.score for rc in out_explicit] def test_inputs_are_never_mutated() -> None: """The ``fuse`` convention: the input list and its scores are untouched — every returned chunk is a fresh ``replace()`` copy.""" chunks = [ _chunk(_doc("a.md", NOW - timedelta(days=30)), 0.02), _chunk(_doc("b.md", NOW), 0.03), ] original_scores = [rc.score for rc in chunks] original_order = [rc.chunk_id for rc in chunks] out = apply_recency_boost(chunks, now=NOW, weight=0.001, half_life_days=365) assert [rc.score for rc in chunks] == original_scores assert [rc.chunk_id for rc in chunks] == original_order assert out is not chunks assert all(o is not i for o, i in zip(out, chunks, strict=True)) def test_non_positive_half_life_fails_loud() -> None: """A ``half_life_days <= 0`` argument would divide the exponent by zero — the settings validator guards startup, the function guards direct calls (the ``fuse`` ``k <= 0`` pattern).""" c = _chunk(_doc("a.md", NOW), 0.02) with pytest.raises(ValueError, match="half_life_days must be > 0"): apply_recency_boost([c], now=NOW, weight=0.001, half_life_days=0)