Files
temp/features/steps/tool_registry_service_fallback_coverage_steps.py
T
2026-02-25 10:03:57 +00:00

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}'"
)