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.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""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 == []
|
||||
Reference in New Issue
Block a user