Files
temp/features/steps/plan_lifecycle_service_coverage_boost_r2_steps.py
freemo 051ee7c290 test(coverage): add Behave BDD tests to improve coverage across 52 source files
Added 52 new .feature files and corresponding _steps.py files targeting
previously uncovered code paths in the following areas:

- TUI layer: app, commands, persona (state/schema/registry), widgets,
  input (shell_exec, reference_parser)
- Application services: plan lifecycle/service/executor, session,
  project, repo indexing, correction, checkpoint, actor, llm_actors,
  strategy coordinator, resource file watcher, service retry wiring
- CLI commands: session, resource, repl, plan, db, automation_profile
- Domain models: retry_policy, resource_type, cost_budget,
  docker_compose_analyzer, detail_level, _sql_string_aware,
  _postgresql_helpers
- Core: circuit_breaker, retry_service_patterns
- Infrastructure: repositories, transaction_sandbox, strategy_registry,
  plugins/loader, container
- Config: settings
- Agents: plan_generation, context_analysis, auto_debug
- A2A: facade

All new tests follow the Behave/Gherkin BDD standard. Resolved step
definition collisions with unique prefixes. Fixed Alembic fileConfig
logger disabling issue (disable_existing_loggers=False).

ISSUES CLOSED: #1068
2026-03-20 21:22:10 +00:00

737 lines
27 KiB
Python

