nexus-ai / tests /test_runtime_migration.py
nexus-deploy
deploy: sync backend b07a8d5
9c3ba60
Raw
History Blame Contribute Delete
4.15 kB
"""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})"