forked from HAL9000/cleveragents-core
02250473ad
Fix all failing CI quality gates (lint, unit_tests, format) without suppressing any quality enforcement. Root causes and fixes: 1. Format: features/steps/plan_namespaced_name_tdd_steps.py had trailing whitespace; fixed by running ruff format. 2. Unit tests - A2A JSON-RPC 2.0 migration (commit9c6d6915) renamed A2aRequest fields (operation→method, request_id→id, a2a_version→jsonrpc) and A2aResponse fields (status+data→result, request_id→id) but did not update all step files and feature files: - a2a_jsonrpc_wire_format_steps.py: added use_step_matcher('re') and reset to 'parse' at end to prevent parallel test interference - a2a_facade_wiring_steps.py: updated operation= to method=, .status/.data to .result - a2a_facade_steps.py: updated request_id→id, a2a_version→jsonrpc, A2aResponse(request_id=..., status=...) to new API - m6_facade_steps.py: updated all old API usage - devcontainer_cleanup_steps.py: updated A2aRequest(operation=...) - plan_prompt_command_steps.py: updated A2aRequest(operation=...) - wf03_plan_prompt_confidence_steps.py: updated A2aRequest(operation=...) - consolidated_misc.feature: updated old A2aRequest/A2aResponse scenarios 3. Unit tests - Session CLI output changed (commit0d5d9cf0and others): - 'Session Created' → 'Session created' (lowercase) - 'Session Details' → 'Session Summary' - 'Sessions (N total)' → 'Sessions' - session list JSON: top-level 'total' → nested 'summary.total' - Fixed in: session_cli.feature, session_cli_coverage_boost.feature, session_cli_uncovered_branches.feature, session_list_error.feature, tdd_session_create_persist_steps.py 4. Unit tests - Plan list output changed (commit1a07a891): - 'V3 Lifecycle Plans' → 'Plans' - 'Lifecycle Plans' → 'Plans' - Name column removed (restored in source) - Invariants column removed (restored in source) - Project truncation removed (restored in source) - Fixed in: plan_cli_cancel_revert_coverage.feature, plan_lifecycle_cli_coverage.feature, plan_cli_coverage_boost_steps.py, plan.py (source code restored) 5. Unit tests - Plan apply command now requires ULID (commit300a5d6d): - plan_cli_coverage_r3.feature: updated 'PLAN-001' to valid ULID - plan_cli_coverage_r3_steps.py: added --yes flag, added new step for no-eligible-plans path 6. Unit tests - Various source code bugs: - ThoughtBlock: converted from @dataclass to Pydantic BaseModel (architecture test requires all dataclasses to use Pydantic) - session.py: added DatabaseError handling to export, import, tell commands - database.py: fixed rollback_to() to reuse checkpoint connection for writes - database.py: added _get_checkpoint_conn() helper - check-tls-cert.py: fixed SSLCertVerificationError.reason AttributeError 7. Unit tests - Test step bugs: - error_recovery_coverage_boost_steps.py: fixed invalid ULID _PLAN_ID - session_service_coverage_steps.py: fixed 'sha256:' prefix bug in checksum - database_models_new_coverage_steps.py: added 'name' field to session mock - async_audit_recording_steps.py: fixed Settings(audit_async=False) via env var - coverage_threshold_config_steps.py: added --coverage-min pattern support - m5_acms_smoke_steps.py: updated usage hint text - actor_cli_yaml_steps.py: updated 'Removed actor' → 'Actor removed' - aimodelscredentials_steps.py: set context.imported_class in import step - domain_base_model.feature: added missing 'When I examine model_config' step - tui_first_run_steps.py: fixed module reload to restore cleveragents.tui.* modules after test (prevented patch interference in subsequent tests) - tui_first_run_steps.py: added set_search('') step for empty string - resource_handler_base_coverage_r3_steps.py: use _MinimalHandler instead of DatabaseResourceHandler for NotImplementedError tests - resource_handler_crud.feature: updated to test new DatabaseHandler behavior - resource_handler_sandbox.feature: updated to test new DatabaseHandler behavior - tdd_json_decode_crash_persistence.feature: fixed @tdd_bug → @tdd_issue tags 8. Parallel test interference: - All step files using use_step_matcher('re') now reset to 'parse' at end to prevent global matcher state leaking to subsequent step files
736 lines
25 KiB
Python
736 lines
25 KiB
Python
"""Step definitions for error_recovery_coverage_boost.feature.
|
|
|
|
Covers uncovered paths in plan.py (errors/diff/artifacts CLI commands),
|
|
plan_executor.py (edge cases, retry loop), and error_recovery.py (policy
|
|
branches) introduced by the error recovery feature.
|
|
|
|
All step patterns use unique 'errcov' prefix to avoid AmbiguousStep collisions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
from io import StringIO
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.core.exceptions import CleverAgentsError, PlanError, ValidationError
|
|
from cleveragents.domain.models.core.error_recovery import (
|
|
ErrorCategory,
|
|
ErrorRecord,
|
|
ErrorRecoveryPolicy,
|
|
get_recovery_hints,
|
|
)
|
|
from cleveragents.domain.models.core.plan import (
|
|
PlanPhase,
|
|
PlanTimestamps,
|
|
ProcessingState,
|
|
)
|
|
|
|
__all__: list[str] = []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_PLAN_ID = "01ERRCVV000000000000000001"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_capture_console(buf: StringIO):
|
|
"""Create a Rich Console that writes to the buffer for assertion."""
|
|
from rich.console import Console
|
|
|
|
return Console(file=buf, force_terminal=False, width=200)
|
|
|
|
|
|
def _make_mock_plan(
|
|
*,
|
|
plan_id: str = _PLAN_ID,
|
|
phase: PlanPhase = PlanPhase.EXECUTE,
|
|
state: ProcessingState = ProcessingState.ERRORED,
|
|
error_message: str | None = None,
|
|
error_details: dict | None = None,
|
|
) -> MagicMock:
|
|
plan = MagicMock()
|
|
plan.identity.plan_id = plan_id
|
|
plan.phase = phase
|
|
plan.state = state
|
|
plan.processing_state = state
|
|
plan.is_terminal = False
|
|
plan.error_message = error_message
|
|
plan.error_details = error_details
|
|
plan.timestamps = PlanTimestamps()
|
|
return plan
|
|
|
|
|
|
def _capture_plan_cli(context: Context, args: list[str]) -> str:
|
|
"""Invoke a plan CLI command, capturing console output."""
|
|
from cleveragents.cli.commands import plan as plan_mod
|
|
|
|
buf = StringIO()
|
|
try:
|
|
with patch.object(
|
|
plan_mod.console,
|
|
"print",
|
|
side_effect=lambda *a, **kw: buf.write(str(a[0]) + "\n" if a else "\n"),
|
|
):
|
|
# Parse which command to call from args
|
|
cmd = args[0]
|
|
if cmd == "errors":
|
|
plan_mod.plan_errors(
|
|
plan_id=args[1],
|
|
fmt=args[3] if len(args) > 3 else "rich",
|
|
)
|
|
elif cmd == "diff":
|
|
plan_mod.plan_diff(
|
|
plan_id=args[1],
|
|
fmt=args[3] if len(args) > 3 else "rich",
|
|
)
|
|
elif cmd == "artifacts":
|
|
plan_mod.plan_artifacts(
|
|
plan_id=args[1],
|
|
fmt=args[3] if len(args) > 3 else "rich",
|
|
)
|
|
except (SystemExit, Exception):
|
|
pass
|
|
return buf.getvalue()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("an errcov test environment")
|
|
def step_errcov_env(context: Context) -> None:
|
|
context.errcov_plan = None
|
|
context.errcov_output = ""
|
|
context.errcov_service = None
|
|
context.errcov_error = None
|
|
context.errcov_apply_svc = None
|
|
context.errcov_lifecycle_svc = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Plan errors CLI -- Given steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('an errcov plan with error details category "{cat}" phase "{phase}"')
|
|
def step_errcov_plan_with_details(context: Context, cat: str, phase: str) -> None:
|
|
context.errcov_plan = _make_mock_plan(
|
|
error_details={
|
|
"error_category": cat,
|
|
"error_phase": phase,
|
|
"retry_count": "0",
|
|
"max_retries": "3",
|
|
"is_retriable": "true",
|
|
},
|
|
)
|
|
|
|
|
|
@given('the errcov plan has error message "{msg}"')
|
|
def step_errcov_plan_error_msg(context: Context, msg: str) -> None:
|
|
context.errcov_plan.error_message = msg
|
|
|
|
|
|
@given('the errcov plan error details include actor "{actor}" and tool "{tool}"')
|
|
def step_errcov_plan_actor_tool(context: Context, actor: str, tool: str) -> None:
|
|
context.errcov_plan.error_details["error_actor"] = actor
|
|
context.errcov_plan.error_details["error_tool_call"] = tool
|
|
|
|
|
|
@given('the errcov plan error details include stack summary "{summary}"')
|
|
def step_errcov_plan_stack(context: Context, summary: str) -> None:
|
|
context.errcov_plan.error_details["stack_summary"] = summary
|
|
|
|
|
|
@given(
|
|
'the errcov plan error details include retry count "{rc}" max "{mx}" retriable "{retr}"'
|
|
)
|
|
def step_errcov_plan_retry_info(context: Context, rc: str, mx: str, retr: str) -> None:
|
|
context.errcov_plan.error_details["retry_count"] = rc
|
|
context.errcov_plan.error_details["max_retries"] = mx
|
|
context.errcov_plan.error_details["is_retriable"] = retr
|
|
|
|
|
|
@given("an errcov plan with no error details and no error message")
|
|
def step_errcov_plan_clean(context: Context) -> None:
|
|
context.errcov_plan = _make_mock_plan(
|
|
error_details=None,
|
|
error_message=None,
|
|
)
|
|
|
|
|
|
@given('an errcov plan with no error details but has error message "{msg}"')
|
|
def step_errcov_plan_msg_only(context: Context, msg: str) -> None:
|
|
context.errcov_plan = _make_mock_plan(
|
|
error_details=None,
|
|
error_message=msg,
|
|
)
|
|
|
|
|
|
@given("an errcov lifecycle service that raises CleverAgentsError on get_plan")
|
|
def step_errcov_lifecycle_error(context: Context) -> None:
|
|
svc = MagicMock()
|
|
svc.get_plan.side_effect = CleverAgentsError("plan lookup failed")
|
|
context.errcov_lifecycle_svc = svc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Plan errors CLI -- When steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I invoke errcov plan errors in "{fmt}" format')
|
|
def step_errcov_invoke_errors(context: Context, fmt: str) -> None:
|
|
import contextlib
|
|
|
|
from cleveragents.cli.commands import plan as plan_mod
|
|
|
|
svc = MagicMock()
|
|
svc.get_plan.return_value = context.errcov_plan
|
|
|
|
buf = StringIO()
|
|
capture_console = _make_capture_console(buf)
|
|
with (
|
|
patch.object(
|
|
plan_mod,
|
|
"_get_lifecycle_service",
|
|
return_value=svc,
|
|
),
|
|
patch.object(plan_mod, "console", capture_console),
|
|
contextlib.redirect_stdout(buf),
|
|
):
|
|
plan_mod.plan_errors(plan_id=_PLAN_ID, fmt=fmt)
|
|
context.errcov_output = buf.getvalue()
|
|
|
|
|
|
@when("I invoke errcov plan errors expecting abort")
|
|
def step_errcov_invoke_errors_abort(context: Context) -> None:
|
|
from cleveragents.cli.commands import plan as plan_mod
|
|
|
|
buf = StringIO()
|
|
capture_console = _make_capture_console(buf)
|
|
with (
|
|
patch.object(
|
|
plan_mod,
|
|
"_get_lifecycle_service",
|
|
return_value=context.errcov_lifecycle_svc,
|
|
),
|
|
patch.object(plan_mod, "console", capture_console),
|
|
contextlib.suppress(SystemExit, Exception),
|
|
):
|
|
plan_mod.plan_errors(plan_id=_PLAN_ID, fmt="rich")
|
|
context.errcov_output = buf.getvalue()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Plan diff CLI -- Given/When steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('an errcov apply service returning diff text "{text}"')
|
|
def step_errcov_apply_diff(context: Context, text: str) -> None:
|
|
svc = MagicMock()
|
|
svc.diff.return_value = text
|
|
context.errcov_apply_svc = svc
|
|
|
|
|
|
@given("an errcov apply service that raises PlanError on diff")
|
|
def step_errcov_apply_diff_plan_error(context: Context) -> None:
|
|
svc = MagicMock()
|
|
svc.diff.side_effect = PlanError("diff failed")
|
|
context.errcov_apply_svc = svc
|
|
|
|
|
|
@given("an errcov apply service that raises CleverAgentsError on diff")
|
|
def step_errcov_apply_diff_agent_error(context: Context) -> None:
|
|
svc = MagicMock()
|
|
svc.diff.side_effect = CleverAgentsError("diff service error")
|
|
context.errcov_apply_svc = svc
|
|
|
|
|
|
@when('I invoke errcov plan diff in "{fmt}" format')
|
|
def step_errcov_invoke_diff(context: Context, fmt: str) -> None:
|
|
from cleveragents.cli.commands import plan as plan_mod
|
|
|
|
buf = StringIO()
|
|
capture_console = _make_capture_console(buf)
|
|
with (
|
|
patch.object(
|
|
plan_mod, "_get_apply_service", return_value=context.errcov_apply_svc
|
|
),
|
|
patch.object(plan_mod, "console", capture_console),
|
|
contextlib.redirect_stdout(buf),
|
|
):
|
|
plan_mod.plan_diff(plan_id=_PLAN_ID, fmt=fmt)
|
|
context.errcov_output = buf.getvalue()
|
|
|
|
|
|
@when("I invoke errcov plan diff expecting abort")
|
|
def step_errcov_invoke_diff_abort(context: Context) -> None:
|
|
from cleveragents.cli.commands import plan as plan_mod
|
|
|
|
buf = StringIO()
|
|
capture_console = _make_capture_console(buf)
|
|
with (
|
|
patch.object(
|
|
plan_mod, "_get_apply_service", return_value=context.errcov_apply_svc
|
|
),
|
|
patch.object(plan_mod, "console", capture_console),
|
|
contextlib.suppress(SystemExit, Exception),
|
|
):
|
|
plan_mod.plan_diff(plan_id=_PLAN_ID, fmt="rich")
|
|
context.errcov_output = buf.getvalue()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Plan artifacts CLI -- Given/When steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('an errcov apply service returning artifacts text "{text}"')
|
|
def step_errcov_apply_artifacts(context: Context, text: str) -> None:
|
|
svc = MagicMock()
|
|
svc.artifacts.return_value = text
|
|
context.errcov_apply_svc = svc
|
|
|
|
|
|
@given("an errcov apply service that raises PlanError on artifacts")
|
|
def step_errcov_apply_artifacts_plan_error(context: Context) -> None:
|
|
svc = MagicMock()
|
|
svc.artifacts.side_effect = PlanError("artifacts failed")
|
|
context.errcov_apply_svc = svc
|
|
|
|
|
|
@given("an errcov apply service that raises CleverAgentsError on artifacts")
|
|
def step_errcov_apply_artifacts_agent_error(context: Context) -> None:
|
|
svc = MagicMock()
|
|
svc.artifacts.side_effect = CleverAgentsError("artifacts service error")
|
|
context.errcov_apply_svc = svc
|
|
|
|
|
|
@when('I invoke errcov plan artifacts in "{fmt}" format')
|
|
def step_errcov_invoke_artifacts(context: Context, fmt: str) -> None:
|
|
from cleveragents.cli.commands import plan as plan_mod
|
|
|
|
buf = StringIO()
|
|
capture_console = _make_capture_console(buf)
|
|
with (
|
|
patch.object(
|
|
plan_mod, "_get_apply_service", return_value=context.errcov_apply_svc
|
|
),
|
|
patch.object(plan_mod, "console", capture_console),
|
|
contextlib.redirect_stdout(buf),
|
|
):
|
|
plan_mod.plan_artifacts(plan_id=_PLAN_ID, fmt=fmt)
|
|
context.errcov_output = buf.getvalue()
|
|
|
|
|
|
@when("I invoke errcov plan artifacts expecting abort")
|
|
def step_errcov_invoke_artifacts_abort(context: Context) -> None:
|
|
from cleveragents.cli.commands import plan as plan_mod
|
|
|
|
buf = StringIO()
|
|
capture_console = _make_capture_console(buf)
|
|
with (
|
|
patch.object(
|
|
plan_mod, "_get_apply_service", return_value=context.errcov_apply_svc
|
|
),
|
|
patch.object(plan_mod, "console", capture_console),
|
|
contextlib.suppress(SystemExit, Exception),
|
|
):
|
|
plan_mod.plan_artifacts(plan_id=_PLAN_ID, fmt="rich")
|
|
context.errcov_output = buf.getvalue()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _get_apply_service
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I invoke errcov _get_apply_service")
|
|
def step_errcov_get_apply_svc(context: Context) -> None:
|
|
from cleveragents.cli.commands import plan as plan_mod
|
|
|
|
mock_lifecycle = MagicMock()
|
|
with patch.object(plan_mod, "_get_lifecycle_service", return_value=mock_lifecycle):
|
|
context.errcov_apply_svc = plan_mod._get_apply_service()
|
|
|
|
|
|
@then("the errcov apply service should be a PlanApplyService instance")
|
|
def step_errcov_apply_svc_type(context: Context) -> None:
|
|
from cleveragents.application.services.plan_apply_service import PlanApplyService
|
|
|
|
assert isinstance(context.errcov_apply_svc, PlanApplyService), (
|
|
f"Expected PlanApplyService, got {type(context.errcov_apply_svc)}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PlanExecutor edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I invoke errcov strategize with empty plan_id")
|
|
def step_errcov_strategize_empty(context: Context) -> None:
|
|
from cleveragents.application.services.plan_executor import StrategizeStubActor
|
|
|
|
actor = StrategizeStubActor()
|
|
try:
|
|
actor.execute(plan_id="", definition_of_done="test", stream_callback=None)
|
|
context.errcov_error = None
|
|
except ValidationError as exc:
|
|
context.errcov_error = exc
|
|
|
|
|
|
@when("I invoke errcov execute stub with empty plan_id")
|
|
def step_errcov_execute_stub_empty(context: Context) -> None:
|
|
from cleveragents.application.services.plan_executor import ExecuteStubActor
|
|
|
|
actor = ExecuteStubActor()
|
|
try:
|
|
actor.execute(
|
|
plan_id="",
|
|
decisions=[],
|
|
tool_runner=MagicMock(),
|
|
sandbox_root=None,
|
|
stream_callback=None,
|
|
)
|
|
context.errcov_error = None
|
|
except ValidationError as exc:
|
|
context.errcov_error = exc
|
|
|
|
|
|
@given("an errcov plan in strategize phase")
|
|
def step_errcov_plan_strategize(context: Context) -> None:
|
|
plan = _make_mock_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.QUEUED)
|
|
lifecycle = MagicMock()
|
|
lifecycle.get_plan.return_value = plan
|
|
context.errcov_lifecycle_svc = lifecycle
|
|
context.errcov_plan = plan
|
|
|
|
|
|
@given("an errcov plan in execute phase but processing state")
|
|
def step_errcov_plan_execute_processing(context: Context) -> None:
|
|
plan = _make_mock_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.PROCESSING)
|
|
lifecycle = MagicMock()
|
|
lifecycle.get_plan.return_value = plan
|
|
context.errcov_lifecycle_svc = lifecycle
|
|
context.errcov_plan = plan
|
|
|
|
|
|
@when("I invoke errcov run_execute on wrong-phase plan")
|
|
def step_errcov_execute_wrong_phase(context: Context) -> None:
|
|
from cleveragents.application.services.plan_executor import PlanExecutor
|
|
|
|
executor = PlanExecutor(
|
|
lifecycle_service=context.errcov_lifecycle_svc,
|
|
tool_runner=MagicMock(),
|
|
)
|
|
try:
|
|
executor.run_execute(plan_id=_PLAN_ID)
|
|
context.errcov_error = None
|
|
except PlanError as exc:
|
|
context.errcov_error = exc
|
|
|
|
|
|
@when("I invoke errcov run_execute on wrong-state plan")
|
|
def step_errcov_execute_wrong_state(context: Context) -> None:
|
|
from cleveragents.application.services.plan_executor import PlanExecutor
|
|
|
|
executor = PlanExecutor(
|
|
lifecycle_service=context.errcov_lifecycle_svc,
|
|
tool_runner=MagicMock(),
|
|
)
|
|
try:
|
|
executor.run_execute(plan_id=_PLAN_ID)
|
|
context.errcov_error = None
|
|
except PlanError as exc:
|
|
context.errcov_error = exc
|
|
|
|
|
|
@when("I parse errcov definition of done with blank lines")
|
|
def step_errcov_parse_dod_blank(context: Context) -> None:
|
|
from cleveragents.application.services.plan_executor import StrategizeStubActor
|
|
|
|
actor = StrategizeStubActor()
|
|
context.errcov_parsed_steps = actor._parse_steps("Step one\n\n\nStep two\n\n")
|
|
|
|
|
|
@when('I parse errcov definition of done with numbered items "{text}"')
|
|
def step_errcov_parse_dod_numbered(context: Context, text: str) -> None:
|
|
from cleveragents.application.services.plan_executor import StrategizeStubActor
|
|
|
|
actor = StrategizeStubActor()
|
|
context.errcov_parsed_steps = actor._parse_steps(text.replace("\\n", "\n"))
|
|
|
|
|
|
@given("an errcov executor with error recovery service")
|
|
def step_errcov_executor_with_recovery(context: Context) -> None:
|
|
from cleveragents.application.services.error_recovery_service import (
|
|
ErrorRecoveryService,
|
|
)
|
|
from cleveragents.application.services.plan_executor import PlanExecutor
|
|
|
|
plan = _make_mock_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.QUEUED)
|
|
plan.definition_of_done = "Test passes"
|
|
plan.metadata = {}
|
|
plan.invariants = []
|
|
|
|
lifecycle = MagicMock()
|
|
lifecycle.get_plan.return_value = plan
|
|
|
|
er_service = ErrorRecoveryService(
|
|
lifecycle_service=lifecycle,
|
|
auto_retry_threshold=0.0,
|
|
max_retries=3,
|
|
)
|
|
|
|
context.errcov_lifecycle_svc = lifecycle
|
|
context.errcov_er_service = er_service
|
|
context.errcov_executor = PlanExecutor(
|
|
lifecycle_service=lifecycle,
|
|
tool_runner=MagicMock(),
|
|
error_recovery_service=er_service,
|
|
)
|
|
|
|
|
|
@given('the errcov strategize actor raises RuntimeError "{msg}"')
|
|
def step_errcov_strategize_fails(context: Context, msg: str) -> None:
|
|
mock_actor = MagicMock()
|
|
mock_actor.execute.side_effect = RuntimeError(msg)
|
|
context.errcov_executor._strategize_actor = mock_actor
|
|
|
|
|
|
@when("I invoke errcov run_strategize expecting failure")
|
|
def step_errcov_run_strategize_fail(context: Context) -> None:
|
|
with contextlib.suppress(RuntimeError):
|
|
context.errcov_executor.run_strategize(plan_id=_PLAN_ID)
|
|
|
|
|
|
@then("the errcov error recovery should have recorded a strategize error")
|
|
def step_errcov_er_strategize_recorded(context: Context) -> None:
|
|
history = context.errcov_er_service.get_error_history(_PLAN_ID)
|
|
assert history.total_errors >= 1, (
|
|
f"Expected at least 1 error recorded, got {history.total_errors}"
|
|
)
|
|
assert history.records[0].phase == "strategize"
|
|
|
|
|
|
@given("an errcov executor without error recovery service")
|
|
def step_errcov_executor_no_recovery(context: Context) -> None:
|
|
from cleveragents.application.services.plan_executor import PlanExecutor
|
|
|
|
plan = _make_mock_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED)
|
|
plan.decision_root_id = "01ROOTDECISION0000000001"
|
|
plan.definition_of_done = "Tests pass"
|
|
plan.metadata = {}
|
|
|
|
lifecycle = MagicMock()
|
|
lifecycle.get_plan.return_value = plan
|
|
|
|
context.errcov_lifecycle_svc = lifecycle
|
|
context.errcov_executor = PlanExecutor(
|
|
lifecycle_service=lifecycle,
|
|
tool_runner=MagicMock(),
|
|
)
|
|
|
|
|
|
@given('the errcov execute actor always fails with "{msg}"')
|
|
def step_errcov_execute_always_fails(context: Context, msg: str) -> None:
|
|
context.errcov_executor._execute_actor = MagicMock()
|
|
context.errcov_executor._execute_actor.execute.side_effect = RuntimeError(msg)
|
|
|
|
|
|
@when("I invoke errcov run_execute expecting failure")
|
|
def step_errcov_run_execute_fail(context: Context) -> None:
|
|
with contextlib.suppress(RuntimeError):
|
|
context.errcov_executor.run_execute(plan_id=_PLAN_ID)
|
|
|
|
|
|
@then("the errcov lifecycle should have called fail_execute")
|
|
def step_errcov_fail_execute_called(context: Context) -> None:
|
|
context.errcov_lifecycle_svc.fail_execute.assert_called_once()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# error_recovery.py policy edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("an errcov policy with threshold {threshold} and max retries {mr}")
|
|
def step_errcov_policy(context: Context, threshold: str, mr: str) -> None:
|
|
context.errcov_policy = ErrorRecoveryPolicy(
|
|
auto_retry_threshold=float(threshold),
|
|
max_retries=int(mr),
|
|
)
|
|
|
|
|
|
@given("an errcov retriable error record with retry count {rc} max {mr}")
|
|
def step_errcov_retriable_record(context: Context, rc: str, mr: str) -> None:
|
|
context.errcov_error_record = ErrorRecord(
|
|
error_id="ERR-ERRCOV-001",
|
|
phase="execute",
|
|
category=ErrorCategory.TRANSIENT,
|
|
message="timeout",
|
|
retry_count=int(rc),
|
|
max_retries=int(mr),
|
|
)
|
|
|
|
|
|
@given("an errcov retriable error record with hints retry count {rc} max {mr}")
|
|
def step_errcov_retriable_record_hints(context: Context, rc: str, mr: str) -> None:
|
|
hints = get_recovery_hints(ErrorCategory.TRANSIENT, _PLAN_ID)
|
|
context.errcov_error_record = ErrorRecord(
|
|
error_id="ERR-ERRCOV-002",
|
|
phase="execute",
|
|
category=ErrorCategory.TRANSIENT,
|
|
message="timeout with hints",
|
|
retry_count=int(rc),
|
|
max_retries=int(mr),
|
|
recovery_hints=hints,
|
|
)
|
|
|
|
|
|
@when("I format errcov recovery output")
|
|
def step_errcov_format_recovery(context: Context) -> None:
|
|
context.errcov_recovery_output = context.errcov_policy.format_recovery_output(
|
|
context.errcov_error_record
|
|
)
|
|
|
|
|
|
@then("the errcov policy should not recommend retry")
|
|
def step_errcov_no_retry(context: Context) -> None:
|
|
assert not context.errcov_policy.should_retry(context.errcov_error_record)
|
|
|
|
|
|
@then("the errcov policy should recommend escalation")
|
|
def step_errcov_escalation(context: Context) -> None:
|
|
assert context.errcov_policy.should_escalate(context.errcov_error_record)
|
|
|
|
|
|
@then('the errcov recovery output should contain "{text}"')
|
|
def step_errcov_recovery_contains(context: Context, text: str) -> None:
|
|
assert text in context.errcov_recovery_output, (
|
|
f"Expected '{text}' in recovery output, got: {context.errcov_recovery_output[:300]}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Plan use command branches (actor_registry, testing_mode)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("an errcov plan creation environment with actor registry")
|
|
def step_errcov_plan_use_actor_reg(context: Context) -> None:
|
|
context.errcov_actor_registry = MagicMock()
|
|
context.errcov_container = MagicMock()
|
|
|
|
|
|
@given("an errcov plan creation environment with testing mode")
|
|
def step_errcov_plan_use_testing(context: Context) -> None:
|
|
context.errcov_actor_registry = None
|
|
context.errcov_container = MagicMock()
|
|
|
|
|
|
@when("I invoke errcov plan use with actor registry")
|
|
def step_errcov_plan_use_with_registry(context: Context) -> None:
|
|
"""Test that actor_registry.ensure_built_in_actors() is called."""
|
|
from contextlib import suppress
|
|
|
|
actor_reg = context.errcov_actor_registry
|
|
with suppress(Exception):
|
|
if actor_reg:
|
|
actor_reg.ensure_built_in_actors()
|
|
context.errcov_output = "done"
|
|
|
|
|
|
@when("I invoke errcov plan use with testing mode")
|
|
def step_errcov_plan_use_with_testing(context: Context) -> None:
|
|
"""Test that container.actor_service().ensure_default_mock_actor() is called in testing mode."""
|
|
from contextlib import suppress
|
|
|
|
container = context.errcov_container
|
|
testing_mode = True
|
|
with suppress(Exception):
|
|
if testing_mode:
|
|
container.actor_service().ensure_default_mock_actor()
|
|
context.errcov_output = "done"
|
|
|
|
|
|
@then("the errcov actor registry ensure_built_in_actors should have been called")
|
|
def step_errcov_actor_reg_called(context: Context) -> None:
|
|
context.errcov_actor_registry.ensure_built_in_actors.assert_called_once()
|
|
|
|
|
|
@then("the errcov container actor_service should have been called")
|
|
def step_errcov_container_actor_svc_called(context: Context) -> None:
|
|
context.errcov_container.actor_service.assert_called()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared Then steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('the errcov errors output should contain "{text}"')
|
|
def step_errcov_errors_contains(context: Context, text: str) -> None:
|
|
assert text in context.errcov_output, (
|
|
f"Expected '{text}' in output, got: {context.errcov_output[:500]}"
|
|
)
|
|
|
|
|
|
@then('the errcov output should contain "{text}"')
|
|
def step_errcov_output_contains(context: Context, text: str) -> None:
|
|
assert text in context.errcov_output, (
|
|
f"Expected '{text}' in output, got: {context.errcov_output[:500]}"
|
|
)
|
|
|
|
|
|
@then('an errcov ValidationError should be raised with "{text}"')
|
|
def step_errcov_validation_error(context: Context, text: str) -> None:
|
|
assert context.errcov_error is not None, "Expected ValidationError but none raised"
|
|
assert isinstance(context.errcov_error, ValidationError), (
|
|
f"Expected ValidationError, got {type(context.errcov_error)}"
|
|
)
|
|
assert text in str(context.errcov_error), (
|
|
f"Expected error to contain '{text}', got: {context.errcov_error}"
|
|
)
|
|
|
|
|
|
@then('an errcov PlanError should be raised containing "{text}"')
|
|
def step_errcov_plan_error(context: Context, text: str) -> None:
|
|
assert context.errcov_error is not None, "Expected PlanError but none raised"
|
|
assert isinstance(context.errcov_error, PlanError), (
|
|
f"Expected PlanError, got {type(context.errcov_error)}"
|
|
)
|
|
assert text in str(context.errcov_error), (
|
|
f"Expected error to contain '{text}', got: {context.errcov_error}"
|
|
)
|
|
|
|
|
|
@then("the parsed steps should not contain empty entries")
|
|
def step_errcov_no_empty_steps(context: Context) -> None:
|
|
for s in context.errcov_parsed_steps:
|
|
assert s.strip() != "", f"Found empty step: '{s}'"
|
|
assert len(context.errcov_parsed_steps) == 2, (
|
|
f"Expected 2 steps, got {len(context.errcov_parsed_steps)}"
|
|
)
|
|
|
|
|
|
@then('the errcov parsed steps should be "{a}" and "{b}" and "{c}"')
|
|
def step_errcov_parsed_numbered(context: Context, a: str, b: str, c: str) -> None:
|
|
assert context.errcov_parsed_steps == [a, b, c], (
|
|
f"Expected [{a}, {b}, {c}], got {context.errcov_parsed_steps}"
|
|
)
|