Files
cleveragents-core/coverage_boost_steps.py
T
HAL9000 e10463419e
CI / push-validation (pull_request) Successful in 9s
CI / helm (pull_request) Successful in 25s
CI / lint (pull_request) Failing after 40s
CI / typecheck (pull_request) Successful in 55s
CI / build (pull_request) Successful in 3m18s
CI / quality (pull_request) Successful in 3m41s
CI / integration_tests (pull_request) Successful in 4m0s
CI / security (pull_request) Successful in 4m27s
CI / coverage (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 5m43s
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 7m43s
CI / status-check (pull_request) Failing after 2s
fix(cli): add timing.started ISO timestamp to plan prompt JSON envelope
- Capture started_at timestamp using datetime.now(UTC) before service call
- Add timing.started field to JSON envelope with ISO 8601 format
- Update step definitions to verify timing.started is present and valid
- Remove @tdd_expected_fail tag from plan_prompt_command.feature scenario

Fixes #9353
2026-04-14 17:57:53 +00:00

294 lines
12 KiB
Python

"""Step definitions for cross_plan_correction_coverage_boost.feature.
Targets uncovered lines in cross_plan_correction_service.py:
- Lines 60, 76, 92: Protocol method bodies (the ``...`` stubs)
- Line 127: ``raise ValidationError`` for unrecognised ChildPlanState
- Line 276: ``else []`` branch when rejection is None on rejected cascade
- Lines 412-416: ``except`` handler in ``_rollback_completed_actions``
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from behave import given, then, when
from cleveragents.application.services.cross_plan_correction_service import (
ChildPlanCanceller,
ChildPlanLookup,
CrossPlanCorrectionService,
SandboxRollbacker,
classify_cascade_action,
)
from cleveragents.core.exceptions import ValidationError
from cleveragents.domain.models.core.correction import (
CascadeAction,
CascadeResult,
ChildPlanState,
)
# -------------------------------------------------------------------
# Protocol-body subclasses — call super() to execute the ``...`` stub
# -------------------------------------------------------------------
class _LookupSubclass(ChildPlanLookup):
"""Calls super() to exercise ChildPlanLookup protocol body."""
def get_child_plan_state(self, child_plan_id: str) -> ChildPlanState:
result = super().get_child_plan_state(child_plan_id) # type: ignore[safe-super]
return result # type: ignore[return-value]
class _CancellerSubclass(ChildPlanCanceller):
"""Calls super() to exercise ChildPlanCanceller protocol body."""
def cancel_child_plan(self, child_plan_id: str) -> None:
return super().cancel_child_plan(child_plan_id) # type: ignore[safe-super]
class _RollbackerSubclass(SandboxRollbacker):
"""Calls super() to exercise SandboxRollbacker protocol body."""
def rollback_child_plan_sandbox(self, child_plan_id: str) -> None:
return super().rollback_child_plan_sandbox(child_plan_id) # type: ignore[safe-super]
# -------------------------------------------------------------------
# Simple mock implementations for service construction
# -------------------------------------------------------------------
class _SimpleLookup:
"""Minimal lookup returning NOT_STARTED for any plan."""
def get_child_plan_state(self, child_plan_id: str) -> ChildPlanState:
return ChildPlanState.NOT_STARTED
class _SimpleCanceller:
"""Minimal canceller that records calls."""
def __init__(self) -> None:
self.cancelled: list[str] = []
def cancel_child_plan(self, child_plan_id: str) -> None:
self.cancelled.append(child_plan_id)
class _SimpleRollbacker:
"""Minimal rollbacker that records calls."""
def __init__(self) -> None:
self.rolled_back: list[str] = []
def rollback_child_plan_sandbox(self, child_plan_id: str) -> None:
self.rolled_back.append(child_plan_id)
# ===================================================================
# Protocol body coverage — lines 60, 76, 92
# ===================================================================
@when("I call ChildPlanLookup protocol body through a subclass")
def step_call_lookup_protocol_body(context: object) -> None:
instance = _LookupSubclass()
context.protocol_result = instance.get_child_plan_state("test-plan") # type: ignore[attr-defined]
@then("the protocol body should return None")
def step_protocol_body_none(context: object) -> None:
# The ``...`` (Ellipsis) expression is the protocol body. It
# evaluates as an expression statement and the method implicitly
# returns None.
assert context.protocol_result is None, ( # type: ignore[attr-defined]
f"Expected None, got {context.protocol_result}" # type: ignore[attr-defined]
)
@when("I call ChildPlanCanceller protocol body through a subclass")
def step_call_canceller_protocol_body(context: object) -> None:
instance = _CancellerSubclass()
context.canceller_protocol_result = instance.cancel_child_plan("test-plan") # type: ignore[attr-defined]
@then("the canceller protocol body should return None")
def step_canceller_protocol_body_none(context: object) -> None:
assert context.canceller_protocol_result is None, ( # type: ignore[attr-defined]
f"Expected None, got {context.canceller_protocol_result}" # type: ignore[attr-defined]
)
@when("I call SandboxRollbacker protocol body through a subclass")
def step_call_rollbacker_protocol_body(context: object) -> None:
instance = _RollbackerSubclass()
context.rollbacker_protocol_result = instance.rollback_child_plan_sandbox(
"test-plan"
) # type: ignore[attr-defined]
@then("the rollbacker protocol body should return None")
def step_rollbacker_protocol_body_none(context: object) -> None:
assert context.rollbacker_protocol_result is None, ( # type: ignore[attr-defined]
f"Expected None, got {context.rollbacker_protocol_result}" # type: ignore[attr-defined]
)
# ===================================================================
# classify_cascade_action with unrecognised state — line 127
# ===================================================================
@when("I classify cascade action for an unrecognised state value")
def step_classify_unrecognised_state(context: object) -> None:
try:
# Pass a plain string that does not match any ChildPlanState member.
classify_cascade_action("totally_unknown_state") # type: ignore[arg-type]
context.boost_error = None # type: ignore[attr-defined]
except ValidationError as exc:
context.boost_error = exc # type: ignore[attr-defined]
@then('a cascade ValidationError should be raised mentioning "{fragment}"')
def step_cascade_validation_error_with_fragment(context: object, fragment: str) -> None:
assert context.boost_error is not None, (
"Expected ValidationError but none was raised"
) # type: ignore[attr-defined]
assert isinstance(context.boost_error, ValidationError), ( # type: ignore[attr-defined]
f"Expected ValidationError, got {type(context.boost_error)}" # type: ignore[attr-defined]
)
assert fragment in str(context.boost_error), ( # type: ignore[attr-defined]
f"'{fragment}' not found in error message: {context.boost_error}" # type: ignore[attr-defined]
)
# ===================================================================
# execute_cascade — rejected=True, rejection=None — line 276
# ===================================================================
@given("a cross-plan correction service for coverage boost")
def step_create_boost_service(context: object) -> None:
lookup = _SimpleLookup()
canceller = _SimpleCanceller()
rollbacker = _SimpleRollbacker()
context.boost_lookup = lookup # type: ignore[attr-defined]
context.boost_canceller = canceller # type: ignore[attr-defined]
context.boost_rollbacker = rollbacker # type: ignore[attr-defined]
context.boost_service = CrossPlanCorrectionService( # type: ignore[attr-defined]
plan_lookup=lookup,
plan_canceller=canceller,
sandbox_rollbacker=rollbacker,
)
context.boost_cascade_result = None # type: ignore[attr-defined]
context.boost_error = None # type: ignore[attr-defined]
@given("evaluate_cascade is patched to return rejected with no rejection object")
def step_patch_evaluate_cascade(context: object) -> None:
"""Prepare a patched evaluate_cascade that returns rejected=True
but rejection=None, which normally never happens but the code
defensively handles it (line 276 else branch)."""
fake_result = CascadeResult(
correction_id="C-BOOST",
cascade_actions=[],
rejected=True,
rejection=None,
all_cancelled_plan_ids=[],
all_rolled_back_plan_ids=[],
)
context.boost_fake_result = fake_result # type: ignore[attr-defined]
@when('I execute a cascade for correction "{cid}" with child plans "{plans}"')
def step_execute_cascade_boost(context: object, cid: str, plans: str) -> None:
plan_ids = [p.strip() for p in plans.split(",")]
service = context.boost_service # type: ignore[attr-defined]
# If a fake result was prepared, patch evaluate_cascade to return it
if hasattr(context, "boost_fake_result") and context.boost_fake_result is not None: # type: ignore[attr-defined]
original_evaluate = service.evaluate_cascade
def patched_evaluate(
correction_id: str, affected_child_plan_ids: list[str]
) -> CascadeResult:
return context.boost_fake_result # type: ignore[attr-defined]
service.evaluate_cascade = patched_evaluate # type: ignore[attr-defined]
try:
context.boost_cascade_result = service.execute_cascade(cid, plan_ids) # type: ignore[attr-defined]
finally:
service.evaluate_cascade = original_evaluate # type: ignore[attr-defined]
else:
context.boost_cascade_result = service.execute_cascade(cid, plan_ids) # type: ignore[attr-defined]
@then("the cascade result should be rejected with no rejection details")
def step_cascade_rejected_no_rejection(context: object) -> None:
result = context.boost_cascade_result # type: ignore[attr-defined]
assert result is not None, "No cascade result captured"
assert result.rejected is True, "Expected cascade to be rejected"
assert result.rejection is None, (
f"Expected rejection to be None, got {result.rejection}"
)
# ===================================================================
# _rollback_completed_actions exception handler — lines 412-416
# ===================================================================
@given("the module logger info method is patched to raise an error")
def step_patch_logger_to_raise(context: object) -> None:
"""Mark that the logger should be patched to raise during rollback."""
context.boost_patch_logger = True # type: ignore[attr-defined]
@when("I call rollback_completed_actions with one completed action")
def step_call_rollback_with_failing_logger(context: object) -> None:
service = context.boost_service # type: ignore[attr-defined]
action = CascadeAction(
child_plan_id="CP-ROLLBACK-TEST",
child_plan_state=ChildPlanState.NOT_STARTED,
action="cancel",
sandbox_rolled_back=False,
)
# Patch the module-level logger so that logger.info raises during
# _rollback_completed_actions, hitting lines 412-416.
mock_logger = MagicMock()
mock_logger.info.side_effect = RuntimeError("Simulated logging failure")
# logger.error must still work so the except block can log the error
mock_logger.error = MagicMock()
with patch(
"cleveragents.application.services.cross_plan_correction_service.logger",
mock_logger,
):
try:
service._rollback_completed_actions([action])
context.boost_rollback_error = None # type: ignore[attr-defined]
except Exception as exc:
context.boost_rollback_error = exc # type: ignore[attr-defined]
# Store the mock for assertion
context.boost_mock_logger = mock_logger # type: ignore[attr-defined]
@then("the rollback should complete without raising")
def step_rollback_no_raise(context: object) -> None:
assert context.boost_rollback_error is None, ( # type: ignore[attr-defined]
f"Expected no error, but got: {context.boost_rollback_error}" # type: ignore[attr-defined]
)
# Verify the except handler called logger.error (line 413-416)
mock_logger = context.boost_mock_logger # type: ignore[attr-defined]
assert mock_logger.error.called, (
"Expected logger.error to be called in the except handler"
)
# Check that the error log included the expected event key
call_args = mock_logger.error.call_args
assert "cross_plan_correction.rollback_action_failed" in str(call_args), (
f"Expected rollback_action_failed event, got: {call_args}"
)