Files
placeholder/features/steps/retry_service_patterns_coverage_steps.py
freemo 051ee7c290 test(coverage): add Behave BDD tests to improve coverage across 52 source files
Added 52 new .feature files and corresponding _steps.py files targeting
previously uncovered code paths in the following areas:

- TUI layer: app, commands, persona (state/schema/registry), widgets,
  input (shell_exec, reference_parser)
- Application services: plan lifecycle/service/executor, session,
  project, repo indexing, correction, checkpoint, actor, llm_actors,
  strategy coordinator, resource file watcher, service retry wiring
- CLI commands: session, resource, repl, plan, db, automation_profile
- Domain models: retry_policy, resource_type, cost_budget,
  docker_compose_analyzer, detail_level, _sql_string_aware,
  _postgresql_helpers
- Core: circuit_breaker, retry_service_patterns
- Infrastructure: repositories, transaction_sandbox, strategy_registry,
  plugins/loader, container
- Config: settings
- Agents: plan_generation, context_analysis, auto_debug
- A2A: facade

All new tests follow the Behave/Gherkin BDD standard. Resolved step
definition collisions with unique prefixes. Fixed Alembic fileConfig
logger disabling issue (disable_existing_loggers=False).

ISSUES CLOSED: #1068
2026-03-20 21:22:10 +00:00

418 lines
14 KiB
Python

