Files
temp/features/steps/tool_registry_service_coverage_steps.py
freemo 1d36449a98 fix(cli): remove extra --mode flag from validation attach
Remove the --mode/-m CLI flag from the validation attach command to align
with the specification, which defines validation mode as an inherent
property of the validation definition set at registration time via
validation add, not as a per-attachment override.

The service layer (ToolRegistryService.attach_validation) no longer
accepts mode as a parameter; instead it reads the mode from the
validation's registered definition. The ToolRegistryRepository
_to_legacy_domain now exposes the mode field so the service can access it.

All tests that passed --mode to the CLI or service have been updated or
removed. The invalid-mode validation scenarios were removed since the
mode is no longer caller-supplied. Coverage remains at 98.7%.

ISSUES CLOSED: #913

Co-authored-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
Co-committed-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
2026-04-02 17:07:26 +00:00

271 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",
}
# ---------------------------------------------------------------------------
# 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}"
)