"""Step definitions for plan_lifecycle_service_persistence_coverage.feature. Targets uncovered persistence-fallback lines in PlanLifecycleService: - Lines 340-341: get_action() persistence fallback when action not in memory - Lines 376-380: get_action_by_name() persistence fallback when action not in memory and linear scan also misses """ from __future__ import annotations from contextlib import contextmanager from unittest.mock import MagicMock from behave import given, then, when from behave.runner import Context from cleveragents.application.services.plan_lifecycle_service import ( PlanLifecycleService, ) from cleveragents.domain.models.core.action import Action, ActionState from cleveragents.domain.models.core.plan import NamespacedName def _make_action(name: str) -> Action: """Create a real Action domain object with the given namespaced name.""" return Action( namespaced_name=NamespacedName.parse(name), description=f"Persisted action {name}", definition_of_done="All tests pass", strategy_actor="openai/gpt-4", execution_actor="openai/gpt-4", state=ActionState.AVAILABLE, ) def _build_mock_uow(action_by_name_map: dict[str, Action | None]) -> MagicMock: """Build a mock UnitOfWork whose transaction context returns actions. ``action_by_name_map`` maps action name strings to Action objects (or None for not-found). The mock ctx.actions.get_by_name(name) will look up the name in this map and return the corresponding value. """ mock_uow = MagicMock() mock_ctx = MagicMock() mock_ctx.actions.get_by_name.side_effect = lambda n: action_by_name_map.get(n) @contextmanager def _transaction(): yield mock_ctx mock_uow.transaction = _transaction return mock_uow # ----------------------------------------------------------------- # Background # ----------------------------------------------------------------- @given("a plan lifecycle service with a mock unit of work") def step_create_service_with_mock_uow(context: Context) -> None: """Create a PlanLifecycleService backed by a mock UnitOfWork.""" settings = MagicMock() # Start with an empty action map; scenarios will populate it. context.action_by_name_map: dict[str, Action | None] = {} mock_uow = _build_mock_uow(context.action_by_name_map) context.service = PlanLifecycleService(settings=settings, unit_of_work=mock_uow) context.result_action = None # ----------------------------------------------------------------- # get_action persistence fallback (lines 340-341) # ----------------------------------------------------------------- @given('an action "{name}" exists only in the persistence layer') def step_action_in_persistence_only(context: Context, name: str) -> None: """Add an action to the mock persistence layer but NOT to in-memory cache.""" action = _make_action(name) # Add to the mock UoW lookup map so ctx.actions.get_by_name finds it context.action_by_name_map[name] = action # Ensure it is NOT in the in-memory cache context.service._actions.pop(name, None) @when('I call get_action with "{name}"') def step_call_get_action(context: Context, name: str) -> None: """Call service.get_action() which should fall back to persistence.""" context.result_action = context.service.get_action(name) @then('the returned action should have namespaced name "{name}"') def step_check_returned_action_name(context: Context, name: str) -> None: """Verify the returned action has the expected namespaced name.""" assert context.result_action is not None, "Expected an action, got None" assert str(context.result_action.namespaced_name) == name, ( f"Expected namespaced name '{name}', " f"got '{context.result_action.namespaced_name}'" ) @then('the action "{name}" should now be cached in memory') def step_check_action_cached(context: Context, name: str) -> None: """Verify the action was added to the in-memory _actions cache.""" assert name in context.service._actions, ( f"Expected '{name}' in _actions cache, " f"but found keys: {list(context.service._actions.keys())}" ) assert str(context.service._actions[name].namespaced_name) == name # ----------------------------------------------------------------- # get_action_by_name persistence fallback (lines 376-380) # ----------------------------------------------------------------- @given('an action "{name}" exists only in the persistence layer for name lookup') def step_action_in_persistence_for_name_lookup(context: Context, name: str) -> None: """Add an action to the mock persistence layer for get_action_by_name. The action must not be in the in-memory _actions dict at all, so that both the direct dict lookup AND the linear scan miss, forcing the persistence fallback on lines 376-380. """ action = _make_action(name) # Add to the mock UoW lookup map context.action_by_name_map[name] = action # Ensure it is NOT in the in-memory cache context.service._actions.pop(name, None) @when('I call get_action_by_name with "{name}"') def step_call_get_action_by_name(context: Context, name: str) -> None: """Call service.get_action_by_name() which should fall back to persistence.""" context.result_action = context.service.get_action_by_name(name) @then('the returned action from name lookup should have namespaced name "{name}"') def step_check_returned_action_by_name(context: Context, name: str) -> None: """Verify the returned action has the expected namespaced name.""" assert context.result_action is not None, "Expected an action, got None" assert str(context.result_action.namespaced_name) == name, ( f"Expected namespaced name '{name}', " f"got '{context.result_action.namespaced_name}'" ) @then('the action "{name}" should now be cached in memory after name lookup') def step_check_action_cached_after_name_lookup(context: Context, name: str) -> None: """Verify the action was added to the in-memory _actions cache.""" assert name in context.service._actions, ( f"Expected '{name}' in _actions cache, " f"but found keys: {list(context.service._actions.keys())}" ) assert str(context.service._actions[name].namespaced_name) == name