Files
temp/features/steps/tool_lifecycle_coverage_boost_steps.py
2026-04-02 16:53:03 +00:00

413 lines
15 KiB
Python

"""Step definitions for tool lifecycle coverage boost feature tests.
Targets uncovered lines and branches in src/cleveragents/tool/lifecycle.py:
- Line 253: ToolLifecycleCache.remove returns None when tool not in cache
- Line 394: ToolRuntime.activate re-raises ToolCancelledError
- Line 473: ToolRuntime.execute raises ToolNotActivatedError for None instance
- Lines 496-502: ToolRuntime.execute handles ToolCancelledError with trace
- Line 550: ToolRuntime.deactivate early return when instance not in cache
- Branch 585:exit: deactivate_plan with empty instances dict
- Branch 235:exit: get_plan_tools iterating over empty cache
- Branch 248:253: remove when tool not found / plan not found
- Branch 250:252: remove pops entry successfully
"""
from __future__ import annotations
from typing import Any
from unittest.mock import patch
from behave import given, then, when
from behave.runner import Context
from cleveragents.domain.models.core.tool import (
Tool,
ToolCapability,
ToolSource,
ToolType,
)
from cleveragents.infrastructure.events.reactive import ReactiveEventBus
from cleveragents.tool.context import (
CancellationToken,
ToolCancelledError,
ToolExecutionContext,
)
from cleveragents.tool.lifecycle import (
ToolDescriptor,
ToolExecutionError,
ToolLifecycleCache,
ToolNotActivatedError,
ToolResult,
ToolRuntime,
)
# ---------------------------------------------------------------------------
# Mock tool instances for coverage-boost scenarios
# ---------------------------------------------------------------------------
class _CBMockToolInstance:
"""Basic mock ToolInstance for coverage-boost tests."""
def __init__(self, name: str = "mock/tool", read_only: bool = True) -> None:
self.name = name
self.read_only = read_only
self.activated = False
self.deactivated = False
def discover(self) -> ToolDescriptor:
return ToolDescriptor(
name=self.name,
description=f"Coverage-boost mock {self.name}",
capability=ToolCapability(read_only=self.read_only),
)
def activate(self, ctx: ToolExecutionContext) -> None:
self.activated = True
def execute(self, params: dict[str, Any], ctx: ToolExecutionContext) -> ToolResult:
return ToolResult(success=True, data={"result": "ok"})
def deactivate(self, ctx: ToolExecutionContext) -> None:
self.deactivated = True
class _CancellingActivateInstance(_CBMockToolInstance):
"""Mock that raises ToolCancelledError on activate()."""
def activate(self, ctx: ToolExecutionContext) -> None:
raise ToolCancelledError("Cancelled during activation")
class _CancellingExecuteInstance(_CBMockToolInstance):
"""Mock that raises ToolCancelledError on execute()."""
def activate(self, ctx: ToolExecutionContext) -> None:
self.activated = True
def execute(self, params: dict[str, Any], ctx: ToolExecutionContext) -> ToolResult:
raise ToolCancelledError("Cancelled during execution")
class _NullCacheInstance(_CBMockToolInstance):
"""Mock whose activate succeeds but we manipulate the cache afterwards."""
def activate(self, ctx: ToolExecutionContext) -> None:
self.activated = True
# ---------------------------------------------------------------------------
# Helper to create Tool domain models
# ---------------------------------------------------------------------------
def _cb_make_tool(
name: str,
*,
read_only: bool = True,
writes: bool = False,
) -> Tool:
"""Create a minimal Tool domain model for coverage-boost tests."""
return Tool(
name=name,
description=f"Coverage-boost test tool {name}",
source=ToolSource.BUILTIN,
tool_type=ToolType.TOOL,
capability=ToolCapability(
read_only=read_only,
writes=writes,
),
)
# ---------------------------------------------------------------------------
# ToolLifecycleCache steps
# ---------------------------------------------------------------------------
@given("I create a coverage-boost lifecycle cache")
def step_create_cb_cache(context: Context) -> None:
context.cb_cache = ToolLifecycleCache()
context.cb_remove_result = None
@given('I have a coverage-boost mock tool instance named "{name}"')
def step_create_cb_mock(context: Context, name: str) -> None:
context.cb_mock_instance = _CBMockToolInstance(name=name)
@given('I have a second coverage-boost mock tool instance named "{name}"')
def step_create_cb_mock_second(context: Context, name: str) -> None:
context.cb_mock_instance_2 = _CBMockToolInstance(name=name)
@when(
'I put the coverage-boost instance in cache for plan "{plan_id}" and tool "{tool_name}"'
)
def step_cb_cache_put(context: Context, plan_id: str, tool_name: str) -> None:
context.cb_cache.put(plan_id, tool_name, context.cb_mock_instance)
@when(
'I put the second coverage-boost instance in cache for plan "{plan_id}" and tool "{tool_name}"'
)
def step_cb_cache_put_second(context: Context, plan_id: str, tool_name: str) -> None:
context.cb_cache.put(plan_id, tool_name, context.cb_mock_instance_2)
@when('I remove tool "{tool_name}" from coverage-boost cache plan "{plan_id}"')
def step_cb_cache_remove(context: Context, tool_name: str, plan_id: str) -> None:
context.cb_remove_result = context.cb_cache.remove(plan_id, tool_name)
@then("the coverage-boost cache should have {count:d} plans")
def step_cb_cache_plan_count(context: Context, count: int) -> None:
assert context.cb_cache.plan_count == count, (
f"Expected {count} plans, got {context.cb_cache.plan_count}"
)
@then('the coverage-boost cache internal dict should not contain plan "{plan_id}"')
def step_cb_cache_no_plan_key(context: Context, plan_id: str) -> None:
# Access internal _cache dict to verify the plan key was fully deleted
assert plan_id not in context.cb_cache._cache, (
f"Plan '{plan_id}' should have been removed from internal _cache dict"
)
@then("the coverage-boost remove result should be None")
def step_cb_remove_result_none(context: Context) -> None:
assert context.cb_remove_result is None, (
f"Expected None, got {context.cb_remove_result}"
)
@then('the coverage-boost plan tools for plan "{plan_id}" should be an empty list')
def step_cb_plan_tools_empty(context: Context, plan_id: str) -> None:
tools = context.cb_cache.get_plan_tools(plan_id)
assert tools == [], f"Expected empty list, got {tools}"
@then(
'the coverage-boost cache get for plan "{plan_id}" and tool "{tool_name}" should not be None'
)
def step_cb_cache_get_not_none(context: Context, plan_id: str, tool_name: str) -> None:
result = context.cb_cache.get(plan_id, tool_name)
assert result is not None, f"Expected non-None for plan={plan_id} tool={tool_name}"
# ---------------------------------------------------------------------------
# ToolRuntime steps — coverage-boost context and runtime setup
# ---------------------------------------------------------------------------
@given("I create a coverage-boost tool runtime")
def step_create_cb_runtime(context: Context) -> None:
context.cb_runtime = ToolRuntime(event_bus=ReactiveEventBus())
context.cb_mocks: dict[str, _CBMockToolInstance] = {}
context.cb_error = None
@given('I create a coverage-boost execution context with plan_id "{plan_id}"')
def step_create_cb_ctx(context: Context, plan_id: str) -> None:
token = CancellationToken()
context.cb_ctx = ToolExecutionContext(
plan_id=plan_id,
cancellation_token=token,
)
# ---------------------------------------------------------------------------
# ToolRuntime.activate — ToolCancelledError re-raise (line 394)
# ---------------------------------------------------------------------------
@given('I register a coverage-boost cancelling-activate tool "{name}"')
def step_register_cb_cancel_activate(context: Context, name: str) -> None:
tool = _cb_make_tool(name, read_only=True)
mock = _CancellingActivateInstance(name=name)
context.cb_runtime.register_tool(tool, mock)
context.cb_mocks[name] = mock
@when('I try to activate the cancelling tool "{name}" in coverage-boost runtime')
def step_try_cb_activate_cancel(context: Context, name: str) -> None:
try:
context.cb_runtime.activate(name, context.cb_ctx)
context.cb_error = None
except ToolCancelledError as exc:
context.cb_error = exc
except Exception as exc:
context.cb_error = exc
@then("a ToolCancelledError should have been raised in coverage-boost activation")
def step_check_cb_activation_cancelled(context: Context) -> None:
assert isinstance(context.cb_error, ToolCancelledError), (
f"Expected ToolCancelledError, got {type(context.cb_error).__name__}: {context.cb_error}"
)
# ---------------------------------------------------------------------------
# ToolRuntime.execute — None cached instance (line 473)
# ---------------------------------------------------------------------------
@given('I register a coverage-boost tool "{name}" with a mock that nullifies cache')
def step_register_cb_null_cache(context: Context, name: str) -> None:
tool = _cb_make_tool(name, read_only=True)
mock = _NullCacheInstance(name=name)
context.cb_runtime.register_tool(tool, mock)
context.cb_mocks[name] = mock
@when('I try to execute the null-cache tool "{name}" in coverage-boost runtime')
def step_try_cb_execute_null_cache(context: Context, name: str) -> None:
# We need activate to succeed (so it passes the auto-activate gate)
# but cache.get to return None right after.
# Strategy: patch the cache's get method to return None after activate
# has put the instance into the cache.
original_get = context.cb_runtime._cache.get
def _patched_get(plan_id: str, tool_name: str) -> Any:
# First call is in activate() to check if already cached — return None
# so activate proceeds. After activate puts it, the second call is in
# execute() at line 471 — we remove it from cache first so it returns None.
result = original_get(plan_id, tool_name)
if result is not None:
# Remove it so the next get (line 471) returns None
context.cb_runtime._cache.remove(plan_id, tool_name)
return None
return result
try:
with patch.object(context.cb_runtime._cache, "get", side_effect=_patched_get):
context.cb_runtime.execute(name, {}, context.cb_ctx)
context.cb_error = None
except ToolNotActivatedError as exc:
context.cb_error = exc
except Exception as exc:
context.cb_error = exc
@then("a ToolNotActivatedError should have been raised in coverage-boost execution")
def step_check_cb_not_activated(context: Context) -> None:
# ToolNotActivatedError is raised at line 473 but caught by the generic
# except Exception handler at line 504 and wrapped in ToolExecutionError.
is_direct = isinstance(context.cb_error, ToolNotActivatedError)
is_wrapped = (
isinstance(context.cb_error, ToolExecutionError)
and "not activated" in str(context.cb_error).lower()
)
assert is_direct or is_wrapped, (
f"Expected ToolNotActivatedError (or ToolExecutionError wrapping it), "
f"got {type(context.cb_error).__name__}: {context.cb_error}"
)
# ---------------------------------------------------------------------------
# ToolRuntime.execute — ToolCancelledError during execution (lines 496-502)
# ---------------------------------------------------------------------------
@given('I register a coverage-boost cancelling-execute tool "{name}"')
def step_register_cb_cancel_execute(context: Context, name: str) -> None:
tool = _cb_make_tool(name, read_only=True)
mock = _CancellingExecuteInstance(name=name)
context.cb_runtime.register_tool(tool, mock)
context.cb_mocks[name] = mock
@when('I try to execute the cancelling-execute tool "{name}" in coverage-boost runtime')
def step_try_cb_execute_cancel(context: Context, name: str) -> None:
try:
context.cb_runtime.execute(name, {}, context.cb_ctx)
context.cb_error = None
except ToolCancelledError as exc:
context.cb_error = exc
except Exception as exc:
context.cb_error = exc
@then("a ToolCancelledError should have been raised in coverage-boost execution")
def step_check_cb_execution_cancelled(context: Context) -> None:
assert isinstance(context.cb_error, ToolCancelledError), (
f"Expected ToolCancelledError, got {type(context.cb_error).__name__}: {context.cb_error}"
)
@then("the coverage-boost context should have {count:d} traces")
def step_check_cb_ctx_traces(context: Context, count: int) -> None:
actual = len(context.cb_ctx.traces)
assert actual == count, f"Expected {count} traces, got {actual}"
@then("the coverage-boost last trace should show success {expected}")
def step_check_cb_last_trace_success(context: Context, expected: str) -> None:
traces = context.cb_ctx.traces
assert len(traces) > 0, "No traces recorded"
last = traces[-1]
expected_bool = expected == "True"
assert last.success == expected_bool, (
f"Expected trace success={expected_bool}, got {last.success}"
)
@then('the coverage-boost last trace error should be "{expected}"')
def step_check_cb_last_trace_error(context: Context, expected: str) -> None:
traces = context.cb_ctx.traces
assert len(traces) > 0, "No traces recorded"
last = traces[-1]
assert last.error == expected, (
f"Expected trace error='{expected}', got '{last.error}'"
)
# ---------------------------------------------------------------------------
# ToolRuntime.deactivate — tool not in cache (line 550)
# ---------------------------------------------------------------------------
@given('I register a coverage-boost read-only tool "{name}" with a mock')
def step_register_cb_readonly(context: Context, name: str) -> None:
tool = _cb_make_tool(name, read_only=True)
mock = _CBMockToolInstance(name=name, read_only=True)
context.cb_runtime.register_tool(tool, mock)
context.cb_mocks[name] = mock
@when('I deactivate tool "{name}" in the coverage-boost runtime')
def step_cb_deactivate_tool(context: Context, name: str) -> None:
try:
context.cb_runtime.deactivate(name, context.cb_ctx)
context.cb_error = None
except Exception as exc:
context.cb_error = exc
@then("no coverage-boost exception should have been raised")
def step_check_cb_no_exception(context: Context) -> None:
assert context.cb_error is None, (
f"Expected no exception, got {type(context.cb_error).__name__}: {context.cb_error}"
)
@then('the coverage-boost mock "{name}" should not have been deactivated')
def step_check_cb_mock_not_deactivated(context: Context, name: str) -> None:
mock = context.cb_mocks[name]
assert not mock.deactivated, f"Mock '{name}' should NOT have been deactivated"
# ---------------------------------------------------------------------------
# ToolRuntime.deactivate_plan — empty plan (branch 585:exit)
# ---------------------------------------------------------------------------
@when("I deactivate plan in the coverage-boost runtime")
def step_cb_deactivate_plan(context: Context) -> None:
try:
context.cb_runtime.deactivate_plan(context.cb_ctx)
context.cb_error = None
except Exception as exc:
context.cb_error = exc