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.
140 lines
5.0 KiB
Python
140 lines
5.0 KiB
Python
"""Unit tests: ``app.rag.doc_dates`` — the date normalization choke point.
|
|
|
|
Phase 106, task 02 (D2/D3). The owner's rules, pinned as a boundary
|
|
matrix on the pure function (no database): an UNDETERMINED date
|
|
(``None``) and a FUTURE date (beyond the 1-day clock-skew tolerance)
|
|
both assume "created today"; naive source timestamps are tz-agnostic
|
|
epoch values rendered as UTC (never local-converted); aware ones are
|
|
converted to UTC; the stored value keeps full precision. The
|
|
strict-greater 1-day boundary is pinned on both sides.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from datetime import UTC, datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
import app.rag.doc_dates as doc_dates
|
|
from app.rag.doc_dates import (
|
|
FUTURE_SKEW_TOLERANCE,
|
|
file_mtime_datetime,
|
|
normalize_doc_date,
|
|
)
|
|
|
|
#: A fixed "today" — every relative case in the matrix hangs off this.
|
|
NOW = datetime(2026, 9, 13, 12, 0, 0, tzinfo=UTC)
|
|
|
|
|
|
# ---------------------------------------------------------------- the matrix
|
|
|
|
|
|
def test_none_is_undetermined_returns_exactly_now() -> None:
|
|
assert normalize_doc_date(None, now=NOW) is NOW
|
|
|
|
|
|
def test_naive_raw_is_utc_attached_not_local_converted() -> None:
|
|
# The homelab host TZ is irrelevant: a naive 12:00 is a UTC 12:00.
|
|
out = normalize_doc_date(datetime(2020, 5, 1, 12, 0), now=NOW)
|
|
assert out == datetime(2020, 5, 1, 12, 0, tzinfo=UTC)
|
|
assert out.utcoffset() == timedelta(0)
|
|
|
|
|
|
def test_aware_raw_is_converted_to_utc() -> None:
|
|
raw = datetime(2020, 5, 1, 8, 0, tzinfo=timezone(timedelta(hours=-4)))
|
|
out = normalize_doc_date(raw, now=NOW)
|
|
assert out == datetime(2020, 5, 1, 12, 0, tzinfo=UTC)
|
|
|
|
|
|
def test_past_date_kept_verbatim() -> None:
|
|
raw = datetime(2024, 6, 15, 7, 30, 12, 123456, tzinfo=UTC)
|
|
assert normalize_doc_date(raw, now=NOW) is raw
|
|
|
|
|
|
def test_future_inside_tolerance_keeps_its_date() -> None:
|
|
# 23 h ahead — a drifting clock, not a future document.
|
|
raw = NOW + timedelta(hours=23)
|
|
assert normalize_doc_date(raw, now=NOW) is raw
|
|
|
|
|
|
def test_future_beyond_tolerance_folds_to_today() -> None:
|
|
# 25 h ahead — beyond the 1-day tolerance → today.
|
|
assert normalize_doc_date(NOW + timedelta(hours=25), now=NOW) is NOW
|
|
|
|
|
|
def test_exactly_at_tolerance_boundary_keeps_its_date() -> None:
|
|
# The check is strict-greater: exactly now + tolerance survives.
|
|
raw = NOW + FUTURE_SKEW_TOLERANCE
|
|
assert normalize_doc_date(raw, now=NOW) is raw
|
|
|
|
|
|
def test_one_second_past_tolerance_folds_to_today() -> None:
|
|
assert normalize_doc_date(NOW + FUTURE_SKEW_TOLERANCE + timedelta(seconds=1), now=NOW) is NOW
|
|
|
|
|
|
def test_result_keeps_full_precision() -> None:
|
|
# No date-truncation — the display formats, the storage doesn't.
|
|
out = normalize_doc_date(datetime(2020, 5, 1, 12, 0, 0, 987654), now=NOW)
|
|
assert out.microsecond == 987654
|
|
|
|
|
|
def test_default_now_is_utc_now_for_none() -> None:
|
|
before = datetime.now(UTC)
|
|
out = normalize_doc_date(None)
|
|
after = datetime.now(UTC)
|
|
assert before <= out <= after
|
|
assert out.tzinfo is not None
|
|
|
|
|
|
def test_default_now_keeps_old_raw() -> None:
|
|
out = normalize_doc_date(datetime(1999, 12, 31, 23, 59, tzinfo=UTC))
|
|
assert out == datetime(1999, 12, 31, 23, 59, tzinfo=UTC)
|
|
|
|
|
|
def test_naive_now_is_treated_as_utc() -> None:
|
|
# The future check runs in aware space; a naive ``now`` is UTC.
|
|
naive_now = datetime(2026, 9, 13, 12, 0)
|
|
assert normalize_doc_date(None, now=naive_now) == datetime(2026, 9, 13, 12, 0, tzinfo=UTC)
|
|
assert normalize_doc_date(
|
|
datetime(2026, 9, 15, 12, 1, tzinfo=UTC), now=naive_now
|
|
) == datetime(2026, 9, 13, 12, 0, tzinfo=UTC)
|
|
|
|
|
|
# ----------------------------------------------------- file_mtime_datetime
|
|
|
|
|
|
def test_file_mtime_datetime_reads_utime_as_utc(tmp_path: Path) -> None:
|
|
# 1585699200 = 2020-04-01T00:00:00Z (epoch — tz-agnostic).
|
|
target = 1_585_699_200
|
|
p = tmp_path / "doc.md"
|
|
p.write_text("hello\n")
|
|
os.utime(p, (target, target))
|
|
out = file_mtime_datetime(p)
|
|
assert out.tzinfo is not None
|
|
# ±1 s: mtime granularity varies by filesystem.
|
|
assert abs((out - datetime(2020, 4, 1, tzinfo=UTC)).total_seconds()) <= 1.0
|
|
|
|
|
|
def test_file_mtime_datetime_future_mtime_stays_future(tmp_path: Path) -> None:
|
|
# The helper is faithful: the FUTURE folding is normalize's job.
|
|
p = tmp_path / "future.md"
|
|
p.write_text("hi\n")
|
|
future = int((datetime.now(UTC) + timedelta(days=10)).timestamp())
|
|
os.utime(p, (future, future))
|
|
out = file_mtime_datetime(p)
|
|
assert out > datetime.now(UTC)
|
|
|
|
|
|
# ------------------------------------------------- the stdlib-only contract
|
|
|
|
|
|
def test_module_is_pure_stdlib() -> None:
|
|
"""Source-level pin (D3 choke point): stdlib imports only."""
|
|
src = Path(doc_dates.__file__).read_text()
|
|
import_re = re.compile(r"^\s*(?:import|from)\s+([A-Za-z_][A-Za-z0-9_.]*)", re.MULTILINE)
|
|
modules = [m.split(".")[0] for m in import_re.findall(src)]
|
|
assert modules # sanity: the regex actually matched the import block
|
|
non_stdlib = [m for m in modules if m not in sys.stdlib_module_names]
|
|
assert non_stdlib == []
|