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