"""Step definitions for persistence_robot_alignment.feature. Mirrors the Robot restart-simulation flow to keep unit/integration persistence expectations aligned. Uses file-backed SQLite so the close-reopen cycle exercises real disk persistence. """ from __future__ import annotations import tempfile from datetime import UTC, datetime from pathlib import Path from typing import Any from behave import given, then, when from behave.runner import Context from sqlalchemy import create_engine from sqlalchemy.orm import Session from cleveragents.domain.models.core.action import Action, ActionState from cleveragents.domain.models.core.plan import ( NamespacedName, Plan, PlanIdentity, PlanPhase, PlanTimestamps, ProcessingState, ProjectLink, ) from cleveragents.infrastructure.database.models import init_database from cleveragents.infrastructure.database.repositories import ( ActionRepository, LifecyclePlanRepository, ) def _phase_from_str(value: str) -> PlanPhase: """Convert a lowercase phase string to PlanPhase enum.""" return PlanPhase(value) def _state_from_str(value: str) -> ProcessingState: """Convert a lowercase state string to ProcessingState enum.""" return ProcessingState(value) @given("a fresh file-backed persistence alignment database") def step_fresh_db(context: Context) -> None: tmp_path: str = tempfile.mktemp(suffix=".db") db_url: str = f"sqlite:///{tmp_path}" engine: Any = init_database(db_url) session: Session = Session(bind=engine) context._pra_db_path = tmp_path context._pra_session = session context._pra_engine = engine @given('a persistence alignment action "{name}"') def step_create_action(context: Context, name: str) -> None: session: Session = context._pra_session factory = lambda: session # noqa: E731 now: datetime = datetime(2026, 2, 1, tzinfo=UTC) action = Action( namespaced_name=NamespacedName.parse(name), description="Alignment test action", definition_of_done="Restart assertions pass", strategy_actor="local/strat", execution_actor="local/exec", state=ActionState.AVAILABLE, created_at=now, updated_at=now, ) repo = ActionRepository(session_factory=factory) repo.create(action) session.commit() context._pra_action_name = name @given( 'a persistence alignment plan "{plan_id}" in phase "{phase}" with state "{state}"' ) def step_create_plan(context: Context, plan_id: str, phase: str, state: str) -> None: session: Session = context._pra_session factory = lambda: session # noqa: E731 now: datetime = datetime(2026, 3, 1, tzinfo=UTC) plan = Plan( identity=PlanIdentity(plan_id=plan_id, attempt=1), namespaced_name=NamespacedName(namespace="local", name="aligned-plan"), action_name=context._pra_action_name, description="Alignment test plan", definition_of_done="Alignment pass", phase=_phase_from_str(phase), processing_state=_state_from_str(state), strategy_actor="local/strat", execution_actor="local/exec", project_links=[ProjectLink(project_name="local/proj-aligned")], timestamps=PlanTimestamps(created_at=now, updated_at=now), created_by="alignment-test", tags=["alignment"], reusable=True, read_only=False, ) repo = LifecyclePlanRepository(session_factory=factory) repo.create(plan) session.commit() @when("the persistence alignment database is closed and reopened") def step_close_reopen(context: Context) -> None: context._pra_session.close() context._pra_engine.dispose() db_url: str = f"sqlite:///{context._pra_db_path}" engine2: Any = create_engine(db_url, echo=False) session2: Session = Session(bind=engine2) context._pra_session = session2 context._pra_engine = engine2 @then('the persistence alignment plan "{plan_id}" should exist') def step_plan_exists(context: Context, plan_id: str) -> None: session: Session = context._pra_session factory = lambda: session # noqa: E731 repo = LifecyclePlanRepository(session_factory=factory) plan = repo.get(plan_id) assert plan is not None, f"Plan {plan_id} not found after reopen" context._pra_plan = plan @then('the persistence alignment plan phase should be "{phase}"') def step_check_phase(context: Context, phase: str) -> None: assert context._pra_plan.phase == _phase_from_str(phase), ( f"Expected phase {phase}, got {context._pra_plan.phase}" ) @then('the persistence alignment plan processing state should be "{state}"') def step_check_state(context: Context, state: str) -> None: assert context._pra_plan.processing_state == _state_from_str(state), ( f"Expected state {state}, got {context._pra_plan.processing_state}" ) @then('the persistence alignment plan description should be "{desc}"') def step_check_desc(context: Context, desc: str) -> None: assert context._pra_plan.description == desc, ( f"Expected '{desc}', got '{context._pra_plan.description}'" ) @then("the persistence alignment plan should have {count:d} project link") def step_check_links(context: Context, count: int) -> None: actual: int = len(context._pra_plan.project_links) assert actual == count, f"Expected {count} links, got {actual}" # Clean up temp file tmp_path: str = context._pra_db_path context._pra_session.close() context._pra_engine.dispose() Path(tmp_path).unlink(missing_ok=True)