forked from HAL9000/cleveragents-core
55aee7cf22
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
515 lines
17 KiB
Python
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}'"
|