"""Helper script for tdd_plan_execute_phase_processing.robot smoke tests. Each subcommand exercises the **CLI orchestration layer** by replicating the logic of the ``execute_plan`` CLI handler in ``plan.py``. The helper uses a real ``PlanLifecycleService`` (in-memory mode) and ``PlanExecutor`` (with stub actors) to verify that the orchestration correctly handles plans in Strategize/QUEUED state. Bug #967: The ``plan execute`` CLI command originally only called ``service.execute_plan(plan_id)``, which is a state transition only (Strategize/COMPLETE → Execute/QUEUED). It did not construct a ``PlanExecutor`` or call ``run_strategize()`` / ``run_execute()``. The fix added phase-aware orchestration; these tests verify it works. This test was written to capture bug #967 per ticket #977. The bug fix is already in the codebase, so these tests serve as permanent regression guards. """ from __future__ import annotations import sys from collections.abc import Callable from pathlib import Path from typing import NoReturn _ROOT: Path = Path(__file__).resolve().parents[1] _SRC: str = str(_ROOT / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) from cleveragents.application.services.plan_executor import PlanExecutor # noqa: E402 from cleveragents.application.services.plan_lifecycle_service import ( # noqa: E402 PlanLifecycleService, ) from cleveragents.config.settings import Settings # noqa: E402 from cleveragents.domain.models.core.plan import ( # noqa: E402 Plan, PlanPhase, ProcessingState, ) def _fail(msg: str) -> NoReturn: """Print an error message to stderr and exit with code 1.""" print(msg, file=sys.stderr) sys.exit(1) def _create_service_and_queued_plan() -> tuple[PlanLifecycleService, str]: """Create a real in-memory PlanLifecycleService and a plan in Strategize/QUEUED.""" service: PlanLifecycleService = PlanLifecycleService(settings=Settings()) action = service.create_action( name="local/tdd-967-robot-action", description="TDD action for bug #967 (Robot)", definition_of_done="Complete the test plan", strategy_actor="local/stub-strategize", execution_actor="local/stub-execute", ) plan: Plan = service.use_action(action_name=str(action.namespaced_name)) return service, plan.identity.plan_id def _cli_execute_from_queued() -> None: """Test the CLI orchestration path for a plan in Strategize/QUEUED. Replicates the ``execute_plan`` CLI handler logic: detect the plan is in Strategize/QUEUED, run strategize via ``PlanExecutor``, then transition to Execute phase. """ service, plan_id = _create_service_and_queued_plan() executor: PlanExecutor = PlanExecutor(lifecycle_service=service) # Replicate CLI orchestration: detect phase, run strategize if QUEUED plan: Plan = service.get_plan(plan_id) if plan.phase == PlanPhase.STRATEGIZE and plan.state == ProcessingState.QUEUED: executor.run_strategize(plan_id) plan = service.get_plan(plan_id) # Transition to Execute if still in Strategize/COMPLETE if plan.phase == PlanPhase.STRATEGIZE and plan.state == ProcessingState.COMPLETE: plan = service.execute_plan(plan_id) elif plan.phase != PlanPhase.EXECUTE: _fail( f"Bug #967: CLI orchestration failed. Plan in unexpected state " f"after strategize: {plan.phase.value}/{plan.state.value}" ) if plan.phase != PlanPhase.EXECUTE: _fail(f"Bug #967: Expected Execute phase, got {plan.phase.value}") print("tdd-cli-execute-from-queued-ok") def _cli_full_orchestration() -> None: """Test the CLI orchestration runs both strategize and execute phases. Replicates the full ``execute_plan`` CLI handler: strategize → transition → run_execute. """ service, plan_id = _create_service_and_queued_plan() executor: PlanExecutor = PlanExecutor(lifecycle_service=service) # Step 1: CLI detects Strategize/QUEUED and runs strategize plan: Plan = service.get_plan(plan_id) if plan.phase == PlanPhase.STRATEGIZE and plan.state in ( ProcessingState.QUEUED, ProcessingState.PROCESSING, ): executor.run_strategize(plan_id) plan = service.get_plan(plan_id) # Step 2: Transition to Execute if needed if plan.phase == PlanPhase.STRATEGIZE and plan.state == ProcessingState.COMPLETE: plan = service.execute_plan(plan_id) elif plan.phase != PlanPhase.EXECUTE: _fail( f"Bug #967: Unexpected state after strategize: " f"{plan.phase.value}/{plan.state.value}" ) # Step 3: Run execute phase plan = service.get_plan(plan_id) if plan.phase == PlanPhase.EXECUTE and plan.state == ProcessingState.QUEUED: executor.run_execute(plan_id) plan = service.get_plan(plan_id) if plan.phase != PlanPhase.EXECUTE: _fail(f"Bug #967: Expected Execute phase, got {plan.phase.value}") if plan.state not in (ProcessingState.COMPLETE, ProcessingState.QUEUED): _fail(f"Bug #967: Expected COMPLETE or QUEUED state, got {plan.state.value}") print("tdd-cli-full-orchestration-ok") def _proper_orchestration() -> None: """Positive control: proper service-level orchestration works on a QUEUED plan.""" service, plan_id = _create_service_and_queued_plan() executor: PlanExecutor = PlanExecutor(lifecycle_service=service) # Run strategize first (QUEUED → PROCESSING → COMPLETE) executor.run_strategize(plan_id) # Re-fetch — auto_progress may have moved it to Execute plan: Plan = service.get_plan(plan_id) if plan.phase == PlanPhase.STRATEGIZE and plan.state == ProcessingState.COMPLETE: plan = service.execute_plan(plan_id) elif plan.phase != PlanPhase.EXECUTE: _fail( f"Bug #967: Unexpected state after orchestration: " f"{plan.phase.value}/{plan.state.value}" ) plan = service.get_plan(plan_id) if plan.phase != PlanPhase.EXECUTE: _fail(f"Bug #967: Expected Execute phase, got {plan.phase.value}") if plan.state != ProcessingState.QUEUED: _fail(f"Bug #967: Expected QUEUED state, got {plan.state.value}") print("tdd-proper-orchestration-ok") def _cli_auto_discovery() -> None: """Test the CLI auto-discovery filter includes Strategize/QUEUED plans. Replicates the CLI auto-discovery logic from the ``execute_plan`` handler: list plans and filter for eligible states including QUEUED. """ service: PlanLifecycleService = PlanLifecycleService(settings=Settings()) # Create an action action = service.create_action( name="local/tdd-967-discovery", description="TDD action for auto-discovery (Robot)", definition_of_done="Test auto-discovery", strategy_actor="local/stub-strategize", execution_actor="local/stub-execute", ) action_name: str = str(action.namespaced_name) # Plan 1: Strategize/QUEUED (fresh from use_action) plan_queued: Plan = service.use_action(action_name=action_name) queued_id: str = plan_queued.identity.plan_id # Apply the PRODUCTION auto-discovery filter (the fixed version): # includes both QUEUED and COMPLETE Strategize plans plans_strat: list[Plan] = service.list_plans(phase=PlanPhase.STRATEGIZE) plans_exec: list[Plan] = service.list_plans(phase=PlanPhase.EXECUTE) eligible: list[Plan] = [ p for p in plans_strat if p.state in (ProcessingState.QUEUED, ProcessingState.COMPLETE) ] + [p for p in plans_exec if p.state == ProcessingState.QUEUED] queued_in_eligible: list[Plan] = [ p for p in eligible if p.identity.plan_id == queued_id ] if len(queued_in_eligible) == 0: _fail( "Bug #967: Auto-discovery filter does not find Strategize/QUEUED " f"plans. Eligible count: {len(eligible)}, QUEUED plan: {queued_id}" ) print("tdd-cli-auto-discovery-ok") _COMMANDS: dict[str, Callable[[], None]] = { "cli-execute-from-queued": _cli_execute_from_queued, "cli-full-orchestration": _cli_full_orchestration, "proper-orchestration": _proper_orchestration, "cli-auto-discovery": _cli_auto_discovery, } if __name__ == "__main__": if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS: print( f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr, ) sys.exit(1) cmd: Callable[[], None] = _COMMANDS[sys.argv[1]] cmd()