| """Phase 6A — migration integrity test for runtime tables. |
| |
| Conftest-free, AST-based. Verifies that the Alembic migration for the dynamic |
| agent runtime exists, declares the right revision chain, and creates the three |
| expected tables (`agent_runs`, `agent_steps`, `artifacts`) with their key |
| indexes — all without importing alembic or executing any SQL. |
| |
| Run:: |
| |
| python -m pytest --noconftest -p no:cacheprovider \ |
| backend/tests/test_runtime_migration.py -v |
| """ |
| from __future__ import annotations |
|
|
| import ast |
| from pathlib import Path |
|
|
|
|
| _MIGRATION = ( |
| Path(__file__).resolve().parents[1] |
| / "database" |
| / "migrations" |
| / "versions" |
| / "0002_agent_runtime.py" |
| ) |
|
|
|
|
| def _load_module() -> ast.Module: |
| return ast.parse(_MIGRATION.read_text(encoding="utf-8"), filename=str(_MIGRATION)) |
|
|
|
|
| def _module_assigns(tree: ast.Module) -> dict[str, ast.AST]: |
| out: dict[str, ast.AST] = {} |
| for node in tree.body: |
| if isinstance(node, (ast.Assign, ast.AnnAssign)): |
| target = node.targets[0] if isinstance(node, ast.Assign) else node.target |
| if isinstance(target, ast.Name) and node.value is not None: |
| out[target.id] = node.value |
| return out |
|
|
|
|
| def _calls_in_function(tree: ast.Module, name: str) -> list[ast.Call]: |
| calls: list[ast.Call] = [] |
| for node in tree.body: |
| if isinstance(node, ast.FunctionDef) and node.name == name: |
| for sub in ast.walk(node): |
| if isinstance(sub, ast.Call): |
| calls.append(sub) |
| return calls |
|
|
|
|
| def _string_arg(call: ast.Call, idx: int) -> str | None: |
| if idx >= len(call.args): |
| return None |
| arg = call.args[idx] |
| if isinstance(arg, ast.Constant) and isinstance(arg.value, str): |
| return arg.value |
| return None |
|
|
|
|
| def _is_op_call(call: ast.Call, fn: str) -> bool: |
| func = call.func |
| return ( |
| isinstance(func, ast.Attribute) |
| and func.attr == fn |
| and isinstance(func.value, ast.Name) |
| and func.value.id == "op" |
| ) |
|
|
|
|
| def test_migration_file_exists() -> None: |
| assert _MIGRATION.exists(), f"missing migration: {_MIGRATION}" |
|
|
|
|
| def test_migration_revision_chain() -> None: |
| tree = _load_module() |
| assigns = _module_assigns(tree) |
| rev = assigns.get("revision") |
| down = assigns.get("down_revision") |
| assert isinstance(rev, ast.Constant) and rev.value == "0002_agent_runtime" |
| assert isinstance(down, ast.Constant) and down.value == "0001_initial" |
|
|
|
|
| def test_upgrade_creates_runtime_tables() -> None: |
| tree = _load_module() |
| upgrade_calls = _calls_in_function(tree, "upgrade") |
| created = { |
| _string_arg(c, 0) |
| for c in upgrade_calls |
| if _is_op_call(c, "create_table") |
| } |
| for expected in ("agent_runs", "agent_steps", "artifacts"): |
| assert expected in created, f"upgrade() must create_table({expected!r})" |
|
|
|
|
| def test_upgrade_creates_required_indexes() -> None: |
| tree = _load_module() |
| upgrade_calls = _calls_in_function(tree, "upgrade") |
| indexes: set[tuple[str, str]] = set() |
| for c in upgrade_calls: |
| if _is_op_call(c, "create_index"): |
| ix = _string_arg(c, 0) |
| tbl = _string_arg(c, 1) |
| if ix and tbl: |
| indexes.add((tbl, ix)) |
| expected = { |
| ("agent_runs", "ix_agent_runs_task_id"), |
| ("agent_runs", "ix_agent_runs_user_id"), |
| ("agent_runs", "ix_agent_runs_status"), |
| ("agent_steps", "ix_agent_steps_run_id"), |
| ("artifacts", "ix_artifacts_run_id"), |
| ("artifacts", "ix_artifacts_task_id"), |
| ("artifacts", "ix_artifacts_artifact_type"), |
| } |
| missing = expected - indexes |
| assert not missing, f"missing indexes in upgrade(): {sorted(missing)}" |
|
|
|
|
| def test_downgrade_drops_runtime_tables() -> None: |
| tree = _load_module() |
| downgrade_calls = _calls_in_function(tree, "downgrade") |
| dropped = { |
| _string_arg(c, 0) |
| for c in downgrade_calls |
| if _is_op_call(c, "drop_table") |
| } |
| for expected in ("agent_runs", "agent_steps", "artifacts"): |
| assert expected in dropped, f"downgrade() must drop_table({expected!r})" |
|
|