feat(docs): save chat answers as docs — edit screen, commit + push to the .env docs branch

This commit is contained in:
2026-09-01 03:52:03 -04:00
parent 7b7a834a1a
commit 725af9fac1
32 changed files with 4356 additions and 107 deletions
+322
View File
@@ -0,0 +1,322 @@
"""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:
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"
)