forked from HAL9000/cleveragents-core
a808c395f9
Add 53 new .feature files and corresponding step definition files targeting uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts in 7 pre-existing step files by disambiguating step text. New tests cover: ACP clients/facade, actor CLI/config, application container, ACMS service/strategies, async worker, automation profile CLI, autonomy guardrail, bridge, change model, config CLI/service, context service, cross-plan correction, database models, decision service, decomposition clustering/service, discovery handler, langchain chat provider, langgraph nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/ preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI, provider registry, reactive application/route, repositories, resolver handler, resource registry service, resume model, retry patterns, sandbox protocol, server CLI, skill CLI/service, skills registry, subplan execution/service, system CLI, UKO loader, UoW, and YAML template engine. Closes #645
666 lines
23 KiB
Python
666 lines
23 KiB
Python
"""Step definitions for plan_executor_new_coverage.feature.
|
|
|
|
Covers uncovered branches and lines in plan_executor.py:
|
|
- PlanExecutor.__init__ with None lifecycle_service
|
|
- has_runtime / changeset_store properties
|
|
- run_strategize guard branches (wrong phase, empty plan_id, exception path)
|
|
- _guard_execute branches (wrong phase, wrong state, None decision_root_id)
|
|
- run_execute routing (stub vs runtime)
|
|
- StrategizeStubActor / ExecuteStubActor with and without stream_callback
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, when
|
|
from ulid import ULID
|
|
|
|
from cleveragents.application.services.plan_execution_context import (
|
|
PlanExecutionContext,
|
|
RuntimeExecuteResult,
|
|
)
|
|
from cleveragents.application.services.plan_executor import (
|
|
ExecuteResult,
|
|
ExecuteStubActor,
|
|
PlanExecutor,
|
|
StrategizeStubActor,
|
|
StrategyDecision,
|
|
)
|
|
from cleveragents.core.exceptions import PlanError, ValidationError
|
|
from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState
|
|
from cleveragents.tool.registry import ToolRegistry
|
|
from cleveragents.tool.runner import ToolRunner
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_plan_mock(
|
|
phase: PlanPhase = PlanPhase.STRATEGIZE,
|
|
state: ProcessingState = ProcessingState.QUEUED,
|
|
decision_root_id: str | None = None,
|
|
definition_of_done: str = "Step one\nStep two",
|
|
invariants: list | None = None,
|
|
) -> MagicMock:
|
|
"""Create a mock Plan object with the given attributes."""
|
|
plan = MagicMock()
|
|
plan.phase = phase
|
|
plan.state = state
|
|
plan.decision_root_id = decision_root_id
|
|
plan.definition_of_done = definition_of_done
|
|
plan.invariants = invariants or []
|
|
plan.timestamps = MagicMock()
|
|
plan.error_details = {}
|
|
plan.changeset_id = None
|
|
plan.sandbox_refs = []
|
|
plan.namespaced_name = "test/plan"
|
|
plan.identity = MagicMock()
|
|
return plan
|
|
|
|
|
|
def _make_lifecycle(**overrides: Any) -> MagicMock:
|
|
"""Create a mock lifecycle service with default methods."""
|
|
lc = MagicMock()
|
|
lc.start_strategize = MagicMock()
|
|
lc.complete_strategize = MagicMock()
|
|
lc.fail_strategize = MagicMock()
|
|
lc.start_execute = MagicMock()
|
|
lc.complete_execute = MagicMock()
|
|
lc.fail_execute = MagicMock()
|
|
lc._commit_plan = MagicMock()
|
|
for key, value in overrides.items():
|
|
setattr(lc, key, value)
|
|
return lc
|
|
|
|
|
|
def _make_decisions(count: int = 2) -> list[StrategyDecision]:
|
|
root_id = str(ULID())
|
|
decisions = []
|
|
for i in range(count):
|
|
decisions.append(
|
|
StrategyDecision(
|
|
decision_id=root_id if i == 0 else str(ULID()),
|
|
step_text=f"Step {i + 1}",
|
|
sequence=i,
|
|
parent_id=root_id if i > 0 else None,
|
|
)
|
|
)
|
|
return decisions
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PlanExecutor __init__ validation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I construct a PlanExecutor with None lifecycle_service")
|
|
def step_construct_pe_none_lc(ctx: Any) -> None:
|
|
ctx.pe_error = None
|
|
try:
|
|
PlanExecutor(lifecycle_service=None)
|
|
except ValidationError as exc:
|
|
ctx.pe_error = exc
|
|
|
|
|
|
@then('a PE ValidationError should be raised containing "{text}"')
|
|
def step_assert_pe_validation_error_contains(ctx: Any, text: str) -> None:
|
|
assert ctx.pe_error is not None, "Expected a ValidationError but none was raised"
|
|
assert isinstance(ctx.pe_error, ValidationError), (
|
|
f"Expected ValidationError, got {type(ctx.pe_error).__name__}"
|
|
)
|
|
assert text in str(ctx.pe_error), (
|
|
f"Expected '{text}' in error message, got: {ctx.pe_error}"
|
|
)
|
|
|
|
|
|
@given("a mock plan lifecycle service for plan executor")
|
|
def step_given_mock_lifecycle(ctx: Any) -> None:
|
|
ctx.lifecycle = _make_lifecycle()
|
|
ctx.plan_id = str(ULID())
|
|
|
|
|
|
@when("I construct a PlanExecutor with the mock lifecycle service")
|
|
def step_construct_pe_with_lc(ctx: Any) -> None:
|
|
ctx.pe_error = None
|
|
try:
|
|
ctx.executor = PlanExecutor(lifecycle_service=ctx.lifecycle)
|
|
except Exception as exc:
|
|
ctx.pe_error = exc
|
|
|
|
|
|
@then("the PlanExecutor should be created without error")
|
|
def step_assert_no_error(ctx: Any) -> None:
|
|
assert ctx.pe_error is None, f"Unexpected error: {ctx.pe_error}"
|
|
assert ctx.executor is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# has_runtime property
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a mock execution context")
|
|
def step_given_mock_exec_ctx(ctx: Any) -> None:
|
|
ctx.exec_ctx = PlanExecutionContext(plan_id=ctx.plan_id)
|
|
|
|
|
|
@when(
|
|
"I construct a PlanExecutor with the mock lifecycle service and execution context"
|
|
)
|
|
def step_construct_pe_with_ctx(ctx: Any) -> None:
|
|
ctx.executor = PlanExecutor(
|
|
lifecycle_service=ctx.lifecycle,
|
|
execution_context=ctx.exec_ctx,
|
|
)
|
|
|
|
|
|
@then("the executor has_runtime property should be True")
|
|
def step_assert_has_runtime_true(ctx: Any) -> None:
|
|
assert ctx.executor.has_runtime is True
|
|
|
|
|
|
@then("the executor has_runtime property should be False")
|
|
def step_assert_has_runtime_false(ctx: Any) -> None:
|
|
assert ctx.executor.has_runtime is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# changeset_store property
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a mock execution context with a changeset store")
|
|
def step_given_exec_ctx_with_store(ctx: Any) -> None:
|
|
ctx.mock_store = MagicMock()
|
|
ctx.exec_ctx = MagicMock()
|
|
ctx.exec_ctx.changeset_store = ctx.mock_store
|
|
ctx.exec_ctx.decision_root_id = None
|
|
|
|
|
|
@then("the executor changeset_store should be the mock store")
|
|
def step_assert_changeset_store_is_mock(ctx: Any) -> None:
|
|
assert ctx.executor.changeset_store is ctx.mock_store
|
|
|
|
|
|
@then("the executor changeset_store should be None")
|
|
def step_assert_changeset_store_none(ctx: Any) -> None:
|
|
assert ctx.executor.changeset_store is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_strategize guard: wrong phase
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PlanExecutor with the mock lifecycle service")
|
|
def step_given_pe_with_lc(ctx: Any) -> None:
|
|
ctx.executor = PlanExecutor(lifecycle_service=ctx.lifecycle)
|
|
|
|
|
|
@given("the lifecycle returns a plan in EXECUTE phase")
|
|
def step_lifecycle_returns_execute_phase(ctx: Any) -> None:
|
|
plan = _make_plan_mock(phase=PlanPhase.EXECUTE)
|
|
ctx.lifecycle.get_plan = MagicMock(return_value=plan)
|
|
|
|
|
|
@when("I attempt to run strategize on the executor")
|
|
def step_attempt_run_strategize(ctx: Any) -> None:
|
|
ctx.pe_error = None
|
|
try:
|
|
ctx.executor.run_strategize(ctx.plan_id)
|
|
except (PlanError, ValidationError) as exc:
|
|
ctx.pe_error = exc
|
|
|
|
|
|
@then('a PE PlanError should be raised containing "{text}"')
|
|
def step_assert_pe_plan_error_contains(ctx: Any, text: str) -> None:
|
|
assert ctx.pe_error is not None, "Expected a PlanError but none was raised"
|
|
assert isinstance(ctx.pe_error, PlanError), (
|
|
f"Expected PlanError, got {type(ctx.pe_error).__name__}"
|
|
)
|
|
assert text in str(ctx.pe_error), (
|
|
f"Expected '{text}' in error message, got: {ctx.pe_error}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_strategize guard: empty plan_id
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I attempt to run strategize with an empty plan_id")
|
|
def step_attempt_strategize_empty_plan_id(ctx: Any) -> None:
|
|
ctx.pe_error = None
|
|
try:
|
|
ctx.executor.run_strategize("")
|
|
except (ValidationError, PlanError) as exc:
|
|
ctx.pe_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_strategize exception handling
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the lifecycle returns a plan in STRATEGIZE phase")
|
|
def step_lifecycle_returns_strategize_phase(ctx: Any) -> None:
|
|
plan = _make_plan_mock(
|
|
phase=PlanPhase.STRATEGIZE,
|
|
definition_of_done="Step one\nStep two",
|
|
)
|
|
ctx.lifecycle.get_plan = MagicMock(return_value=plan)
|
|
ctx.mock_plan = plan
|
|
|
|
|
|
@given("the lifecycle start_strategize will raise a RuntimeError")
|
|
def step_lifecycle_start_strategize_raises(ctx: Any) -> None:
|
|
"""Make the strategize actor itself raise inside the try block.
|
|
|
|
start_strategize is called *before* the try, so we need to make
|
|
the actor.execute raise instead to hit the except branch (line 314).
|
|
"""
|
|
ctx._patch_strategize_actor_to_fail = True
|
|
|
|
|
|
@when("I attempt to run strategize and expect an exception")
|
|
def step_attempt_strategize_expect_exception(ctx: Any) -> None:
|
|
# If the actor should fail, patch it before running
|
|
if getattr(ctx, "_patch_strategize_actor_to_fail", False):
|
|
ctx.executor._strategize_actor.execute = MagicMock(
|
|
side_effect=RuntimeError("simulated actor failure")
|
|
)
|
|
if getattr(ctx, "_patch_actor_to_fail", False):
|
|
ctx.executor._strategize_actor.execute = MagicMock(
|
|
side_effect=RuntimeError("simulated actor failure")
|
|
)
|
|
ctx.pe_error = None
|
|
ctx.raised_exception = None
|
|
try:
|
|
ctx.executor.run_strategize(ctx.plan_id)
|
|
except Exception as exc:
|
|
ctx.pe_error = exc
|
|
ctx.raised_exception = exc
|
|
|
|
|
|
@then("fail_strategize should have been called with the plan_id")
|
|
def step_assert_fail_strategize_called(ctx: Any) -> None:
|
|
ctx.lifecycle.fail_strategize.assert_called()
|
|
call_args = ctx.lifecycle.fail_strategize.call_args
|
|
assert ctx.plan_id == call_args[0][0], (
|
|
f"Expected plan_id={ctx.plan_id}, got {call_args[0][0]}"
|
|
)
|
|
|
|
|
|
@then("the raised exception should be a RuntimeError")
|
|
def step_assert_raised_runtime_error(ctx: Any) -> None:
|
|
assert ctx.raised_exception is not None, "No exception was raised"
|
|
assert isinstance(ctx.raised_exception, RuntimeError), (
|
|
f"Expected RuntimeError, got {type(ctx.raised_exception).__name__}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_strategize exception during actor execute (except branch line 314)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given(
|
|
"the lifecycle returns a plan in STRATEGIZE phase that fails during actor execute"
|
|
)
|
|
def step_lifecycle_strategize_fails_in_actor(ctx: Any) -> None:
|
|
"""Set up lifecycle so start_strategize succeeds but actor.execute raises."""
|
|
plan = _make_plan_mock(
|
|
phase=PlanPhase.STRATEGIZE,
|
|
definition_of_done="Step one\nStep two",
|
|
)
|
|
ctx.lifecycle.get_plan = MagicMock(return_value=plan)
|
|
ctx.lifecycle.start_strategize = MagicMock() # succeeds
|
|
ctx.mock_plan = plan
|
|
ctx._patch_actor_to_fail = True
|
|
|
|
|
|
@then("the plan error_details should contain the exception_type")
|
|
def step_assert_error_details_exception_type(ctx: Any) -> None:
|
|
plan = ctx.lifecycle.get_plan.return_value
|
|
assert plan.error_details is not None
|
|
if isinstance(plan.error_details, dict):
|
|
assert "exception_type" in plan.error_details
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _guard_execute: wrong phase
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the lifecycle returns a plan in STRATEGIZE phase for execute guard")
|
|
def step_lifecycle_returns_strategize_for_execute(ctx: Any) -> None:
|
|
plan = _make_plan_mock(phase=PlanPhase.STRATEGIZE)
|
|
ctx.lifecycle.get_plan = MagicMock(return_value=plan)
|
|
|
|
|
|
@when("I attempt to run execute on the executor")
|
|
def step_attempt_run_execute(ctx: Any) -> None:
|
|
ctx.pe_error = None
|
|
try:
|
|
ctx.executor.run_execute(ctx.plan_id)
|
|
except (PlanError, ValidationError) as exc:
|
|
ctx.pe_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _guard_execute: wrong state
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the lifecycle returns a plan in EXECUTE phase with PROCESSING state")
|
|
def step_lifecycle_returns_execute_processing(ctx: Any) -> None:
|
|
plan = _make_plan_mock(
|
|
phase=PlanPhase.EXECUTE,
|
|
state=ProcessingState.PROCESSING,
|
|
)
|
|
ctx.lifecycle.get_plan = MagicMock(return_value=plan)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _guard_execute: None decision_root_id
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given(
|
|
"the lifecycle returns a plan in EXECUTE phase with QUEUED state but no decision root"
|
|
)
|
|
def step_lifecycle_returns_execute_queued_no_root(ctx: Any) -> None:
|
|
plan = _make_plan_mock(
|
|
phase=PlanPhase.EXECUTE,
|
|
state=ProcessingState.QUEUED,
|
|
decision_root_id=None,
|
|
)
|
|
ctx.lifecycle.get_plan = MagicMock(return_value=plan)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_execute routing to stub
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PlanExecutor with the mock lifecycle service and no execution context")
|
|
def step_given_pe_no_ctx(ctx: Any) -> None:
|
|
ctx.executor = PlanExecutor(lifecycle_service=ctx.lifecycle)
|
|
|
|
|
|
@given("the lifecycle returns a fully valid plan for stub execution")
|
|
def step_lifecycle_returns_valid_plan_for_stub(ctx: Any) -> None:
|
|
root_id = str(ULID())
|
|
plan = _make_plan_mock(
|
|
phase=PlanPhase.EXECUTE,
|
|
state=ProcessingState.QUEUED,
|
|
decision_root_id=root_id,
|
|
definition_of_done="Do thing one\nDo thing two",
|
|
)
|
|
ctx.lifecycle.get_plan = MagicMock(return_value=plan)
|
|
ctx.mock_plan = plan
|
|
|
|
|
|
@when("I run execute on the executor")
|
|
def step_run_execute(ctx: Any) -> None:
|
|
ctx.pe_error = None
|
|
try:
|
|
ctx.result = ctx.executor.run_execute(ctx.plan_id)
|
|
except Exception as exc:
|
|
ctx.pe_error = exc
|
|
|
|
|
|
@then("the plan executor result should be an ExecuteResult")
|
|
def step_assert_execute_result_type(ctx: Any) -> None:
|
|
assert ctx.pe_error is None, f"Unexpected error: {ctx.pe_error}"
|
|
assert isinstance(ctx.result, ExecuteResult), (
|
|
f"Expected ExecuteResult, got {type(ctx.result).__name__}"
|
|
)
|
|
|
|
|
|
@then("complete_execute should have been called")
|
|
def step_assert_complete_execute_called(ctx: Any) -> None:
|
|
ctx.lifecycle.complete_execute.assert_called_once()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_execute routing to runtime
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given(
|
|
"a PlanExecutor with the mock lifecycle service and execution context and tool runner"
|
|
)
|
|
def step_given_pe_with_ctx_and_runner(ctx: Any) -> None:
|
|
ctx.tool_runner = ToolRunner(registry=ToolRegistry())
|
|
ctx.executor = PlanExecutor(
|
|
lifecycle_service=ctx.lifecycle,
|
|
tool_runner=ctx.tool_runner,
|
|
execution_context=ctx.exec_ctx,
|
|
)
|
|
|
|
|
|
@given("the lifecycle returns a fully valid plan for runtime execution")
|
|
def step_lifecycle_returns_valid_plan_for_runtime(ctx: Any) -> None:
|
|
root_id = str(ULID())
|
|
plan = _make_plan_mock(
|
|
phase=PlanPhase.EXECUTE,
|
|
state=ProcessingState.QUEUED,
|
|
decision_root_id=root_id,
|
|
definition_of_done="Do thing one\nDo thing two",
|
|
)
|
|
ctx.lifecycle.get_plan = MagicMock(return_value=plan)
|
|
ctx.mock_plan = plan
|
|
|
|
|
|
@then("the plan executor result should be a RuntimeExecuteResult")
|
|
def step_assert_runtime_execute_result_type(ctx: Any) -> None:
|
|
assert ctx.pe_error is None, f"Unexpected error: {ctx.pe_error}"
|
|
assert isinstance(ctx.result, RuntimeExecuteResult), (
|
|
f"Expected RuntimeExecuteResult, got {type(ctx.result).__name__}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# StrategizeStubActor with stream_callback
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a stream event collector")
|
|
def step_given_stream_collector(ctx: Any) -> None:
|
|
ctx.stream_events = []
|
|
ctx.stream_callback = lambda event_type, data: ctx.stream_events.append(
|
|
(event_type, data)
|
|
)
|
|
|
|
|
|
@when("I execute the StrategizeStubActor with a plan_id and stream callback")
|
|
def step_exec_strategize_actor_with_cb(ctx: Any) -> None:
|
|
actor = StrategizeStubActor()
|
|
plan_id = str(ULID())
|
|
ctx.actor_plan_id = plan_id
|
|
ctx.strategize_result = actor.execute(
|
|
plan_id=plan_id,
|
|
definition_of_done="Step A\nStep B",
|
|
stream_callback=ctx.stream_callback,
|
|
)
|
|
|
|
|
|
@then('the collector should contain a "{event_type}" event')
|
|
def step_assert_collector_contains_event(ctx: Any, event_type: str) -> None:
|
|
event_types = [e[0] for e in ctx.stream_events]
|
|
assert event_type in event_types, (
|
|
f"Expected '{event_type}' in events, got: {event_types}"
|
|
)
|
|
|
|
|
|
@when("I execute the StrategizeStubActor with a plan_id and no callback")
|
|
def step_exec_strategize_actor_no_cb(ctx: Any) -> None:
|
|
actor = StrategizeStubActor()
|
|
plan_id = str(ULID())
|
|
ctx.strategize_result = actor.execute(
|
|
plan_id=plan_id,
|
|
definition_of_done="Step A\nStep B",
|
|
stream_callback=None,
|
|
)
|
|
|
|
|
|
@then("the strategize result should have a valid decision_root_id")
|
|
def step_assert_strategize_result_root_id(ctx: Any) -> None:
|
|
assert ctx.strategize_result.decision_root_id
|
|
assert len(ctx.strategize_result.decision_root_id) == 26
|
|
|
|
|
|
@then("the strategize result should have at least one decision")
|
|
def step_assert_strategize_result_has_decisions(ctx: Any) -> None:
|
|
assert len(ctx.strategize_result.decisions) >= 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ExecuteStubActor with stream_callback
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a list of two strategy decisions")
|
|
def step_given_two_decisions(ctx: Any) -> None:
|
|
ctx.decisions = _make_decisions(2)
|
|
|
|
|
|
@when("I execute the ExecuteStubActor with the decisions and stream callback")
|
|
def step_exec_execute_actor_with_cb(ctx: Any) -> None:
|
|
actor = ExecuteStubActor()
|
|
plan_id = str(ULID())
|
|
ctx.actor_plan_id = plan_id
|
|
ctx.execute_result = actor.execute(
|
|
plan_id=plan_id,
|
|
decisions=ctx.decisions,
|
|
stream_callback=ctx.stream_callback,
|
|
)
|
|
|
|
|
|
@then('the collector should contain an "{event_type}" event')
|
|
def step_assert_collector_contains_event_an(ctx: Any, event_type: str) -> None:
|
|
event_types = [e[0] for e in ctx.stream_events]
|
|
assert event_type in event_types, (
|
|
f"Expected '{event_type}' in events, got: {event_types}"
|
|
)
|
|
|
|
|
|
@when("I execute the ExecuteStubActor with the decisions and no callback")
|
|
def step_exec_execute_actor_no_cb(ctx: Any) -> None:
|
|
actor = ExecuteStubActor()
|
|
plan_id = str(ULID())
|
|
ctx.execute_result = actor.execute(
|
|
plan_id=plan_id,
|
|
decisions=ctx.decisions,
|
|
stream_callback=None,
|
|
)
|
|
|
|
|
|
@then("the execute result should have a valid changeset_id")
|
|
def step_assert_execute_result_changeset_id(ctx: Any) -> None:
|
|
assert ctx.execute_result.changeset_id
|
|
assert len(ctx.execute_result.changeset_id) == 26
|
|
|
|
|
|
@then("the execute result tool_calls_count should be zero")
|
|
def step_assert_execute_result_tool_calls_zero(ctx: Any) -> None:
|
|
assert ctx.execute_result.tool_calls_count == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_strategize happy path with stream_callback
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I run strategize with the stream callback")
|
|
def step_run_strategize_with_callback(ctx: Any) -> None:
|
|
ctx.pe_error = None
|
|
try:
|
|
ctx.strategize_result = ctx.executor.run_strategize(
|
|
ctx.plan_id,
|
|
stream_callback=ctx.stream_callback,
|
|
)
|
|
except Exception as exc:
|
|
ctx.pe_error = exc
|
|
|
|
|
|
@then("the strategize result should have decisions")
|
|
def step_assert_result_has_decisions(ctx: Any) -> None:
|
|
assert ctx.pe_error is None, f"Unexpected error: {ctx.pe_error}"
|
|
assert len(ctx.strategize_result.decisions) >= 1
|
|
|
|
|
|
@then("complete_strategize should have been called")
|
|
def step_assert_complete_strategize_called(ctx: Any) -> None:
|
|
ctx.lifecycle.complete_strategize.assert_called_once()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PlanExecutor with execution context (for run_strategize + context scenario)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a PlanExecutor with the mock lifecycle and execution context")
|
|
def step_given_pe_with_lc_and_ctx(ctx: Any) -> None:
|
|
ctx.executor = PlanExecutor(
|
|
lifecycle_service=ctx.lifecycle,
|
|
execution_context=ctx.exec_ctx,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_strategize sets execution_context.decision_root_id
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I run strategize successfully")
|
|
def step_run_strategize_successfully(ctx: Any) -> None:
|
|
ctx.pe_error = None
|
|
try:
|
|
ctx.strategize_result = ctx.executor.run_strategize(ctx.plan_id)
|
|
except Exception as exc:
|
|
ctx.pe_error = exc
|
|
|
|
|
|
@then("the execution context decision_root_id should be set")
|
|
def step_assert_exec_ctx_decision_root_id_set(ctx: Any) -> None:
|
|
assert ctx.pe_error is None, f"Unexpected error: {ctx.pe_error}"
|
|
assert ctx.exec_ctx.decision_root_id is not None
|
|
assert ctx.exec_ctx.decision_root_id == ctx.strategize_result.decision_root_id
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run_execute stub exception path
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the stub execute actor is patched to raise an error")
|
|
def step_patch_stub_execute_to_fail(ctx: Any) -> None:
|
|
ctx.executor._execute_actor.execute = MagicMock(
|
|
side_effect=RuntimeError("simulated stub execution failure")
|
|
)
|
|
|
|
|
|
@when("I attempt to run execute and expect an exception")
|
|
def step_attempt_run_execute_expect_exception(ctx: Any) -> None:
|
|
ctx.pe_error = None
|
|
ctx.raised_exception = None
|
|
try:
|
|
ctx.result = ctx.executor.run_execute(ctx.plan_id)
|
|
except Exception as exc:
|
|
ctx.pe_error = exc
|
|
ctx.raised_exception = exc
|
|
|
|
|
|
@then("fail_execute should have been called with the plan_id")
|
|
def step_assert_fail_execute_called(ctx: Any) -> None:
|
|
ctx.lifecycle.fail_execute.assert_called()
|
|
call_args = ctx.lifecycle.fail_execute.call_args
|
|
assert ctx.plan_id == call_args[0][0], (
|
|
f"Expected plan_id={ctx.plan_id}, got {call_args[0][0]}"
|
|
)
|