""" Tests for Scribe migrations 006 and 009. Runs against `core_api_test` only (see conftest.py — skip when TEST_DATABASE_URL is unset, hard failure if it resolves to `core_api`). Self-contained: starts and ends at head regardless of what ran before it. """ import pytest from sqlalchemy import text from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.pool import NullPool from tests.conftest import run_alembic async def _table_exists(url: str, table: str) -> bool: asyncpg_url = url.replace("postgresql://", "postgresql+asyncpg://", 1) engine = create_async_engine(asyncpg_url, poolclass=NullPool) try: async with engine.connect() as conn: result = await conn.execute( text("SELECT to_regclass(:name) IS NOT NULL AS present"), {"name": table}, ) return bool(result.scalar()) finally: await engine.dispose() async def _columns(url: str, table: str) -> set: asyncpg_url = url.replace("postgresql://", "postgresql+asyncpg://", 1) engine = create_async_engine(asyncpg_url, poolclass=NullPool) try: async with engine.connect() as conn: result = await conn.execute( text( "SELECT column_name FROM information_schema.columns " "WHERE table_name = :table" ), {"table": table}, ) return {row[0] for row in result.all()} finally: await engine.dispose() class TestMigration006RoundTrip: """Migration 006 must upgrade and downgrade cleanly (dossier verification #1).""" @pytest.mark.asyncio async def test_upgrade_creates_scribe_tables(self, test_database_url): result = run_alembic(test_database_url, "upgrade", "head") assert result.returncode == 0, result.stderr for table in ("scribe_addresses", "scribe_messages", "scribe_mailbox_entries"): assert await _table_exists(test_database_url, table), f"{table} missing after upgrade" @pytest.mark.asyncio async def test_downgrade_then_upgrade_round_trips_cleanly(self, test_database_url): baseline = run_alembic(test_database_url, "upgrade", "head") assert baseline.returncode == 0, baseline.stderr down = run_alembic(test_database_url, "downgrade", "005") assert down.returncode == 0, down.stderr assert not await _table_exists(test_database_url, "scribe_addresses") assert not await _table_exists(test_database_url, "scribe_messages") assert not await _table_exists(test_database_url, "scribe_mailbox_entries") # Book tables, created by the migration below this one, must survive # a downgrade of 006 — proves 006 only touches its own tables. assert await _table_exists(test_database_url, "book_records") # Leave the schema at head for the rest of the suite. up = run_alembic(test_database_url, "upgrade", "006") assert up.returncode == 0, up.stderr assert await _table_exists(test_database_url, "scribe_addresses") assert await _table_exists(test_database_url, "scribe_messages") assert await _table_exists(test_database_url, "scribe_mailbox_entries") class TestMigration009RoundTrip: """Migration 009 (core-api T-13) must upgrade and downgrade cleanly — dossier verification #2: 008 -> 009 -> 008 -> 009.""" @pytest.mark.asyncio async def test_upgrade_adds_archived_and_trashed_at(self, test_database_url): result = run_alembic(test_database_url, "upgrade", "head") assert result.returncode == 0, result.stderr columns = await _columns(test_database_url, "scribe_mailbox_entries") assert {"archived_at", "trashed_at"} <= columns @pytest.mark.asyncio async def test_downgrade_then_upgrade_round_trips_cleanly(self, test_database_url): baseline = run_alembic(test_database_url, "upgrade", "head") assert baseline.returncode == 0, baseline.stderr down = run_alembic(test_database_url, "downgrade", "008") assert down.returncode == 0, down.stderr columns_at_008 = await _columns(test_database_url, "scribe_mailbox_entries") assert "archived_at" not in columns_at_008 assert "trashed_at" not in columns_at_008 # The table itself, created by 006, must survive a downgrade of # 009 — proves 009 only adds/drops its own two columns. assert await _table_exists(test_database_url, "scribe_mailbox_entries") # Leave the schema at head (009 is head) for the rest of the suite. up = run_alembic(test_database_url, "upgrade", "head") assert up.returncode == 0, up.stderr columns_at_head = await _columns(test_database_url, "scribe_mailbox_entries") assert {"archived_at", "trashed_at"} <= columns_at_head