Files
ss-tools/backend/tests/test_scenario_automation_migration.py

103 lines
4.4 KiB
Python

# #region Test.ScenarioAutomation.Migration [C:3] [TYPE Module] [SEMANTICS test,alembic,migration,scenario,automation,schema]
# @defgroup ScenarioAutomation Test.Migration Migration smoke tests — chain linkage + real upgrade/downgrade on SQLite.
# @LAYER Test
# @RELATION VERIFIES -> [Alembic.ScenarioAutomationTables]
# @TEST_EDGE: migration is the head; upgrade creates 4 tables with expected columns; 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
_BACKEND_DIR = Path(__file__).parent.parent
_MIGRATION_PATH = (
_BACKEND_DIR
/ "alembic"
/ "versions"
/ "a8b9c0d1e2f3_add_scenario_automation_tables.py"
)
_EXPECTED_TABLES = {
"scenario_schedules",
"scenario_trigger_rules",
"scenario_automation_policies",
"scenario_notification_events",
}
def _load_migration_module():
spec = importlib.util.spec_from_file_location("scenario_automation_migration", _MIGRATION_PATH)
assert spec and spec.loader
migration = importlib.util.module_from_spec(spec)
spec.loader.exec_module(migration)
return migration
# #region Test.ScenarioAutomation.Migration.TestChain [C:2] [TYPE Class]
class TestChain:
"""The migration is the terminal head of a single linear alembic chain."""
def test_migration_is_head_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 == "z7a8b9c0d1e2", "must chain onto the 043/044 merge head"
heads = sd.get_heads()
assert len(heads) == 1, f"chain must be linear, found heads: {heads}"
assert migration.revision == heads[0], "migration must be the terminal head"
# #endregion Test.ScenarioAutomation.Migration.TestChain
# #region Test.ScenarioAutomation.Migration.TestUpgradeDowngrade [C:2] [TYPE Class]
class TestUpgradeDowngrade:
"""Real upgrade()/downgrade() run against in-memory SQLite."""
def test_upgrade_creates_automation_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}"
schedule_cols = {c["name"] for c in inspect(conn).get_columns("scenario_schedules")}
assert {"scenario_id", "revision_policy", "cron_expr", "timezone",
"missed_execution_policy", "max_instances"} <= schedule_cols
rule_cols = {c["name"] for c in inspect(conn).get_columns("scenario_trigger_rules")}
assert {"trigger", "scenario_id", "revision_policy", "environment_id"} <= rule_cols
policy_cols = {c["name"] for c in inspect(conn).get_columns("scenario_automation_policies")}
assert {"max_concurrent_per_env", "dedup_window_seconds", "overlap_rule",
"retention_days", "prod_gate_required"} <= policy_cols
def test_downgrade_drops_automation_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.ScenarioAutomation.Migration.TestUpgradeDowngrade
# #endregion Test.ScenarioAutomation.Migration