forked from HAL9000/cleveragents-core
55aee7cf22
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
156 lines
6.2 KiB
Python
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
|