Files
brain-of-reese/tests/integration/test_migration_0020.py
T
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

360 lines
15 KiB
Python

"""Integration: migration 0020 (documents.created_at / created_at_manual)
schema contract (phase 106, task 01).
Drives the **real Alembic engine** against the live dev database
(``podman compose up -d db``), mirroring the house pattern of
``test_migration_0019.py`` (information_schema assertions on the state the
migration must leave). The tests target the 0019 → 0020 step explicitly so
later migrations cannot break the pins:
* upgrade 0019 → 0020 → both columns exist with the full contract —
``created_at`` TIMESTAMP WITH TIME ZONE NOT NULL, server default
``now()``; ``created_at_manual`` BOOLEAN NOT NULL, server default
``false`` — while the 0019 ``documents`` schema (incl. ``indexed_at``,
``summary``) survives;
* a ``documents`` row inserted while the DB is at 0019 backfills
``created_at ≈ now()`` (the D1 backfill-to-today, within a few seconds
of the upgrade moment) and ``created_at_manual is False``; a row written
after the upgrade without the columns takes both server defaults;
* the ORM contract agrees: a freshly inserted ``Document`` (nothing passed)
reads ``created_at_manual is False`` + non-null ``created_at``, and an
explicit ``created_at`` + ``created_at_manual=True`` round-trips through
a fresh session;
* downgrade to 0019 → both columns GONE (A13) while the row + its content
survive; upgrade back to 0020 → both columns back (round-trip).
The ``alembic`` fixture guarantees the DB ends at head even if a test
fails or the process is interrupted.
"""
from __future__ import annotations
import hashlib
import uuid
from collections.abc import Iterator
from datetime import UTC, datetime, timedelta
from typing import Any
import pytest
from alembic.config import Config
from sqlalchemy import text
from sqlalchemy.orm import Session
from alembic import command
from app.db import SessionLocal, db_available
from app.models import Document
SOURCE = "mig0020"
CONTENT = "content here"
CONTENT_HASH = hashlib.sha256(CONTENT.encode()).hexdigest()
# The backfill is evaluated by the ALTER at the upgrade moment; the 5 s
# slack each side absorbs test-process scheduling without weakening the
# "≈ now()" pin (the DB and the test share the host clock).
SLACK = timedelta(seconds=5)
@pytest.fixture()
def alembic(db: Session) -> Iterator[Config]:
"""Real Alembic config bound to the dev DB (URL from app settings).
Starts at head (repairs an interrupted earlier run); teardown upgrades
to head no matter what happened, so the dev DB is never left below
head.
"""
if not db_available():
pytest.skip("Postgres not reachable — run `podman compose up -d db` first")
cfg = Config() # no alembic.ini file — env.py gets the URL from app config
cfg.set_main_option("script_location", "alembic")
command.upgrade(cfg, "head")
try:
yield cfg
finally:
# Release the test session's open transaction BEFORE the repair
# DDL: an idle-in-transaction SELECT holds an ACCESS SHARE lock
# on ``documents``, which would deadlock the repair's
# ``ALTER TABLE`` (0020) forever.
db.rollback()
command.upgrade(cfg, "head")
def _version(db: Session) -> str | None:
return db.execute(text("SELECT version_num FROM alembic_version")).scalar()
def _column(db: Session, column: str) -> tuple[Any, ...] | None:
"""(data_type, is_nullable, column_default) for one documents column."""
row = db.execute(
text(
"SELECT data_type, is_nullable, column_default"
" FROM information_schema.columns"
" WHERE table_name = 'documents' AND column_name = :c"
),
{"c": column},
).fetchone()
return tuple(row) if row is not None else None
def _row_dates(db: Session, path: str) -> tuple[Any, Any]:
"""(created_at, created_at_manual) for one documents row."""
row = db.execute(
text("SELECT created_at, created_at_manual FROM documents WHERE path = :p"),
{"p": path},
).fetchone()
assert row is not None, f"documents row {path!r} must exist"
return row[0], row[1]
def _insert_sql(db: Session, path: str) -> uuid.UUID:
"""Insert one documents row (the pre-0020 column shape — the new
columns, when present, are omitted so the server defaults apply)."""
doc_id = uuid.uuid4()
db.execute(
text(
"INSERT INTO documents (id, source, path, full_path, title, content,"
" content_hash) VALUES (:id, :s, :p, :fp, :t, :c, :h)"
),
{
"id": doc_id,
"s": SOURCE,
"p": path,
"fp": f"/tmp/{path}",
"t": f"Doc {path}",
"c": CONTENT,
"h": CONTENT_HASH,
},
)
db.commit()
return doc_id
def _delete_by_path(db: Session, path: str) -> None:
db.execute(text("DELETE FROM documents WHERE path = :p"), {"p": path})
db.commit()
def test_upgrade_to_0020_adds_created_at(db: Session, alembic: Config) -> None:
"""Upgrade 0019 → 0020: both columns exist with the full contract
(``created_at`` TIMESTAMP WITH TIME ZONE NOT NULL default ``now()``;
``created_at_manual`` BOOLEAN NOT NULL default ``false``), are ABSENT
at 0019, a pre-0020 row backfills ``created_at ≈ now()`` + the
manual flag ``false`` (D1), a new row without the columns takes both
server defaults, and an explicit ``true`` round-trips — while the
0019 table contract (``indexed_at``, ``summary``) survives."""
command.downgrade(alembic, "0019") # start from the pre-0020 state
assert _version(db) == "0019"
assert _column(db, "created_at") is None, "created_at must be absent at 0019"
assert _column(db, "created_at_manual") is None, (
"created_at_manual must be absent at 0019"
)
path_pre = "pre-existing.md"
_insert_sql(db, path_pre) # no created_at* columns exist at 0019
try:
window_start = datetime.now(UTC)
command.upgrade(alembic, "0020")
window_end = datetime.now(UTC)
assert _version(db) == "0020", "alembic_version must be at 0020"
created = _column(db, "created_at")
assert created is not None, "documents.created_at is missing"
assert created[0] == "timestamp with time zone", (
"created_at must be TIMESTAMP WITH TIME ZONE"
)
assert created[1] == "NO", "created_at must be NOT NULL"
assert created[2] is not None and "now()" in str(created[2]), (
"created_at must carry the `now()` server default"
)
manual = _column(db, "created_at_manual")
assert manual is not None, "documents.created_at_manual is missing"
assert manual[0] == "boolean", "created_at_manual must be BOOLEAN"
assert manual[1] == "NO", "created_at_manual must be NOT NULL"
assert manual[2] is not None and "false" in str(manual[2]), (
"created_at_manual must carry the `false` server default"
)
# The pre-0020 row backfilled created_at ≈ now() (D1 — the owner's
# "set it to today's date during the migration") + flag false.
backfilled, manual_pre = _row_dates(db, path_pre)
assert backfilled is not None, "the backfilled created_at must be non-null"
assert backfilled.tzinfo is not None, "created_at must be tz-aware"
backfilled_utc = backfilled.astimezone(UTC)
assert window_start - SLACK <= backfilled_utc <= window_end + SLACK, (
f"the backfill must be ≈ the upgrade moment (got {backfilled_utc})"
)
assert manual_pre is False, (
"the backfilled row must read created_at_manual is False"
)
# A row written without the columns takes both server defaults.
path_new = "new-row.md"
_insert_sql(db, path_new)
try:
created_new, manual_new = _row_dates(db, path_new)
assert created_new is not None
created_new_utc = created_new.astimezone(UTC)
assert window_end - SLACK <= created_new_utc <= datetime.now(UTC) + SLACK, (
f"an omitted created_at takes the `now()` server default"
f" (got {created_new_utc})"
)
assert manual_new is False, (
"an omitted flag takes the `false` server default"
)
# The flag round-trips through an explicit ``true``.
db.execute(
text("UPDATE documents SET created_at_manual = true WHERE path = :p"),
{"p": path_new},
)
db.commit()
assert _row_dates(db, path_new)[1] is True, (
"created_at_manual = true must round-trip"
)
finally:
_delete_by_path(db, path_new)
# The 0019 schema survives the additive upgrade.
indexed = _column(db, "indexed_at")
assert indexed is not None, "documents.indexed_at (0001) must survive the upgrade"
assert indexed[0] == "timestamp with time zone" and indexed[1] == "NO", (
"documents.indexed_at (0001) must keep its 0019 contract after the upgrade"
)
summary = _column(db, "summary")
assert summary is not None and summary[0] == "text" and summary[1] == "YES", (
"documents.summary (0004) must survive the upgrade"
)
finally:
_delete_by_path(db, path_pre)
def test_orm_fresh_row_defaults_and_explicit_round_trips(
db: Session, alembic: Config
) -> None:
"""The ORM contract agrees with the column contract: a freshly
inserted ``Document`` (nothing passed for the new columns) reads
``created_at_manual is False`` + non-null ``created_at`` (the server
default took effect — D1), and an explicit ``created_at`` +
``created_at_manual=True`` round-trips through a fresh session."""
command.upgrade(alembic, "head")
path_default = "orm-default.md"
path_explicit = "orm-explicit.md"
try:
# Fresh row, both new columns omitted → server/Python defaults.
row_default = Document(
source=SOURCE,
path=path_default,
full_path=f"/tmp/{path_default}",
title="Default",
content=CONTENT,
content_hash=CONTENT_HASH,
)
db.add(row_default)
db.commit()
db.expire_all()
reloaded_default = db.get(Document, row_default.id)
assert reloaded_default is not None, "the fresh row must be readable"
assert reloaded_default.created_at is not None, (
"a fresh row must read a non-null created_at (server default)"
)
assert reloaded_default.created_at_manual is False, (
"a fresh row must read created_at_manual is False"
)
# Explicit created_at + created_at_manual=True round-trip through
# a FRESH session.
explicit = datetime(2020, 6, 15, 12, 30, 45, 123456, tzinfo=UTC)
row_explicit = Document(
source=SOURCE,
path=path_explicit,
full_path=f"/tmp/{path_explicit}",
title="Explicit",
content=CONTENT,
content_hash=CONTENT_HASH,
created_at=explicit,
created_at_manual=True,
)
db.add(row_explicit)
db.commit()
with SessionLocal() as fresh:
reloaded = fresh.get(Document, row_explicit.id)
assert reloaded is not None, "the row must exist in a fresh session"
assert reloaded.created_at is not None
assert reloaded.created_at.astimezone(UTC) == explicit, (
"the explicit created_at must round-trip through the DB"
)
assert reloaded.created_at_manual is True, (
"created_at_manual=True must round-trip through the DB"
)
finally:
_delete_by_path(db, path_default)
_delete_by_path(db, path_explicit)
def test_downgrade_to_0019_drops_the_columns(db: Session, alembic: Config) -> None:
"""Downgrade 0020 → 0019: both columns are gone (A13 — fully
reversible) while the row + its content survive, and the rest of the
0019 table contract (``indexed_at``) is intact."""
command.upgrade(alembic, "head")
path = "survivor.md"
_insert_sql(db, path)
try:
command.downgrade(alembic, "0019")
assert _version(db) == "0019"
assert _column(db, "created_at") is None, "created_at must be dropped"
assert _column(db, "created_at_manual") is None, (
"created_at_manual must be dropped"
)
row = db.execute(
text(
"SELECT path, title, content, content_hash, indexed_at"
" FROM documents WHERE path = :p"
),
{"p": path},
).fetchone()
assert row is not None and row[0] == path, (
"the row must survive the column drops"
)
assert row[1] == "Doc survivor.md" and row[2] == CONTENT, (
"title + content must survive the column drops"
)
assert row[3] == CONTENT_HASH, "the content hash must survive the drop"
assert row[4] is not None, "indexed_at must survive the column drops"
indexed = _column(db, "indexed_at")
assert indexed is not None and indexed[0] == "timestamp with time zone", (
"documents.indexed_at must survive the downgrade"
)
finally:
_delete_by_path(db, path)
# Repair: the fixture teardown re-upgrades to head.
def test_upgrade_round_trip_restores_the_columns(db: Session, alembic: Config) -> None:
"""Downgrade to 0019, then upgrade back to 0020: both columns are
back with the full contract (TIMESTAMPTZ NOT NULL default ``now()``;
BOOLEAN NOT NULL default ``false``)."""
command.downgrade(alembic, "0019")
command.upgrade(alembic, "0020")
assert _version(db) == "0020", "round-trip upgrade must land at 0020"
created = _column(db, "created_at")
assert created is not None, "documents.created_at must be back"
assert created[0] == "timestamp with time zone", (
"created_at must be TIMESTAMP WITH TIME ZONE after the round-trip"
)
assert created[1] == "NO", "created_at must be NOT NULL after the round-trip"
assert created[2] is not None and "now()" in str(created[2]), (
"the `now()` server default must survive the round-trip"
)
manual = _column(db, "created_at_manual")
assert manual is not None, "documents.created_at_manual must be back"
assert manual[0] == "boolean", (
"created_at_manual must be BOOLEAN after the round-trip"
)
assert manual[1] == "NO", (
"created_at_manual must be NOT NULL after the round-trip"
)
assert manual[2] is not None and "false" in str(manual[2]), (
"the `false` server default must survive the round-trip"
)