"""Step definitions for plan_lifecycle_service_coverage_boost_r2.feature.
Targets uncovered lines in PlanLifecycleService (99 lines remaining):
- Lines 263-276: _consult_error_patterns guidance injection path
- Lines 277-282: _consult_error_patterns exception handler
- Line 339: save_plan public convenience method
- Lines 416-422: _resolve_actor_registry_entry exception handler
- Lines 778-784: use_action event_bus.emit exception handler
- Lines 847-850: list_plans DatabaseError fallback
- Lines 909-913: start_strategize action loading from persistence
- Lines 1070-1080: execute_plan event_bus.emit exception handler
- Lines 1256-1282: complete_apply event_bus emit (success + exception)
- Lines 1366-1388: cancel_plan event_bus emit (success + exception)
- Lines 1417-1426: _cleanup_devcontainers (success + exception)
"""
from __future__ import annotations
import tempfile
from contextlib import contextmanager
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.error_pattern import (
ErrorPattern,
PreventiveGuidance,
)
from cleveragents.domain.models.core.plan import (
PlanPhase,
ProcessingState,
ProjectLink,
)
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
from cleveragents.infrastructure.events.types import EventType
# -----------------------------------------------------------------
# Background
# -----------------------------------------------------------------
@given("I have a fresh plan lifecycle service for coverage boost r2")
def step_create_fresh_service_r2(context: Context) -> None:
"""Create a clean PlanLifecycleService instance."""
Settings._instance = None
settings = Settings()
context.service = PlanLifecycleService(settings=settings)
context.error = None
# -----------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------
def _create_action_br2(context: Context, name: str, **kwargs):
"""Helper to create a basic action with sensible defaults."""
defaults = {
"name": name,
"description": f"Action {name}",
"definition_of_done": "Tests pass",
"strategy_actor": "openai/gpt-4",
"execution_actor": "openai/gpt-4",
}
defaults.update(kwargs)
return context.service.create_action(**defaults)
def _advance_plan_to_strategize_complete(context: Context):
"""Advance the current plan to Strategize/COMPLETE."""
pid = context.plan.identity.plan_id
context.service.start_strategize(pid)
# Manually set to COMPLETE to avoid auto-progress side effects
plan = context.service.get_plan(pid)
plan.processing_state = ProcessingState.COMPLETE
context.plan = plan
def _advance_plan_to_execute_complete(context: Context):
"""Advance plan from Strategize/COMPLETE through Execute to COMPLETE."""
pid = context.plan.identity.plan_id
# First transition to execute
context.service.execute_plan(pid)
context.service.start_execute(pid)
# Manually set to COMPLETE to avoid auto-progress side effects
plan = context.service.get_plan(pid)
plan.processing_state = ProcessingState.COMPLETE
context.plan = plan
def _advance_plan_to_apply_processing(context: Context):
"""Advance plan all the way to Apply/PROCESSING."""
_advance_plan_to_strategize_complete(context)
_advance_plan_to_execute_complete(context)
pid = context.plan.identity.plan_id
context.service.apply_plan(pid)
context.service.start_apply(pid)
context.plan = context.service.get_plan(pid)
class RecordingEventBus:
"""A simple event bus that records emitted events."""
def __init__(self):
self.events = []
def emit(self, event):
self.events.append(event)
def subscribe(self, event_type, handler):
pass
class FailingEventBus:
"""An event bus that always raises on emit."""
def emit(self, event):
raise RuntimeError("Simulated event bus failure")
def subscribe(self, event_type, handler):
pass
# =================================================================
# Lines 263-276: _consult_error_patterns — guidance injection
# =================================================================
@given("a plan lifecycle service with a configured error pattern service for r2")
def step_service_with_error_pattern_service(context: Context) -> None:
"""Create a service with an ErrorPatternService that returns guidance."""
Settings._instance = None
settings = Settings()
mock_eps = MagicMock()
mock_eps.match_patterns.return_value = PreventiveGuidance(
matched_patterns=(
ErrorPattern(
pattern="test pattern",
historical_failures=("failure-1",),
preventive_checks=("Check imports", "Validate config"),
keywords=("test",),
),
),
preventive_checks=("Check imports", "Validate config"),
)
context.service = PlanLifecycleService(
settings=settings,
error_pattern_service=mock_eps,
)
context.error = None
@given('an action "{name}" exists for coverage boost r2')
def step_create_action_br2(context: Context, name: str) -> None:
"""Create an action with the given name."""
context.action = _create_action_br2(context, name)
@given("a plan advanced to strategize complete for coverage boost r2")
def step_plan_to_strategize_complete_br2(context: Context) -> None:
"""Create a plan and advance it to Strategize/COMPLETE."""
context.plan = context.service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name="proj-br2")],
)
_advance_plan_to_strategize_complete(context)
@when("I execute the plan so error patterns are consulted")
def step_execute_with_error_patterns(context: Context) -> None:
"""Execute the plan — _consult_error_patterns should inject guidance."""
context.error = None
try:
context.plan = context.service.execute_plan(context.plan.identity.plan_id)
except Exception as e:
context.error = e
@then("the plan error_details should contain preventive_guidance")
def step_verify_preventive_guidance(context: Context) -> None:
"""Verify that preventive guidance was injected into error_details."""
assert context.error is None, f"Unexpected error: {context.error}"
details = context.plan.error_details
assert details is not None, "Expected error_details to be set"
assert "preventive_guidance" in details, (
f"Expected 'preventive_guidance' in error_details, got keys: {list(details.keys())}"
)
assert "PREVENTIVE GUIDANCE" in details["preventive_guidance"], (
f"Expected formatted guidance text, got: {details['preventive_guidance']}"
)
# =================================================================
# Lines 277-282: _consult_error_patterns — exception handler
# =================================================================
@given("a plan lifecycle service with a failing error pattern service for r2")
def step_service_with_failing_eps(context: Context) -> None:
"""Create a service with an ErrorPatternService that raises."""
Settings._instance = None
settings = Settings()
mock_eps = MagicMock()
mock_eps.match_patterns.side_effect = RuntimeError("Simulated pattern DB failure")
context.service = PlanLifecycleService(
settings=settings,
error_pattern_service=mock_eps,
)
context.error = None
@when("I execute the plan and error pattern consultation fails")
def step_execute_with_failing_eps(context: Context) -> None:
"""Execute the plan — _consult_error_patterns should catch the exception."""
context.error = None
try:
context.plan = context.service.execute_plan(context.plan.identity.plan_id)
except Exception as e:
context.error = e
@then("the plan should still transition to execute phase without error")
def step_verify_execute_despite_eps_failure(context: Context) -> None:
"""Verify the plan transitioned to Execute despite EPS failure."""
assert context.error is None, f"Expected no error, got: {context.error}"
assert context.plan.phase == PlanPhase.EXECUTE, (
f"Expected EXECUTE phase, got {context.plan.phase}"
)
# =================================================================
# Line 339: save_plan — public convenience method
# =================================================================
@given('a plan created from "{action_name}" for coverage boost r2')
def step_create_plan_br2(context: Context, action_name: str) -> None:
"""Create a plan from the named action."""
context.plan = context.service.use_action(
action_name=action_name,
project_links=[ProjectLink(project_name="proj-br2")],
)
@when("I call save_plan on the plan")
def step_call_save_plan(context: Context) -> None:
"""Call save_plan — should be a no-op in in-memory mode."""
context.error = None
try:
context.service.save_plan(context.plan)
except Exception as e:
context.error = e
@then("save_plan completes without error")
def step_verify_save_plan_no_error(context: Context) -> None:
"""Verify save_plan did not raise."""
assert context.error is None, (
f"Expected no error from save_plan, got: {context.error}"
)
# =================================================================
# Lines 416-422: _resolve_actor_registry_entry exception handler
# =================================================================
@given("a plan lifecycle service with a mock unit of work that fails actor lookup")
def step_service_with_failing_actor_uow(context: Context) -> None:
"""Create a service with a mock UoW where actors.get_by_name raises."""
Settings._instance = None
settings = Settings()
mock_uow = MagicMock()
mock_ctx = MagicMock()
mock_ctx.actors.get_by_name.side_effect = RuntimeError("DB connection failed")
@contextmanager
def _fake_transaction():
yield mock_ctx
mock_uow.transaction = _fake_transaction
context.service = PlanLifecycleService(
settings=settings,
unit_of_work=mock_uow,
)
context.error = None
@when('I resolve an actor registry entry for "{actor_name}"')
def step_resolve_actor_entry(context: Context, actor_name: str) -> None:
"""Call _resolve_actor_registry_entry directly."""
context.resolved_entry = context.service._resolve_actor_registry_entry(actor_name)
@then("the resolved actor entry should be None")
def step_verify_resolved_none(context: Context) -> None:
"""Verify the resolved entry is None (exception was caught)."""
assert context.resolved_entry is None, (
f"Expected None, got {context.resolved_entry}"
)
# =================================================================
# Lines 778-784: use_action — event_bus.emit exception
# =================================================================
@given("a plan lifecycle service with a failing event bus for r2")
def step_service_with_failing_event_bus(context: Context) -> None:
"""Create a service with an event bus that raises on emit."""
Settings._instance = None
settings = Settings()
context.service = PlanLifecycleService(
settings=settings,
event_bus=FailingEventBus(),
)
context.error = None
@when('I use action "{action_name}" and event bus emit fails')
def step_use_action_with_failing_bus(context: Context, action_name: str) -> None:
"""Use an action — the event bus will raise but the plan should still be created."""
context.error = None
try:
context.plan = context.service.use_action(
action_name=action_name,
project_links=[ProjectLink(project_name="proj-event-fail")],
)
except Exception as e:
context.error = e
@then("the plan should still be created in strategize phase")
def step_verify_plan_created_despite_bus_fail(context: Context) -> None:
"""Verify the plan was created even though event bus failed."""
assert context.error is None, f"Expected no error, got: {context.error}"
assert context.plan is not None
assert context.plan.phase == PlanPhase.STRATEGIZE, (
f"Expected STRATEGIZE, got {context.plan.phase}"
)
# =================================================================
# Lines 847-850: list_plans — DatabaseError fallback
# =================================================================
@given("a plan lifecycle service with a mock unit of work that fails list_all")
def step_service_with_failing_list_all(context: Context) -> None:
"""Create a service with a mock UoW where lifecycle_plans.list_all raises DatabaseError."""
Settings._instance = None
settings = Settings()
mock_uow = MagicMock()
mock_ctx = MagicMock()
mock_ctx.lifecycle_plans.list_all.side_effect = DatabaseError("DB read failed")
@contextmanager
def _fake_transaction():
yield mock_ctx
mock_uow.transaction = _fake_transaction
context.service = PlanLifecycleService(
settings=settings,
unit_of_work=mock_uow,
)
# Also need to mock create so create_action doesn't fail
mock_ctx.actions.create.return_value = None
context.error = None
@given("an in-memory plan exists for the fallback test")
def step_add_inmemory_plan(context: Context) -> None:
"""Create an action and plan in-memory so list_plans returns something."""
# We create the action directly in-memory (no persistence needed)
action = _create_action_br2(context, "local/fallback-test")
context.plan = context.service.use_action(
action_name=str(action.namespaced_name),
project_links=[ProjectLink(project_name="proj-fallback")],
)
@when("I call list_plans on the service with the broken persistence")
def step_call_list_plans_with_broken_persistence(context: Context) -> None:
"""Call list_plans — should fall back to in-memory cache."""
context.error = None
try:
context.plan_list = context.service.list_plans()
except Exception as e:
context.error = e
@then("the result should contain the in-memory plan")
def step_verify_fallback_result(context: Context) -> None:
"""Verify the in-memory plan is in the result list."""
assert context.error is None, f"Expected no error, got: {context.error}"
assert len(context.plan_list) >= 1, (
f"Expected at least 1 plan, got {len(context.plan_list)}"
)
plan_ids = [p.identity.plan_id for p in context.plan_list]
assert context.plan.identity.plan_id in plan_ids, (
f"Expected plan {context.plan.identity.plan_id} in results {plan_ids}"
)
# =================================================================
# Lines 909-913: start_strategize — action loading from persistence
# =================================================================
@given("a plan lifecycle service with a persisted action for cache miss test")
def step_service_with_persisted_action_cache_miss(context: Context) -> None:
"""Create a service backed by a real DB with an action + plan, then clear cache."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as db_file:
db_path = db_file.name
db_url = f"sqlite:///{db_path}"
uow = UnitOfWork(database_url=db_url)
uow.init_database()
Settings._instance = None
settings = Settings()
context.service = PlanLifecycleService(settings=settings, unit_of_work=uow)
context.uow = uow
# Create action and plan (both persisted to DB)
context.action = _create_action_br2(context, "local/cache-miss-test")
context.plan = context.service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name="proj-cache-miss")],
)
context.error = None
@given("a plan whose action is removed from the in-memory cache")
def step_clear_actions_cache(context: Context) -> None:
"""Remove the action from the in-memory _actions dict to force DB lookup."""
context.service._actions.clear()
@when("I start strategize on the plan with the cleared cache")
def step_start_strategize_cache_miss(context: Context) -> None:
"""Start strategize — should load action from persistence layer."""
context.error = None
try:
pid = context.plan.identity.plan_id
context.plan = context.service.start_strategize(pid)
except Exception as e:
context.error = e
@then("start_strategize should complete successfully")
def step_verify_strategize_started(context: Context) -> None:
"""Verify start_strategize completed and the action was loaded from DB."""
assert context.error is None, f"Expected no error, got: {context.error}"
assert context.plan.processing_state == ProcessingState.PROCESSING, (
f"Expected PROCESSING, got {context.plan.processing_state}"
)
# Verify the action was loaded back into the cache
assert len(context.service._actions) >= 1, (
"Expected action to be loaded back into _actions cache"
)
# =================================================================
# Lines 1070-1080: execute_plan — event_bus.emit exception
# =================================================================
@when("I execute the plan and event bus emit fails during phase change")
def step_execute_plan_with_failing_bus(context: Context) -> None:
"""Execute the plan — event bus will raise but plan should still transition."""
context.error = None
try:
context.plan = context.service.execute_plan(context.plan.identity.plan_id)
except Exception as e:
context.error = e
@then("the plan should still transition to execute phase")
def step_verify_execute_despite_bus_fail(context: Context) -> None:
"""Verify the plan transitioned to Execute despite event bus failure."""
assert context.error is None, f"Expected no error, got: {context.error}"
assert context.plan.phase == PlanPhase.EXECUTE, (
f"Expected EXECUTE, got {context.plan.phase}"
)
# =================================================================
# Lines 1256-1276: complete_apply — event_bus emit success
# =================================================================
@given("a plan lifecycle service with a recording event bus for r2")
def step_service_with_recording_event_bus(context: Context) -> None:
"""Create a service with a recording event bus."""
Settings._instance = None
settings = Settings()
context.recording_bus = RecordingEventBus()
context.service = PlanLifecycleService(
settings=settings,
event_bus=context.recording_bus,
)
context.error = None
@given("a plan advanced to apply processing for coverage boost r2")
def step_plan_to_apply_processing_br2(context: Context) -> None:
"""Create a plan and advance it to Apply/PROCESSING."""
context.plan = context.service.use_action(
action_name=str(context.action.namespaced_name),
project_links=[ProjectLink(project_name="proj-br2")],
)
_advance_plan_to_apply_processing(context)
@when("I complete apply on the plan with event bus")
def step_complete_apply_with_event_bus(context: Context) -> None:
"""Complete apply — should emit PLAN_APPLIED event."""
context.error = None
try:
with patch(
"cleveragents.application.services.plan_lifecycle_service.CleanupService",
create=True,
) as mock_cleanup_cls:
mock_cleanup_cls.stop_active_devcontainers.return_value = []
# Patch the import inside _cleanup_devcontainers
with patch(
"cleveragents.application.services.cleanup_service.stop_all_active_containers",
return_value=[],
):
context.plan = context.service.complete_apply(
context.plan.identity.plan_id
)
except Exception as e:
context.error = e
@then("the event bus should have recorded a PLAN_APPLIED event")
def step_verify_plan_applied_event(context: Context) -> None:
"""Verify PLAN_APPLIED was emitted."""
assert context.error is None, f"Expected no error, got: {context.error}"
applied_events = [
e
for e in context.recording_bus.events
if e.event_type == EventType.PLAN_APPLIED
]
assert len(applied_events) >= 1, (
f"Expected at least 1 PLAN_APPLIED event, got {len(applied_events)}. "
f"All events: {[e.event_type for e in context.recording_bus.events]}"
)
# =================================================================
# Lines 1277-1282: complete_apply — event_bus emit exception
# =================================================================
@when("I complete apply on the plan and event bus emit fails")
def step_complete_apply_with_failing_bus(context: Context) -> None:
"""Complete apply — event bus will raise but plan should still be applied."""
context.error = None
try:
with patch(
"cleveragents.application.services.cleanup_service.stop_all_active_containers",
return_value=[],
):
context.plan = context.service.complete_apply(context.plan.identity.plan_id)
except Exception as e:
context.error = e
@then("the plan should still be in applied state")
def step_verify_applied_despite_bus_fail(context: Context) -> None:
"""Verify the plan is in APPLIED state despite event bus failure."""
assert context.error is None, f"Expected no error, got: {context.error}"
assert context.plan.processing_state == ProcessingState.APPLIED, (
f"Expected APPLIED, got {context.plan.processing_state}"
)
# =================================================================
# Lines 1366-1378: cancel_plan — event_bus emit success
# =================================================================
@when("I cancel the plan with event bus recording")
def step_cancel_plan_with_recording_bus(context: Context) -> None:
"""Cancel the plan — should emit PLAN_CANCELLED event."""
context.error = None
try:
with patch(
"cleveragents.application.services.cleanup_service.stop_all_active_containers",
return_value=[],
):
context.plan = context.service.cancel_plan(
context.plan.identity.plan_id,
reason="test cancellation",
)
except Exception as e:
context.error = e
@then("the event bus should have recorded a PLAN_CANCELLED event")
def step_verify_plan_cancelled_event(context: Context) -> None:
"""Verify PLAN_CANCELLED was emitted."""
assert context.error is None, f"Expected no error, got: {context.error}"
cancelled_events = [
e
for e in context.recording_bus.events
if e.event_type == EventType.PLAN_CANCELLED
]
assert len(cancelled_events) >= 1, (
f"Expected at least 1 PLAN_CANCELLED event, got {len(cancelled_events)}. "
f"All events: {[e.event_type for e in context.recording_bus.events]}"
)
# =================================================================
# Lines 1382-1388: cancel_plan — event_bus emit exception
# =================================================================
@when("I cancel the plan and event bus emit fails")
def step_cancel_plan_with_failing_bus(context: Context) -> None:
"""Cancel the plan — event bus will raise but plan should still be cancelled."""
context.error = None
try:
with patch(
"cleveragents.application.services.cleanup_service.stop_all_active_containers",
return_value=[],
):
context.plan = context.service.cancel_plan(
context.plan.identity.plan_id,
reason="test cancellation failure",
)
except Exception as e:
context.error = e
@then("the plan should still be in cancelled state")
def step_verify_cancelled_despite_bus_fail(context: Context) -> None:
"""Verify the plan is in CANCELLED state despite event bus failure."""
assert context.error is None, f"Expected no error, got: {context.error}"
assert context.plan.processing_state == ProcessingState.CANCELLED, (
f"Expected CANCELLED, got {context.plan.processing_state}"
)
# =================================================================
# Lines 1417-1421: _cleanup_devcontainers — success path
# =================================================================
@given("a plan lifecycle service with mocked devcontainer cleanup for r2")
def step_service_with_mocked_cleanup(context: Context) -> None:
"""Create a service where devcontainer cleanup returns stopped containers."""
Settings._instance = None
settings = Settings()
context.service = PlanLifecycleService(settings=settings)
context.cleanup_called = False
context.error = None
@when("I complete apply and devcontainer cleanup returns stopped containers")
def step_complete_apply_with_cleanup_success(context: Context) -> None:
"""Complete apply with mocked cleanup that returns container IDs."""
context.error = None
try:
with patch(
"cleveragents.application.services.cleanup_service.stop_all_active_containers",
return_value=["container-1", "container-2"],
):
context.plan = context.service.complete_apply(context.plan.identity.plan_id)
context.cleanup_called = True
except Exception as e:
context.error = e
@then("the plan should be applied and cleanup should have been called")
def step_verify_applied_with_cleanup(context: Context) -> None:
"""Verify the plan is APPLIED and cleanup was invoked."""
assert context.error is None, f"Expected no error, got: {context.error}"
assert context.plan.processing_state == ProcessingState.APPLIED, (
f"Expected APPLIED, got {context.plan.processing_state}"
)
assert context.cleanup_called, "Expected cleanup to have been called"
# =================================================================
# Lines 1422-1426: _cleanup_devcontainers — exception path
# =================================================================
@given("a plan lifecycle service with failing devcontainer cleanup for r2")
def step_service_with_failing_cleanup(context: Context) -> None:
"""Create a service where devcontainer cleanup will raise."""
Settings._instance = None
settings = Settings()
context.service = PlanLifecycleService(settings=settings)
context.error = None
@when("I complete apply and devcontainer cleanup raises an exception")
def step_complete_apply_with_cleanup_failure(context: Context) -> None:
"""Complete apply with mocked cleanup that raises."""
context.error = None
try:
with patch(
"cleveragents.application.services.cleanup_service.stop_all_active_containers",
side_effect=RuntimeError("Docker daemon not available"),
):
context.plan = context.service.complete_apply(context.plan.identity.plan_id)
except Exception as e:
context.error = e
@then("the plan should still be applied despite cleanup failure")
def step_verify_applied_despite_cleanup_failure(context: Context) -> None:
"""Verify the plan is APPLIED even though cleanup failed."""
assert context.error is None, f"Expected no error, got: {context.error}"
assert context.plan.processing_state == ProcessingState.APPLIED, (
f"Expected APPLIED, got {context.plan.processing_state}"
)