**Phase 188 — final verification pass: all criteria green.** - **Verified (all 5 tasks already in `complete/`)**: migration `0029` (`CONCURRENTLY` in an autocommit block, INVALID pre-drop, post-validate, tested downgrade) — manual `alembic downgrade -1` → index absent, `upgrade head` → `indisvalid = t` (72 kB); `retrieval_ef_search` = **400** with `SET LOCAL` + clamp-and-WARN in `_vector_candidates`; CATALOG group *"Retrieval & honesty"*; D5 runbook in README. - `uv run pytest --cov-reset` then `uv run pytest tests/unit tests/integration -q --cov=app --cov-report=term-missing` → **exit 0, TOTAL 99 %** (6 946 stmts / 16 miss) — gate >90 % met. (A stale `.coverage` left by an earlier session produced a spurious 90.00 %/3-failure reading; `--cov-reset` cleared it.) - `uv run pytest tests/e2e/test_vector_index_recall.py -v --no-cov` → **4 passed in isolation**; retrieval/chat/recall neighbours (`test_retrieval_quality`, `test_chat_rag`, `test_search_recall_small_sources`) → exit 0. - `uv run ruff check .` → **All checks passed**; `uv run pyright` (whole repo) → **0 errors**. - **Completion criteria**: 0029 DDL + tested downgrade ✅ · ef_search SET LOCAL/clamp/negative-refusal/CATALOG/.env.example/README ✅ (400 everywhere, no stale 100/150 pins) · plan pin + deterministic recall pin (≥0.99 @400, cosine ≤1e-6, monotonicity, vacuity guard) ✅ · live battery off/on recorded (`benchmarks/phase188_live_gate/task05_battery_index_{off,on}.txt` byte-identical, `deterministic_gate_recall.txt`, build 161 s / 1 625 MB / 81 MB RSS, phase report `.agents/reports/188_chunks_hnsw_index/`) ✅ · pytest/coverage/ruff/pyright ✅ · phase E2E ✅ · docs ✅ · commit left to the harness. - **Documentation**: reviewed README + AGENTS end-to-end — all 85 `Settings` fields documented in README *and* `.env.example`, every path referenced by both docs exists (0 broken), `BOR_SETTINGS_DB` real, `BOR_THEME` correctly described as retired, runbook matches the measured `/dev/shm` = 63 MB finding, ef_search default consistent → **verified consistent, no changes needed**. - **Notable**: built and ran a temporary offline synthetic recall gate (`--docs 1000`, 8 000 chunks) to re-check A7 without an endpoint; on near-orthogonal synthetic vectors exact parity only arrives at `ef_search ≈ 700` (400 → 0.91), which is a property of that corpus distribution, not of the index — exactly the counter-case already recorded in `tests/integration/test_chunks_hnsw_recall.py`. Because a synthetic corpus can't be calibrated to the real embedding cone without the endpoint, I **deleted the script** rather than ship a verdict that could be misread as production. `AIPI_KEY` is absent in this environment (and `100.117.2.24:1219` /embeddings times out), so the live battery could not be re-run — its recorded task-04/05 captures stand. Scratch DB `brain_of_reese_hnswgate` dropped; repo left clean. - **Next pending phase**: `189_chat_save_long_turns`.
329 lines
13 KiB
Python
329 lines
13 KiB
Python
"""Integration: migration 0011 (doc_drafts) schema contract.
|
|
|
|
Drives the **real Alembic engine** against the live dev database
|
|
(``podman compose up -d db``), mirroring the house pattern of
|
|
``test_migration_0010.py`` (information_schema / pg_indexes assertions
|
|
on the state the migration must leave). The tests target revision
|
|
``0011`` explicitly so later migrations cannot break them:
|
|
|
|
* upgrade 0010 → 0011 → the ``doc_drafts`` table exists with the full
|
|
column contract (``id`` UUID PK; ``token`` UUID NOT NULL + the UNIQUE
|
|
index ``ix_doc_drafts_token`` — the URL credential; ``title`` /
|
|
``path`` / ``body`` TEXT NOT NULL; ``status`` TEXT NOT NULL default
|
|
'draft'; ``branch`` / ``commit_sha`` TEXT NULL; ``created_at`` /
|
|
``updated_at`` TIMESTAMPTZ NOT NULL default now());
|
|
* inserted rows round-trip: an omitted ``status`` defaults to 'draft'
|
|
with NULL ``branch`` / ``commit_sha`` (the pre-push state) and both
|
|
timestamps are stamped server-side; explicit push-state values
|
|
round-trip verbatim;
|
|
* two identical tokens are rejected by the unique index (the token is
|
|
a unique handle — the share-token precedent, phase 51);
|
|
* downgrade to 0010 → the table and index are gone (A13 — reversible),
|
|
the rest of the schema (e.g. ``saved_chats.share_token``) survives;
|
|
* upgrade back to 0011 → the table and the unique index are 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 uuid
|
|
from collections.abc import Iterator
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from alembic.config import Config
|
|
from sqlalchemy import text
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from alembic import command
|
|
from app.db import db_available
|
|
|
|
|
|
@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: psycopg3 keeps its snapshot alive while idle-in-transaction,
|
|
# and 0029's CREATE INDEX CONCURRENTLY phase 3 waits for every
|
|
# older-snapshot transaction — an unreleased session would park
|
|
# the build 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 _table_exists(db: Session, table: str) -> bool:
|
|
count: Any = db.execute(
|
|
text(
|
|
"SELECT count(*) FROM information_schema.tables"
|
|
" WHERE table_schema = 'public' AND table_name = :t"
|
|
),
|
|
{"t": table},
|
|
).scalar()
|
|
assert count is not None, "information_schema count must be an int"
|
|
return int(count) == 1
|
|
|
|
|
|
def _column(db: Session, table: str, column: str) -> tuple[Any, ...] | None:
|
|
"""(data_type, is_nullable, column_default) for one table column."""
|
|
row = db.execute(
|
|
text(
|
|
"SELECT data_type, is_nullable, column_default"
|
|
" FROM information_schema.columns"
|
|
" WHERE table_name = :t AND column_name = :c"
|
|
),
|
|
{"t": table, "c": column},
|
|
).fetchone()
|
|
return tuple(row) if row is not None else None
|
|
|
|
|
|
def _unique_token_index(db: Session) -> int:
|
|
"""1 iff ``ix_doc_drafts_token`` exists as a UNIQUE index."""
|
|
count: Any = db.execute(
|
|
text(
|
|
"SELECT count(*) FROM pg_indexes"
|
|
" WHERE tablename = 'doc_drafts'"
|
|
" AND indexname = 'ix_doc_drafts_token'"
|
|
),
|
|
).scalar()
|
|
assert count is not None, "pg_indexes count must be an int"
|
|
is_unique: Any = db.execute(
|
|
text(
|
|
"SELECT indisunique FROM pg_index"
|
|
" WHERE indexrelid = (SELECT oid FROM pg_class WHERE relname = 'ix_doc_drafts_token')"
|
|
),
|
|
).scalar()
|
|
return int(count) if is_unique else 0
|
|
|
|
|
|
def _insert(
|
|
db: Session,
|
|
*,
|
|
token: uuid.UUID | None = None,
|
|
status: str | None = None,
|
|
branch: str | None = None,
|
|
commit_sha: str | None = None,
|
|
) -> uuid.UUID:
|
|
"""Insert one doc_drafts row. ``status=None`` omits the column
|
|
(server-default path); a ``token`` is always supplied — the
|
|
migration carries no server default (the ORM/API supplies it)."""
|
|
cols = ["id", "token", "title", "path", "body"]
|
|
params: dict[str, Any] = {
|
|
"t": "Mig 0011",
|
|
"p": "docs/mig-0011.md",
|
|
"b": "# Phase 59 migration probe\n",
|
|
}
|
|
if token is not None:
|
|
params["tok"] = token
|
|
if status is not None:
|
|
cols.append("status")
|
|
params["s"] = status
|
|
if branch is not None:
|
|
cols.append("branch")
|
|
params["br"] = branch
|
|
if commit_sha is not None:
|
|
cols.append("commit_sha")
|
|
params["sha"] = commit_sha
|
|
sql = (
|
|
f"INSERT INTO doc_drafts ({', '.join(cols)}) VALUES ("
|
|
"gen_random_uuid(), :tok, :t, :p, :b"
|
|
+ (", :s" if status is not None else "")
|
|
+ (", :br" if branch is not None else "")
|
|
+ (", :sha" if commit_sha is not None else "")
|
|
+ ") RETURNING id"
|
|
)
|
|
draft_id: uuid.UUID = db.execute(text(sql), params).scalar_one()
|
|
db.commit()
|
|
return draft_id
|
|
|
|
|
|
def _delete(db: Session, draft_id: uuid.UUID) -> None:
|
|
db.execute(text("DELETE FROM doc_drafts WHERE id = :i"), {"i": draft_id})
|
|
db.commit()
|
|
|
|
|
|
def test_upgrade_to_0011_adds_doc_drafts(db: Session, alembic: Config) -> None:
|
|
"""Upgrade 0010 → 0011: the table + the unique token index exist
|
|
with the full column contract; the table is absent at 0010."""
|
|
command.downgrade(alembic, "0010") # start from the pre-0011 state
|
|
assert _version(db) == "0010"
|
|
assert not _table_exists(db, "doc_drafts"), "doc_drafts must be absent at 0010"
|
|
assert _unique_token_index(db) == 0, "the token index must be absent at 0010"
|
|
|
|
command.upgrade(alembic, "0011")
|
|
assert _version(db) == "0011", "alembic_version must be at 0011"
|
|
assert _table_exists(db, "doc_drafts"), "doc_drafts must exist at 0011"
|
|
|
|
id_col = _column(db, "doc_drafts", "id")
|
|
assert id_col is not None, "doc_drafts.id is missing"
|
|
assert id_col[0] == "uuid", "doc_drafts.id must be UUID"
|
|
assert id_col[1] == "NO", "doc_drafts.id must be NOT NULL (PK)"
|
|
|
|
token = _column(db, "doc_drafts", "token")
|
|
assert token is not None, "doc_drafts.token is missing"
|
|
assert token[0] == "uuid", "doc_drafts.token must be UUID"
|
|
assert token[1] == "NO", "doc_drafts.token must be NOT NULL (no un-drafted state)"
|
|
assert _unique_token_index(db) == 1, "the unique token index is missing"
|
|
|
|
for name in ("title", "path", "body"):
|
|
col = _column(db, "doc_drafts", name)
|
|
assert col is not None, f"doc_drafts.{name} is missing"
|
|
assert col[0] == "text", f"doc_drafts.{name} must be TEXT"
|
|
assert col[1] == "NO", f"doc_drafts.{name} must be NOT NULL"
|
|
|
|
status = _column(db, "doc_drafts", "status")
|
|
assert status is not None, "doc_drafts.status is missing"
|
|
assert status[0] == "text", "doc_drafts.status must be TEXT"
|
|
assert status[1] == "NO", "doc_drafts.status must be NOT NULL"
|
|
assert str(status[2]).startswith("'draft'"), (
|
|
"doc_drafts.status must have server default 'draft'"
|
|
)
|
|
|
|
for name in ("branch", "commit_sha"):
|
|
col = _column(db, "doc_drafts", name)
|
|
assert col is not None, f"doc_drafts.{name} is missing"
|
|
assert col[0] == "text", f"doc_drafts.{name} must be TEXT"
|
|
assert col[1] == "YES", f"doc_drafts.{name} must be NULL until pushed"
|
|
|
|
for name in ("created_at", "updated_at"):
|
|
col = _column(db, "doc_drafts", name)
|
|
assert col is not None, f"doc_drafts.{name} is missing"
|
|
assert col[0] == "timestamp with time zone", (
|
|
f"doc_drafts.{name} must be TIMESTAMPTZ"
|
|
)
|
|
assert col[1] == "NO", f"doc_drafts.{name} must be NOT NULL"
|
|
assert str(col[2]).startswith("now("), (
|
|
f"doc_drafts.{name} must have server default now()"
|
|
)
|
|
|
|
|
|
def test_inserted_rows_round_trip_the_pre_push_and_pushed_states(
|
|
db: Session, alembic: Config
|
|
) -> None:
|
|
"""At 0011, an omitted status defaults to 'draft' with NULL
|
|
branch/commit_sha (the pre-push state) and both timestamps are
|
|
stamped server-side; explicit push-state values round-trip
|
|
verbatim."""
|
|
command.upgrade(alembic, "head")
|
|
draft_token = uuid.uuid4()
|
|
draft_id = _insert(db, token=draft_token)
|
|
pushed_token = uuid.uuid4()
|
|
pushed_id = _insert(
|
|
db,
|
|
token=pushed_token,
|
|
status="pushed",
|
|
branch="bor-docs",
|
|
commit_sha="a" * 40,
|
|
)
|
|
try:
|
|
row = db.execute(
|
|
text(
|
|
"SELECT token, status, branch, commit_sha, created_at, updated_at"
|
|
" FROM doc_drafts WHERE id = :i"
|
|
),
|
|
{"i": draft_id},
|
|
).fetchone()
|
|
assert row is not None, "the draft row must exist"
|
|
assert row[0] == draft_token, "the token must round-trip verbatim"
|
|
assert row[1] == "draft", "an omitted status must default to 'draft'"
|
|
assert row[2] is None and row[3] is None, (
|
|
"branch/commit_sha must be NULL before the push endpoint runs"
|
|
)
|
|
assert row[4] is not None and row[5] is not None, (
|
|
"created_at/updated_at must be stamped server-side"
|
|
)
|
|
|
|
pushed = db.execute(
|
|
text(
|
|
"SELECT status, branch, commit_sha FROM doc_drafts WHERE id = :i"
|
|
),
|
|
{"i": pushed_id},
|
|
).fetchone()
|
|
assert pushed is not None, "the pushed row must exist"
|
|
assert tuple(pushed) == ("pushed", "bor-docs", "a" * 40), (
|
|
"explicit push-state values must round-trip verbatim"
|
|
)
|
|
finally:
|
|
_delete(db, draft_id)
|
|
_delete(db, pushed_id)
|
|
|
|
|
|
def test_unique_index_rejects_duplicate_tokens(db: Session, alembic: Config) -> None:
|
|
"""Two identical tokens are rejected by the unique index — the
|
|
token is the unique URL credential (the share-token precedent,
|
|
phase 51); a distinct token still lands."""
|
|
command.upgrade(alembic, "head")
|
|
dup_token = uuid.uuid4()
|
|
first_id = _insert(db, token=dup_token)
|
|
other_id: uuid.UUID | None = None
|
|
try:
|
|
try:
|
|
_insert(db, token=dup_token)
|
|
except IntegrityError:
|
|
db.rollback() # the aborted transaction must not leak
|
|
else:
|
|
pytest.fail("a duplicate doc_drafts.token must be rejected")
|
|
|
|
# A different token is fine — only the exact duplicate is unique.
|
|
other_id = _insert(db, token=uuid.uuid4())
|
|
finally:
|
|
_delete(db, first_id)
|
|
if other_id is not None:
|
|
_delete(db, other_id)
|
|
|
|
|
|
def test_downgrade_to_0010_drops_the_table(db: Session, alembic: Config) -> None:
|
|
"""Downgrade to 0010: the table and the unique index are gone
|
|
(A13 — reversible) while the rest of the schema survives."""
|
|
command.downgrade(alembic, "0010")
|
|
assert _version(db) == "0010"
|
|
assert not _table_exists(db, "doc_drafts"), "doc_drafts must be dropped"
|
|
assert _unique_token_index(db) == 0, "the token index must be dropped"
|
|
|
|
token_col = _column(db, "saved_chats", "share_token")
|
|
assert token_col is not None and token_col[0] == "uuid", (
|
|
"saved_chats.share_token must survive the downgrade"
|
|
)
|
|
meta = _column(db, "sources_meta", "version")
|
|
assert meta is not None and meta[0] == "integer", (
|
|
"sources_meta.version must survive the downgrade"
|
|
)
|
|
|
|
|
|
def test_upgrade_round_trip_restores_the_table(db: Session, alembic: Config) -> None:
|
|
"""Downgrade to 0010, then upgrade back to 0011: the table and the
|
|
unique index are back."""
|
|
command.downgrade(alembic, "0010")
|
|
command.upgrade(alembic, "0011")
|
|
assert _version(db) == "0011", "round-trip upgrade must land at 0011"
|
|
|
|
assert _table_exists(db, "doc_drafts"), "doc_drafts must be back"
|
|
assert _unique_token_index(db) == 1, "the unique token index must be back"
|
|
|
|
status = _column(db, "doc_drafts", "status")
|
|
assert status is not None and status[1] == "NO", (
|
|
"status must be TEXT NOT NULL after the round-trip"
|
|
)
|
|
assert str(status[2]).startswith("'draft'"), (
|
|
"status must default to 'draft' after the round-trip"
|
|
)
|