Files
temp/features/steps/tool_registry_service_fallback_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

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