from datetime import timedelta import pytest from alembic.autogenerate import compare_metadata from alembic.migration import MigrationContext from sqlalchemy import inspect, text from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncEngine from app.db.migrate import downgrade, upgrade from app.db.models import Base, ChangeOp, Draft, DraftChange, User, utcnow from app.db.session import SessionMaker, make_engine TABLES = { "users", "invites", "access_attempts", "fsm_state", "drafts", "draft_changes", "pushes", "pipeline_runs", "container_snapshots", "alerts", "alert_deliveries", "log_queries", "agent_requests", } async def table_names(engine: AsyncEngine) -> set[str]: async with engine.connect() as conn: return set(await conn.run_sync(lambda c: inspect(c).get_table_names())) - {"alembic_version"} async def test_upgrade_creates_all_tables(engine: AsyncEngine) -> None: assert await table_names(engine) == TABLES async def test_models_match_migrations(engine: AsyncEngine) -> None: # A model change without a migration shows up here as a non-empty diff. async with engine.connect() as conn: diff = await conn.run_sync(lambda c: compare_metadata(MigrationContext.configure(c), Base.metadata)) assert diff == [] async def test_downgrade_to_base_and_back() -> None: eng = make_engine("sqlite+aiosqlite://") try: await upgrade(eng) await downgrade(eng, "base") assert await table_names(eng) == set() await upgrade(eng) assert await table_names(eng) == TABLES finally: await eng.dispose() INSERT_USER = text( "INSERT INTO users (tg_id, full_name, role, is_active, joined_at) VALUES (:id, 'u', :role, 1, '2026-01-01')" ) async def test_enum_check_rejects_garbage(engine: AsyncEngine) -> None: async with engine.begin() as conn: await conn.execute(INSERT_USER, {"id": 1, "role": "admin"}) with pytest.raises(IntegrityError): async with engine.begin() as conn: await conn.execute(INSERT_USER, {"id": 2, "role": "root"}) async def test_deleting_draft_cascades_to_changes(sessions: SessionMaker) -> None: async with sessions.begin() as s: s.add(User(tg_id=1, full_name="a")) await s.flush() draft = Draft(user_id=1, base_sha="0" * 40, expires_at=utcnow() + timedelta(hours=1)) draft.changes.append(DraftChange(path="services/a/main.py", op=ChangeOp.ADD, content=b"print(1)")) s.add(draft) async with sessions.begin() as s: await s.execute(text("DELETE FROM drafts")) async with sessions() as s: assert (await s.execute(text("SELECT count(*) FROM draft_changes"))).scalar_one() == 0