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

201 lines
8.5 KiB
Python

"""Session-level DB self-heal for the integration suite (phase 106, task 02).
Incident 2026-09-13: the dev DB's ``documents`` table hit PostgreSQL's
1600-attribute hard limit and every ``ALTER TABLE … ADD COLUMN`` failed
with ``TooManyColumns``, red-lining the full suite. Cause: the house
migration-test pattern (A13 — every migration exercises a real
downgrade, then repairs back to head) leaks *dropped-column
placeholder* attributes on every downgrade→upgrade round-trip
(``pg_attribute`` rows with ``attisdropped=true``). ``VACUUM (FULL)``
does NOT reclaim them (verified on PG 17.11) — only a table rewrite
does — and at ~95 leaks per full-suite run the shared dev DB bricks
every ~17 runs (faster when two runs race, which is how the incident
triggered).
This session fixture rebuilds any ``public`` table whose dropped-
attribute count exceeds :data:`DROPPED_ATTR_LIMIT` **before** the first
integration test of the session runs: rename + ``CREATE TABLE
(LIKE … INCLUDING ALL)`` + row copy + FK rewiring (both directions,
original constraint names and actions preserved). The normal case costs
one small catalog query per session; the heal path only fires while a
table is far below the 1600 cap (the limit is 200 — a tenth of the
headroom), so the migration tests never run on a near-bricked DB.
"""
from __future__ import annotations
import logging
import uuid
from collections.abc import Iterator
import pytest
from sqlalchemy import text
from app.db import db_available, engine
logger = logging.getLogger("bor.integration.self_heal")
#: Rebuild a table once its *dropped* attribute count passes this.
#: PostgreSQL's hard cap is 1600 TOTAL attributes (dropped included),
#: and the migration round-trips leak ~95 per full-suite run.
DROPPED_ATTR_LIMIT = 200
_BLOATED_SQL = text(
"SELECT c.relname FROM pg_class c"
" JOIN pg_namespace n ON n.oid = c.relnamespace"
" WHERE c.relkind = 'r' AND n.nspname = 'public'"
" AND (SELECT count(*) FROM pg_attribute a"
" WHERE a.attrelid = c.oid AND a.attnum > 0 AND a.attisdropped)"
" > :limit"
" ORDER BY 1"
)
_FKS_SQL = text(
"SELECT c.conname,"
" c.conrelid::regclass::text AS child,"
" c.confrelid::regclass::text AS parent,"
" c.confdeltype, c.confupdtype, c.confmatchtype AS matchtype,"
" (SELECT string_agg(ca.attname, ', ' ORDER BY ck.ord)"
" FROM unnest(c.conkey) WITH ORDINALITY ck(attnum, ord)"
" JOIN pg_attribute ca ON ca.attrelid = c.conrelid AND ca.attnum = ck.attnum)"
" AS child_cols,"
" (SELECT string_agg(pa.attname, ', ' ORDER BY pk.ord)"
" FROM unnest(c.confkey) WITH ORDINALITY pk(attnum, ord)"
" JOIN pg_attribute pa ON pa.attrelid = c.confrelid AND pa.attnum = pk.attnum)"
" AS parent_cols"
" FROM pg_constraint c"
" WHERE c.contype = 'f'"
" AND (:t = c.conrelid::regclass::text OR :t = c.confrelid::regclass::text)"
)
#: pg_constraint confdeltype/confupdtype codes → the DDL clause (``None``
#: = NO ACTION, the default — the clause is omitted).
_FK_ACTION: dict[str, str | None] = {
"a": None, # NO ACTION — the default, the clause is omitted
"r": "RESTRICT",
"c": "CASCADE",
"n": "SET NULL",
"d": "SET DEFAULT",
}
_FK_MATCH: dict[str, str] = {"f": "MATCH FULL", "p": "MATCH PARTIAL"}
def _fk_clause(fk) -> str:
"""The trailing ``MATCH …/ON DELETE …/ON UPDATE …`` of an FK."""
parts = [
_FK_MATCH.get(fk.matchtype, ""),
f"ON DELETE {_FK_ACTION[fk.confdeltype]}" if _FK_ACTION[fk.confdeltype] else "",
f"ON UPDATE {_FK_ACTION[fk.confupdtype]}" if _FK_ACTION[fk.confupdtype] else "",
]
return " ".join(p for p in parts if p)
def _rebuild_table(table: str) -> None:
"""Rewrite *table* to purge its dropped-column placeholders.
Rename + ``LIKE … INCLUDING ALL`` (live columns, constraints,
indexes, defaults) + row copy + FK rewiring (incoming AND outgoing,
original constraint names/actions). One transaction — a failure
rolls the whole table's surgery back and fails the session loudly
(a half-healed DB must never feed the migration tests).
The staging names carry a per-run suffix: a previous (interrupted or
repeated) heal may still own the plain names, and a collision would
make PG auto-suffix the LIKE-copied constraint names (…``_pkey1``)
and defeat the PK rename below.
Note: the PK is renamed back to its conventional ``<table>_pkey``;
other LIKE-copied objects keep PG's auto-generated names (nothing in
the repo references constraint/index names by name — DDL is
alembic-only, the ORM never issues DDL).
"""
new_name = f"{table}_heal_new_{uuid.uuid4().hex[:8]}"
old_name = f"{table}_heal_old_{uuid.uuid4().hex[:8]}"
with engine.begin() as conn:
fks = conn.execute(_FKS_SQL, {"t": table}).fetchall()
for fk in fks:
conn.execute(
text(f'ALTER TABLE "{fk.child}" DROP CONSTRAINT "{fk.conname}"')
)
conn.execute(
text(f'CREATE TABLE "{new_name}" (LIKE "{table}" INCLUDING ALL)')
)
# Explicit non-generated column list (attnum order): ``SELECT *``
# cannot be used — chunks.tsv is a STORED generated column, and
# generated columns refuse explicit values (it recomputes them).
cols = conn.execute(
text(
"SELECT string_agg('\"' || a.attname || '\"', ', '"
" ORDER BY a.attnum)"
" FROM pg_attribute a JOIN pg_class tc ON tc.oid = a.attrelid"
" WHERE tc.relname = :t AND a.attnum > 0"
" AND NOT a.attisdropped AND a.attgenerated NOT IN ('s', 'v')"
),
{"t": table},
).scalar()
conn.execute(
text(f'INSERT INTO "{new_name}" ({cols}) SELECT {cols} FROM "{table}"')
)
conn.execute(text(f'ALTER TABLE "{table}" RENAME TO "{old_name}"'))
# Drop the old table BEFORE the staging table takes its name: the
# old table's index/constraint names (including ``<table>_pkey``
# from a previous heal) live in the schema namespace until the
# DROP, and the PK rename below needs that name free.
conn.execute(text(f'DROP TABLE "{old_name}"'))
conn.execute(text(f'ALTER TABLE "{new_name}" RENAME TO "{table}"'))
# The rename above does NOT follow to LIKE-copied objects: put the
# PK back on its conventional name (every migration here auto-names
# PKs ``<table>_pkey`` — the one name tools/scripts reference).
has_auto_pkey = conn.execute(
text(
"SELECT 1 FROM pg_constraint c"
" JOIN pg_class tc ON tc.oid = c.conrelid"
" WHERE tc.relname = :t AND c.conname = :n AND c.contype = 'p'"
),
{"t": table, "n": f"{new_name}_pkey"},
).fetchone()
if has_auto_pkey:
conn.execute(
text(
f'ALTER TABLE "{table}" RENAME CONSTRAINT'
f' "{new_name}_pkey" TO "{table}_pkey"'
)
)
# FK rewiring LAST: only now does ``<table>`` refer to the rebuilt
# table with all staging names gone.
for fk in fks:
clause = _fk_clause(fk)
conn.execute(
text(
f'ALTER TABLE "{fk.child}" ADD CONSTRAINT "{fk.conname}"'
f" FOREIGN KEY ({fk.child_cols})"
f' REFERENCES "{fk.parent}" ({fk.parent_cols})'
+ (f" {clause}" if clause else "")
)
)
@pytest.fixture(autouse=True, scope="session")
def heal_bloated_tables() -> Iterator[None]:
"""Rebuild bloated ``public`` tables before the session's first test.
One cheap catalog query per session while healthy (the normal
case); the rebuild path only fires when a table's dropped-attribute
count passes :data:`DROPPED_ATTR_LIMIT` (see the module docstring
for the 2026-09-13 incident this exists to outlive).
"""
if db_available():
with engine.connect() as conn:
bloated = [
row[0]
for row in conn.execute(_BLOATED_SQL, {"limit": DROPPED_ATTR_LIMIT})
]
for table in bloated:
logger.warning(
"integration self-heal: rebuilding %r (dropped attributes"
" > %d — migration round-trip placeholders)",
table,
DROPPED_ATTR_LIMIT,
)
_rebuild_table(table)
yield