""" Agent 写工具持久回执的 SQLite 迁移与模型一致性测试。 """ import importlib import sqlalchemy as sa from alembic.migration import MigrationContext from alembic.operations import Operations from sqlalchemy.dialects import postgresql from sqlalchemy.schema import CreateTable from app.db.models.agentinvocation import AgentInvocation MIGRATION = "database.versions.b2d4f6a8c1e3_3_0_33" def _bind_migration(monkeypatch, connection): """ 将迁移指令绑定到测试独占数据库。 """ migration = importlib.import_module(MIGRATION) monkeypatch.setattr(migration, "op", Operations(MigrationContext.configure(connection))) return migration def test_invocation_migration_upgrade_and_downgrade_are_replay_safe(monkeypatch): """ 已有 SQLite 能重复升级和降级,原有业务表保持不变。 """ engine = sa.create_engine("sqlite://") with engine.begin() as connection: connection.execute(sa.text("CREATE TABLE existing_data (id INTEGER PRIMARY KEY)")) migration = _bind_migration(monkeypatch, connection) migration.upgrade() migration.upgrade() inspector = sa.inspect(connection) assert "agentinvocation" in inspector.get_table_names() assert {column["name"] for column in inspector.get_columns("agentinvocation")} == { column.name for column in AgentInvocation.__table__.columns } indexes = {index["name"]: index for index in inspector.get_indexes("agentinvocation")} identity = indexes["ix_agentinvocation_identity"] assert identity["unique"] assert identity["column_names"] == ["principal_id", "session_id", "invocation_id"] assert inspector.get_check_constraints("agentinvocation")[0]["name"] == "ck_agentinvocation_status" connection.execute(sa.insert(AgentInvocation).values( principal_id="owner", session_id="session", invocation_id="call", tool_name="moviepilot_api", arguments_digest="a" * 64, claim_token="b" * 32, status="pending", summary="已确认提交", created_at="2026-09-09T00:00:00+00:00", updated_at="2026-09-09T00:00:00+00:00", )) assert connection.execute(sa.select(AgentInvocation.status)).scalar_one() == "pending" migration.downgrade() migration.downgrade() assert sa.inspect(connection).get_table_names() == ["existing_data"] engine.dispose() def test_invocation_migration_accepts_current_model_tables(monkeypatch): """ 全新数据库 create_all 后运行 Alembic 不应重复创建表或索引。 """ engine = sa.create_engine("sqlite://") with engine.begin() as connection: AgentInvocation.__table__.create(connection) migration = _bind_migration(monkeypatch, connection) migration.upgrade() migration.upgrade() actual = { (index["name"], tuple(index["column_names"]), bool(index["unique"])) for index in sa.inspect(connection).get_indexes("agentinvocation") } expected = { (index.name, tuple(column.name for column in index.columns), index.unique) for index in AgentInvocation.__table__.indexes } assert actual == expected engine.dispose() def test_invocation_migration_uses_postgresql_identity(): """ PostgreSQL 独立迁移保留宿主主键 Identity 约定。 """ migration = importlib.import_module(MIGRATION) table = sa.Table("agentinvocation", sa.MetaData(), migration._id_column("postgresql")) assert table.c.id.identity.start == 1 assert table.c.id.identity.cycle is True assert "GENERATED BY DEFAULT AS IDENTITY" in str(CreateTable(table).compile(dialect=postgresql.dialect()))