Files
cleveragents-core/features/steps/plan_execution_hierarchical_steps.py
T
CoreRasurae 8548644819
CI / push-validation (pull_request) Successful in 1m1s
CI / helm (pull_request) Successful in 1m12s
CI / lint (pull_request) Successful in 2m22s
CI / build (pull_request) Successful in 2m17s
CI / typecheck (pull_request) Successful in 3m2s
CI / security (pull_request) Successful in 3m1s
CI / quality (pull_request) Successful in 57s
CI / integration_tests (pull_request) Successful in 5m0s
CI / unit_tests (pull_request) Successful in 6m8s
CI / docker (pull_request) Successful in 1m30s
CI / coverage (pull_request) Successful in 11m51s
CI / status-check (pull_request) Successful in 6s
CI / push-validation (push) Successful in 31s
CI / helm (push) Successful in 37s
CI / build (push) Successful in 1m7s
CI / lint (push) Successful in 1m28s
CI / quality (push) Successful in 1m30s
CI / typecheck (push) Successful in 1m44s
CI / security (push) Successful in 1m49s
CI / benchmark-regression (push) Failing after 40s
CI / e2e_tests (push) Successful in 54s
CI / integration_tests (push) Successful in 3m33s
CI / unit_tests (push) Successful in 5m21s
CI / docker (push) Successful in 1m29s
CI / coverage (push) Successful in 11m6s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h39m32s
fix(cli): wire SubplanExecutionService in _get_plan_executor() to enable child plan execution
Wire subplan_service from the DI container into _get_plan_executor() so
PlanExecutor can spawn child plans during the Execute phase.  When
SubplanService is available but SubplanExecutionService is not explicitly
injected, _execute_subplans() now lazily creates a SubplanExecutionService
on-the-fly using the parent plan's subplan_config and the new
_execute_child_plan callback.

- _get_plan_executor(): retrieve subplan_service from container, pass to
  PlanExecutor constructor
