"""Step definitions for plan phase/state constraint rebaseline tests.""" from __future__ import annotations from datetime import UTC, datetime from typing import Any from behave import given, then, when from sqlalchemy import create_engine, text from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, sessionmaker from cleveragents.infrastructure.database.models import ( Base, LifecycleActionModel, LifecyclePlanModel, ) _VALID_ULID_PLAN = "01HV000000000000000000PH01" _VALID_ULID_COUNTER = 0 def _next_ulid() -> str: global _VALID_ULID_COUNTER _VALID_ULID_COUNTER += 1 suffix = str(_VALID_ULID_COUNTER).zfill(4) return f"01HV0000000000000000PH{suffix}" def _now_iso() -> str: return datetime.now(tz=UTC).isoformat() def _ensure_action(session: Session) -> None: """Ensure a parent action exists for FK references.""" existing = ( session.query(LifecycleActionModel) .filter_by(namespaced_name="local/phase-test-action") .first() ) if existing is not None: return action = LifecycleActionModel() action.namespaced_name = "local/phase-test-action" action.namespace = "local" action.name = "phase-test-action" action.description = "Test action for phase migration" action.definition_of_done = "Done" action.strategy_actor = "local/s" action.execution_actor = "local/e" action.state = "available" action.tags_json = "[]" action.created_at = _now_iso() action.updated_at = _now_iso() session.add(action) session.commit() @given("the plan phase rebaseline database is initialized") def step_plan_phase_rebaseline_db_init(context: Any) -> None: """Set up an in-memory database with the schema including new constraints.""" engine = create_engine("sqlite:///:memory:") # Enable FK enforcement from sqlalchemy import event @event.listens_for(engine, "connect") def set_sqlite_pragma(dbapi_connection: Any, _: Any) -> None: cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() Base.metadata.create_all(engine) factory = sessionmaker(bind=engine) session = factory() _ensure_action(session) context.phase_rebaseline_engine = engine context.phase_rebaseline_session = session @when('I insert a plan with phase "{phase}" and state "{state}"') def step_insert_plan_with_phase_and_state(context: Any, phase: str, state: str) -> None: """Insert a plan with the specified phase and state.""" session: Session = context.phase_rebaseline_session ulid = _next_ulid() now = _now_iso() plan = LifecyclePlanModel() plan.plan_id = ulid plan.root_plan_id = ulid plan.action_name = "local/phase-test-action" plan.namespaced_name = "local/phase-test-plan" plan.namespace = "local" plan.phase = phase plan.processing_state = state plan.description = "Test plan" plan.tags_json = "[]" plan.effective_profile_snapshot = "{}" plan.created_at = now plan.updated_at = now try: session.add(plan) session.commit() context.phase_rebaseline_result = "ok" context.phase_rebaseline_plan_id = ulid except (IntegrityError, Exception) as exc: session.rollback() context.phase_rebaseline_result = "error" context.phase_rebaseline_error = str(exc) @when('I try to insert a plan with phase "{phase}" and state "{state}"') def step_try_insert_plan_with_phase_and_state( context: Any, phase: str, state: str ) -> None: """Try inserting a plan without ORM defaults to exercise the phase constraint. The direct SQL insert must provide both plan_id and root_plan_id so the phase constraint is validated instead of failing on missing root_plan_id. """ session: Session = context.phase_rebaseline_session ulid = _next_ulid() now = _now_iso() try: session.execute( text( "INSERT INTO v3_plans " "(plan_id, root_plan_id, action_name, namespaced_name, namespace, " "phase, processing_state, description, tags_json, " "created_at, updated_at) " "VALUES (:pid, :rpid, :aname, :nname, :ns, :phase, :state, " ":desc, :tags, :cat, :uat)" ), { "pid": ulid, "rpid": ulid, "aname": "local/phase-test-action", "nname": "local/try-plan", "ns": "local", "phase": phase, "state": state, "desc": "Test plan", "tags": "[]", "cat": now, "uat": now, }, ) session.commit() context.phase_rebaseline_result = "ok" except (IntegrityError, Exception) as exc: session.rollback() context.phase_rebaseline_result = "error" context.phase_rebaseline_error = str(exc) @when("I insert a plan using default phase") def step_insert_plan_default_phase(context: Any) -> None: """Insert a plan without specifying a phase to test the ORM default.""" session: Session = context.phase_rebaseline_session ulid = _next_ulid() now = _now_iso() # Use ORM to create plan without specifying phase — should default to 'action' plan = LifecyclePlanModel() plan.plan_id = ulid plan.root_plan_id = ulid plan.action_name = "local/phase-test-action" plan.namespaced_name = "local/default-phase-plan" plan.namespace = "local" plan.description = "Default phase plan" plan.tags_json = "[]" plan.effective_profile_snapshot = "{}" plan.created_at = now plan.updated_at = now session.add(plan) session.commit() context.phase_rebaseline_plan_id = ulid @then('the plan should be persisted successfully with phase "{phase}"') def step_verify_plan_persisted_phase(context: Any, phase: str) -> None: """Verify the plan was persisted with the expected phase.""" assert context.phase_rebaseline_result == "ok", ( f"Expected ok, got {context.phase_rebaseline_result}" ) session: Session = context.phase_rebaseline_session plan = ( session.query(LifecyclePlanModel) .filter_by(plan_id=context.phase_rebaseline_plan_id) .one() ) assert plan.phase == phase @then('the plan should be persisted successfully with state "{state}"') def step_verify_plan_persisted_state(context: Any, state: str) -> None: """Verify the plan was persisted with the expected state.""" assert context.phase_rebaseline_result == "ok", ( f"Expected ok, got {context.phase_rebaseline_result}" ) session: Session = context.phase_rebaseline_session plan = ( session.query(LifecyclePlanModel) .filter_by(plan_id=context.phase_rebaseline_plan_id) .one() ) assert plan.processing_state == state @then("the insert should fail with a constraint violation") def step_verify_constraint_violation(context: Any) -> None: """Verify the insert failed.""" assert context.phase_rebaseline_result == "error", ( f"Expected error, got {context.phase_rebaseline_result}" ) @then('the rebaselined plan phase should be "{phase}"') def step_verify_rebaselined_plan_phase_default(context: Any, phase: str) -> None: """Verify the plan's phase matches the expected value.""" session: Session = context.phase_rebaseline_session plan = ( session.query(LifecyclePlanModel) .filter_by(plan_id=context.phase_rebaseline_plan_id) .one() ) assert plan.phase == phase, f"Expected phase '{phase}', got '{plan.phase}'"