"""Step definitions for tool_registry_service_coverage.feature. Exercises the error-handling and delegation branches in ToolRegistryService. 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 DatabaseError, NotFoundError from cleveragents.infrastructure.database.repositories import ( DuplicateToolError, ToolInUseError, ToolNotFoundError, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- class _MockToolRepo: """Minimal mock that satisfies ToolRegistryService's expectations.""" def __init__(self) -> None: self._get_by_name_return: Any = None self._create_side_effect: Exception | None = None self._update_side_effect: Exception | None = None self._delete_side_effect: Exception | None = None self._delete_return: bool = True def create(self, tool: Any) -> Any: if self._create_side_effect is not None: raise self._create_side_effect 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 [] def update(self, tool: Any) -> Any: if self._update_side_effect is not None: raise self._update_side_effect return tool def delete(self, name: str) -> bool: if self._delete_side_effect is not None: raise self._delete_side_effect return self._delete_return class _MockAttachmentRepo: """No-op attachment repository mock.""" def attach(self, **kwargs: Any) -> dict[str, str]: return {"attachment_id": "mock-attachment-id-001"} def detach(self, attachment_id: str) -> bool: return True def list_for_resource( self, resource_id: str, project_name: str | None = None, plan_id: str | None = None, ) -> list[Any]: return [] # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("a mock-based tool registry service") def step_mock_service(context: Context) -> None: context.cov_tool_repo = _MockToolRepo() context.cov_attachment_repo = _MockAttachmentRepo() context.cov_service = ToolRegistryService( tool_repo=context.cov_tool_repo, attachment_repo=context.cov_attachment_repo, ) context.cov_error: Exception | None = None context.cov_result: Any = None # --------------------------------------------------------------------------- # Given: repo behaviour setup # --------------------------------------------------------------------------- @given("the mock tool repo create method raises DuplicateToolError") def step_create_raises_duplicate(context: Context) -> None: context.cov_tool_repo._create_side_effect = DuplicateToolError("test-tool") @given("the mock tool repo create method raises DatabaseError") def step_create_raises_db_error(context: Context) -> None: context.cov_tool_repo._create_side_effect = DatabaseError( "Failed to create tool: disk I/O error" ) @given("the mock tool repo update method raises ToolNotFoundError") def step_update_raises_not_found(context: Context) -> None: context.cov_tool_repo._update_side_effect = ToolNotFoundError("gone") @given("the mock tool repo update method raises DatabaseError") def step_update_raises_db_error(context: Context) -> None: context.cov_tool_repo._update_side_effect = DatabaseError( "Failed to update tool: disk I/O error" ) @given("the mock tool repo delete method returns False") def step_delete_returns_false(context: Context) -> None: context.cov_tool_repo._delete_return = False @given("the mock tool repo delete method raises ToolInUseError") def step_delete_raises_in_use(context: Context) -> None: context.cov_tool_repo._delete_side_effect = ToolInUseError("test-tool", 2) @given("the mock tool repo returns None for get_by_name") def step_repo_returns_none(context: Context) -> None: context.cov_tool_repo._get_by_name_return = None @given("the mock tool repo returns a sentinel for get_by_name") def step_repo_returns_sentinel(context: Context) -> None: context.cov_tool_repo._get_by_name_return = { "name": "local/some-check", "mode": "required", } # --------------------------------------------------------------------------- # When # --------------------------------------------------------------------------- @when("I attempt to register the tool via the coverage service") def step_when_register(context: Context) -> None: tool_config = {"name": "local/test-tool", "tool_type": "tool", "source": "builtin"} try: context.cov_result = context.cov_service.register_tool(tool_config) context.cov_error = None except Exception as exc: context.cov_error = exc @when("I attempt to update a tool via the coverage service") def step_when_update(context: Context) -> None: tool_config = {"name": "local/test-tool", "tool_type": "tool"} try: context.cov_result = context.cov_service.update_tool(tool_config) context.cov_error = None except Exception as exc: context.cov_error = exc @when('I attempt to remove tool "{name}" via the coverage service') def step_when_remove(context: Context, name: str) -> None: try: context.cov_result = context.cov_service.remove_tool(name) context.cov_error = None except Exception as exc: context.cov_error = exc @when("I attempt to attach validation via the coverage service") def step_when_attach(context: Context) -> None: try: context.cov_result = context.cov_service.attach_validation( resource_id="res-001", validation_name="local/some-check", ) context.cov_error = None except Exception as exc: context.cov_error = exc # --------------------------------------------------------------------------- # Then # --------------------------------------------------------------------------- @then("a coverage DuplicateToolError should be raised") def step_then_duplicate_tool_error(context: Context) -> None: assert context.cov_error is not None, ( "Expected DuplicateToolError but no error was raised" ) assert isinstance(context.cov_error, DuplicateToolError), ( f"Expected DuplicateToolError, got {type(context.cov_error).__name__}: {context.cov_error}" ) @then('a coverage DatabaseError should be raised with message containing "{text}"') def step_then_database_error(context: Context, text: str) -> None: assert context.cov_error is not None, ( "Expected DatabaseError but no error was raised" ) assert isinstance(context.cov_error, DatabaseError), ( f"Expected DatabaseError, got {type(context.cov_error).__name__}: {context.cov_error}" ) assert text in str(context.cov_error), ( f"Expected '{text}' in error message, got '{context.cov_error}'" ) @then("a coverage ToolNotFoundError should be raised") def step_then_tool_not_found(context: Context) -> None: assert context.cov_error is not None, ( "Expected ToolNotFoundError but no error was raised" ) assert isinstance(context.cov_error, ToolNotFoundError), ( f"Expected ToolNotFoundError, got {type(context.cov_error).__name__}: {context.cov_error}" ) @then("a coverage ToolInUseError should be raised") def step_then_tool_in_use(context: Context) -> None: assert context.cov_error is not None, ( "Expected ToolInUseError but no error was raised" ) assert isinstance(context.cov_error, ToolInUseError), ( f"Expected ToolInUseError, got {type(context.cov_error).__name__}: {context.cov_error}" ) @then("the removal result should be False") def step_then_result_false(context: Context) -> None: assert context.cov_error is None, f"Expected no error, got {context.cov_error}" assert context.cov_result is False, f"Expected False, got {context.cov_result}" @then('a coverage NotFoundError should be raised with message containing "{text}"') def step_then_not_found_error(context: Context, text: str) -> None: assert context.cov_error is not None, ( "Expected NotFoundError but no error was raised" ) assert isinstance(context.cov_error, NotFoundError), ( f"Expected NotFoundError, got {type(context.cov_error).__name__}: {context.cov_error}" ) assert text in str(context.cov_error), ( f"Expected '{text}' in error message, got '{context.cov_error}'" ) @then("no error should be raised by the coverage service") def step_then_no_error(context: Context) -> None: assert context.cov_error is None, ( f"Expected no error, got {type(context.cov_error).__name__}: {context.cov_error}" )