"""Step definitions for plan_executor.py coverage tests. Targets uncovered lines: 144, 233-235, 273, 483, 490. """ from __future__ import annotations from typing import Any from unittest.mock import MagicMock from behave import given, then, when from behave.runner import Context from cleveragents.application.services.plan_executor import ( ExecuteResult, ExecuteStubActor, PlanExecutor, StrategizeResult, StrategizeStubActor, StrategyDecision, ) from cleveragents.core.exceptions import PlanError, ValidationError from cleveragents.domain.models.core.plan import ( InvariantSource, PlanInvariant, PlanPhase, PlanTimestamps, ProcessingState, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- PLAN_ID = "01HABCDE12345678901234PLAN" def _make_mock_plan( *, phase: PlanPhase = PlanPhase.STRATEGIZE, state: ProcessingState = ProcessingState.QUEUED, definition_of_done: str | None = "Step one\nStep two", decision_root_id: str | None = "01HROOT000000000000000ROOT", invariants: list[PlanInvariant] | None = None, ) -> MagicMock: """Build a mock plan object with sensible defaults.""" plan = MagicMock() plan.phase = phase plan.state = state plan.processing_state = state plan.definition_of_done = definition_of_done plan.decision_root_id = decision_root_id plan.invariants = invariants or [] plan.timestamps = PlanTimestamps() plan.changeset_id = None plan.sandbox_refs = [] plan.error_details = None return plan def _make_lifecycle(plan: Any | None = None) -> MagicMock: """Build a mock lifecycle service returning *plan* on get_plan.""" lcs = MagicMock() if plan is not None: lcs.get_plan.return_value = plan lcs.start_strategize = MagicMock() lcs.complete_strategize = MagicMock() lcs.fail_strategize = MagicMock() lcs.start_execute = MagicMock() lcs.complete_execute = MagicMock() lcs.fail_execute = MagicMock() lcs._commit_plan = MagicMock() return lcs # --------------------------------------------------------------------------- # StrategizeStubActor._parse_steps (line 144) # --------------------------------------------------------------------------- @given("a StrategizeStubActor instance for coverage") def step_given_strategize_stub(context: Context) -> None: context.strategize_actor = StrategizeStubActor() @when("I call _parse_steps with an empty string") def step_parse_steps_empty(context: Context) -> None: context.parsed_steps = context.strategize_actor._parse_steps("") @when("I call _parse_steps with whitespace only") def step_parse_steps_whitespace(context: Context) -> None: context.parsed_steps = context.strategize_actor._parse_steps(" \n\t ") @then("the parsed steps should be the default fallback") def step_check_default_steps(context: Context) -> None: assert context.parsed_steps == ["Complete the plan objectives"], ( f"Expected default fallback, got {context.parsed_steps}" ) # --------------------------------------------------------------------------- # StrategizeStubActor.execute happy paths # --------------------------------------------------------------------------- @when("I execute the strategize stub with a valid plan and definition of done") def step_exec_strategize_valid(context: Context) -> None: context.strategize_result = context.strategize_actor.execute( plan_id=PLAN_ID, definition_of_done="First step\nSecond step", ) @then("the strategize result should contain decisions matching the steps") def step_check_strategize_decisions(context: Context) -> None: result: StrategizeResult = context.strategize_result assert len(result.decisions) == 2 assert result.decisions[0].step_text == "First step" assert result.decisions[1].step_text == "Second step" assert result.decision_root_id == result.decisions[0].decision_id @when("I execute the strategize stub with a None definition of done") def step_exec_strategize_none_dod(context: Context) -> None: context.strategize_result = context.strategize_actor.execute( plan_id=PLAN_ID, definition_of_done=None, ) @then("the strategize result should contain the default decision") def step_check_default_decision(context: Context) -> None: result: StrategizeResult = context.strategize_result assert len(result.decisions) == 1 assert result.decisions[0].step_text == "Complete the plan objectives" @when("I execute the strategize stub with an empty plan_id") def step_exec_strategize_empty_plan_id(context: Context) -> None: try: context.strategize_actor.execute(plan_id="", definition_of_done="x") context.raised_exception = None except Exception as exc: context.raised_exception = exc @then('a plan executor ValidationError should be raised containing "{text}"') def step_check_validation_error(context: Context, text: str) -> None: assert context.raised_exception is not None, ( "Expected an exception but none was raised" ) assert isinstance(context.raised_exception, ValidationError), ( f"Expected ValidationError, got {type(context.raised_exception).__name__}" ) assert text in str(context.raised_exception), ( f"Expected '{text}' in '{context.raised_exception}'" ) @when("I execute the strategize stub with invariants") def step_exec_strategize_with_invariants(context: Context) -> None: invariants = [ PlanInvariant(text="Must be safe", source=InvariantSource.PLAN), PlanInvariant(text="Must be fast", source=InvariantSource.PROJECT), ] context.strategize_result = context.strategize_actor.execute( plan_id=PLAN_ID, definition_of_done="Do it", invariants=invariants, ) @then("the strategize result should contain invariant records") def step_check_invariant_records(context: Context) -> None: result: StrategizeResult = context.strategize_result assert len(result.invariant_records) == 2 assert result.invariant_records[0]["text"] == "Must be safe" assert result.invariant_records[1]["source"] == "project" @when("I execute the strategize stub with a stream callback") def step_exec_strategize_with_callback(context: Context) -> None: context.stream_events = [] def _cb(event: str, data: dict) -> None: context.stream_events.append((event, data)) context.strategize_result = context.strategize_actor.execute( plan_id=PLAN_ID, definition_of_done="A step", stream_callback=_cb, ) @then("the stream callback should have been called with strategize events") def step_check_stream_events(context: Context) -> None: event_names = [e[0] for e in context.stream_events] assert "strategize_started" in event_names assert "strategize_decisions" in event_names assert "strategize_complete" in event_names # --------------------------------------------------------------------------- # ExecuteStubActor # --------------------------------------------------------------------------- @given("an ExecuteStubActor instance for coverage") def step_given_execute_stub(context: Context) -> None: context.execute_actor = ExecuteStubActor() @when("I execute the execute stub with decisions") def step_exec_execute_stub(context: Context) -> None: decisions = [ StrategyDecision( decision_id="01HDEC0000000000000000DEC0", step_text="Do the thing", sequence=0, ), ] context.execute_result = context.execute_actor.execute( plan_id=PLAN_ID, decisions=decisions, ) @then("the execute result should contain a changeset id") def step_check_execute_changeset(context: Context) -> None: result: ExecuteResult = context.execute_result assert result.changeset_id is not None assert len(result.changeset_id) > 0 @when("I execute the execute stub with an empty plan_id") def step_exec_execute_stub_empty_plan_id(context: Context) -> None: try: context.execute_actor.execute(plan_id="", decisions=[]) context.raised_exception = None except Exception as exc: context.raised_exception = exc # --------------------------------------------------------------------------- # PlanExecutor construction # --------------------------------------------------------------------------- @when("I construct a PlanExecutor with None lifecycle service") def step_construct_none_lifecycle(context: Context) -> None: try: PlanExecutor(lifecycle_service=None) context.raised_exception = None except Exception as exc: context.raised_exception = exc @given("a fresh mock lifecycle for plan executor coverage") def step_given_fresh_mock_lifecycle(context: Context) -> None: context.lifecycle = _make_lifecycle() @when("I construct a PlanExecutor without execution context") def step_construct_no_ctx(context: Context) -> None: context.executor = PlanExecutor( lifecycle_service=context.lifecycle, execution_context=None, ) @then("plan executor has_runtime should be False") def step_check_no_runtime(context: Context) -> None: assert context.executor.has_runtime is False @then("plan executor changeset_store should be None") def step_check_no_changeset_store(context: Context) -> None: assert context.executor.changeset_store is None @then("plan executor execution_context should be None") def step_check_no_execution_context(context: Context) -> None: assert context.executor.execution_context is None # --------------------------------------------------------------------------- # PlanExecutor.run_strategize happy path # --------------------------------------------------------------------------- @given("a mock lifecycle with a plan in Strategize phase for executor coverage") def step_given_lifecycle_strategize(context: Context) -> None: plan = _make_mock_plan( phase=PlanPhase.STRATEGIZE, state=ProcessingState.QUEUED, definition_of_done="Build the widget", ) context.lifecycle = _make_lifecycle(plan) context.plan_id = PLAN_ID @given("a PlanExecutor using that lifecycle for coverage") def step_given_executor(context: Context) -> None: context.executor = PlanExecutor(lifecycle_service=context.lifecycle) @when("I call run_strategize on the PlanExecutor for coverage") def step_run_strategize(context: Context) -> None: context.strategize_result = context.executor.run_strategize(context.plan_id) @then("the strategize result should be returned successfully") def step_check_strategize_ok(context: Context) -> None: assert isinstance(context.strategize_result, StrategizeResult) assert len(context.strategize_result.decisions) > 0 @then("the lifecycle should have called start_strategize for coverage") def step_check_start_strategize(context: Context) -> None: context.lifecycle.start_strategize.assert_called_once_with(context.plan_id) @then("the lifecycle should have called complete_strategize for coverage") def step_check_complete_strategize(context: Context) -> None: context.lifecycle.complete_strategize.assert_called_once_with(context.plan_id) # --------------------------------------------------------------------------- # PlanExecutor.run_strategize exception path (line 273) # --------------------------------------------------------------------------- @given("a mock lifecycle with a plan in Strategize phase that will fail during execute") def step_given_lifecycle_strategize_fail(context: Context) -> None: plan = _make_mock_plan( phase=PlanPhase.STRATEGIZE, state=ProcessingState.QUEUED, definition_of_done="Build the widget", ) context.lifecycle = _make_lifecycle(plan) context.plan_id = PLAN_ID @given("a PlanExecutor using that failing lifecycle for coverage") def step_given_executor_failing(context: Context) -> None: context.executor = PlanExecutor(lifecycle_service=context.lifecycle) # Patch the internal strategize actor to raise context.executor._strategize_actor = MagicMock() context.executor._strategize_actor.execute.side_effect = RuntimeError( "Boom in strategize" ) @when("I call run_strategize expecting an exception for coverage") def step_run_strategize_fail(context: Context) -> None: try: context.executor.run_strategize(context.plan_id) context.raised_exception = None except RuntimeError as exc: context.raised_exception = exc @then("the lifecycle should have called fail_strategize for coverage") def step_check_fail_strategize(context: Context) -> None: assert context.raised_exception is not None, "Expected an exception" context.lifecycle.fail_strategize.assert_called_once() call_args = context.lifecycle.fail_strategize.call_args assert context.plan_id in call_args[0] assert "RuntimeError" in call_args[0][1] # --------------------------------------------------------------------------- # PlanExecutor.run_strategize wrong phase # --------------------------------------------------------------------------- @given("a mock lifecycle with a plan in Execute phase for executor coverage") def step_given_lifecycle_execute_phase(context: Context) -> None: plan = _make_mock_plan(phase=PlanPhase.EXECUTE) context.lifecycle = _make_lifecycle(plan) context.plan_id = PLAN_ID @when("I call run_strategize expecting a PlanError for coverage") def step_run_strategize_wrong_phase(context: Context) -> None: try: context.executor.run_strategize(context.plan_id) context.raised_exception = None except PlanError as exc: context.raised_exception = exc @then("a PlanError should be raised about wrong phase for coverage") def step_check_wrong_phase_error(context: Context) -> None: assert context.raised_exception is not None, "Expected a PlanError" assert isinstance(context.raised_exception, PlanError) assert "not in Strategize phase" in str(context.raised_exception) # --------------------------------------------------------------------------- # PlanExecutor._guard_execute: decision_root_id is None (lines 233-235) # --------------------------------------------------------------------------- @given("a mock lifecycle with an execute-phase plan missing decision_root_id") def step_given_lifecycle_no_root(context: Context) -> None: plan = _make_mock_plan( phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED, decision_root_id=None, ) context.lifecycle = _make_lifecycle(plan) context.plan_id = PLAN_ID @when("I call guard_execute for the plan for coverage") def step_call_guard_execute(context: Context) -> None: try: context.executor._guard_execute(context.plan_id) context.raised_exception = None except PlanError as exc: context.raised_exception = exc @then("a PlanError should be raised about missing decision tree") def step_check_no_decision_tree(context: Context) -> None: assert context.raised_exception is not None, "Expected a PlanError" assert isinstance(context.raised_exception, PlanError) assert "no decision tree" in str(context.raised_exception) @given("a mock lifecycle with a plan in Strategize phase for guard coverage") def step_given_lifecycle_strategize_for_guard(context: Context) -> None: plan = _make_mock_plan( phase=PlanPhase.STRATEGIZE, state=ProcessingState.QUEUED, ) context.lifecycle = _make_lifecycle(plan) context.plan_id = PLAN_ID @when("I call guard_execute expecting wrong phase for coverage") def step_call_guard_execute_wrong_phase(context: Context) -> None: try: context.executor._guard_execute(context.plan_id) context.raised_exception = None except PlanError as exc: context.raised_exception = exc @then("a PlanError should be raised about not in Execute phase") def step_check_not_execute_phase(context: Context) -> None: assert context.raised_exception is not None, "Expected a PlanError" assert isinstance(context.raised_exception, PlanError) assert "not in Execute phase" in str(context.raised_exception) @given("a mock lifecycle with an execute-phase plan in processing state for coverage") def step_given_lifecycle_processing(context: Context) -> None: plan = _make_mock_plan( phase=PlanPhase.EXECUTE, state=ProcessingState.PROCESSING, ) context.lifecycle = _make_lifecycle(plan) context.plan_id = PLAN_ID @when("I call guard_execute expecting wrong state for coverage") def step_call_guard_execute_wrong_state(context: Context) -> None: try: context.executor._guard_execute(context.plan_id) context.raised_exception = None except PlanError as exc: context.raised_exception = exc @then("a PlanError should be raised about not queued") def step_check_not_queued(context: Context) -> None: assert context.raised_exception is not None, "Expected a PlanError" assert isinstance(context.raised_exception, PlanError) assert "not queued" in str(context.raised_exception) # --------------------------------------------------------------------------- # PlanExecutor._run_execute_with_stub happy path # --------------------------------------------------------------------------- @given("a mock lifecycle with a fully configured execute-phase plan for coverage") def step_given_lifecycle_full_execute(context: Context) -> None: plan = _make_mock_plan( phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED, definition_of_done="Implement feature\nWrite tests", decision_root_id="01HROOT000000000000000ROOT", ) context.lifecycle = _make_lifecycle(plan) context.plan_id = PLAN_ID @given("a PlanExecutor using that lifecycle without runtime for coverage") def step_given_executor_no_runtime(context: Context) -> None: context.executor = PlanExecutor( lifecycle_service=context.lifecycle, execution_context=None, ) @when("I call run_execute on the PlanExecutor for coverage") def step_run_execute(context: Context) -> None: context.execute_result = context.executor.run_execute(context.plan_id) @then("the execute result should be returned successfully") def step_check_execute_ok(context: Context) -> None: assert isinstance(context.execute_result, ExecuteResult) assert context.execute_result.changeset_id is not None @then("the lifecycle should have called start_execute for coverage") def step_check_start_execute(context: Context) -> None: context.lifecycle.start_execute.assert_called_once_with(context.plan_id) @then("the lifecycle should have called complete_execute for coverage") def step_check_complete_execute(context: Context) -> None: context.lifecycle.complete_execute.assert_called_once_with(context.plan_id) # --------------------------------------------------------------------------- # PlanExecutor._run_execute_with_stub exception path (lines 483, 490) # --------------------------------------------------------------------------- @given("a mock lifecycle with an execute-phase plan that will fail during stub execute") def step_given_lifecycle_stub_fail(context: Context) -> None: plan = _make_mock_plan( phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED, definition_of_done="Something", decision_root_id="01HROOT000000000000000ROOT", ) context.lifecycle = _make_lifecycle(plan) context.plan_id = PLAN_ID @given("a PlanExecutor using that failing stub lifecycle for coverage") def step_given_executor_failing_stub(context: Context) -> None: context.executor = PlanExecutor( lifecycle_service=context.lifecycle, execution_context=None, ) # Patch the internal execute actor to raise context.executor._execute_actor = MagicMock() context.executor._execute_actor.execute.side_effect = RuntimeError( "Boom in execute" ) @when("I call run_execute expecting an exception from stub for coverage") def step_run_execute_fail(context: Context) -> None: try: context.executor.run_execute(context.plan_id) context.raised_exception = None except RuntimeError as exc: context.raised_exception = exc @then("the lifecycle should have called fail_execute for coverage") def step_check_fail_execute(context: Context) -> None: assert context.raised_exception is not None, "Expected an exception" context.lifecycle.fail_execute.assert_called_once() call_args = context.lifecycle.fail_execute.call_args assert context.plan_id in call_args[0] assert "RuntimeError" in call_args[0][1] # --------------------------------------------------------------------------- # PlanExecutor.run_execute validation # --------------------------------------------------------------------------- @when("I call run_execute with an empty plan_id for coverage") def step_run_execute_empty_plan_id(context: Context) -> None: try: context.executor.run_execute("") context.raised_exception = None except Exception as exc: context.raised_exception = exc # --------------------------------------------------------------------------- # PlanExecutor._build_decisions # --------------------------------------------------------------------------- @when("I call build_decisions for the plan for coverage") def step_call_build_decisions(context: Context) -> None: plan = context.lifecycle.get_plan(context.plan_id) context.built_decisions = context.executor._build_decisions(plan) @then("the decisions should match the plan definition of done steps") def step_check_built_decisions(context: Context) -> None: decisions = context.built_decisions assert len(decisions) == 2 assert decisions[0].step_text == "Implement feature" assert decisions[1].step_text == "Write tests" assert decisions[0].parent_id is None assert decisions[1].parent_id is not None