"""Step definitions for retry_service_patterns_coverage.feature.
Targets uncovered lines in retry_service_patterns.py:
- Line 82: _is_async_callable partial(async coroutine) → True
(unreachable on Python 3.13 where iscoroutinefunction handles
partials natively; covered indirectly via partial(callable_obj))
- Line 85: _is_async_callable partial(callable_obj_with_async_call) → True
- Line 86: _is_async_callable partial(SomeClass) → False
- Line 90: _is_async_callable(non_callable) → False
- Line 254: async retry_service_operation CircuitBreakerOpen with no breaker
- Lines 444-446: RetryContext.execute rejects async callables
- Lines 452-453: RetryContext.execute nesting guard
- Lines 507-508: RetryContext.async_execute nesting guard
- Line 482: RetryContext.execute RuntimeError safeguard
- Line 537: RetryContext.async_execute RuntimeError safeguard
- Lines 575-577: retry_auto_debug rejects sync callables
- Line 639: retry_auto_debug returns None with zero attempts
"""
import asyncio
from functools import partial
from unittest.mock import patch
from behave import given, then, when
from cleveragents.core.retry_patterns import CircuitBreakerOpen
from cleveragents.core.retry_service_patterns import (
RetryContext,
_is_async_callable,
_retry_depth,
retry_auto_debug,
retry_service_operation,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _run_async(coro):
"""Run a coroutine in a fresh event loop."""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
# ---------------------------------------------------------------------------
# Scenario: _is_async_callable detects partial wrapping async callable object
# (line 85 — partial wrapping a non-type callable with async __call__)
# ---------------------------------------------------------------------------
@given("I have a functools.partial wrapping a callable object with async __call__")
def step_partial_wrapping_async_callable_obj(context):
class AsyncCallableObj:
async def __call__(self, x):
return x
context.partial_async_callable = partial(AsyncCallableObj(), 42)
@when("I check if the partial wrapping async callable is async")
def step_check_partial_async_callable(context):
context.is_async_callable_result = _is_async_callable(
context.partial_async_callable
)
@then("the async callable partial result should be True")
def step_assert_partial_async_callable_true(context):
assert context.is_async_callable_result is True, (
f"Expected True, got {context.is_async_callable_result}"
)
# ---------------------------------------------------------------------------
# Scenario: _is_async_callable returns False for partial wrapping a class type
# (line 86)
# ---------------------------------------------------------------------------
@given("I have a functools.partial wrapping a class type")
def step_partial_wrapping_class(context):
class MyClass:
def __init__(self, x=10):
self.x = x
context.partial_type = partial(MyClass, x=5)
@when("I check if the partial-wrapped type is async callable")
def step_check_partial_type_async(context):
context.is_async_result_type = _is_async_callable(context.partial_type)
@then("rspcov the result should be False for partial type detection")
def step_assert_partial_type_false(context):
assert context.is_async_result_type is False, (
f"Expected False, got {context.is_async_result_type}"
)
# ---------------------------------------------------------------------------
# Scenario: _is_async_callable returns False for non-callable (line 90)
# ---------------------------------------------------------------------------
@given("I have a plain integer value")
def step_plain_integer(context):
context.non_callable = 42
@when("I check if the integer is async callable")
def step_check_integer_async(context):
context.is_async_integer_result = _is_async_callable(context.non_callable)
@then("rspcov the result should be False for the non-callable value")
def step_assert_integer_false(context):
assert context.is_async_integer_result is False, (
f"Expected False, got {context.is_async_integer_result}"
)
# ---------------------------------------------------------------------------
# Scenario: Async retry_service_operation with CircuitBreakerOpen, no breaker
# (line 254)
# ---------------------------------------------------------------------------
@given(
"I have an async function decorated with retry_service_operation"
" and no circuit breaker"
)
def step_async_decorated_no_breaker(context):
@retry_service_operation(
service_name="test-svc",
operation_name="test-op",
max_attempts=2,
base_delay=0.01,
max_delay=0.02,
circuit_breaker=None,
total_timeout=5.0,
)
async def _failing_op():
raise CircuitBreakerOpen("circuit is open")
context.async_cb_func = _failing_op
@given("the async function raises CircuitBreakerOpen")
def step_noop_marker(_context):
pass # Configured in the previous step
@when("I call the decorated async service operation")
def step_call_async_service_op(context):
context.cb_error = None
try:
_run_async(context.async_cb_func())
except CircuitBreakerOpen as exc:
context.cb_error = exc
@then("CircuitBreakerOpen should propagate with failure count zero")
def step_assert_cb_open_propagated(context):
assert context.cb_error is not None, "Expected CircuitBreakerOpen to be raised"
assert isinstance(context.cb_error, CircuitBreakerOpen)
# ---------------------------------------------------------------------------
# Scenario: RetryContext.execute rejects async callables (lines 444-446)
# ---------------------------------------------------------------------------
@given('I have a RetryContext for operation "{name}"')
def step_create_retry_context(context, name):
context.retry_ctx = RetryContext(operation_name=name, max_attempts=2)
@given("I have an async callable to pass to sync execute")
def step_create_async_for_sync(context):
async def _async_fn():
return "async_result"
context.async_for_sync = _async_fn
@when("I call execute with the async callable")
def step_call_execute_with_async(context):
context.type_error = None
try:
context.retry_ctx.execute(context.async_for_sync)
except TypeError as exc:
context.type_error = exc
@then("a TypeError should be raised indicating async_execute is needed")
def step_assert_type_error_async(context):
assert context.type_error is not None, "Expected TypeError to be raised"
assert "async_execute" in str(context.type_error)
# ---------------------------------------------------------------------------
# Scenario: RetryContext.execute nesting guard (lines 452-453)
# ---------------------------------------------------------------------------
@given("the retry depth is already at maximum")
def step_set_max_depth(context):
# Set depth to 1 (the max nesting depth) so the guard kicks in
context.depth_token = _retry_depth.set(1)
def restore():
_retry_depth.reset(context.depth_token)
context.add_cleanup(restore)
@when("I call execute at max nesting depth with a sync function")
def step_call_execute_at_max_depth(context):
context.nesting_call_count = 0
def _sync_fn():
context.nesting_call_count += 1
return "nested_result"
context.nesting_result = context.retry_ctx.execute(_sync_fn)
@then("the function should execute once without retry wrapping")
def step_assert_executed_once(context):
assert context.nesting_result == "nested_result"
assert context.nesting_call_count == 1
@then("the attempt count should be 1")
def step_assert_attempt_count_1(context):
assert context.retry_ctx.attempt_count == 1
# ---------------------------------------------------------------------------
# Scenario: RetryContext.async_execute nesting guard (lines 507-508)
# ---------------------------------------------------------------------------
@given("the retry depth is already at maximum for async")
def step_set_max_depth_async(context):
context.depth_token_async = _retry_depth.set(1)
def restore():
_retry_depth.reset(context.depth_token_async)
context.add_cleanup(restore)
@when("I call async_execute at max nesting depth")
def step_call_async_execute_at_max_depth(context):
context.async_nesting_call_count = 0
async def _async_fn():
context.async_nesting_call_count += 1
return "async_nested_result"
context.async_nesting_result = _run_async(
context.retry_ctx.async_execute(_async_fn)
)
@then("the async function should execute once without retry wrapping")
def step_assert_async_executed_once(context):
assert context.async_nesting_result == "async_nested_result"
assert context.async_nesting_call_count == 1
@then("the async attempt count should be 1")
def step_assert_async_attempt_count_1(context):
assert context.retry_ctx.attempt_count == 1
# ---------------------------------------------------------------------------
# Scenario: RetryContext.execute RuntimeError safeguard (line 482)
# ---------------------------------------------------------------------------
@given("Retrying is patched to yield zero iterations")
def step_patch_retrying_empty(context):
# Patch is applied in the When step; this step documents intent.
pass
@when("I call execute expecting a RuntimeError from empty retrying")
def step_call_execute_empty_retrying(context):
"""Patch Retrying to return an empty iterator, then call execute."""
class _EmptyRetrying:
def __init__(self, **kwargs):
pass
def __iter__(self):
return iter([])
context.execute_runtime_error = None
with patch("cleveragents.core.retry_service_patterns.Retrying", _EmptyRetrying):
try:
context.retry_ctx.execute(lambda: "should_not_run")
except RuntimeError as exc:
context.execute_runtime_error = exc
@then("a RuntimeError about Retrying executing at least once should be raised")
def step_assert_retrying_runtime_error(context):
assert context.execute_runtime_error is not None, (
"Expected RuntimeError to be raised"
)
assert "at least once" in str(context.execute_runtime_error)
# ---------------------------------------------------------------------------
# Scenario: RetryContext.async_execute RuntimeError safeguard (line 537)
# ---------------------------------------------------------------------------
@given("AsyncRetrying is patched to yield zero async iterations")
def step_patch_async_retrying_empty(context):
pass # Patch applied in the When step
@when("I call async_execute expecting a RuntimeError from empty async retrying")
def step_call_async_execute_empty_retrying(context):
"""Patch AsyncRetrying to return an empty async iterator."""
class _EmptyAsyncRetrying:
def __init__(self, **kwargs):
pass
def __aiter__(self):
return self
async def __anext__(self):
raise StopAsyncIteration
async def _async_fn():
return "should_not_run"
context.async_execute_runtime_error = None
with patch(
"cleveragents.core.retry_service_patterns.AsyncRetrying",
_EmptyAsyncRetrying,
):
try:
_run_async(context.retry_ctx.async_execute(_async_fn))
except RuntimeError as exc:
context.async_execute_runtime_error = exc
@then("a RuntimeError about AsyncRetrying executing at least once should be raised")
def step_assert_async_retrying_runtime_error(context):
assert context.async_execute_runtime_error is not None, (
"Expected RuntimeError to be raised"
)
assert "at least once" in str(context.async_execute_runtime_error)
# ---------------------------------------------------------------------------
# Scenario: retry_auto_debug rejects sync callables (lines 575-577)
# ---------------------------------------------------------------------------
@given("I have a synchronous function decorated with retry_auto_debug")
def step_sync_func_with_auto_debug(context):
def _sync_fn():
return "sync_result"
context.auto_debug_sync_wrapper = retry_auto_debug(max_debug_attempts=3)(_sync_fn)
@when("I invoke the retry_auto_debug wrapper for the sync function")
def step_invoke_auto_debug_sync(context):
context.auto_debug_type_error = None
try:
_run_async(context.auto_debug_sync_wrapper())
except TypeError as exc:
context.auto_debug_type_error = exc
@then("a TypeError about async callables should be raised")
def step_assert_auto_debug_type_error(context):
assert context.auto_debug_type_error is not None, "Expected TypeError to be raised"
assert "async" in str(context.auto_debug_type_error).lower()
# ---------------------------------------------------------------------------
# Scenario: retry_auto_debug returns None with zero attempts (line 639)
# ---------------------------------------------------------------------------
@given("I have an async function decorated with retry_auto_debug with zero attempts")
def step_auto_debug_zero_attempts(context):
async def _async_fn():
return "should_not_be_called"
context.auto_debug_zero_wrapper = retry_auto_debug(max_debug_attempts=0)(_async_fn)
@when("I invoke the retry_auto_debug wrapper with zero attempts")
def step_invoke_auto_debug_zero(context):
context.auto_debug_zero_result = _run_async(context.auto_debug_zero_wrapper())
@then("rspcov the result should be None from exhausted attempts")
def step_assert_auto_debug_none(context):
assert context.auto_debug_zero_result is None, (
f"Expected None, got {context.auto_debug_zero_result!r}"
)