forked from HAL9000/cleveragents-core
051ee7c290
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
737 lines
27 KiB
Python
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}"
|
|
)
|