Files
temp/features/steps/resource_handler_service_coverage_steps.py
T
freemo 55aee7cf22 fix(test): commit after each add_skill to prevent session GC rollback, and improved coverage.
The step_register_skills_table step called add_skill in a loop but only
committed once at the end. Because SkillRepository.create() obtains a
new session per call and only flushes (never commits), the intermediate
sessions could be garbage-collected before the final commit, rolling
back their transactions on the shared SQLite :memory: connection. Moving
_commit_pending inside the loop ensures each skill is durably committed
before the next session is created.

ISSUES CLOSED: #418
2026-02-24 12:19:04 -05:00

515 lines
17 KiB
Python

"""Step definitions for ResourceHandlerService full coverage tests.
All step patterns are prefixed with 'rhs' to avoid collisions with other
step definition files in the same features/steps directory.
"""
from __future__ import annotations
import contextlib
from unittest.mock import MagicMock, patch
from behave import given, then, when
from cleveragents.application.services.resource_handler_service import (
ResourceHandlerService,
)
from cleveragents.core.exceptions import NotFoundError
from cleveragents.domain.models.core.resource_slot import BindingResult
from cleveragents.resource.handlers.resolver import HandlerResolutionError
from cleveragents.tool.context import BoundResource
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_resource_mock(
resource_id,
resource_type_name,
location=None,
sandbox_strategy=None,
):
"""Create a mock Resource with the given attributes."""
resource = MagicMock()
resource.resource_id = resource_id
resource.resource_type_name = resource_type_name
resource.location = location
# sandbox_strategy=None means no override; "none" is a valid strategy string
resource.sandbox_strategy = sandbox_strategy
return resource
def _make_type_spec_mock(
name,
handler=None,
sandbox_strategy="copy_on_write",
):
"""Create a mock ResourceTypeSpec with the given attributes."""
spec = MagicMock()
spec.name = name
spec.handler = handler
spec.sandbox_strategy = sandbox_strategy
return spec
def _make_binding_obj(
slot_name,
resource_id=None,
deferred=False,
):
"""Create a BindingResult with the given attributes."""
return BindingResult(
slot_name=slot_name,
resource_id=resource_id,
binding_mode="parameter" if deferred else "contextual",
deferred=deferred,
)
def _make_sandbox_mock(sandbox_path):
"""Create a mock Sandbox with a context carrying sandbox_path."""
sandbox = MagicMock()
if sandbox_path is not None:
sandbox.context = MagicMock()
sandbox.context.sandbox_path = sandbox_path
else:
sandbox.context = None
return sandbox
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a mock sandbox manager for rhs")
def step_rhs_mock_sandbox_manager(context):
context.rhs_sandbox_manager = MagicMock()
context.rhs_sandbox_call_args = None
@given("a mock resource lookup for rhs")
def step_rhs_mock_resource_lookup(context):
context.rhs_resources = {}
context.rhs_resource_lookup_error = {}
def _lookup(name_or_id):
if name_or_id in context.rhs_resource_lookup_error:
raise context.rhs_resource_lookup_error[name_or_id]
return context.rhs_resources[name_or_id]
context.rhs_resource_lookup = _lookup
@given("a mock type lookup for rhs")
def step_rhs_mock_type_lookup(context):
context.rhs_type_specs = {}
def _lookup(name):
return context.rhs_type_specs[name]
context.rhs_type_lookup = _lookup
@given("a ResourceHandlerService under test")
def step_rhs_service_instance(context):
context.rhs_service = ResourceHandlerService(
sandbox_manager=context.rhs_sandbox_manager,
resource_lookup=context.rhs_resource_lookup,
type_lookup=context.rhs_type_lookup,
)
context.rhs_result = None
context.rhs_error = None
context.rhs_resolve_handler_patcher = None
# ---------------------------------------------------------------------------
# Binding setup
# ---------------------------------------------------------------------------
@given('an rhs binding that is deferred with slot name "{slot}"')
def step_rhs_deferred_binding(context, slot):
context.rhs_binding = _make_binding_obj(slot_name=slot, deferred=True)
@given('an rhs binding with no resource_id and slot name "{slot}"')
def step_rhs_binding_no_resource_id(context, slot):
context.rhs_binding = _make_binding_obj(
slot_name=slot, resource_id=None, deferred=False
)
@given('an rhs non-deferred binding with slot "{slot}" and resource_id "{rid}"')
def step_rhs_non_deferred_binding(context, slot, rid):
context.rhs_binding = _make_binding_obj(
slot_name=slot, resource_id=rid, deferred=False
)
# ---------------------------------------------------------------------------
# Resource setup — single step for location + strategy
# ---------------------------------------------------------------------------
@given(
'rhs resource "{rid}" typed "{rtype}" located at "{loc}" with strategy "{strat}"'
)
def step_rhs_resource_with_loc_and_strat(context, rid, rtype, loc, strat):
# Convert the string "none" to Python None for sandbox_strategy
strategy = None if strat == "none" else strat
context.rhs_resources[rid] = _make_resource_mock(
resource_id=rid,
resource_type_name=rtype,
location=loc,
sandbox_strategy=strategy,
)
@given('rhs resource "{rid}" typed "{rtype}" with no location')
def step_rhs_resource_no_location(context, rid, rtype):
context.rhs_resources[rid] = _make_resource_mock(
resource_id=rid,
resource_type_name=rtype,
location=None,
sandbox_strategy=None,
)
# ---------------------------------------------------------------------------
# Type spec setup
# ---------------------------------------------------------------------------
@given('an rhs type spec "{name}" with no handler and sandbox strategy "{strat}"')
def step_rhs_type_spec_no_handler(context, name, strat):
context.rhs_type_specs[name] = _make_type_spec_mock(
name=name, handler=None, sandbox_strategy=strat
)
@given(
'an rhs type spec "{name}" with handler "{handler}" and sandbox strategy "{strat}"'
)
def step_rhs_type_spec_with_handler(context, name, handler, strat):
context.rhs_type_specs[name] = _make_type_spec_mock(
name=name, handler=handler, sandbox_strategy=strat
)
# ---------------------------------------------------------------------------
# Sandbox manager setup
# ---------------------------------------------------------------------------
@given('the rhs sandbox manager returns a sandbox with path "{path}"')
def step_rhs_sandbox_returns_path(context, path):
sandbox = _make_sandbox_mock(sandbox_path=path)
def _capture_call(**kwargs):
context.rhs_sandbox_call_args = kwargs
return sandbox
context.rhs_sandbox_manager.get_or_create_sandbox = MagicMock(
side_effect=_capture_call
)
@given("the rhs sandbox manager returns a sandbox with no context")
def step_rhs_sandbox_returns_no_context(context):
sandbox = _make_sandbox_mock(sandbox_path=None)
context.rhs_sandbox_manager.get_or_create_sandbox = MagicMock(return_value=sandbox)
# ---------------------------------------------------------------------------
# Handler mock setup
# ---------------------------------------------------------------------------
@given("the rhs resolve_handler function returns a mock handler")
def step_rhs_resolve_handler_returns_mock(context):
context.rhs_mock_handler = MagicMock()
context.rhs_resolve_handler_patcher = patch(
"cleveragents.application.services.resource_handler_service.resolve_handler",
return_value=context.rhs_mock_handler,
)
context.rhs_resolve_handler_patcher.start()
@given('the rhs mock handler resolves to a BoundResource with sandbox_path "{path}"')
def step_rhs_mock_handler_returns_bound(context, path):
def _resolve(**kwargs):
return BoundResource(
slot_name=kwargs["slot_name"],
resource_id=kwargs["resource"].resource_id,
resource_type=kwargs["resource"].resource_type_name,
sandbox_path=path,
access=kwargs.get("access", "read_only"),
)
context.rhs_mock_handler.resolve = MagicMock(side_effect=_resolve)
@given("the rhs resolve_handler function raises HandlerResolutionError")
def step_rhs_resolve_handler_raises(context):
context.rhs_resolve_handler_patcher = patch(
"cleveragents.application.services.resource_handler_service.resolve_handler",
side_effect=HandlerResolutionError("handler not found"),
)
context.rhs_resolve_handler_patcher.start()
# ---------------------------------------------------------------------------
# Error setup for resource lookup
# ---------------------------------------------------------------------------
@given('the rhs resource lookup raises NotFoundError for "{rid}"')
def step_rhs_resource_lookup_raises(context, rid):
context.rhs_resource_lookup_error[rid] = NotFoundError(
message=f"Resource '{rid}' not found"
)
# ---------------------------------------------------------------------------
# Binding list setup
# ---------------------------------------------------------------------------
@given("an rhs binding list with:")
def step_rhs_binding_list(context):
context.rhs_bindings = []
for row in context.table:
slot_name = row["slot_name"]
resource_id = row["resource_id"] if row["resource_id"] else None
deferred = row["deferred"].lower() == "true"
context.rhs_bindings.append(
_make_binding_obj(
slot_name=slot_name, resource_id=resource_id, deferred=deferred
)
)
# ---------------------------------------------------------------------------
# Cleanup helper
# ---------------------------------------------------------------------------
def _rhs_cleanup_patcher(context):
"""Stop the resolve_handler patcher if active."""
patcher = getattr(context, "rhs_resolve_handler_patcher", None)
if patcher is not None:
with contextlib.suppress(RuntimeError):
patcher.stop()
context.rhs_resolve_handler_patcher = None
# ---------------------------------------------------------------------------
# When: resolve_binding
# ---------------------------------------------------------------------------
@when("I resolve the rhs single binding")
def step_rhs_resolve_single_binding_default(context):
try:
context.rhs_result = context.rhs_service.resolve_binding(
binding=context.rhs_binding,
plan_id="default-plan",
access="read_only",
)
except Exception as exc:
context.rhs_error = exc
finally:
_rhs_cleanup_patcher(context)
@when('I resolve the rhs single binding with plan_id "{pid}" and access "{access}"')
def step_rhs_resolve_single_binding(context, pid, access):
try:
context.rhs_result = context.rhs_service.resolve_binding(
binding=context.rhs_binding,
plan_id=pid,
access=access,
)
except Exception as exc:
context.rhs_error = exc
finally:
_rhs_cleanup_patcher(context)
# ---------------------------------------------------------------------------
# When: resolve_bindings
# ---------------------------------------------------------------------------
@when('I resolve all rhs bindings with plan_id "{pid}" and access "{access}"')
def step_rhs_resolve_all_bindings(context, pid, access):
try:
context.rhs_result = context.rhs_service.resolve_bindings(
bindings=context.rhs_bindings,
plan_id=pid,
access=access,
)
except Exception as exc:
context.rhs_error = exc
finally:
_rhs_cleanup_patcher(context)
@when("I resolve all rhs bindings expecting an error")
def step_rhs_resolve_all_bindings_error(context):
try:
context.rhs_result = context.rhs_service.resolve_bindings(
bindings=context.rhs_bindings,
plan_id="error-plan",
access="read_only",
)
except Exception as exc:
context.rhs_error = exc
finally:
_rhs_cleanup_patcher(context)
# ---------------------------------------------------------------------------
# When: resolve_resource
# ---------------------------------------------------------------------------
@when(
'I resolve the rhs resource directly with plan_id "{pid}" slot "{slot}" and access "{access}"'
)
def step_rhs_resolve_resource_directly(context, pid, slot, access):
# Use the most recently added resource
rid = list(context.rhs_resources.keys())[-1]
resource = context.rhs_resources[rid]
try:
context.rhs_result = context.rhs_service.resolve_resource(
resource=resource,
plan_id=pid,
slot_name=slot,
access=access,
)
except Exception as exc:
context.rhs_error = exc
finally:
_rhs_cleanup_patcher(context)
# ---------------------------------------------------------------------------
# Then: BoundResource assertions
# ---------------------------------------------------------------------------
@then('the rhs result is a BoundResource with slot "{slot}" and sandbox_path "{path}"')
def step_rhs_assert_bound_resource(context, slot, path):
assert context.rhs_error is None, f"Unexpected error: {context.rhs_error}"
assert isinstance(context.rhs_result, BoundResource), (
f"Expected BoundResource, got {type(context.rhs_result)}"
)
assert context.rhs_result.slot_name == slot, (
f"Expected slot_name '{slot}', got '{context.rhs_result.slot_name}'"
)
assert context.rhs_result.sandbox_path == path, (
f"Expected sandbox_path '{path}', got '{context.rhs_result.sandbox_path}'"
)
@then('the rhs BoundResource has resource_id "{rid}" and access "{access}"')
def step_rhs_assert_bound_resource_details(context, rid, access):
assert context.rhs_result.resource_id == rid, (
f"Expected resource_id '{rid}', got '{context.rhs_result.resource_id}'"
)
assert context.rhs_result.access == access, (
f"Expected access '{access}', got '{context.rhs_result.access}'"
)
# ---------------------------------------------------------------------------
# Then: error assertions
# ---------------------------------------------------------------------------
@then('an rhs ValueError is raised with message containing "{fragment}"')
def step_rhs_assert_value_error(context, fragment):
assert context.rhs_error is not None, (
"Expected a ValueError but no error was raised"
)
assert isinstance(context.rhs_error, ValueError), (
f"Expected ValueError, got {type(context.rhs_error).__name__}: {context.rhs_error}"
)
assert fragment in str(context.rhs_error), (
f"Expected '{fragment}' in error message, got: {context.rhs_error}"
)
@then("an rhs NotFoundError is raised")
def step_rhs_assert_not_found_error(context):
assert context.rhs_error is not None, (
"Expected a NotFoundError but no error was raised"
)
assert isinstance(context.rhs_error, NotFoundError), (
f"Expected NotFoundError, got {type(context.rhs_error).__name__}: {context.rhs_error}"
)
@then('an rhs RuntimeError is raised with message containing "{fragment}"')
def step_rhs_assert_runtime_error(context, fragment):
assert context.rhs_error is not None, (
"Expected a RuntimeError but no error was raised"
)
assert isinstance(context.rhs_error, RuntimeError), (
f"Expected RuntimeError, got {type(context.rhs_error).__name__}: {context.rhs_error}"
)
assert fragment in str(context.rhs_error), (
f"Expected '{fragment}' in error message, got: {context.rhs_error}"
)
# ---------------------------------------------------------------------------
# Then: dict result assertions
# ---------------------------------------------------------------------------
@then('the rhs result dict has keys "{key1}" and "{key2}"')
def step_rhs_assert_dict_keys(context, key1, key2):
assert context.rhs_error is None, f"Unexpected error: {context.rhs_error}"
assert isinstance(context.rhs_result, dict), (
f"Expected dict, got {type(context.rhs_result)}"
)
assert key1 in context.rhs_result, f"Key '{key1}' not found in result dict"
assert key2 in context.rhs_result, f"Key '{key2}' not found in result dict"
@then('the rhs result dict does not contain key "{key}"')
def step_rhs_assert_dict_missing_key(context, key):
assert key not in context.rhs_result, (
f"Key '{key}' should not be in result dict but was found"
)
@then("the rhs result dict is empty")
def step_rhs_assert_dict_empty(context):
assert context.rhs_error is None, f"Unexpected error: {context.rhs_error}"
assert isinstance(context.rhs_result, dict), (
f"Expected dict, got {type(context.rhs_result)}"
)
assert len(context.rhs_result) == 0, (
f"Expected empty dict, got {len(context.rhs_result)} entries"
)
# ---------------------------------------------------------------------------
# Then: sandbox manager call assertions
# ---------------------------------------------------------------------------
@then('the rhs sandbox manager was called with strategy "{strategy}"')
def step_rhs_assert_sandbox_strategy(context, strategy):
assert context.rhs_error is None, f"Unexpected error: {context.rhs_error}"
assert context.rhs_sandbox_call_args is not None, (
"sandbox_manager.get_or_create_sandbox was not called"
)
actual = context.rhs_sandbox_call_args.get("sandbox_strategy")
assert actual == strategy, f"Expected sandbox_strategy '{strategy}', got '{actual}'"