115 lines
5.1 KiB
Python
115 lines
5.1 KiB
Python
# #region Test.ScenarioRegistry.Migration [C:3] [TYPE Module] [SEMANTICS test,alembic,migration,scenario,registry,schema]
|
|
# @defgroup ScenarioRegistry Test.Migration Migration smoke tests — chain linkage + real upgrade/downgrade on SQLite.
|
|
# @LAYER Test
|
|
# @RELATION VERIFIES -> [Alembic.ScenarioRegistryTables]
|
|
# @TEST_EDGE: migration is the head; upgrade creates 4 tables with expected columns/FK; downgrade drops them.
|
|
# @RATIONALE The migration is written with portable DDL only (no PG-specific types), so a real
|
|
# upgrade/downgrade can run on in-memory SQLite via alembic.operations — stronger than mocks.
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import create_engine, inspect
|
|
|
|
_MIGRATION_PATH = (
|
|
Path(__file__).parent.parent
|
|
/ "alembic"
|
|
/ "versions"
|
|
/ "v1w2x3y4z5a6_add_scenario_registry_tables.py"
|
|
)
|
|
_BACKEND_DIR = Path(__file__).parent.parent
|
|
|
|
_EXPECTED_TABLES = {
|
|
"scenario_registry_entries",
|
|
"scenario_revisions",
|
|
"scenario_staleness_signals",
|
|
"scenario_lifecycle_audit",
|
|
}
|
|
|
|
|
|
def _load_migration_module():
|
|
spec = importlib.util.spec_from_file_location("scenario_registry_migration", _MIGRATION_PATH)
|
|
assert spec and spec.loader
|
|
migration = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(migration)
|
|
return migration
|
|
|
|
|
|
# #region Test.ScenarioRegistry.Migration.TestChain [C:2] [TYPE Class]
|
|
class TestChain:
|
|
"""The migration is wired into the existing alembic chain (links onto its predecessor)."""
|
|
|
|
def test_migration_is_in_chain_and_links_previous_head(self):
|
|
from alembic.config import Config
|
|
from alembic.script import ScriptDirectory
|
|
|
|
migration = _load_migration_module()
|
|
cfg = Config(str(_BACKEND_DIR / "alembic.ini"))
|
|
cfg.set_main_option("script_location", str(_BACKEND_DIR / "alembic"))
|
|
sd = ScriptDirectory.from_config(cfg)
|
|
|
|
assert migration.down_revision == "u1v2w3x4y5z6", "must chain onto the previous head"
|
|
# The revision is part of the single linear chain (no branches); it may no longer be the
|
|
# terminal head once later migrations (046 automation) append to the same chain.
|
|
chain = list(sd.walk_revisions("base", "head"))
|
|
revisions_in_chain = {rev.revision for rev in chain}
|
|
assert migration.revision in revisions_in_chain, "migration must be part of the linear chain"
|
|
assert len(sd.get_heads()) == 1, "chain must stay linear (single head)"
|
|
# #endregion Test.ScenarioRegistry.Migration.TestChain
|
|
|
|
|
|
# #region Test.ScenarioRegistry.Migration.TestUpgradeDowngrade [C:2] [TYPE Class]
|
|
class TestUpgradeDowngrade:
|
|
"""Real upgrade()/downgrade() run against in-memory SQLite."""
|
|
|
|
def test_upgrade_creates_registry_tables(self, monkeypatch):
|
|
from alembic.migration import MigrationContext
|
|
from alembic.operations import Operations
|
|
|
|
migration = _load_migration_module()
|
|
engine = create_engine("sqlite:///:memory:")
|
|
with engine.begin() as conn:
|
|
ctx = MigrationContext.configure(conn)
|
|
monkeypatch.setattr(migration, "op", Operations(ctx))
|
|
migration.upgrade()
|
|
|
|
tables = set(inspect(conn).get_table_names())
|
|
assert tables >= _EXPECTED_TABLES, f"missing: {_EXPECTED_TABLES - tables}"
|
|
|
|
entry_cols = {c["name"] for c in inspect(conn).get_columns("scenario_registry_entries")}
|
|
assert "current_revision_id" in entry_cols
|
|
assert "scenario_key" in entry_cols
|
|
assert "tags" in entry_cols
|
|
|
|
revision_cols = {c["name"] for c in inspect(conn).get_columns("scenario_revisions")}
|
|
assert {"revision_id", "scenario_id", "content_hash", "parent_revision_id",
|
|
"graph_snapshot", "activation_status"} <= revision_cols
|
|
|
|
signal_fks = inspect(conn).get_foreign_keys("scenario_staleness_signals")
|
|
assert any(fk["referred_table"] == "scenario_registry_entries" for fk in signal_fks)
|
|
|
|
entry_fks = inspect(conn).get_foreign_keys("scenario_registry_entries")
|
|
assert not any(
|
|
fk["referred_table"] == "scenario_revisions" for fk in entry_fks
|
|
), "current_revision_id must be a plain pointer — no circular FK"
|
|
|
|
unique = inspect(conn).get_unique_constraints("scenario_staleness_signals")
|
|
assert any("scenario_id" in u["column_names"] for u in unique)
|
|
|
|
def test_downgrade_drops_registry_tables(self, monkeypatch):
|
|
from alembic.migration import MigrationContext
|
|
from alembic.operations import Operations
|
|
|
|
migration = _load_migration_module()
|
|
engine = create_engine("sqlite:///:memory:")
|
|
with engine.begin() as conn:
|
|
ctx = MigrationContext.configure(conn)
|
|
monkeypatch.setattr(migration, "op", Operations(ctx))
|
|
migration.upgrade()
|
|
migration.downgrade()
|
|
tables = set(inspect(conn).get_table_names())
|
|
assert _EXPECTED_TABLES.isdisjoint(tables), f"still present: {_EXPECTED_TABLES & tables}"
|
|
# #endregion Test.ScenarioRegistry.Migration.TestUpgradeDowngrade
|
|
# #endregion Test.ScenarioRegistry.Migration
|