"""Step definitions for tool_registry_service_fallback_coverage.feature. Exercises the fallback method-resolution branches in ToolRegistryService: - register_tool: falls back from create -> add -> create (AttributeError) - update_tool: tool_config parameter paths (dict, Tool model) - remove_tool: falls back from delete -> remove -> delete (AttributeError) - list_tools: delegates to list_all - attach_validation: invalid mode raises ValidationError All repository dependencies are lightweight mocks — no database needed. """ from __future__ import annotations from typing import Any from behave import given, then, when from behave.runner import Context from cleveragents.application.services.tool_registry_service import ( ToolRegistryService, ) from cleveragents.core.exceptions import ValidationError from cleveragents.domain.models.core.tool import Tool, ToolSource # --------------------------------------------------------------------------- # Mock repos with specific method presence # --------------------------------------------------------------------------- class _AddOnlyToolRepo: """A repo that has add() but no create() method.""" def __init__(self) -> None: self._added: Any = None # Deliberately no create() method def add(self, tool: Any) -> Any: self._added = tool return tool def get_by_name(self, name: str) -> Any: return None def list_all(self, **kwargs: Any) -> list[Any]: return [] def update(self, tool: Any) -> Any: return tool def delete(self, name: str) -> bool: return True class _NoCreateNoAddToolRepo: """A repo that has neither create() nor add() as callable methods. Has create as a non-callable (string) to trigger the final fallback. """ def __init__(self) -> None: # create is set to a non-callable to force the fallback path pass def get_by_name(self, name: str) -> Any: return None def list_all(self, **kwargs: Any) -> list[Any]: return [] def update(self, tool: Any) -> Any: return tool class _RemoveOnlyToolRepo: """A repo that has remove() but no delete() method.""" def __init__(self) -> None: self._removed: str | None = None # Deliberately no delete() method def remove(self, name: str) -> bool: self._removed = name return True def get_by_name(self, name: str) -> Any: return None def create(self, tool: Any) -> Any: return tool def list_all(self, **kwargs: Any) -> list[Any]: return [] def update(self, tool: Any) -> Any: return tool class _NoDeleteNoRemoveToolRepo: """A repo that has neither delete() nor remove().""" def __init__(self) -> None: pass def get_by_name(self, name: str) -> Any: return None def create(self, tool: Any) -> Any: return tool def list_all(self, **kwargs: Any) -> list[Any]: return [] def update(self, tool: Any) -> Any: return tool class _StandardMockToolRepo: """Standard mock with all methods present.""" def __init__(self) -> None: self._last_updated: Any = None self._list_all_return: list[Any] = [ {"name": "local/tool-1"}, {"name": "local/tool-2"}, ] self._get_by_name_return: Any = {"name": "local/some-check"} def create(self, tool: Any) -> Any: return tool def get_by_name(self, name: str) -> Any: return self._get_by_name_return def list_all( self, namespace: str | None = None, tool_type: str | None = None, source: str | None = None, ) -> list[Any]: return self._list_all_return def update(self, tool: Any) -> Any: self._last_updated = tool return tool def delete(self, name: str) -> bool: return True class _MinimalAttachmentRepo: """Attachment repo that satisfies the service interface.""" def attach(self, **kwargs: Any) -> dict[str, str]: return {"attachment_id": "fallback-att-001"} def detach(self, attachment_id: str) -> bool: return True def list_for_resource(self, **kwargs: Any) -> list[Any]: return [] # --------------------------------------------------------------------------- # Given steps # --------------------------------------------------------------------------- @given("a tool registry service with a repo that only has add method") def step_given_add_only_repo(context: Context) -> None: context.fb_tool_repo = _AddOnlyToolRepo() context.fb_attachment_repo = _MinimalAttachmentRepo() context.fb_service = ToolRegistryService( tool_repo=context.fb_tool_repo, attachment_repo=context.fb_attachment_repo, ) context.fb_error: Exception | None = None context.fb_result: Any = None @given("a tool registry service with a repo that has no create or add methods") def step_given_no_create_no_add(context: Context) -> None: context.fb_tool_repo = _NoCreateNoAddToolRepo() context.fb_attachment_repo = _MinimalAttachmentRepo() context.fb_service = ToolRegistryService( tool_repo=context.fb_tool_repo, attachment_repo=context.fb_attachment_repo, ) context.fb_error = None context.fb_result = None @given("a tool registry service with a standard mock repo") def step_given_standard_repo(context: Context) -> None: context.fb_tool_repo = _StandardMockToolRepo() context.fb_attachment_repo = _MinimalAttachmentRepo() context.fb_service = ToolRegistryService( tool_repo=context.fb_tool_repo, attachment_repo=context.fb_attachment_repo, ) context.fb_error = None context.fb_result = None @given("a tool registry service with a repo that only has remove method") def step_given_remove_only_repo(context: Context) -> None: context.fb_tool_repo = _RemoveOnlyToolRepo() context.fb_attachment_repo = _MinimalAttachmentRepo() context.fb_service = ToolRegistryService( tool_repo=context.fb_tool_repo, attachment_repo=context.fb_attachment_repo, ) context.fb_error = None context.fb_result = None @given("a tool registry service with a repo that has no delete or remove methods") def step_given_no_delete_no_remove(context: Context) -> None: context.fb_tool_repo = _NoDeleteNoRemoveToolRepo() context.fb_attachment_repo = _MinimalAttachmentRepo() context.fb_service = ToolRegistryService( tool_repo=context.fb_tool_repo, attachment_repo=context.fb_attachment_repo, ) context.fb_error = None context.fb_result = None # --------------------------------------------------------------------------- # When steps # --------------------------------------------------------------------------- @when("I register a tool via the fallback service") def step_when_register_fallback(context: Context) -> None: tool = {"name": "local/fallback-tool", "source": "builtin"} try: context.fb_result = context.fb_service.register_tool(tool) context.fb_error = None except Exception as exc: context.fb_error = exc @when("I register a tool via the fallback service and expect error") def step_when_register_fallback_error(context: Context) -> None: tool = {"name": "local/fallback-tool", "source": "builtin"} try: context.fb_result = context.fb_service.register_tool(tool) context.fb_error = None except Exception as exc: context.fb_error = exc @when('I update tool "{name}" with dict config via the fallback service') def step_when_update_dict_config(context: Context, name: str) -> None: try: context.fb_result = context.fb_service.update_tool( name, tool_config={"description": "Updated description"}, ) context.fb_error = None except Exception as exc: context.fb_error = exc @when('I update tool "{name}" with Tool model config via the fallback service') def step_when_update_tool_model(context: Context, name: str) -> None: try: tool_model = Tool( name="local/my-tool", description="A tool model", source=ToolSource.BUILTIN, tool_type="tool", timeout=300, ) context.fb_result = context.fb_service.update_tool(name, tool_config=tool_model) context.fb_error = None except Exception as exc: context.fb_error = exc @when("I update with dict tool and dict config via the fallback service") def step_when_update_dict_tool_dict_config(context: Context) -> None: try: tool_dict = {"name": "local/dict-tool", "source": "builtin"} context.fb_result = context.fb_service.update_tool( tool_dict, tool_config={"description": "Updated"}, ) context.fb_error = None except Exception as exc: context.fb_error = exc @when('I remove tool "{name}" via the fallback service') def step_when_remove_fallback(context: Context, name: str) -> None: try: context.fb_result = context.fb_service.remove_tool(name) context.fb_error = None except Exception as exc: context.fb_error = exc @when('I remove tool "{name}" via the fallback service and expect error') def step_when_remove_fallback_error(context: Context, name: str) -> None: try: context.fb_result = context.fb_service.remove_tool(name) context.fb_error = None except Exception as exc: context.fb_error = exc @when('I list tools with namespace "{ns}" and type "{tt}" via the fallback service') def step_when_list_tools(context: Context, ns: str, tt: str) -> None: try: context.fb_result = context.fb_service.list_tools(namespace=ns, tool_type=tt) context.fb_error = None except Exception as exc: context.fb_error = exc @when('I attach validation with invalid mode "{mode}" via the fallback service') def step_when_attach_invalid_mode(context: Context, mode: str) -> None: try: context.fb_result = context.fb_service.attach_validation( validation_name="local/check", resource_id="res-001", ) context.fb_error = None except Exception as exc: context.fb_error = exc # --------------------------------------------------------------------------- # Then steps # --------------------------------------------------------------------------- @then("the tool should be registered via the add method") def step_then_registered_via_add(context: Context) -> None: assert context.fb_result is not None, "Expected a result from register_tool" assert hasattr(context.fb_tool_repo, "_added"), "Expected _added attribute on repo" assert context.fb_tool_repo._added is not None, ( "Expected tool to be passed to add()" ) @then("no error should be raised by the fallback service") def step_then_no_fallback_error(context: Context) -> None: assert context.fb_error is None, ( f"Expected no error, got {type(context.fb_error).__name__}: {context.fb_error}" ) @then("a fallback AttributeError should be raised") def step_then_attribute_error(context: Context) -> None: assert context.fb_error is not None, "Expected an error but no error was raised" assert isinstance(context.fb_error, AttributeError), ( f"Expected AttributeError, got {type(context.fb_error).__name__}: " f"{context.fb_error}" ) @then('the updated tool should have name "{name}"') def step_then_updated_has_name(context: Context, name: str) -> None: assert context.fb_result is not None, "Expected a result from update_tool" if isinstance(context.fb_result, dict): assert context.fb_result.get("name") == name, ( f"Expected name '{name}', got {context.fb_result.get('name')}" ) else: assert getattr(context.fb_result, "name", None) == name @then("the update should use the Tool model directly") def step_then_update_uses_model(context: Context) -> None: assert context.fb_result is not None, "Expected a result" assert isinstance(context.fb_result, Tool), ( f"Expected Tool instance, got {type(context.fb_result).__name__}" ) @then("the updated tool should be the original dict") def step_then_updated_is_dict(context: Context) -> None: assert context.fb_result is not None, "Expected a result" assert isinstance(context.fb_result, dict), ( f"Expected dict, got {type(context.fb_result).__name__}" ) @then("the tool should be removed via the remove method") def step_then_removed_via_remove(context: Context) -> None: assert context.fb_result is True, ( f"Expected True from remove_tool, got {context.fb_result}" ) assert hasattr(context.fb_tool_repo, "_removed"), ( "Expected _removed attribute on repo" ) assert context.fb_tool_repo._removed is not None, ( "Expected tool name to be passed to remove()" ) @then("the list result should be returned from list_all") def step_then_list_result(context: Context) -> None: assert context.fb_result is not None, "Expected a list result" assert isinstance(context.fb_result, list), ( f"Expected list, got {type(context.fb_result).__name__}" ) assert len(context.fb_result) == 2, ( f"Expected 2 items, got {len(context.fb_result)}" ) @then('a fallback ValidationError should be raised with message containing "{text}"') def step_then_validation_error(context: Context, text: str) -> None: assert context.fb_error is not None, ( "Expected ValidationError but no error was raised" ) assert isinstance(context.fb_error, ValidationError), ( f"Expected ValidationError, got {type(context.fb_error).__name__}: " f"{context.fb_error}" ) assert text in str(context.fb_error), ( f"Expected '{text}' in error message, got '{context.fb_error}'" )