"""Robot helper for schema-parity migration verification. Checks the migration-produced SQLite schema for: 1. ``resource_links.link_type`` defaulting to ``contains``. 2. ``checkpoint_metadata`` foreign keys to ``decisions`` and ``resources``. 3. ``idx_decisions_superseded`` partial index with ``WHERE superseded_by IS NOT NULL``. 4. Runtime SQLite foreign key enforcement for ``checkpoint_metadata``. Subcommands (each independent for isolated failure reporting): - ``schema-parity-link-type``: Verifies resource_links.link_type column and default. - ``schema-parity-fks``: Verifies checkpoint_metadata FK constraints and runtime enforcement (orphan rejection + positive path). - ``schema-parity-index``: Verifies idx_decisions_superseded partial index. """ from __future__ import annotations import sys import tempfile from collections.abc import Callable from pathlib import Path from typing import Any from sqlalchemy import create_engine, inspect, text from sqlalchemy.exc import IntegrityError from cleveragents.infrastructure.database.migration_runner import MigrationRunner def _cleanup_db_files(path: str) -> None: Path(path).unlink(missing_ok=True) Path(f"{path}-wal").unlink(missing_ok=True) Path(f"{path}-shm").unlink(missing_ok=True) def _setup_migrated_db() -> tuple[Any, Any, str]: """Create a temp DB, run migrations, return (engine, inspector, db_path).""" with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp_file: db_path = tmp_file.name db_url = f"sqlite:///{db_path}" MigrationRunner(db_url).init_or_upgrade() engine = create_engine(db_url, connect_args={"check_same_thread": False}) inspector = inspect(engine) return engine, inspector, db_path def _teardown_db(engine: Any, db_path: str) -> None: if engine is not None: engine.dispose() _cleanup_db_files(db_path) def _ensure_test_action_and_plan(conn: Any) -> None: """Insert prerequisite action and plan rows for FK tests. .. note:: This function is **not idempotent** — it performs blind INSERTs without existence checks. It is safe only when called against a freshly created database (as ``_setup_migrated_db`` provides). If idempotent behaviour is needed, see the Behave counterpart in ``features/steps/db_schema_parity_steps.py``. """ conn.execute( text( """ INSERT INTO actions ( namespaced_name, namespace, name, description, definition_of_done, strategy_actor, execution_actor, created_at, updated_at ) VALUES ( :namespaced_name, :namespace, :name, :description, :definition_of_done, :strategy_actor, :execution_actor, :created_at, :updated_at ) """ ), { "namespaced_name": "local/test-action", "namespace": "local", "name": "test-action", "description": "test action", "definition_of_done": "test dod", "strategy_actor": "local/strategy", "execution_actor": "local/execution", "created_at": "2026-01-01T00:00:00", "updated_at": "2026-01-01T00:00:00", }, ) conn.execute( text( """ INSERT INTO v3_plans ( plan_id, root_plan_id, action_name, namespaced_name, namespace, description, created_at, updated_at ) VALUES ( :plan_id, :root_plan_id, :action_name, :namespaced_name, :namespace, :description, :created_at, :updated_at ) """ ), { "plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "root_plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "action_name": "local/test-action", "namespaced_name": "local/test-plan", "namespace": "local", "description": "test plan", "created_at": "2026-01-01T00:00:00", "updated_at": "2026-01-01T00:00:00", }, ) # --------------------------------------------------------------------------- # Subcommand: schema-parity-link-type # --------------------------------------------------------------------------- def _schema_parity_link_type() -> None: engine, inspector, db_path = _setup_migrated_db() try: resource_link_columns = inspector.get_columns("resource_links") link_type = next( ( column for column in resource_link_columns if column["name"] == "link_type" ), None, ) assert link_type is not None, "resource_links.link_type column is missing" default = str(link_type.get("default") or "").lower() assert "contains" in default, ( "resource_links.link_type default must include 'contains', " f"got {link_type.get('default')!r}" ) print("schema-parity-link-type-ok") finally: _teardown_db(engine, db_path) # --------------------------------------------------------------------------- # Subcommand: schema-parity-fks # --------------------------------------------------------------------------- def _schema_parity_fks() -> None: engine, inspector, db_path = _setup_migrated_db() try: checkpoint_fks = inspector.get_foreign_keys("checkpoint_metadata") signatures = { ( tuple(fk.get("constrained_columns") or []), fk.get("referred_table"), tuple(fk.get("referred_columns") or []), ) for fk in checkpoint_fks } assert (("decision_id",), "decisions", ("decision_id",)) in signatures, ( "Missing checkpoint_metadata FK decision_id -> decisions.decision_id" ) assert (("resource_id",), "resources", ("resource_id",)) in signatures, ( "Missing checkpoint_metadata FK resource_id -> resources.resource_id" ) with engine.begin() as conn: _ensure_test_action_and_plan(conn) try: conn.execute( text( """ INSERT INTO checkpoint_metadata ( checkpoint_id, plan_id, decision_id, checkpoint_type, resource_id, sandbox_ref, filesystem_path, created_at ) VALUES ( :checkpoint_id, :plan_id, :decision_id, :checkpoint_type, :resource_id, :sandbox_ref, :filesystem_path, :created_at ) """ ), { "checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FAW", "plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "decision_id": "01ARZ3NDEKTSV4RRFFQ69G5FAX", "checkpoint_type": "manual", "resource_id": "01ARZ3NDEKTSV4RRFFQ69G5FAY", "sandbox_ref": "test-ref", "filesystem_path": "", "created_at": "2026-01-01T00:00:00", }, ) except IntegrityError: pass else: raise AssertionError( "checkpoint_metadata accepted orphan decision/resource references" ) # Verify each FK independently: orphan decision_id only with engine.begin() as conn: try: conn.execute( text( """ INSERT INTO checkpoint_metadata ( checkpoint_id, plan_id, decision_id, checkpoint_type, sandbox_ref, filesystem_path, created_at ) VALUES ( :checkpoint_id, :plan_id, :decision_id, :checkpoint_type, :sandbox_ref, :filesystem_path, :created_at ) """ ), { "checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FC0", "plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "decision_id": "01ARZ3NDEKTSV4RRFFQ69G5FC1", "checkpoint_type": "manual", "sandbox_ref": "test-ref", "filesystem_path": "", "created_at": "2026-01-01T00:00:00", }, ) except IntegrityError: pass else: raise AssertionError( "checkpoint_metadata accepted orphan decision_id independently" ) # Verify each FK independently: orphan resource_id only with engine.begin() as conn: try: conn.execute( text( """ INSERT INTO checkpoint_metadata ( checkpoint_id, plan_id, resource_id, checkpoint_type, sandbox_ref, filesystem_path, created_at ) VALUES ( :checkpoint_id, :plan_id, :resource_id, :checkpoint_type, :sandbox_ref, :filesystem_path, :created_at ) """ ), { "checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FC2", "plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "resource_id": "01ARZ3NDEKTSV4RRFFQ69G5FC3", "checkpoint_type": "manual", "sandbox_ref": "test-ref", "filesystem_path": "", "created_at": "2026-01-01T00:00:00", }, ) except IntegrityError: pass else: raise AssertionError( "checkpoint_metadata accepted orphan resource_id independently" ) # Positive test: valid FK references should be accepted with engine.begin() as conn: decision_id = "01ARZ3NDEKTSV4RRFFQ69G5FC4" resource_id = "01ARZ3NDEKTSV4RRFFQ69G5FC5" conn.execute( text( """ INSERT INTO decisions ( decision_id, plan_id, decision_type, question, chosen_option, context_snapshot_json, sequence_number, created_at ) VALUES ( :decision_id, :plan_id, :decision_type, :question, :chosen_option, :context_snapshot_json, :sequence_number, :created_at ) """ ), { "decision_id": decision_id, "plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "decision_type": "strategy_choice", "question": "test question", "chosen_option": "test option", "context_snapshot_json": "{}", "sequence_number": 1, "created_at": "2026-01-01T00:00:00", }, ) resource_columns = { col["name"] for col in inspect(engine).get_columns("resources") } cols = "resource_id, type_name, resource_kind, created_at, updated_at" vals = ":resource_id, :type_name, :resource_kind, :created_at, :updated_at" res_params: dict[str, str] = { "resource_id": resource_id, "type_name": "git-checkout", "resource_kind": "physical", "created_at": "2026-01-01T00:00:00", "updated_at": "2026-01-01T00:00:00", } if "namespaced_name" in resource_columns: cols = ( "resource_id, namespaced_name, type_name," " resource_kind, created_at, updated_at" ) vals = ( ":resource_id, :namespaced_name, :type_name," " :resource_kind, :created_at, :updated_at" ) res_params["namespaced_name"] = f"local/{resource_id}" conn.execute( text(f"INSERT INTO resources ({cols}) VALUES ({vals})"), res_params, ) conn.execute( text( """ INSERT INTO checkpoint_metadata ( checkpoint_id, plan_id, decision_id, checkpoint_type, resource_id, sandbox_ref, filesystem_path, created_at ) VALUES ( :checkpoint_id, :plan_id, :decision_id, :checkpoint_type, :resource_id, :sandbox_ref, :filesystem_path, :created_at ) """ ), { "checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FC6", "plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "decision_id": decision_id, "checkpoint_type": "manual", "resource_id": resource_id, "sandbox_ref": "test-ref", "filesystem_path": "", "created_at": "2026-01-01T00:00:00", }, ) print("schema-parity-fks-ok") finally: _teardown_db(engine, db_path) # --------------------------------------------------------------------------- # Subcommand: schema-parity-index # --------------------------------------------------------------------------- def _schema_parity_index() -> None: engine, inspector, db_path = _setup_migrated_db() try: decision_indexes = inspector.get_indexes("decisions") partial_index = next( ( index for index in decision_indexes if index.get("name") == "idx_decisions_superseded" ), None, ) assert partial_index is not None, "idx_decisions_superseded is missing" assert list(partial_index.get("column_names") or []) == ["superseded_by"], ( "idx_decisions_superseded must index decisions.superseded_by" ) with engine.connect() as conn: row = conn.execute( text( "SELECT sql FROM sqlite_master " "WHERE type = 'index' AND name = :index_name" ), {"index_name": "idx_decisions_superseded"}, ).fetchone() assert row is not None, "sqlite_master is missing idx_decisions_superseded" sql = str(row[0] or "").lower() assert "where superseded_by is not null" in sql, ( "idx_decisions_superseded is not a partial index" ) print("schema-parity-index-ok") finally: _teardown_db(engine, db_path) # --------------------------------------------------------------------------- # Legacy combined subcommand (kept for backward compatibility) # --------------------------------------------------------------------------- def _schema_parity() -> None: _schema_parity_link_type() _schema_parity_fks() _schema_parity_index() print("schema-parity-ok") _COMMANDS: dict[str, Callable[[], None]] = { "schema-parity": _schema_parity, "schema-parity-link-type": _schema_parity_link_type, "schema-parity-fks": _schema_parity_fks, "schema-parity-index": _schema_parity_index, } def main() -> None: if len(sys.argv) < 2: raise SystemExit(f"Expected command argument. Valid: {', '.join(_COMMANDS)}") command = sys.argv[1] handler = _COMMANDS.get(command) if handler is None: raise SystemExit(f"Unknown command: {command}. Valid: {', '.join(_COMMANDS)}") handler() if __name__ == "__main__": main()