"""Step definitions for db_migration_lifecycle.feature — schema parity. FK structure verification, orphan rejection, valid references, and partial index checks for the ``resource_links``, ``checkpoint_metadata``, and ``decisions`` tables. """ from __future__ import annotations from typing import Any from behave import then from sqlalchemy import inspect as sa_inspect from sqlalchemy import text from sqlalchemy.exc import IntegrityError # --------------------------------------------------------------------------- # Shared test-data helpers # --------------------------------------------------------------------------- def _ensure_test_action_and_plan(conn: Any, action_name: str, plan_id: str) -> None: """Insert prerequisite action and plan rows if absent.""" existing_action = conn.execute( text("SELECT 1 FROM actions WHERE namespaced_name = :n"), {"n": action_name}, ).fetchone() existing_plan = conn.execute( text("SELECT 1 FROM v3_plans WHERE plan_id = :pid"), {"pid": plan_id}, ).fetchone() if existing_action is not None and existing_plan is not None: return if existing_action is None: 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": action_name, "namespace": "local", "name": action_name.split("/")[-1], "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", }, ) if existing_plan is None: 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": plan_id, "root_plan_id": plan_id, "action_name": action_name, "namespaced_name": "local/test-plan-fk", "namespace": "local", "description": "test plan", "created_at": "2026-01-01T00:00:00", "updated_at": "2026-01-01T00:00:00", }, ) def _insert_test_resource( conn: Any, resource_id: str, type_name: str = "git-checkout", resource_kind: str = "physical", ) -> None: """Insert a test resource row, handling the optional namespaced_name column.""" resource_columns = { col["name"] for col in sa_inspect(conn).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" params: dict[str, str] = { "resource_id": resource_id, "type_name": type_name, "resource_kind": resource_kind, "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" ) params["namespaced_name"] = f"local/{resource_id}" conn.execute(text(f"INSERT INTO resources ({cols}) VALUES ({vals})"), params) # --------------------------------------------------------------------------- # Then — link_type default verification # --------------------------------------------------------------------------- @then('the "resource_links" table should include "link_type" with default "contains"') def step_resource_links_has_link_type_default(context: Any) -> None: inspector = sa_inspect(context.engine) columns = inspector.get_columns("resource_links") link_type = next( (column for column in 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, ( "Expected resource_links.link_type default to include 'contains', " f"got: {link_type.get('default')!r}" ) # --------------------------------------------------------------------------- # Then — checkpoint FK structure verification # --------------------------------------------------------------------------- @then("checkpoint_metadata should enforce decision and resource foreign keys") def step_checkpoint_metadata_foreign_keys(context: Any) -> None: inspector = sa_inspect(context.engine) foreign_keys = 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 foreign_keys } decision_fk = (("decision_id",), "decisions", ("decision_id",)) resource_fk = (("resource_id",), "resources", ("resource_id",)) assert decision_fk in signatures, ( "Missing checkpoint_metadata foreign key for decision_id -> " "decisions.decision_id" ) assert resource_fk in signatures, ( "Missing checkpoint_metadata foreign key for resource_id -> " "resources.resource_id" ) # --------------------------------------------------------------------------- # Then — orphan FK rejection (combined) # --------------------------------------------------------------------------- @then("checkpoint_metadata foreign keys should reject orphan references") def step_checkpoint_metadata_foreign_keys_reject_orphans(context: Any) -> None: action_name = "local/test-action" plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FAV" with context.engine.begin() as conn: _ensure_test_action_and_plan(conn, action_name, plan_id) 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": plan_id, "decision_id": "01ARZ3NDEKTSV4RRFFQ69G5FAX", "checkpoint_type": "manual", "resource_id": "01ARZ3NDEKTSV4RRFFQ69G5FAY", "sandbox_ref": "test-ref", "filesystem_path": "", "created_at": "2026-01-01T00:00:00", }, ) except IntegrityError: return raise AssertionError( "checkpoint_metadata accepted orphan decision/resource references" ) # --------------------------------------------------------------------------- # Then — independent orphan FK rejection # --------------------------------------------------------------------------- @then("checkpoint_metadata should reject orphan decision_id independently") def step_checkpoint_reject_orphan_decision_only(context: Any) -> None: action_name = "local/test-action-fk-d" plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FB0" with context.engine.begin() as conn: _ensure_test_action_and_plan(conn, action_name, plan_id) 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": "01ARZ3NDEKTSV4RRFFQ69G5FB1", "plan_id": plan_id, "decision_id": "01ARZ3NDEKTSV4RRFFQ69G5FB2", "checkpoint_type": "manual", "sandbox_ref": "test-ref", "filesystem_path": "", "created_at": "2026-01-01T00:00:00", }, ) except IntegrityError: return raise AssertionError( "checkpoint_metadata accepted orphan decision_id with NULL resource_id" ) @then("checkpoint_metadata should reject orphan resource_id independently") def step_checkpoint_reject_orphan_resource_only(context: Any) -> None: action_name = "local/test-action-fk-r" plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FB3" with context.engine.begin() as conn: _ensure_test_action_and_plan(conn, action_name, plan_id) 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": "01ARZ3NDEKTSV4RRFFQ69G5FB4", "plan_id": plan_id, "resource_id": "01ARZ3NDEKTSV4RRFFQ69G5FB5", "checkpoint_type": "manual", "sandbox_ref": "test-ref", "filesystem_path": "", "created_at": "2026-01-01T00:00:00", }, ) except IntegrityError: return raise AssertionError( "checkpoint_metadata accepted orphan resource_id with NULL decision_id" ) # --------------------------------------------------------------------------- # Then — valid FK acceptance # --------------------------------------------------------------------------- @then("checkpoint_metadata should accept valid decision and resource references") def step_checkpoint_accept_valid_references(context: Any) -> None: action_name = "local/test-action-fk-v" plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FB6" decision_id = "01ARZ3NDEKTSV4RRFFQ69G5FB7" resource_id = "01ARZ3NDEKTSV4RRFFQ69G5FB8" with context.engine.begin() as conn: _ensure_test_action_and_plan(conn, action_name, plan_id) 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": plan_id, "decision_type": "strategy_choice", "question": "test question", "chosen_option": "test option", "context_snapshot_json": "{}", "sequence_number": 1, "created_at": "2026-01-01T00:00:00", }, ) _insert_test_resource(conn, resource_id) 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": "01ARZ3NDEKTSV4RRFFQ69G5FB9", "plan_id": plan_id, "decision_id": decision_id, "checkpoint_type": "manual", "resource_id": resource_id, "sandbox_ref": "test-ref", "filesystem_path": "", "created_at": "2026-01-01T00:00:00", }, ) # --------------------------------------------------------------------------- # Then — partial index verification # --------------------------------------------------------------------------- @then( 'the "decisions" table should have partial index "{index_name}" on "{column_name}"' ) def step_decisions_has_partial_index( context: Any, index_name: str, column_name: str, ) -> None: inspector = sa_inspect(context.engine) indexes = inspector.get_indexes("decisions") index = next((idx for idx in indexes if idx.get("name") == index_name), None) assert index is not None, f"Index {index_name} not found on decisions" columns = list(index.get("column_names") or []) assert columns == [column_name], ( f"Index {index_name} expected on [{column_name!r}], got {columns!r}" ) # Verify partial WHERE clause via sqlite_master (SQLite-only). if context.engine.dialect.name == "sqlite": with context.engine.connect() as conn: row = conn.execute( text( "SELECT sql FROM sqlite_master " "WHERE type = 'index' AND name = :index_name" ), {"index_name": index_name}, ).fetchone() assert row is not None, f"sqlite_master entry missing for index {index_name}" sql = str(row[0] or "").lower() assert "where superseded_by is not null" in sql, ( f"Expected partial WHERE clause on {index_name}, got SQL: {row[0]!r}" )