Files
temp/features/steps/plan_lifecycle_service_persistence_coverage_steps.py
freemo 55aee7cf22 fix(test): commit after each add_skill to prevent session GC rollback, and improved coverage.
The step_register_skills_table step called add_skill in a loop but only
committed once at the end. Because SkillRepository.create() obtains a
new session per call and only flushes (never commits), the intermediate
sessions could be garbage-collected before the final commit, rolling
back their transactions on the shared SQLite :memory: connection. Moving
_commit_pending inside the loop ensures each skill is durably committed
before the next session is created.

ISSUES CLOSED: #418
2026-02-24 12:19:04 -05:00

156 lines
6.2 KiB
Python

"""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