forked from HAL9000/cleveragents-core
1d36449a98
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>
271 lines
9.2 KiB
Python
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}"
|
|
)
|