Files
brain-of-reese/tests/integration/test_migration_0007.py
T
ducoterra f20d40e4fe
Build and Push Containers / build-and-push-app (push) Successful in 1m44s
Build and Push Containers / build-and-push-db (push) Successful in 12s
phase: 188_chunks_hnsw_index
**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`.
2026-10-17 03:18:23 -04:00

268 lines
10 KiB
Python

"""Integration: migration 0007 (git_sources.kind + path) schema contract.
Drives the **real Alembic engine** against the live dev database
(``podman compose up -d db``), mirroring the style of
``test_migration_0004.py`` / ``test_migration_0006.py``
(information_schema assertions on the state the migration must leave).
The tests target revision ``0007`` explicitly so later migrations
cannot break them:
* upgrade 0006 → 0007 → ``git_sources`` gains ``kind TEXT NOT NULL``
(server default ``'git'``, check constraint ``ck_git_sources_kind``:
``kind IN ('git', 'local')``) and ``path TEXT`` (nullable) with the
unique index ``uq_git_sources_path``; a row inserted before the
upgrade (the pre-0007 insert shape) keeps ``kind='git'`` /
``path=NULL`` after it;
* the check constraint rejects any kind other than ``git``/``local``;
* the unique index rejects duplicate local paths but tolerates NULL
paths (git rows);
* downgrade to 0006 → both columns, the constraint, and the index are
gone;
* upgrade back to 0007 → they 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
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
URL_BASE = "https://git.example.com/mig0007"
PATH_BASE = "/tmp/brain-of-reese-mig0007"
@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 _column(db: Session, column: str) -> tuple[Any, ...] | None:
"""(data_type, is_nullable, column_default) for one git_sources column."""
row = db.execute(
text(
"SELECT data_type, is_nullable, column_default"
" FROM information_schema.columns"
" WHERE table_name = 'git_sources' AND column_name = :c"
),
{"c": column},
).fetchone()
return tuple(row) if row is not None else None
def _version(db: Session) -> str | None:
return db.execute(text("SELECT version_num FROM alembic_version")).scalar()
def _check_constraint_def(db: Session) -> str | None:
"""Definition of ``ck_git_sources_kind``, or None if it does not exist.
Only call while ``git_sources`` exists (the ``::regclass`` cast errors
otherwise).
"""
row = db.execute(
text(
"SELECT pg_get_constraintdef(oid) FROM pg_constraint"
" WHERE conname = 'ck_git_sources_kind'"
" AND conrelid = 'git_sources'::regclass"
)
).fetchone()
return row[0] if row is not None else None
def _unique_path_index(db: Session) -> int:
"""1 iff ``uq_git_sources_path`` exists as a UNIQUE index."""
count: Any = db.execute(
text(
"SELECT count(*) FROM pg_indexes"
" WHERE tablename = 'git_sources' AND indexname = 'uq_git_sources_path'"
" AND indexdef ILIKE 'CREATE UNIQUE%'"
)
).scalar()
assert count is not None, "pg_indexes count must be an int"
return int(count)
def _insert(db: Session, url: str, kind: str | None = None, path: str | None = None) -> None:
"""Insert one git_sources row; kind/path omitted → pre-0007 shape."""
if kind is None and path is None:
db.execute(
text("INSERT INTO git_sources (id, url) VALUES (gen_random_uuid(), :u)"),
{"u": url},
)
else:
db.execute(
text(
"INSERT INTO git_sources (id, url, kind, path)"
" VALUES (gen_random_uuid(), :u, :k, :p)"
),
{"u": url, "k": kind, "p": path},
)
db.commit()
def _delete_by_url(db: Session, url: str) -> None:
db.execute(text("DELETE FROM git_sources WHERE url = :u"), {"u": url})
db.commit()
def test_upgrade_to_0007_adds_kind_and_path(db: Session, alembic: Config) -> None:
"""Upgrade 0006 → 0007: both columns exist with the locked types,
nullability, and defaults, plus the CHECK constraint and the unique
path index."""
command.downgrade(alembic, "0006") # start from the pre-0007 state
assert _version(db) == "0006"
command.upgrade(alembic, "0007")
assert _version(db) == "0007", "alembic_version must be at 0007"
kind = _column(db, "kind")
assert kind is not None, "git_sources.kind is missing"
assert kind[0] == "text", "git_sources.kind must be TEXT"
assert kind[1] == "NO", "git_sources.kind must be NOT NULL"
assert kind[2] is not None and "'git'" in kind[2], (
"git_sources.kind must have server default 'git'"
)
path = _column(db, "path")
assert path is not None, "git_sources.path is missing"
assert path[0] == "text", "git_sources.path must be TEXT"
assert path[1] == "YES", "git_sources.path must be NULLABLE"
constraint = _check_constraint_def(db)
assert constraint is not None, "ck_git_sources_kind is missing"
assert "git" in constraint and "local" in constraint, (
f"ck_git_sources_kind must restrict kind to git|local, got: {constraint}"
)
assert _unique_path_index(db) == 1, "uq_git_sources_path unique index is missing"
def test_pre_0007_row_reads_as_git(db: Session, alembic: Config) -> None:
"""A row inserted before the upgrade (url only — the pre-0007 insert
shape) reads as ``kind='git'``, ``path=NULL`` after it."""
command.downgrade(alembic, "0006")
url = f"{URL_BASE}/pre-existing.git"
_insert(db, url) # no kind/path columns exist at 0006
try:
command.upgrade(alembic, "0007")
kind, path = db.execute(
text("SELECT kind, path FROM git_sources WHERE url = :u"), {"u": url}
).one()
assert kind == "git", "a pre-0007 row must read as kind='git'"
assert path is None, "a pre-0007 row must keep path=NULL"
finally:
_delete_by_url(db, url)
def test_kind_defaults_to_git_for_new_inserts(db: Session, alembic: Config) -> None:
"""An insert that omits kind (the API's pre-phase-38 shape) lands as
``kind='git'`` via the server default."""
command.upgrade(alembic, "head")
url = f"{URL_BASE}/default-kind.git"
_insert(db, url)
try:
kind, path = db.execute(
text("SELECT kind, path FROM git_sources WHERE url = :u"), {"u": url}
).one()
assert kind == "git", "git_sources.kind must default to 'git'"
assert path is None, "git_sources.path must default to NULL"
finally:
_delete_by_url(db, url)
def test_check_constraint_rejects_unknown_kind(db: Session, alembic: Config) -> None:
"""``ck_git_sources_kind`` is what later API validation relies on:
any kind other than git|local raises IntegrityError."""
command.upgrade(alembic, "head")
with pytest.raises(IntegrityError):
_insert(db, f"{URL_BASE}/bogus-kind.git", kind="bogus")
db.rollback() # the IntegrityError aborts the open transaction
def test_duplicate_local_path_rejected(db: Session, alembic: Config) -> None:
"""The unique path index is what the API's 409 relies on: two local
rows with the same path are rejected (NULL paths stay distinct —
git rows are unaffected)."""
command.upgrade(alembic, "head")
url_a = f"{URL_BASE}/dup-path-a.git"
url_b = f"{URL_BASE}/dup-path-b.git"
url_c = f"{URL_BASE}/dup-path-c.git"
path = f"{PATH_BASE}/shared"
try:
_insert(db, url_a, kind="local", path=path)
with pytest.raises(IntegrityError):
_insert(db, url_b, kind="local", path=path)
db.rollback()
# NULL paths are distinct under the unique index (git rows).
_insert(db, url_b)
_insert(db, url_c)
finally:
db.rollback()
_delete_by_url(db, url_a)
_delete_by_url(db, url_b)
_delete_by_url(db, url_c)
def test_downgrade_to_0006_drops_columns(db: Session, alembic: Config) -> None:
"""Downgrade to 0006: both columns, the CHECK constraint, and the
unique index are dropped (A13 — reversible)."""
command.downgrade(alembic, "0006")
assert _version(db) == "0006"
assert _column(db, "kind") is None, "git_sources.kind must be dropped"
assert _column(db, "path") is None, "git_sources.path must be dropped"
assert _check_constraint_def(db) is None, "ck_git_sources_kind must be dropped"
assert _unique_path_index(db) == 0, "uq_git_sources_path must be dropped"
def test_upgrade_round_trip_restores_columns(db: Session, alembic: Config) -> None:
"""Downgrade to 0006, then upgrade back to 0007: columns, default,
constraint, and index are back."""
command.downgrade(alembic, "0006")
command.upgrade(alembic, "0007")
assert _version(db) == "0007", "round-trip upgrade must land at 0007"
kind = _column(db, "kind")
assert kind is not None, "git_sources.kind must be back"
assert kind[1] == "NO" and kind[2] is not None and "'git'" in kind[2], (
"git_sources.kind must keep its NOT NULL 'git' default after the round-trip"
)
path = _column(db, "path")
assert path is not None and path[1] == "YES", "git_sources.path must be back"
constraint = _check_constraint_def(db)
assert constraint is not None, "ck_git_sources_kind must be back"
assert _unique_path_index(db) == 1, "uq_git_sources_path must be back"