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>
429 lines
14 KiB
Python
429 lines
14 KiB
Python
"""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}'"
|
|
)
|