Files
brain-of-reese/tests/unit/test_retriever_recency.py
ducoterra ee3efb28c9
Build and Push Containers / build-and-push-app (push) Successful in 4m35s
Build and Push Containers / build-and-push-db (push) Successful in 14s
phase: 106_document_dates
Everything is verified green. Final report:

**Phase 106 — Document dates (final verification pass; all 10 tasks already complete)**

- Verified all phase artifacts: alembic `0020` (dev DB at `0020`), `doc_dates.py`, git `file_commit_dates`, importer `doc_dates_by_root`/`dates_updated`, both entry-point wirings, date APIs + tree `created_at`/`updated_at`, LLM surfaces (prompt block, `read` line 2, appended `ls` field), `apply_recency_boost` in `retrieve()`, UI columns/badge, admin editor, mock-LLM regex — all present and correct; no defects found, no fixes needed.
- `uv run pytest --cov=app --cov-report=term-missing` → **2299 passed, TOTAL 99%** (>90% ✓)
- `uv run pytest tests/e2e/test_document_dates.py -v --no-cov` → **6/6 passed** in isolation (DB up)
- 12 regression E2E suites (retrieval_quality, whole_document_context, agent_document_tools, ls_tree_drilldown, read_truncation_cap, kb_tree, kb_tree_nav, document_viewer, edit_summaries, import_documents, sync_button, hidden_folders_toggle, smoke) → **all green in isolation**
- `uv run ruff check .` → clean; `uv run pyright` → **0 errors, 0 warnings**

**Completion criteria:** 1) non-null `created_at` + 0020 upgrade/downgrade on dev DB ✓ (real-Alembic integration tests) 2) sync refresh/older/manual-persists/content-reset/no sources_meta bump ✓ 3) zip/tar mtime + future→today ✓ 4) LLM date surfaces + cross-check ✓ 5) UI Created/Updated/badge positions ✓ 6) admin editor set+revert round-trip ✓ 7) old-correct-beats-new-similar (defaults & boost-off) + near-tie + `BOR_RECENCY_BOOST=0` byte-identical ✓ 8) full gate ✓ 9) commit/phase-move — left to harness per instructions.

- **Notable:** recency default tuned 0.001 → **0.0007** (task 07 step 5 explicitly permits; measured margins recorded in `test_recency_boost.py` docstring).
- **Next pending phase:** none — `todo/` holds only this phase.
2026-09-13 19:28:05 -04:00

176 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)