forked from HAL9000/cleveragents-core
4874f2ad6f
ISSUES CLOSED: #175
430 lines
14 KiB
Python
430 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",
|
|
mode=mode,
|
|
)
|
|
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}'"
|
|
)
|