- _execute_subplans(): lazily build SubplanExecutionService when only
  SubplanService is wired (Forgejo #10268)
- _execute_child_plan(): new PlanExecutor method that runs a child plan's
  strategize + execute phases, registered as executor_fn callback

ISSUES CLOSED: #10268
2026-05-19 19:28:35 +01:00

208 lines
6.3 KiB
Python

"""Step definitions for plan_execution_hierarchical.feature.
Tests that PlanExecutor lazily creates SubplanExecutionService when
SubplanService is wired but SubplanExecutionService is not pre-configured
(Forgejo #10268).
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from behave import given, then
from behave.runner import Context
from cleveragents.application.services.plan_executor import PlanExecutor
from cleveragents.application.services.subplan_execution_service import (
SubplanExecutionResult,
SubplanExecutionService,
)
from cleveragents.application.services.subplan_service import (
SpawnEntry,
SpawnResult,
SubplanService,
)
from cleveragents.domain.models.core.decision import (
ContextSnapshot,
Decision,
DecisionType,
)
from cleveragents.domain.models.core.plan import (
ExecutionMode,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
ProcessingState,
SubplanStatus,
)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_PLAN_ID = "01KNFMGJSG67S6RG9TVXV205TQ"
_ROOT_ID = "01KNFMGJSH67S6RG9TVXV205TR"
_DEC_ID = "01KNFMGJSH67S6RG9TVXV205TS"
_SUBPLAN_ID = "01KNFMGJSH67S6RG9TVXV205TT"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_plan(*, phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED):
return Plan(
identity=PlanIdentity(plan_id=_PLAN_ID, root_plan_id=_ROOT_ID),
namespaced_name=NamespacedName(namespace="local", name="test-plan"),
description="Test plan for hierarchical execution",
action_name="local/test-action",
phase=phase,
processing_state=state,
)
def _make_spawn_decision(
decision_type: DecisionType = DecisionType.SUBPLAN_SPAWN,
) -> Decision:
return Decision(
decision_id=_DEC_ID,
plan_id=_PLAN_ID,
decision_type=decision_type,
sequence_number=0,
question="Spawn a child plan?",
chosen_option="local/sub-action",
context_snapshot=ContextSnapshot(relevant_resources=[]),
)
def _make_spawn_result(
execution_mode: str = ExecutionMode.SEQUENTIAL,
) -> SpawnResult:
sub_status = SubplanStatus(
subplan_id=_SUBPLAN_ID,
action_name="local/sub-action",
)
return SpawnResult(
spawned_statuses=[sub_status],
metadata={},
total_spawned=1,
execution_mode=execution_mode,
child_plans=[],
)
def _make_subplan_service(
decisions: list[Decision] | None = None,
spawn_result: SpawnResult | None = None,
) -> MagicMock:
svc = MagicMock(spec=SubplanService)
svc.get_spawn_decisions.return_value = decisions or []
svc.build_spawn_entries.return_value = [
SpawnEntry(decision=d, action_name="local/sub-action")
for d in (decisions or [])
]
svc.spawn.return_value = spawn_result or _make_spawn_result()
return svc
def _make_lifecycle(plan: Plan) -> MagicMock:
lcs = MagicMock()
lcs.get_plan.return_value = plan
lcs.start_execute = MagicMock()
lcs.complete_execute = MagicMock()
lcs.fail_execute = MagicMock()
lcs._commit_plan = MagicMock()
return lcs
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given("a PlanExecutor with SubplanService but no SubplanExecutionService")
def step_given_executor_with_subplan_service_only(context: Context) -> None:
decision = _make_spawn_decision(DecisionType.SUBPLAN_SPAWN)
spawn_result = _make_spawn_result()
context.spawn_decision = decision
context.spawn_result = spawn_result
mock_subplan_svc = _make_subplan_service(
decisions=[decision], spawn_result=spawn_result
)
plan = _make_plan()
plan.decision_root_id = _ROOT_ID
context.plan = plan
lcs = _make_lifecycle(plan)
execute_actor = MagicMock()
execute_result = MagicMock()
execute_result.changeset_id = "01JSPAWN0000000000000CS0001"
execute_result.sandbox_refs = []
execute_result.tool_calls_count = 0
execute_actor.execute.return_value = execute_result
executor = PlanExecutor(
lifecycle_service=lcs,
execute_actor=execute_actor,
subplan_service=mock_subplan_svc,
subplan_execution_service=None,
)
sub_status = SubplanStatus(
subplan_id=_SUBPLAN_ID,
action_name="local/sub-action",
status=ProcessingState.COMPLETE,
)
patcher = patch.object(SubplanExecutionService, "execute_all")
mock_exec_all = patcher.start()
mock_exec_all.return_value = SubplanExecutionResult(
all_succeeded=True,
statuses=[sub_status],
merge_result=None,
total_duration_ms=5,
failed_subplan_ids=[],
)
context._superb_patcher = patcher
context._mock_execute_all = mock_exec_all
context.executor = executor
context.lcs = lcs
context.mock_subplan_svc = mock_subplan_svc
@given("a child plan ready for execution in the lifecycle service")
def step_given_child_plan_ready(context: Context) -> None:
child_plan = Plan(
identity=PlanIdentity(
plan_id=_SUBPLAN_ID,
parent_plan_id=_PLAN_ID,
root_plan_id=_ROOT_ID,
),
namespaced_name=NamespacedName(namespace="local", name="child-plan"),
description="Child plan for testing",
action_name="local/sub-action",
phase=PlanPhase.STRATEGIZE,
processing_state=ProcessingState.QUEUED,
definition_of_done="- [ ] Step one",
)
context.lcs.get_plan.side_effect = lambda pid: (
child_plan if pid == _SUBPLAN_ID else context.plan
)
context.child_plan = child_plan
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then("SubplanExecutionService.execute_all should have been called via lazy creation")
def step_then_execute_all_called_via_lazy(context: Context) -> None:
try:
context._mock_execute_all.assert_called()
finally:
context._superb_patcher.stop()