"""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}'"