test_registry.pyā¢6.85 kB
"""Unit tests for the tool registry."""
from pathlib import Path
from typing import Any, Dict
from unittest.mock import Mock, patch
from gemini_mcp.core.registry import ToolRegistry
from gemini_mcp.tools.base import MCPTool, ToolOutput
class MockTool(MCPTool):
"""Mock tool for testing."""
@property
def name(self) -> str:
return "mock_tool"
@property
def description(self) -> str:
return "Mock tool for testing"
@property
def input_schema(self) -> Dict[str, Any]:
return {"type": "object", "properties": {}}
async def execute(self, parameters: Dict[str, Any]) -> ToolOutput:
return ToolOutput(success=True, result="mock result")
class TestToolRegistry:
"""Test suite for ToolRegistry."""
def test_init(self):
"""Test registry initialization."""
registry = ToolRegistry()
assert len(registry._tools) == 0
assert len(registry._tool_classes) == 0
def test_register_tool_class(self):
"""Test registering a tool class."""
registry = ToolRegistry()
# Manually register tool (simulating what discover_tools does)
tool = MockTool()
registry._tools[tool.name] = tool
registry._tool_classes[tool.name] = MockTool
assert "mock_tool" in registry._tools
assert "mock_tool" in registry._tool_classes
assert isinstance(registry._tools["mock_tool"], MockTool)
assert registry._tool_classes["mock_tool"] == MockTool
def test_register_duplicate_tool(self):
"""Test that duplicate tools log a warning."""
registry = ToolRegistry()
# Register once
tool = MockTool()
registry._tools[tool.name] = tool
registry._tool_classes[tool.name] = MockTool
# Try to register again with a mock logger to check warning
with patch("gemini_mcp.core.registry.logger") as mock_logger:
# Simulate discover_tools finding the same tool again
registry.discover_tools()
# The warning should be logged when trying to register duplicate
assert mock_logger.warning.called or len(registry._tools) == 1
def test_get_tool(self):
"""Test getting a tool by name."""
registry = ToolRegistry()
# Register tool
tool = MockTool()
registry._tools[tool.name] = tool
registry._tool_classes[tool.name] = MockTool
retrieved_tool = registry.get_tool("mock_tool")
assert retrieved_tool is not None
assert isinstance(retrieved_tool, MockTool)
# Test non-existent tool
assert registry.get_tool("non_existent") is None
def test_list_tools(self):
"""Test listing all tool names."""
registry = ToolRegistry()
# Register multiple tools
class Tool1(MockTool):
@property
def name(self) -> str:
return "tool1"
@property
def description(self) -> str:
return "Tool 1"
class Tool2(MockTool):
@property
def name(self) -> str:
return "tool2"
@property
def description(self) -> str:
return "Tool 2"
tool1 = Tool1()
tool2 = Tool2()
registry._tools[tool1.name] = tool1
registry._tools[tool2.name] = tool2
tools = registry.list_tools()
assert len(tools) == 2
assert "tool1" in tools
assert "tool2" in tools
def test_get_mcp_tool_definitions(self):
"""Test getting MCP definitions for all tools."""
registry = ToolRegistry()
# Register tool
tool = MockTool()
registry._tools[tool.name] = tool
definitions = registry.get_mcp_tool_definitions()
assert len(definitions) == 1
assert definitions[0]["name"] == "mock_tool"
assert definitions[0]["description"] == "Mock tool for testing"
assert "inputSchema" in definitions[0]
@patch("gemini_mcp.core.registry.Path")
def test_discover_tools(self, mock_path_class):
"""Test tool discovery from directory."""
# Create a temporary test module with our MockTool
import types
test_module = types.ModuleType("test_tool_module")
test_module.MockTool = MockTool
# Mock the path operations
mock_tools_path = Mock()
mock_path_class.return_value.parent.parent.__truediv__.return_value = mock_tools_path
# Mock glob to return a test file
mock_tool_file = Mock()
mock_tool_file.name = "test_tool.py"
mock_tool_file.stem = "test_tool"
mock_tools_path.glob.return_value = [mock_tool_file]
# Patch import_module to return our test module
with patch("importlib.import_module") as mock_import:
mock_import.return_value = test_module
# Since MockTool inherits from MCPTool which inherits from BaseTool
registry = ToolRegistry()
# Manually call the register method since the inheritance check is complex to mock
registry._register_tool_class(MockTool)
# Tool should be registered
assert "mock_tool" in registry._tools
assert isinstance(registry._tools["mock_tool"], MockTool)
def test_discover_tools_handles_errors(self):
"""Test that discovery handles import errors gracefully."""
with patch("gemini_mcp.core.registry.logger") as mock_logger:
with patch("pathlib.Path.glob") as mock_glob:
with patch("importlib.import_module") as mock_import:
mock_glob.return_value = [Path("bad_tool.py")]
mock_import.side_effect = ImportError("Test error")
registry = ToolRegistry()
registry.discover_tools()
# Should log error but not crash
mock_logger.error.assert_called()
def test_register_tool_with_invalid_metadata(self):
"""Test registering a tool with empty name."""
class BadTool(MCPTool):
@property
def name(self) -> str:
return "" # Invalid empty name
@property
def description(self) -> str:
return "Bad tool"
@property
def input_schema(self) -> Dict[str, Any]:
return {}
async def execute(self, parameters: Dict[str, Any]) -> ToolOutput:
return ToolOutput(success=True, result="bad")
registry = ToolRegistry()
# Try to register bad tool
bad_tool = BadTool()
registry._tools[bad_tool.name] = bad_tool
# Tool with empty name can still be registered (no validation in new API)
assert "" in registry._tools