Files
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

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 == []