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
388 lines
14 KiB
Python
388 lines
14 KiB
Python
"""Step definitions for subplan execution defensive code-path coverage.
|
|
|
|
Targets the following uncovered lines in subplan_execution_service.py:
|
|
- 298-303: _execute_parallel generic Exception handler
|
|
- 331-334: _execute_parallel safety-net for uncaptured futures
|
|
- 381: _execute_dependency_ordered empty-ready break
|
|
- 573-575: _execute_wave CancelledError handler
|
|
- 576-578: _execute_wave generic Exception handler
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from concurrent.futures import CancelledError
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.application.services.subplan_execution_service import (
|
|
SubplanExecutionOutput,
|
|
SubplanExecutionService,
|
|
)
|
|
from cleveragents.domain.models.core.plan import (
|
|
ExecutionMode,
|
|
ProcessingState,
|
|
SubplanConfig,
|
|
SubplanStatus,
|
|
)
|
|
|
|
# Stable fake subplan IDs (valid ULIDs)
|
|
_P1 = "01HGZ6FE0AQDYTR4BXVQZ6EA00"
|
|
_P2 = "01HGZ6FE0AQDYTR4BXVQZ6EB00"
|
|
_P3 = "01HGZ6FE0AQDYTR4BXVQZ6EC00"
|
|
|
|
|
|
def _make_status(subplan_id: str) -> SubplanStatus:
|
|
return SubplanStatus(subplan_id=subplan_id, action_name="test/action")
|
|
|
|
|
|
def _ok_output(status: SubplanStatus) -> SubplanExecutionOutput:
|
|
return SubplanExecutionOutput(
|
|
subplan_id=status.subplan_id,
|
|
success=True,
|
|
files={f"src/{status.subplan_id[-4:]}.py": f"# ok {status.subplan_id}\n"},
|
|
files_changed=1,
|
|
changeset_summary="ok",
|
|
)
|
|
|
|
|
|
# ========================================================================
|
|
# Scenario: Parallel execution catches generic exception (lines 298-303)
|
|
# ========================================================================
|
|
|
|
|
|
@given("a subplan execution service in parallel mode with 2 subplans")
|
|
def step_parallel_2_subplans(context: Context) -> None:
|
|
context.cb_statuses = [_make_status(_P1), _make_status(_P2)]
|
|
context.cb_config = SubplanConfig(
|
|
execution_mode=ExecutionMode.PARALLEL,
|
|
max_parallel=2,
|
|
)
|
|
|
|
|
|
@given(
|
|
"the internal retry method is patched to raise a RuntimeError for the first subplan"
|
|
)
|
|
def step_patch_retry_raises_runtime(context: Context) -> None:
|
|
"""Record that we want _execute_one_with_retry to raise for _P1."""
|
|
context.raise_for_subplan_id = _P1
|
|
context.raise_exception = RuntimeError("unexpected internal failure")
|
|
|
|
|
|
@when("parallel execution is invoked")
|
|
def step_invoke_parallel(context: Context) -> None:
|
|
service = SubplanExecutionService(
|
|
config=context.cb_config,
|
|
executor_fn=_ok_output,
|
|
)
|
|
|
|
target_id = context.raise_for_subplan_id
|
|
target_exc = context.raise_exception
|
|
original_method = service._execute_one_with_retry
|
|
|
|
def _patched_retry(status: SubplanStatus):
|
|
if status.subplan_id == target_id:
|
|
raise target_exc
|
|
return original_method(status)
|
|
|
|
with patch.object(service, "_execute_one_with_retry", side_effect=_patched_retry):
|
|
context.cb_result = service._execute_parallel(context.cb_statuses)
|
|
|
|
|
|
@then("the first subplan should be marked ERRORED with the RuntimeError message")
|
|
def step_first_errored_runtime(context: Context) -> None:
|
|
updated_statuses, _outputs = context.cb_result
|
|
p1_status = next(s for s in updated_statuses if s.subplan_id == _P1)
|
|
assert p1_status.status == ProcessingState.ERRORED, (
|
|
f"Expected ERRORED, got {p1_status.status}"
|
|
)
|
|
assert "unexpected internal failure" in (p1_status.error or ""), (
|
|
f"Expected RuntimeError message in error, got: {p1_status.error}"
|
|
)
|
|
|
|
|
|
@then("the second subplan should complete normally")
|
|
def step_second_completes(context: Context) -> None:
|
|
updated_statuses, _outputs = context.cb_result
|
|
p2_status = next(s for s in updated_statuses if s.subplan_id == _P2)
|
|
assert p2_status.status == ProcessingState.COMPLETE, (
|
|
f"Expected COMPLETE, got {p2_status.status}"
|
|
)
|
|
|
|
|
|
# ========================================================================
|
|
# Scenario: Parallel safety-net for uncaptured futures (lines 331-334)
|
|
# ========================================================================
|
|
|
|
|
|
@given("a subplan execution service in parallel fail-fast mode with 3 subplans")
|
|
def step_parallel_failfast_3(context: Context) -> None:
|
|
context.cb_statuses = [_make_status(_P1), _make_status(_P2), _make_status(_P3)]
|
|
context.cb_config = SubplanConfig(
|
|
execution_mode=ExecutionMode.PARALLEL,
|
|
max_parallel=3,
|
|
fail_fast=True,
|
|
)
|
|
|
|
|
|
@given("as_completed is patched to skip the third future and the first subplan fails")
|
|
def step_patch_as_completed_skip_third(context: Context) -> None:
|
|
"""Record that the first subplan will fail and as_completed should skip _P3's future."""
|
|
context.skip_subplan_id = _P3
|
|
context.fail_subplan_id = _P1
|
|
|
|
|
|
@when("parallel execution is invoked with patched as_completed")
|
|
def step_invoke_parallel_patched_as_completed(context: Context) -> None:
|
|
skip_id = context.skip_subplan_id
|
|
fail_id = context.fail_subplan_id
|
|
|
|
def _failing_executor(status: SubplanStatus) -> SubplanExecutionOutput:
|
|
if status.subplan_id == fail_id:
|
|
return SubplanExecutionOutput(
|
|
subplan_id=status.subplan_id,
|
|
success=False,
|
|
error="ValidationError: forced failure",
|
|
)
|
|
return _ok_output(status)
|
|
|
|
service = SubplanExecutionService(
|
|
config=context.cb_config,
|
|
executor_fn=_failing_executor,
|
|
)
|
|
|
|
# We need to intercept as_completed to skip the future for _P3.
|
|
# We patch it at the module level where it's imported.
|
|
import cleveragents.application.services.subplan_execution_service as svc_mod
|
|
|
|
original_as_completed = svc_mod.as_completed
|
|
|
|
def _filtered_as_completed(fs, **kwargs):
|
|
"""Yield all futures except the one for the skipped subplan."""
|
|
for future in original_as_completed(fs, **kwargs):
|
|
# fs is the future_to_id dict; check if this future maps to skip_id
|
|
if isinstance(fs, dict) and fs.get(future) == skip_id:
|
|
continue
|
|
yield future
|
|
|
|
with patch.object(svc_mod, "as_completed", _filtered_as_completed):
|
|
context.cb_result = service._execute_parallel(context.cb_statuses)
|
|
|
|
|
|
@then("the third subplan should be marked CANCELLED by the safety net")
|
|
def step_third_cancelled_safety_net(context: Context) -> None:
|
|
updated_statuses, _outputs = context.cb_result
|
|
p3_status = next(s for s in updated_statuses if s.subplan_id == _P3)
|
|
assert p3_status.status == ProcessingState.CANCELLED, (
|
|
f"Expected CANCELLED for P3 (safety net), got {p3_status.status}"
|
|
)
|
|
|
|
|
|
@then("the results should contain all 3 subplans")
|
|
def step_results_contain_all_3(context: Context) -> None:
|
|
updated_statuses, _outputs = context.cb_result
|
|
ids = {s.subplan_id for s in updated_statuses}
|
|
assert _P1 in ids and _P2 in ids and _P3 in ids, (
|
|
f"Expected all 3 subplan IDs, got {ids}"
|
|
)
|
|
|
|
|
|
# ========================================================================
|
|
# Scenario: Dependency-ordered empty ready list break (line 381)
|
|
# ========================================================================
|
|
|
|
|
|
@given("a subplan execution service in dependency-ordered mode with 2 subplans")
|
|
def step_dep_ordered_2(context: Context) -> None:
|
|
context.cb_statuses = [_make_status(_P1), _make_status(_P2)]
|
|
context.cb_config = SubplanConfig(
|
|
execution_mode=ExecutionMode.DEPENDENCY_ORDERED,
|
|
)
|
|
|
|
|
|
@given("topological sort is patched to accept a circular dependency graph")
|
|
def step_patch_topo_sort(context: Context) -> None:
|
|
"""We'll provide a circular graph but patch _topological_sort to not raise."""
|
|
context.circular_graph = {
|
|
_P1: [_P2],
|
|
_P2: [_P1],
|
|
}
|
|
|
|
|
|
@when("dependency-ordered execution is invoked with the circular graph")
|
|
def step_invoke_dep_ordered_circular(context: Context) -> None:
|
|
service = SubplanExecutionService(
|
|
config=context.cb_config,
|
|
executor_fn=_ok_output,
|
|
)
|
|
|
|
# Patch _topological_sort to accept the circular graph (return node_ids as-is)
|
|
def _fake_topo_sort(node_ids, graph):
|
|
return list(node_ids)
|
|
|
|
with patch.object(
|
|
SubplanExecutionService, "_topological_sort", staticmethod(_fake_topo_sort)
|
|
):
|
|
context.cb_result = service._execute_dependency_ordered(
|
|
context.cb_statuses, context.circular_graph
|
|
)
|
|
|
|
|
|
@then("execution should complete without raising an error")
|
|
def step_no_error(context: Context) -> None:
|
|
# If we got here, no exception was raised
|
|
assert context.cb_result is not None
|
|
|
|
|
|
@then("the number of completed subplans should be less than 2")
|
|
def step_fewer_completed(context: Context) -> None:
|
|
updated_statuses, _outputs = context.cb_result
|
|
# With a circular graph and patched topo sort, the while loop should break
|
|
# when ready becomes empty. Neither _P1 nor _P2 can ever become ready because
|
|
# each depends on the other (remaining_deps is never emptied).
|
|
assert len(updated_statuses) < 2, (
|
|
f"Expected fewer than 2 completed statuses, got {len(updated_statuses)}"
|
|
)
|
|
|
|
|
|
# ========================================================================
|
|
# Scenario: Wave CancelledError (lines 573-575)
|
|
# ========================================================================
|
|
|
|
|
|
@given(
|
|
"a subplan execution service in dependency-ordered mode with 3 independent subplans"
|
|
)
|
|
def step_dep_ordered_3_independent(context: Context) -> None:
|
|
context.cb_statuses = [_make_status(_P1), _make_status(_P2), _make_status(_P3)]
|
|
context.cb_config = SubplanConfig(
|
|
execution_mode=ExecutionMode.DEPENDENCY_ORDERED,
|
|
max_parallel=3,
|
|
)
|
|
# All independent (no dependencies) -> single wave of 3
|
|
context.dep_graph = {_P1: [], _P2: [], _P3: []}
|
|
|
|
|
|
@given(
|
|
"the internal retry method is patched to raise CancelledError "
|
|
"for the second subplan in wave"
|
|
)
|
|
def step_patch_retry_cancelled(context: Context) -> None:
|
|
context.wave_cancel_subplan_id = _P2
|
|
|
|
|
|
@when("dependency-ordered execution is invoked for the wave scenario")
|
|
def step_invoke_wave_cancelled(context: Context) -> None:
|
|
cancel_id = context.wave_cancel_subplan_id
|
|
|
|
service = SubplanExecutionService(
|
|
config=context.cb_config,
|
|
executor_fn=_ok_output,
|
|
)
|
|
|
|
original_method = service._execute_one_with_retry
|
|
|
|
def _patched_retry(status: SubplanStatus):
|
|
if status.subplan_id == cancel_id:
|
|
raise CancelledError("simulated cancellation")
|
|
return original_method(status)
|
|
|
|
with patch.object(service, "_execute_one_with_retry", side_effect=_patched_retry):
|
|
context.cb_result = service._execute_dependency_ordered(
|
|
context.cb_statuses, context.dep_graph
|
|
)
|
|
|
|
|
|
@then("the second subplan should be marked CANCELLED")
|
|
def step_second_cancelled(context: Context) -> None:
|
|
updated_statuses, _outputs = context.cb_result
|
|
p2_status = next(s for s in updated_statuses if s.subplan_id == _P2)
|
|
assert p2_status.status == ProcessingState.CANCELLED, (
|
|
f"Expected CANCELLED, got {p2_status.status}"
|
|
)
|
|
|
|
|
|
@then("the other subplans in the wave should complete successfully")
|
|
def step_others_complete_wave(context: Context) -> None:
|
|
updated_statuses, _outputs = context.cb_result
|
|
for status in updated_statuses:
|
|
if status.subplan_id == _P2:
|
|
continue
|
|
assert status.status == ProcessingState.COMPLETE, (
|
|
f"Expected COMPLETE for {status.subplan_id}, got {status.status}"
|
|
)
|
|
|
|
|
|
# ========================================================================
|
|
# Scenario: Wave generic Exception (lines 576-578)
|
|
# ========================================================================
|
|
|
|
|
|
@given(
|
|
"a subplan execution service in dependency-ordered mode "
|
|
"with 3 independent subplans for exception test"
|
|
)
|
|
def step_dep_ordered_3_independent_exc(context: Context) -> None:
|
|
context.cb_statuses = [_make_status(_P1), _make_status(_P2), _make_status(_P3)]
|
|
context.cb_config = SubplanConfig(
|
|
execution_mode=ExecutionMode.DEPENDENCY_ORDERED,
|
|
max_parallel=3,
|
|
)
|
|
context.dep_graph = {_P1: [], _P2: [], _P3: []}
|
|
|
|
|
|
@given(
|
|
"the internal retry method is patched to raise ValueError "
|
|
"for the second subplan in wave"
|
|
)
|
|
def step_patch_retry_valueerror(context: Context) -> None:
|
|
context.wave_error_subplan_id = _P2
|
|
context.wave_error_exception = ValueError("bad value in wave")
|
|
|
|
|
|
@when("dependency-ordered execution is invoked for the wave exception scenario")
|
|
def step_invoke_wave_exception(context: Context) -> None:
|
|
error_id = context.wave_error_subplan_id
|
|
error_exc = context.wave_error_exception
|
|
|
|
service = SubplanExecutionService(
|
|
config=context.cb_config,
|
|
executor_fn=_ok_output,
|
|
)
|
|
|
|
original_method = service._execute_one_with_retry
|
|
|
|
def _patched_retry(status: SubplanStatus):
|
|
if status.subplan_id == error_id:
|
|
raise error_exc
|
|
return original_method(status)
|
|
|
|
with patch.object(service, "_execute_one_with_retry", side_effect=_patched_retry):
|
|
context.cb_result = service._execute_dependency_ordered(
|
|
context.cb_statuses, context.dep_graph
|
|
)
|
|
|
|
|
|
@then("the second subplan should be marked ERRORED with the ValueError message")
|
|
def step_second_errored_valueerror(context: Context) -> None:
|
|
updated_statuses, _outputs = context.cb_result
|
|
p2_status = next(s for s in updated_statuses if s.subplan_id == _P2)
|
|
assert p2_status.status == ProcessingState.ERRORED, (
|
|
f"Expected ERRORED, got {p2_status.status}"
|
|
)
|
|
assert "bad value in wave" in (p2_status.error or ""), (
|
|
f"Expected ValueError message in error, got: {p2_status.error}"
|
|
)
|
|
|
|
|
|
@then("the other wave subplans should still complete")
|
|
def step_others_complete_wave_exc(context: Context) -> None:
|
|
updated_statuses, _outputs = context.cb_result
|
|
for status in updated_statuses:
|
|
if status.subplan_id == _P2:
|
|
continue
|
|
assert status.status == ProcessingState.COMPLETE, (
|
|
f"Expected COMPLETE for {status.subplan_id}, got {status.status}"
|
|
)
|