Files
temp/features/steps/plan_lifecycle_service_coverage_r3_steps.py
freemo 31472b5413 test(coverage): add Behave scenarios for 39 under-covered modules
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate.

ISSUES CLOSED: #1232
2026-03-31 21:47:12 +00:00

590 lines
21 KiB
Python

"""Step definitions for plan_lifecycle_service_coverage_r3.feature.
Targets uncovered lines in PlanLifecycleService:
- Lines 318-323: _run_estimation exception handler
- Line 452: _resolve_actor_registry_entry dunder prefix
- Lines 640-659: list_actions persistence paths (namespace, list_all, cache,
state filter, DatabaseError fallback)
- Line 917: list_plans persisted cache refresh
- Lines 1592-1614: _complete_apply_if_queued branches
- Lines 1687-1741: try_auto_run full lifecycle auto-progression
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.plan_lifecycle_service import (
PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import DatabaseError
from cleveragents.domain.models.core.action import Action, ActionState
from cleveragents.domain.models.core.plan import (
AutomationProfileProvenance,
AutomationProfileRef,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
ProcessingState,
ProjectLink,
)
# -----------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------
def _make_settings(**overrides) -> Settings:
"""Create a Settings instance, clearing singleton."""
Settings._instance = None
return Settings(**overrides)
def _create_action_r3(context: Context, name: str, **kwargs) -> Action:
"""Create an action with sensible defaults."""
defaults = {
"name": name,
"description": f"Action for {name}",
"definition_of_done": "Tests pass",
"strategy_actor": "openai/gpt-4",
"execution_actor": "openai/gpt-4",
}
defaults.update(kwargs)
return context.plscov3_svc.create_action(**defaults)
def _advance_to_strat_complete_r3(context: Context) -> None:
"""Advance plan to Strategize/COMPLETE."""
pid = context.plscov3_plan.identity.plan_id
svc = context.plscov3_svc
svc.start_strategize(pid)
plan = svc.get_plan(pid)
plan.processing_state = ProcessingState.COMPLETE
context.plscov3_plan = plan
def _advance_to_exec_complete_r3(context: Context) -> None:
"""Advance plan from Strategize/COMPLETE to Execute/COMPLETE."""
pid = context.plscov3_plan.identity.plan_id
svc = context.plscov3_svc
svc.execute_plan(pid)
svc.start_execute(pid)
plan = svc.get_plan(pid)
plan.processing_state = ProcessingState.COMPLETE
context.plscov3_plan = plan
def _advance_to_apply_queued_r3(context: Context) -> None:
"""Advance plan to Apply/QUEUED."""
_advance_to_strat_complete_r3(context)
_advance_to_exec_complete_r3(context)
pid = context.plscov3_plan.identity.plan_id
svc = context.plscov3_svc
svc.apply_plan(pid)
context.plscov3_plan = svc.get_plan(pid)
def _advance_to_apply_processing_r3(context: Context) -> None:
"""Advance plan to Apply/PROCESSING."""
_advance_to_apply_queued_r3(context)
pid = context.plscov3_plan.identity.plan_id
svc = context.plscov3_svc
svc.start_apply(pid)
context.plscov3_plan = svc.get_plan(pid)
def _make_mock_action(name: str, state: ActionState = ActionState.AVAILABLE) -> Action:
"""Create a real Action domain object for test doubles."""
return Action(
namespaced_name=NamespacedName.parse(name),
description=f"Test action {name}",
definition_of_done="Done",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
state=state,
)
def _make_mock_plan(plan_id: str) -> Plan:
"""Create a real Plan domain object for test doubles."""
return Plan(
identity=PlanIdentity(plan_id=plan_id),
namespaced_name=NamespacedName.parse("local/test-plan"),
description="Test plan",
action_name="local/test",
phase=PlanPhase.STRATEGIZE,
processing_state=ProcessingState.QUEUED,
timestamps=PlanTimestamps(),
)
def _setup_mock_uow():
"""Create a mock UnitOfWork returning a mock context."""
uow = MagicMock()
ctx_mock = MagicMock()
uow.transaction.return_value.__enter__ = MagicMock(return_value=ctx_mock)
uow.transaction.return_value.__exit__ = MagicMock(return_value=False)
return uow, ctx_mock
# =================================================================
# Background
# =================================================================
@given("plscov3 a fresh service is ready")
def step_bg_fresh_service(context: Context) -> None:
settings = _make_settings()
context.plscov3_svc = PlanLifecycleService(settings=settings)
context.plscov3_error = None
context.plscov3_plan = None
# =================================================================
# Estimation actor failure (lines 318-323)
# =================================================================
@given('plscov3 an action with estimation actor exists named "{name}" actor "{actor}"')
def step_create_action_est(context: Context, name: str, actor: str) -> None:
_create_action_r3(context, name, estimation_actor=actor)
@given('plscov3 the action "{name}" is used on project "{project}"')
def step_use_action(context: Context, name: str, project: str) -> None:
plan = context.plscov3_svc.use_action(
name, project_links=[ProjectLink(project_name=project)]
)
context.plscov3_plan = plan
@given("plscov3 the current plan is advanced to Strategize COMPLETE")
def step_advance_strat_complete(context: Context) -> None:
_advance_to_strat_complete_r3(context)
@when("plscov3 execute_plan is invoked with estimation stub raising")
def step_execute_estimation_raises(context: Context) -> None:
pid = context.plscov3_plan.identity.plan_id
svc = context.plscov3_svc
# Patch the EstimationStubActor import inside _run_estimation
patcher = patch(
"cleveragents.application.services.plan_executor.EstimationStubActor"
)
mock_cls = patcher.start()
context.add_cleanup(patcher.stop)
mock_instance = MagicMock()
mock_instance.estimate.side_effect = RuntimeError("estimation boom")
mock_cls.return_value = mock_instance
# Ensure plan has estimation_actor set so _run_estimation runs
plan = svc.get_plan(pid)
plan.estimation_actor = "est-actor"
try:
svc.execute_plan(pid)
except Exception as exc:
context.plscov3_error = exc
@then("plscov3 the current plan should be in Execute phase")
def step_assert_execute_phase(context: Context) -> None:
pid = context.plscov3_plan.identity.plan_id
plan = context.plscov3_svc.get_plan(pid)
assert plan.phase == PlanPhase.EXECUTE, f"Expected Execute, got {plan.phase}"
# =================================================================
# _resolve_actor_registry_entry dunder prefix (line 452)
# =================================================================
@when('plscov3 resolve_actor_registry_entry is invoked for "{actor_name}"')
def step_resolve_dunder(context: Context, actor_name: str) -> None:
context.plscov3_resolved = context.plscov3_svc._resolve_actor_registry_entry(
actor_name
)
@then("plscov3 the resolved actor entry is None")
def step_assert_resolved_none(context: Context) -> None:
assert context.plscov3_resolved is None
# =================================================================
# list_actions persistence: namespace filter (lines 640-645)
# =================================================================
@given("plscov3 a service with mock UoW is ready")
def step_svc_mock_uow(context: Context) -> None:
settings = _make_settings()
uow, ctx_mock = _setup_mock_uow()
ctx_mock.actions.get_by_namespace.return_value = []
ctx_mock.actions.list_all.return_value = []
context.plscov3_svc = PlanLifecycleService(settings=settings, unit_of_work=uow)
context.plscov3_ctx_mock = ctx_mock
context.plscov3_error = None
@when('plscov3 list_actions is invoked with namespace "{ns}" and state "{state}"')
def step_list_actions_ns_state(context: Context, ns: str, state: str) -> None:
state_enum = ActionState(state)
context.plscov3_actions_result = context.plscov3_svc.list_actions(
namespace=ns, state=state_enum
)
@then('plscov3 the mock get_by_namespace was invoked with "{ns}"')
def step_assert_get_by_namespace(context: Context, ns: str) -> None:
context.plscov3_ctx_mock.actions.get_by_namespace.assert_called_once()
args = context.plscov3_ctx_mock.actions.get_by_namespace.call_args
assert args[0][0] == ns, f"Expected namespace '{ns}', got {args}"
# =================================================================
# list_actions persistence: no namespace (line 648)
# =================================================================
@when("plscov3 list_actions is invoked without any namespace")
def step_list_actions_no_ns(context: Context) -> None:
context.plscov3_actions_result = context.plscov3_svc.list_actions()
@then("plscov3 the mock actions list_all was invoked")
def step_assert_actions_list_all(context: Context) -> None:
context.plscov3_ctx_mock.actions.list_all.assert_called_once()
# =================================================================
# list_actions persistence: cache refresh (lines 650-653)
# =================================================================
@given("plscov3 a service with mock UoW returning DB actions is ready")
def step_svc_uow_db_actions(context: Context) -> None:
settings = _make_settings()
uow, ctx_mock = _setup_mock_uow()
db_action = _make_mock_action("local/db-action")
ctx_mock.actions.get_by_namespace.return_value = [db_action]
context.plscov3_svc = PlanLifecycleService(settings=settings, unit_of_work=uow)
context.plscov3_ctx_mock = ctx_mock
context.plscov3_db_action = db_action
context.plscov3_error = None
@when('plscov3 list_actions is invoked filtering by namespace "{ns}"')
def step_list_actions_filter_ns(context: Context, ns: str) -> None:
context.plscov3_actions_result = context.plscov3_svc.list_actions(namespace=ns)
@then("plscov3 the actions in-memory cache has the DB action")
def step_assert_cache_has_action(context: Context) -> None:
key = str(context.plscov3_db_action.namespaced_name)
assert key in context.plscov3_svc._actions, (
f"Expected '{key}' in cache, got {list(context.plscov3_svc._actions.keys())}"
)
# =================================================================
# list_actions persistence: state filter on list_all (lines 655-656)
# =================================================================
@given("plscov3 a service with mock UoW returning mixed-state actions is ready")
def step_svc_uow_mixed_actions(context: Context) -> None:
settings = _make_settings()
uow, ctx_mock = _setup_mock_uow()
avail = _make_mock_action("local/avail-action", ActionState.AVAILABLE)
archived = _make_mock_action("local/archived-action", ActionState.ARCHIVED)
ctx_mock.actions.list_all.return_value = [avail, archived]
context.plscov3_svc = PlanLifecycleService(settings=settings, unit_of_work=uow)
context.plscov3_ctx_mock = ctx_mock
context.plscov3_error = None
@when('plscov3 list_actions is invoked without namespace but with state "{state}"')
def step_list_actions_state_only(context: Context, state: str) -> None:
state_enum = ActionState(state)
context.plscov3_actions_result = context.plscov3_svc.list_actions(state=state_enum)
@then("plscov3 only actions in available state are returned")
def step_assert_only_available(context: Context) -> None:
result = context.plscov3_actions_result
assert len(result) == 1, f"Expected 1 action, got {len(result)}"
assert result[0].state == ActionState.AVAILABLE
# =================================================================
# list_actions persistence: DatabaseError fallback (lines 658-659)
# =================================================================
@given("plscov3 a service with failing DB actions is ready")
def step_svc_failing_db(context: Context) -> None:
settings = _make_settings()
uow, ctx_mock = _setup_mock_uow()
ctx_mock.actions.list_all.side_effect = DatabaseError("DB down")
ctx_mock.actions.get_by_namespace.side_effect = DatabaseError("DB down")
context.plscov3_svc = PlanLifecycleService(settings=settings, unit_of_work=uow)
context.plscov3_error = None
@given('plscov3 an in-memory action "{name}" is added')
def step_add_inmem_action(context: Context, name: str) -> None:
action = _make_mock_action(name)
context.plscov3_svc._actions[str(action.namespaced_name)] = action
context.plscov3_inmem_action = action
@when("plscov3 list_actions is invoked expecting DB fallback")
def step_list_actions_fallback(context: Context) -> None:
context.plscov3_actions_result = context.plscov3_svc.list_actions()
@then("plscov3 the fallback in-memory action is returned")
def step_assert_fallback_action(context: Context) -> None:
result = context.plscov3_actions_result
assert len(result) >= 1, f"Expected >= 1 actions, got {len(result)}"
names = [str(a.namespaced_name) for a in result]
expected = str(context.plscov3_inmem_action.namespaced_name)
assert expected in names, f"Expected '{expected}' in {names}"
# =================================================================
# list_plans persisted cache refresh (line 917)
# =================================================================
@given("plscov3 a service with mock UoW returning DB plans is ready")
def step_svc_uow_db_plans(context: Context) -> None:
settings = _make_settings()
uow, ctx_mock = _setup_mock_uow()
from ulid import ULID
plan_id = str(ULID())
db_plan = _make_mock_plan(plan_id)
ctx_mock.lifecycle_plans.list_all.return_value = [db_plan]
context.plscov3_svc = PlanLifecycleService(settings=settings, unit_of_work=uow)
context.plscov3_db_plan = db_plan
context.plscov3_error = None
@when("plscov3 list_plans is invoked")
def step_list_plans(context: Context) -> None:
context.plscov3_plans_result = context.plscov3_svc.list_plans()
@then("plscov3 the plans in-memory cache was refreshed")
def step_assert_plan_cache(context: Context) -> None:
pid = context.plscov3_db_plan.identity.plan_id
assert pid in context.plscov3_svc._plans, f"Expected '{pid}' in plan cache"
# =================================================================
# _complete_apply_if_queued - not in Apply phase (line 1592)
# =================================================================
@given('plscov3 a basic action named "{name}" is created')
def step_create_basic_action(context: Context, name: str) -> None:
_create_action_r3(context, name)
@when("plscov3 complete_apply_if_queued is invoked")
def step_call_caiq(context: Context) -> None:
pid = context.plscov3_plan.identity.plan_id
try:
result = context.plscov3_svc._complete_apply_if_queued(pid)
context.plscov3_plan = result
except Exception as exc:
context.plscov3_error = exc
@then("plscov3 the current plan phase remains Strategize")
def step_assert_phase_strategize(context: Context) -> None:
assert context.plscov3_plan.phase == PlanPhase.STRATEGIZE, (
f"Expected Strategize, got {context.plscov3_plan.phase}"
)
# =================================================================
# _complete_apply_if_queued - not QUEUED (line 1594)
# =================================================================
@given("plscov3 the current plan is advanced to Apply PROCESSING")
def step_advance_apply_processing(context: Context) -> None:
_advance_to_apply_processing_r3(context)
@then("plscov3 the current plan remains in Apply PROCESSING")
def step_assert_apply_processing(context: Context) -> None:
plan = context.plscov3_plan
assert plan.phase == PlanPhase.APPLY, f"Expected Apply, got {plan.phase}"
assert plan.processing_state == ProcessingState.PROCESSING, (
f"Expected PROCESSING, got {plan.processing_state}"
)
# =================================================================
# _complete_apply_if_queued - async enabled (lines 1599-1603)
# =================================================================
@given("plscov3 an async-enabled service with job store is ready")
def step_svc_async(context: Context) -> None:
settings = _make_settings()
settings.async_enabled = True
job_store = MagicMock()
context.plscov3_svc = PlanLifecycleService(settings=settings, job_store=job_store)
context.plscov3_error = None
context.plscov3_plan = None
@given("plscov3 the current plan is advanced to Apply QUEUED")
def step_advance_apply_queued(context: Context) -> None:
_advance_to_apply_queued_r3(context)
@then("plscov3 the current plan remains in Apply QUEUED")
def step_assert_apply_queued(context: Context) -> None:
plan = context.plscov3_plan
assert plan.phase == PlanPhase.APPLY, f"Expected Apply, got {plan.phase}"
assert plan.processing_state == ProcessingState.QUEUED, (
f"Expected QUEUED, got {plan.processing_state}"
)
# =================================================================
# _complete_apply_if_queued - exception path (lines 1608-1614)
# =================================================================
@when("plscov3 complete_apply_if_queued is invoked with start_apply raising")
def step_caiq_start_apply_raises(context: Context) -> None:
pid = context.plscov3_plan.identity.plan_id
svc = context.plscov3_svc
patcher = patch.object(svc, "start_apply", side_effect=RuntimeError("boom"))
patcher.start()
context.add_cleanup(patcher.stop)
patcher2 = patch.object(svc, "_cleanup_devcontainers")
patcher2.start()
context.add_cleanup(patcher2.stop)
try:
result = svc._complete_apply_if_queued(pid)
context.plscov3_plan = result
except Exception as exc:
context.plscov3_error = exc
@then("plscov3 the current plan processing state is ERRORED")
def step_assert_errored(context: Context) -> None:
plan = context.plscov3_plan
assert plan.processing_state == ProcessingState.ERRORED, (
f"Expected ERRORED, got {plan.processing_state}"
)
# =================================================================
# try_auto_run - terminal plan (lines 1687-1690)
# =================================================================
@given("plscov3 the current plan is forced to CANCELLED state")
def step_force_cancelled(context: Context) -> None:
context.plscov3_plan.processing_state = ProcessingState.CANCELLED
@when("plscov3 try_auto_run is invoked")
def step_call_try_auto_run(context: Context) -> None:
pid = context.plscov3_plan.identity.plan_id
svc = context.plscov3_svc
patcher = patch.object(svc, "_cleanup_devcontainers")
patcher.start()
context.add_cleanup(patcher.stop)
try:
result = svc.try_auto_run(pid)
context.plscov3_plan = result
except Exception as exc:
context.plscov3_error = exc
@then("plscov3 the current plan is still CANCELLED")
def step_assert_still_cancelled(context: Context) -> None:
plan = context.plscov3_plan
assert plan.processing_state == ProcessingState.CANCELLED, (
f"Expected CANCELLED, got {plan.processing_state}"
)
# =================================================================
# try_auto_run - full auto with CI profile (lines 1697-1741)
# =================================================================
@given('plscov3 an action with ci automation profile named "{name}" is created')
def step_create_ci_action(context: Context, name: str) -> None:
_create_action_r3(context, name, automation_profile="ci")
@given(
'plscov3 that profiled action "{name}" is used on project "{project}" with profile "{profile}"'
)
def step_use_profiled_action(
context: Context, name: str, project: str, profile: str
) -> None:
plan = context.plscov3_svc.use_action(
name, project_links=[ProjectLink(project_name=project)]
)
plan.automation_profile = AutomationProfileRef(
profile_name=profile,
provenance=AutomationProfileProvenance.ACTION,
)
context.plscov3_plan = plan
@then("plscov3 the current plan has reached terminal APPLIED")
def step_assert_terminal_applied(context: Context) -> None:
assert context.plscov3_error is None, f"Unexpected error: {context.plscov3_error}"
plan = context.plscov3_plan
assert plan.processing_state == ProcessingState.APPLIED, (
f"Expected APPLIED, got {plan.processing_state}"
)
assert plan.phase == PlanPhase.APPLY, f"Expected Apply phase, got {plan.phase}"
# =================================================================
# try_auto_run - strategize only with supervised profile (1697-1709)
# =================================================================
@given('plscov3 an action with supervised automation profile named "{name}" is created')
def step_create_supervised_action(context: Context, name: str) -> None:
_create_action_r3(context, name, automation_profile="supervised")
@then("plscov3 the current plan is in Strategize COMPLETE")
def step_assert_strat_complete(context: Context) -> None:
plan = context.plscov3_plan
assert plan.phase == PlanPhase.STRATEGIZE, f"Expected Strategize, got {plan.phase}"
assert plan.processing_state == ProcessingState.COMPLETE, (
f"Expected COMPLETE, got {plan.processing_state}"
)