forked from HAL9000/cleveragents-core
ab911dbdc4
The format_output() function returned a string that callers passed to Rich console.print(), which wraps long lines at terminal width. This injected literal newline characters into JSON string values (e.g. in definition_of_done fields), producing invalid JSON that downstream parsers could not decode (JSONDecodeError: Invalid control character). For machine-readable formats (json, yaml, plain), format_output() now writes the rendered output directly to sys.stdout and returns an empty string. This preserves the exact serialization from json.dumps/ yaml.dump without Rich text processing artifacts. Refs: #746
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 = "01ERRCOV0000000000000001"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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}"
|
|
)
|