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

557 lines
19 KiB
Python

"""Step definitions for tool_registry_service_uncovered_branches.feature.
Exercises the fallback delegation branches in ToolRegistryService that are
not covered by the primary test suite:
- register_tool: create not callable → add fallback → direct create fallback
- remove_tool: delete not callable → remove fallback → direct delete fallback
- update_tool: tool_config as Tool instance, dict with string name, dict with dict tool
- list_tools: with namespace/type/source filters
- attach_validation: invalid mode raises ValidationError
All step names are prefixed with "trs branch" to avoid collisions with the
existing ``tool_registry_service_coverage_steps.py``.
"""
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, ToolType
# ---------------------------------------------------------------------------
# Mock repos
# ---------------------------------------------------------------------------
class _NoCreateRepo:
"""Tool repo where ``create`` is a non-callable attribute, but ``add`` works.
``getattr(repo, "create", None)`` returns a truthy, non-callable value,
so ``callable(create_fn)`` is False → the service falls to the ``add`` path.
"""
def __init__(self) -> None:
self.create: str = "not-a-function" # non-callable attribute
self.add_called = False
self._last_added: Any = None
def add(self, tool: Any) -> Any:
self.add_called = True
self._last_added = tool
return tool
# Required by other service methods
def get_by_name(self, name: str) -> Any:
return None
def update(self, tool: Any) -> Any:
return tool
def delete(self, name: str) -> bool:
return True
def list_all(
self,
namespace: str | None = None,
tool_type: str | None = None,
source: str | None = None,
) -> list[Any]:
return []
class _NeitherCreateNorAddRepo:
"""Tool repo where both ``create`` and ``add`` are non-callable strings.
Forces the service to fall through to ``self._tool_repo.create(tool)``
(line 57), which will raise ``TypeError`` because ``create`` is a string.
"""
def __init__(self) -> None:
self.create: str = "not-a-function" # type: ignore[assignment]
self.add: str = "also-not-a-function" # type: ignore[assignment]
def get_by_name(self, name: str) -> Any:
return None
def update(self, tool: Any) -> Any:
return tool
def delete(self, name: str) -> bool:
return True
def list_all(
self,
namespace: str | None = None,
tool_type: str | None = None,
source: str | None = None,
) -> list[Any]:
return []
class _NoDeleteRepo:
"""Tool repo where ``delete`` is a non-callable attribute, but ``remove`` works."""
def __init__(self) -> None:
self.delete: str = "not-a-function" # type: ignore[assignment]
self.remove_called = False
def remove(self, name: str) -> bool:
self.remove_called = True
return True
def create(self, tool: Any) -> Any:
return tool
def get_by_name(self, name: str) -> Any:
return None
def update(self, tool: Any) -> Any:
return tool
def list_all(
self,
namespace: str | None = None,
tool_type: str | None = None,
source: str | None = None,
) -> list[Any]:
return []
class _NeitherDeleteNorRemoveRepo:
"""Tool repo where both ``delete`` and ``remove`` are non-callable strings."""
def __init__(self) -> None:
self.delete: str = "not-a-function" # type: ignore[assignment]
self.remove: str = "also-not-a-function" # type: ignore[assignment]
def create(self, tool: Any) -> Any:
return tool
def get_by_name(self, name: str) -> Any:
return None
def update(self, tool: Any) -> Any:
return tool
def list_all(
self,
namespace: str | None = None,
tool_type: str | None = None,
source: str | None = None,
) -> list[Any]:
return []
class _StandardToolRepo:
"""Normal tool repo with all methods callable — used for update/list/attach tests."""
def __init__(self) -> None:
self._last_updated: Any = None
self._list_all_kwargs: dict[str, Any] = {}
def create(self, tool: Any) -> Any:
return tool
def get_by_name(self, name: str) -> Any:
return {"name": name}
def update(self, tool: Any) -> Any:
self._last_updated = tool
return tool
def delete(self, name: str) -> bool:
return True
def list_all(
self,
namespace: str | None = None,
tool_type: str | None = None,
source: str | None = None,
) -> list[Any]:
self._list_all_kwargs = {
"namespace": namespace,
"tool_type": tool_type,
"source": source,
}
return [{"name": "local/listed-tool"}]
class _MockAttachmentRepo:
"""Minimal attachment repo mock."""
def attach(self, **kwargs: Any) -> dict[str, str]:
return {"attachment_id": "branch-test-attachment-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 []
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_SAMPLE_TOOL = {
"name": "local/branch-test-tool",
"tool_type": "tool",
"source": "builtin",
}
def _make_tool_instance() -> Tool:
"""Create a real Tool domain object for tests."""
return Tool(
name="local/branch-tool",
description="A tool for branch tests",
source=ToolSource.BUILTIN,
tool_type=ToolType.TOOL,
)
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a trs branch mock-based tool registry service")
def step_trs_branch_background(context: Context) -> None:
"""Set up default context variables — repos are assigned per-scenario."""
context.trs_b_tool_repo: Any = None
context.trs_b_attachment_repo = _MockAttachmentRepo()
context.trs_b_service: ToolRegistryService | None = None
context.trs_b_error: Exception | None = None
context.trs_b_result: Any = None
# ---------------------------------------------------------------------------
# Given: repo variants
# ---------------------------------------------------------------------------
@given("a trs branch tool repo where create is not callable but add is callable")
def step_trs_branch_no_create_repo(context: Context) -> None:
context.trs_b_tool_repo = _NoCreateRepo()
context.trs_b_service = ToolRegistryService(
tool_repo=context.trs_b_tool_repo,
attachment_repo=context.trs_b_attachment_repo,
)
@given("a trs branch tool repo where neither create nor add is callable")
def step_trs_branch_neither_create_nor_add(context: Context) -> None:
context.trs_b_tool_repo = _NeitherCreateNorAddRepo()
context.trs_b_service = ToolRegistryService(
tool_repo=context.trs_b_tool_repo,
attachment_repo=context.trs_b_attachment_repo,
)
@given("a trs branch tool repo where delete is not callable but remove is callable")
def step_trs_branch_no_delete_repo(context: Context) -> None:
context.trs_b_tool_repo = _NoDeleteRepo()
context.trs_b_service = ToolRegistryService(
tool_repo=context.trs_b_tool_repo,
attachment_repo=context.trs_b_attachment_repo,
)
@given("a trs branch tool repo where neither delete nor remove is callable")
def step_trs_branch_neither_delete_nor_remove(context: Context) -> None:
context.trs_b_tool_repo = _NeitherDeleteNorRemoveRepo()
context.trs_b_service = ToolRegistryService(
tool_repo=context.trs_b_tool_repo,
attachment_repo=context.trs_b_attachment_repo,
)
@given("a trs branch standard tool repo")
def step_trs_branch_standard_repo(context: Context) -> None:
context.trs_b_tool_repo = _StandardToolRepo()
context.trs_b_service = ToolRegistryService(
tool_repo=context.trs_b_tool_repo,
attachment_repo=context.trs_b_attachment_repo,
)
# ---------------------------------------------------------------------------
# When: register_tool
# ---------------------------------------------------------------------------
@when("I trs branch register a tool")
def step_trs_branch_register(context: Context) -> None:
try:
context.trs_b_result = context.trs_b_service.register_tool(_SAMPLE_TOOL)
context.trs_b_error = None
except Exception as exc:
context.trs_b_error = exc
@when("I trs branch register a tool expecting an error")
def step_trs_branch_register_error(context: Context) -> None:
try:
context.trs_b_result = context.trs_b_service.register_tool(_SAMPLE_TOOL)
context.trs_b_error = None
except Exception as exc:
context.trs_b_error = exc
# ---------------------------------------------------------------------------
# When: remove_tool
# ---------------------------------------------------------------------------
@when('I trs branch remove tool "{name}"')
def step_trs_branch_remove(context: Context, name: str) -> None:
try:
context.trs_b_result = context.trs_b_service.remove_tool(name)
context.trs_b_error = None
except Exception as exc:
context.trs_b_error = exc
@when('I trs branch remove tool "{name}" expecting an error')
def step_trs_branch_remove_error(context: Context, name: str) -> None:
try:
context.trs_b_result = context.trs_b_service.remove_tool(name)
context.trs_b_error = None
except Exception as exc:
context.trs_b_error = exc
# ---------------------------------------------------------------------------
# When: update_tool with tool_config
# ---------------------------------------------------------------------------
@when("I trs branch update tool with a Tool instance as tool_config")
def step_trs_branch_update_tool_instance(context: Context) -> None:
tool_instance = _make_tool_instance()
context.trs_b_tool_instance = tool_instance
try:
context.trs_b_result = context.trs_b_service.update_tool(
tool="local/branch-tool",
tool_config=tool_instance,
)
context.trs_b_error = None
except Exception as exc:
context.trs_b_error = exc
@when("I trs branch update tool with a string name and dict tool_config")
def step_trs_branch_update_string_dict(context: Context) -> None:
try:
context.trs_b_result = context.trs_b_service.update_tool(
tool="local/updated-tool",
tool_config={"description": "updated desc", "timeout": 60},
)
context.trs_b_error = None
except Exception as exc:
context.trs_b_error = exc
@when("I trs branch update tool with a dict tool and dict tool_config")
def step_trs_branch_update_dict_dict(context: Context) -> None:
tool_dict = {"name": "local/dict-tool", "description": "orig"}
try:
context.trs_b_result = context.trs_b_service.update_tool(
tool=tool_dict,
tool_config={"description": "overridden"},
)
context.trs_b_error = None
except Exception as exc:
context.trs_b_error = exc
# ---------------------------------------------------------------------------
# When: list_tools with filters
# ---------------------------------------------------------------------------
@when('I trs branch list tools with namespace "{ns}" type "{tt}" source "{src}"')
def step_trs_branch_list_tools(context: Context, ns: str, tt: str, src: str) -> None:
try:
context.trs_b_result = context.trs_b_service.list_tools(
namespace=ns, tool_type=tt, source=src
)
context.trs_b_error = None
except Exception as exc:
context.trs_b_error = exc
# ---------------------------------------------------------------------------
# When: attach_validation with invalid mode
# ---------------------------------------------------------------------------
@when('I trs branch attach validation with mode "{mode}"')
def step_trs_branch_attach_invalid_mode(context: Context, mode: str) -> None:
try:
context.trs_b_result = context.trs_b_service.attach_validation(
validation_name="local/some-validation",
resource_id="res-001",
)
context.trs_b_error = None
except Exception as exc:
context.trs_b_error = exc
# ---------------------------------------------------------------------------
# Then: register_tool assertions
# ---------------------------------------------------------------------------
@then("the trs branch result should equal the registered tool")
def step_trs_branch_result_equals_tool(context: Context) -> None:
assert context.trs_b_error is None, (
f"Expected no error, got {type(context.trs_b_error).__name__}: "
f"{context.trs_b_error}"
)
assert context.trs_b_result == _SAMPLE_TOOL
@then("the trs branch add method should have been called")
def step_trs_branch_add_called(context: Context) -> None:
assert hasattr(context.trs_b_tool_repo, "add_called"), (
"Repo does not track add_called"
)
assert context.trs_b_tool_repo.add_called is True, "add() was not called"
@then("a trs branch TypeError should be raised")
def step_trs_branch_type_error(context: Context) -> None:
assert context.trs_b_error is not None, "Expected TypeError but no error was raised"
assert isinstance(context.trs_b_error, TypeError), (
f"Expected TypeError, got {type(context.trs_b_error).__name__}: "
f"{context.trs_b_error}"
)
# ---------------------------------------------------------------------------
# Then: remove_tool assertions
# ---------------------------------------------------------------------------
@then("the trs branch removal result should be True")
def step_trs_branch_removal_true(context: Context) -> None:
assert context.trs_b_error is None, (
f"Expected no error, got {type(context.trs_b_error).__name__}: "
f"{context.trs_b_error}"
)
assert context.trs_b_result is True, f"Expected True, got {context.trs_b_result}"
@then("the trs branch remove method should have been called")
def step_trs_branch_remove_called(context: Context) -> None:
assert hasattr(context.trs_b_tool_repo, "remove_called"), (
"Repo does not track remove_called"
)
assert context.trs_b_tool_repo.remove_called is True, "remove() was not called"
# ---------------------------------------------------------------------------
# Then: update_tool assertions
# ---------------------------------------------------------------------------
@then("the trs branch updated result should be the Tool instance")
def step_trs_branch_updated_is_tool(context: Context) -> None:
assert context.trs_b_error is None, (
f"Expected no error, got {type(context.trs_b_error).__name__}: "
f"{context.trs_b_error}"
)
assert isinstance(context.trs_b_result, Tool), (
f"Expected Tool instance, got {type(context.trs_b_result).__name__}"
)
assert context.trs_b_result is context.trs_b_tool_instance, (
"Expected the exact Tool instance passed as tool_config"
)
@then("the trs branch updated result should contain the merged name and config")
def step_trs_branch_updated_merged(context: Context) -> None:
assert context.trs_b_error is None, (
f"Expected no error, got {type(context.trs_b_error).__name__}: "
f"{context.trs_b_error}"
)
result = context.trs_b_result
assert isinstance(result, dict), f"Expected dict, got {type(result).__name__}"
assert result["name"] == "local/updated-tool", (
f"Expected name 'local/updated-tool', got '{result.get('name')}'"
)
assert result["description"] == "updated desc", (
f"Expected description 'updated desc', got '{result.get('description')}'"
)
assert result["timeout"] == 60, f"Expected timeout 60, got {result.get('timeout')}"
@then("the trs branch updated result should be the original tool dict")
def step_trs_branch_updated_original(context: Context) -> None:
assert context.trs_b_error is None, (
f"Expected no error, got {type(context.trs_b_error).__name__}: "
f"{context.trs_b_error}"
)
result = context.trs_b_result
assert isinstance(result, dict), f"Expected dict, got {type(result).__name__}"
# When tool is not a string, the original tool dict is used directly
assert result["name"] == "local/dict-tool", (
f"Expected name 'local/dict-tool', got '{result.get('name')}'"
)
# ---------------------------------------------------------------------------
# Then: list_tools assertions
# ---------------------------------------------------------------------------
@then("the trs branch list result should come from the repo")
def step_trs_branch_list_result(context: Context) -> None:
assert context.trs_b_error is None, (
f"Expected no error, got {type(context.trs_b_error).__name__}: "
f"{context.trs_b_error}"
)
assert isinstance(context.trs_b_result, list), (
f"Expected list, got {type(context.trs_b_result).__name__}"
)
assert len(context.trs_b_result) == 1
assert context.trs_b_result[0]["name"] == "local/listed-tool"
# Verify filters were passed through
kwargs = context.trs_b_tool_repo._list_all_kwargs
assert kwargs["namespace"] == "local"
assert kwargs["tool_type"] == "tool"
assert kwargs["source"] == "builtin"
# ---------------------------------------------------------------------------
# Then: attach_validation assertions
# ---------------------------------------------------------------------------
@then('a trs branch ValidationError should be raised with message containing "{text}"')
def step_trs_branch_validation_error(context: Context, text: str) -> None:
assert context.trs_b_error is not None, (
"Expected ValidationError but no error was raised"
)
assert isinstance(context.trs_b_error, ValidationError), (
f"Expected ValidationError, got {type(context.trs_b_error).__name__}: "
f"{context.trs_b_error}"
)
assert text in str(context.trs_b_error), (
f"Expected '{text}' in error message, got '{context.trs_b_error}'"
)