Files
temp/features/steps/tool_registry_service_coverage_steps.py
2026-04-06 07:55:09 +00:00

272 lines
9.2 KiB
Python

"""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",
"tool_type": "validation",
}
# ---------------------------------------------------------------------------
# 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}"
)