"""Unit tests for the tool registry."""
from pathlib import Path
from typing import Any, Dict
from unittest.mock import Mock, patch
from council.core.registry import ToolRegistry
from council.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("council.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("council.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("council.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