# #region Test.SmokeMigrationChain [C:2] [TYPE Module] [SEMANTICS test, smoke, alembic, migration, chain, heads] # @BRIEF Smoke test: verifies real Alembic migration files form a valid single-head chain # without broken down_revision links. Runs with NO database connection. # @RELATION BINDS_TO -> [Tooling.CheckMigrationChain] # @RELATION BINDS_TO -> [Alembic.MergeThreeHeads] # @INVARIANT: Exactly one migration head exists. # @INVARIANT: All down_revision references point to existing migrations. # @TEST_EDGE: multiple_heads → `alembic upgrade head` fails with "Multiple head revisions are present" # @TEST_EDGE: broken_chain → Broken down_revision link causes undeployable state # @TEST_EDGE: merge_migration → Merge migration (tuple down_revision) must be handled # @RATIONALE: ScriptDirectory.from_config() only reads version files — no DB connection needed. # Unlike test_alembic_migrations.py (requires PostgreSQL) and test_check_migration_chain.py # (uses mocks), this test loads REAL migration files and catches: # - Branch divergence (multiple heads from new migrations after a merge) # - Broken down_revision links (orphaned parents) # - Empty migration directory # @REJECTED: Relying only on `alembic upgrade head` at deploy time was rejected — the error # surfaces too late (after certs, DB wait, container startup). This test runs in CI without DB. """Smoke test: verify real Alembik migration chain integrity without a database connection. Catches before deployment: - Multiple migration heads (branch divergence) - Broken down_revision links (orphaned revisions) - Empty migration directory No database required — ScriptDirectory reads only the version files' Python metadata (revision, down_revision, doc, branch_labels). """ import sys from pathlib import Path # Ensure backend root is on sys.path for Alembic config resolution _src_path = str(Path(__file__).resolve().parent.parent) if _src_path not in sys.path: sys.path.insert(0, _src_path) def _load_script_dir(): """Load Alembic ScriptDirectory from real files (no DB connection).""" from alembic.config import Config as AlembicConfig from alembic.script import ScriptDirectory backend_dir = Path(__file__).resolve().parent.parent alembic_cfg = AlembicConfig(str(backend_dir / "alembic.ini")) alembic_cfg.set_main_option("script_location", str(backend_dir / "alembic")) return ScriptDirectory.from_config(alembic_cfg) # #region test_single_migration_head [C:1] [TYPE Function] # @ingroup Test.SmokeMigrationChain # @BRIEF Checks that exactly one migration head exists. def test_single_migration_head(): """Exactly one migration head must exist. Multiple heads mean branch divergence — `alembic upgrade head` will fail with: "Multiple head revisions are present for given argument 'head'" """ script_dir = _load_script_dir() heads = list(script_dir.get_heads()) assert len(heads) > 0, ( "No migration heads found — migration directory is empty" ) assert len(heads) == 1, ( f"Multiple migration heads detected ({len(heads)}). " f"This breaks 'alembic upgrade head'. " f"Heads: {[(h, script_dir.get_revision(h).doc) for h in heads]}" ) # #endregion test_single_migration_head # #region test_migration_chain_integrity [C:1] [TYPE Function] # @ingroup Test.SmokeMigrationChain # @BRIEF Walks the full chain from head to root verifying all down_revision links. def test_migration_chain_integrity(): """Walk the full migration chain from head(s) to root. Verifies: - Every down_revision (including tuple parents in merge migrations) exists - No circular references - All migrations are reachable from the head """ script_dir = _load_script_dir() heads = list(script_dir.get_heads()) assert len(heads) > 0, "No migration heads found" visited: set[str] = set() queue: list[str] = list(heads) while queue: rev_id = queue.pop(0) if rev_id in visited: continue visited.add(rev_id) rev = script_dir.get_revision(rev_id) assert rev is not None, ( f"Revision {rev_id} referenced but not found in migration files" ) down = rev.down_revision if down is None: continue # root migration (base) if isinstance(down, str): # Linear dependency parent_rev = script_dir.get_revision(down) assert parent_rev is not None, ( f"Broken chain link: {rev_id} -> {down} (revision not found)" ) queue.append(down) elif isinstance(down, tuple): # Merge migration — check all parents for parent_id in down: parent_rev = script_dir.get_revision(parent_id) assert parent_rev is not None, ( f"Broken merge link: {rev_id} -> {parent_id} (revision not found)" ) queue.append(parent_id) # Get total revision count for reporting (walk_revisions yields all) all_revisions = list(script_dir.walk_revisions()) total = len(all_revisions) assert len(visited) <= total, ( f"More revisions visited ({len(visited)}) than exist ({total}) — " "possible duplicate or circular reference" ) # Warn if some revisions are unreachable from heads (orphaned) # Note: some revisions may be alternative ancestry paths via merge # that are reachable even if not in the primary linear walk. if len(visited) < total: visited_hex = {v[:8] for v in visited} all_ids = [r.revision for r in all_revisions] orphaned = [r[:8] for r in all_ids if r[:8] not in visited_hex and r not in visited] print( f"NOTE: {total - len(visited)} revision(s) not traversed from head " f"(may be alternative ancestry paths via merge): {orphaned}" ) # #endregion test_migration_chain_integrity # #region test_migration_revisions_exist [C:1] [TYPE Function] # @ingroup Test.SmokeMigrationChain # @BRIEF Verifies all migration files parse and were discovered by ScriptDirectory. def test_migration_revisions_exist(): """All migration .py files in versions/ directory are loaded as revisions.""" script_dir = _load_script_dir() revisions = list(script_dir.walk_revisions()) # Count actual .py files in versions/ versions_dir = Path(__file__).resolve().parent.parent / "alembic" / "versions" py_files = sorted( f for f in versions_dir.iterdir() if f.suffix == ".py" and f.name != "__init__.py" ) revision_ids = {r.revision for r in revisions} assert len(revisions) > 0, "No revisions loaded by ScriptDirectory" print( f"Migration revisions loaded: {len(revisions)}, " f".py files in versions/: {len(py_files)}" ) # Verify each .py file maps to a loaded revision by checking file paths rev_ids_sorted = sorted(revision_ids) print(f"Revision IDs: {[r[:8] for r in rev_ids_sorted]}") # #endregion test_migration_revisions_exist # #endregion Test.SmokeMigrationChain