"""Step definitions for UnitOfWork lifecycle repository wiring. Targets ``UnitOfWork`` and ``UnitOfWorkContext`` in ``src/cleveragents/infrastructure/database/unit_of_work.py``. """ from __future__ import annotations from datetime import datetime from typing import Any from behave import given, then, when from behave.runner import Context from sqlalchemy import create_engine, event from sqlalchemy.orm import Session, sessionmaker from cleveragents.domain.models.core.action import Action, ActionState from cleveragents.domain.models.core.plan import ( NamespacedName, PlanIdentity, PlanPhase, PlanTimestamps, ProcessingState, ) from cleveragents.domain.models.core.plan import Plan as V3Plan from cleveragents.infrastructure.database.models import Base from cleveragents.infrastructure.database.repositories import ( ActionRepository, LifecyclePlanRepository, PlanRepository, ) from cleveragents.infrastructure.database.unit_of_work import ( UnitOfWorkContext, ) def _make_uow_action(name: str = "local/uow-action") -> Action: """Create a minimal Action domain object for testing.""" parts = name.split("/", 1) namespace = parts[0] if len(parts) == 2 else "local" short_name = parts[1] if len(parts) == 2 else parts[0] now = datetime.now() return Action( namespaced_name=NamespacedName(namespace=namespace, name=short_name), description=f"UoW test action {short_name}", long_description=None, definition_of_done=f"Verify {short_name} completes", strategy_actor="local/strategist", execution_actor="local/executor", estimation_actor=None, review_actor=None, arguments=[], reusable=True, read_only=False, state=ActionState("available"), created_at=now, updated_at=now, created_by=None, tags=[], ) def _make_uow_plan( plan_id: str, action_name: str = "local/uow-action", ) -> V3Plan: """Create a minimal Plan domain object for testing.""" now = datetime.now() return V3Plan( identity=PlanIdentity(plan_id=plan_id, attempt=1), namespaced_name=NamespacedName(namespace="local", name="uow-plan"), action_name=action_name, description="UoW test plan", definition_of_done="Verify UoW plan completes", strategy_actor="local/strategist", execution_actor="local/executor", phase=PlanPhase("action"), processing_state=ProcessingState("queued"), timestamps=PlanTimestamps( created_at=now, updated_at=now, ), project_links=[], arguments={}, arguments_order=[], invariants=[], reusable=True, read_only=False, created_by=None, tags=[], ) # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("a UnitOfWork backed by an in-memory database for lifecycle tests") def step_uow_lifecycle_background(context: Context) -> None: """Set up an in-memory database with schema for UoW tests. We bypass the migration runner and use ``Base.metadata.create_all`` directly since this is a unit test with an in-memory DB. """ engine = create_engine( "sqlite:///:memory:", echo=False, future=True, connect_args={"check_same_thread": False}, ) # Enable FK enforcement for SQLite @event.listens_for(engine, "connect") def _set_sqlite_fk(dbapi_conn: Any, _rec: Any) -> None: cursor = dbapi_conn.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() Base.metadata.create_all(engine) sf: sessionmaker[Session] = sessionmaker( bind=engine, expire_on_commit=False, autoflush=False, autocommit=False, class_=Session, ) # Store on context for reuse context.uow_engine = engine context.uow_session_factory = sf # --------------------------------------------------------------------------- # Repository exposure # --------------------------------------------------------------------------- @when("I access the actions repository from the UoW context") def step_access_actions_repo(context: Context) -> None: session = context.uow_session_factory() ctx = UnitOfWorkContext(session) context.uow_ctx_result = ctx.actions session.close() @then("the actions repository should be an ActionRepository instance") def step_check_actions_repo_type(context: Context) -> None: assert isinstance(context.uow_ctx_result, ActionRepository), ( f"Expected ActionRepository, got {type(context.uow_ctx_result)}" ) @when("I access the lifecycle_plans repository from the UoW context") def step_access_lifecycle_plans_repo(context: Context) -> None: session = context.uow_session_factory() ctx = UnitOfWorkContext(session) context.uow_ctx_result = ctx.lifecycle_plans session.close() @then("the lifecycle_plans repository should be a LifecyclePlanRepository instance") def step_check_lifecycle_plans_repo_type(context: Context) -> None: assert isinstance(context.uow_ctx_result, LifecyclePlanRepository), ( f"Expected LifecyclePlanRepository, got {type(context.uow_ctx_result)}" ) # --------------------------------------------------------------------------- # CRUD via UoW # --------------------------------------------------------------------------- @when('I create an action "{name}" via the UoW transaction') def step_create_action_via_uow(context: Context, name: str) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) action = _make_uow_action(name) ctx.actions.create(action) session.commit() except Exception: session.rollback() raise finally: session.close() @then('the action "{name}" should be retrievable from the actions repository') def step_check_action_retrievable(context: Context, name: str) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) action = ctx.actions.get_by_name(name) assert action is not None, f"Action {name} not found" finally: session.close() @given('an action "{name}" exists via the UoW') def step_ensure_action_exists(context: Context, name: str) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) existing = ctx.actions.get_by_name(name) if existing is None: action = _make_uow_action(name) ctx.actions.create(action) session.commit() except Exception: session.rollback() raise finally: session.close() @when('I create a lifecycle plan "{plan_id}" via the UoW transaction') def step_create_plan_via_uow(context: Context, plan_id: str) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) plan = _make_uow_plan(plan_id, action_name="local/uow-plan-action") ctx.lifecycle_plans.create(plan) session.commit() except Exception: session.rollback() raise finally: session.close() @then('the plan "{plan_id}" should be retrievable from the lifecycle_plans repository') def step_check_plan_retrievable(context: Context, plan_id: str) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) plan = ctx.lifecycle_plans.get(plan_id) assert plan is not None, f"Plan {plan_id} not found" finally: session.close() # --------------------------------------------------------------------------- # Cross-repository commit # --------------------------------------------------------------------------- @when( 'I create an action "{action_name}" and a plan "{plan_id}" in a single UoW transaction' ) def step_create_action_and_plan( context: Context, action_name: str, plan_id: str ) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) action = _make_uow_action(action_name) ctx.actions.create(action) plan = _make_uow_plan(plan_id, action_name=action_name) ctx.lifecycle_plans.create(plan) session.commit() except Exception: session.rollback() raise finally: session.close() @then('the action "{name}" should be retrievable in a new transaction') def step_check_action_new_txn(context: Context, name: str) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) action = ctx.actions.get_by_name(name) assert action is not None, f"Action {name} not found in new transaction" finally: session.close() @then('the plan "{plan_id}" should be retrievable in a new transaction') def step_check_plan_new_txn(context: Context, plan_id: str) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) plan = ctx.lifecycle_plans.get(plan_id) assert plan is not None, f"Plan {plan_id} not found in new transaction" finally: session.close() # --------------------------------------------------------------------------- # Rollback # --------------------------------------------------------------------------- @when('I create an action "{action_name}" and a plan "{plan_id}" but force a rollback') def step_create_and_rollback(context: Context, action_name: str, plan_id: str) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) action = _make_uow_action(action_name) ctx.actions.create(action) plan = _make_uow_plan(plan_id, action_name=action_name) ctx.lifecycle_plans.create(plan) # Force rollback instead of commit session.rollback() finally: session.close() @then('the action "{name}" should not exist in a new transaction') def step_check_action_not_exists(context: Context, name: str) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) action = ctx.actions.get_by_name(name) assert action is None, f"Action {name} should not exist after rollback" finally: session.close() @then('the plan "{plan_id}" should not exist in a new transaction') def step_check_plan_not_exists(context: Context, plan_id: str) -> None: session = context.uow_session_factory() try: ctx = UnitOfWorkContext(session) plan = ctx.lifecycle_plans.get(plan_id) assert plan is None, f"Plan {plan_id} should not exist after rollback" finally: session.close() # --------------------------------------------------------------------------- # Legacy accessor # --------------------------------------------------------------------------- @when("I access the legacy plans repository from the UoW context") def step_access_legacy_plans_repo(context: Context) -> None: session = context.uow_session_factory() ctx = UnitOfWorkContext(session) context.uow_ctx_result = ctx.plans session.close() @then("the legacy plans repository should be a PlanRepository instance") def step_check_legacy_plans_repo_type(context: Context) -> None: assert isinstance(context.uow_ctx_result, PlanRepository), ( f"Expected PlanRepository, got {type(context.uow_ctx_result)}" )