Files
hkuds--nanobot/tests/agent/test_tool_loader_entrypoints.py
T
wehub-resource-sync ba1d0b91a4
Test Suite / webui (push) Failing after 1s
Test Suite / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:06:36 +08:00

145 lines
4.2 KiB
Python

from unittest.mock import MagicMock, patch
import pytest
from nanobot.agent.tools.base import Tool
from nanobot.agent.tools.context import ToolContext
from nanobot.agent.tools.loader import ToolLoader
from nanobot.agent.tools.registry import ToolRegistry, is_tool_error_result
def test_loader_discovers_entry_point_tools():
"""Simulate an entry-point plugin being discovered."""
mock_ep = MagicMock()
mock_ep.name = "my_plugin"
class _FakeTool(Tool):
__name__ = "FakeTool"
_plugin_discoverable = True
_scopes = {"core"}
@property
def name(self) -> str:
return "fake_tool"
@property
def description(self) -> str:
return "A fake tool for testing."
@property
def parameters(self) -> dict:
return {"type": "object"}
@classmethod
def enabled(cls, ctx):
return True
@classmethod
def create(cls, ctx):
return MagicMock()
async def execute(self, **_):
return "ok"
mock_ep.load.return_value = _FakeTool
with patch("nanobot.agent.tools.loader.entry_points", return_value=[mock_ep]):
loader = ToolLoader()
discovered = loader._discover_plugins()
assert "my_plugin" in discovered
assert discovered["my_plugin"] is _FakeTool
def test_loader_skips_abstract_entry_point_tools():
"""Verify abstract tool classes registered via entry_points are skipped."""
mock_ep = MagicMock()
mock_ep.name = "abstract_plugin"
class _AbstractTool(Tool):
__name__ = "AbstractTool"
_plugin_discoverable = True
_scopes = {"core"}
@classmethod
def enabled(cls, ctx):
return True
@classmethod
def create(cls, ctx):
return MagicMock()
# Intentionally missing abstract properties (name, description, parameters, execute)
mock_ep.load.return_value = _AbstractTool
with patch("nanobot.agent.tools.loader.entry_points", return_value=[mock_ep]):
loader = ToolLoader()
discovered = loader._discover_plugins()
assert "abstract_plugin" not in discovered
@pytest.mark.asyncio
async def test_loader_entry_point_error_wrapper_preserves_tool_api(tmp_path):
"""Only adapt legacy plugin error strings; keep the wrapped tool API intact."""
mock_ep = MagicMock()
mock_ep.name = "api_plugin"
class _ApiPluginTool(Tool):
config_key = "api_plugin"
@property
def name(self) -> str:
return "api_plugin"
@property
def description(self) -> str:
return "Entry-point plugin with custom tool API methods."
@property
def parameters(self) -> dict:
return {"type": "object", "properties": {"value": {"type": "string"}}}
@property
def read_only(self) -> bool:
return True
@property
def concurrency_safe(self) -> bool:
return False
def cast_params(self, params: dict) -> dict:
return {"value": str(params["value"])}
def validate_params(self, params: dict) -> list[str]:
return [] if params == {"value": "1"} else ["bad value"]
def to_schema(self) -> dict:
return {"name": self.name, "custom": True}
async def execute(self, **_):
return "Error: plugin failed"
mock_ep.load.return_value = _ApiPluginTool
registry = ToolRegistry()
with patch("nanobot.agent.tools.loader.entry_points", return_value=[mock_ep]):
ToolLoader(test_classes=[]).load(
ToolContext(config=None, workspace=str(tmp_path)),
registry,
)
tool = registry.get("api_plugin")
assert tool is not None
assert tool.config_key == "api_plugin"
assert tool.read_only is True
assert tool.concurrency_safe is False
assert tool.cast_params({"value": 1}) == {"value": "1"}
assert tool.validate_params({"value": "1"}) == []
assert tool.to_schema() == {"name": "api_plugin", "custom": True}
result = await tool.execute(value="1")
assert is_tool_error_result("api_plugin", result) is True
assert str(result) == "Error: plugin failed"