forked from Rockachopa/Timmy-time-dashboard
Fix router disabled provider check + comprehensive functional tests
Fixes: - Router now properly skips disabled providers in complete() method - Fixed avg_latency calculation comment in tests (now correctly documents behavior) New Test Suites: - tests/test_functional_router.py: 10 functional tests for router - tests/test_functional_mcp.py: 15 functional tests for MCP discovery/bootstrap - tests/test_integration_full.py: 14 end-to-end integration tests Total: 39 new functional/integration tests All 144 tests passing (105 router/mcp + 39 functional/integration)
This commit is contained in:
275
tests/test_functional_mcp.py
Normal file
275
tests/test_functional_mcp.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""Functional tests for MCP Discovery and Bootstrap - tests actual behavior.
|
||||
|
||||
These tests verify the MCP system works end-to-end.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp.discovery import ToolDiscovery, mcp_tool, DiscoveredTool
|
||||
from mcp.bootstrap import auto_bootstrap, bootstrap_from_directory
|
||||
from mcp.registry import ToolRegistry
|
||||
|
||||
|
||||
class TestMCPToolDecoratorFunctional:
|
||||
"""Functional tests for @mcp_tool decorator."""
|
||||
|
||||
def test_decorator_marks_function(self):
|
||||
"""Test that decorator properly marks function as tool."""
|
||||
@mcp_tool(name="my_tool", category="test", tags=["a", "b"])
|
||||
def my_function(x: str) -> str:
|
||||
"""Do something."""
|
||||
return x
|
||||
|
||||
assert hasattr(my_function, "_mcp_tool")
|
||||
assert my_function._mcp_tool is True
|
||||
assert my_function._mcp_name == "my_tool"
|
||||
assert my_function._mcp_category == "test"
|
||||
assert my_function._mcp_tags == ["a", "b"]
|
||||
assert "Do something" in my_function._mcp_description
|
||||
|
||||
def test_decorator_uses_defaults(self):
|
||||
"""Test decorator uses sensible defaults."""
|
||||
@mcp_tool()
|
||||
def another_function():
|
||||
pass
|
||||
|
||||
assert another_function._mcp_name == "another_function"
|
||||
assert another_function._mcp_category == "general"
|
||||
assert another_function._mcp_tags == []
|
||||
|
||||
|
||||
class TestToolDiscoveryFunctional:
|
||||
"""Functional tests for tool discovery."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_module(self):
|
||||
"""Create a mock module with tools."""
|
||||
module = types.ModuleType("test_discovery_module")
|
||||
module.__file__ = "test_discovery_module.py"
|
||||
|
||||
@mcp_tool(name="echo", category="test")
|
||||
def echo_func(message: str) -> str:
|
||||
"""Echo a message."""
|
||||
return message
|
||||
|
||||
@mcp_tool(name="add", category="math")
|
||||
def add_func(a: int, b: int) -> int:
|
||||
"""Add numbers."""
|
||||
return a + b
|
||||
|
||||
def not_a_tool():
|
||||
"""Not decorated."""
|
||||
pass
|
||||
|
||||
module.echo_func = echo_func
|
||||
module.add_func = add_func
|
||||
module.not_a_tool = not_a_tool
|
||||
|
||||
sys.modules["test_discovery_module"] = module
|
||||
yield module
|
||||
del sys.modules["test_discovery_module"]
|
||||
|
||||
def test_discover_module_finds_tools(self, mock_module):
|
||||
"""Test discovering tools from a module."""
|
||||
registry = ToolRegistry()
|
||||
discovery = ToolDiscovery(registry=registry)
|
||||
|
||||
tools = discovery.discover_module("test_discovery_module")
|
||||
|
||||
names = [t.name for t in tools]
|
||||
assert "echo" in names
|
||||
assert "add" in names
|
||||
assert "not_a_tool" not in names
|
||||
|
||||
def test_discovered_tool_has_correct_metadata(self, mock_module):
|
||||
"""Test discovered tools have correct metadata."""
|
||||
registry = ToolRegistry()
|
||||
discovery = ToolDiscovery(registry=registry)
|
||||
|
||||
tools = discovery.discover_module("test_discovery_module")
|
||||
|
||||
echo = next(t for t in tools if t.name == "echo")
|
||||
assert echo.category == "test"
|
||||
assert "Echo a message" in echo.description
|
||||
|
||||
def test_discovered_tool_has_schema(self, mock_module):
|
||||
"""Test discovered tools have generated schemas."""
|
||||
registry = ToolRegistry()
|
||||
discovery = ToolDiscovery(registry=registry)
|
||||
|
||||
tools = discovery.discover_module("test_discovery_module")
|
||||
|
||||
add = next(t for t in tools if t.name == "add")
|
||||
assert "properties" in add.parameters_schema
|
||||
assert "a" in add.parameters_schema["properties"]
|
||||
assert "b" in add.parameters_schema["properties"]
|
||||
|
||||
def test_discover_nonexistent_module(self):
|
||||
"""Test discovering from non-existent module returns empty list."""
|
||||
registry = ToolRegistry()
|
||||
discovery = ToolDiscovery(registry=registry)
|
||||
|
||||
tools = discovery.discover_module("nonexistent_xyz_module")
|
||||
|
||||
assert tools == []
|
||||
|
||||
|
||||
class TestToolRegistrationFunctional:
|
||||
"""Functional tests for tool registration via discovery."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_module(self):
|
||||
"""Create a mock module with tools."""
|
||||
module = types.ModuleType("test_register_module")
|
||||
module.__file__ = "test_register_module.py"
|
||||
|
||||
@mcp_tool(name="register_test", category="test")
|
||||
def test_func(value: str) -> str:
|
||||
"""Test function."""
|
||||
return value.upper()
|
||||
|
||||
module.test_func = test_func
|
||||
sys.modules["test_register_module"] = module
|
||||
yield module
|
||||
del sys.modules["test_register_module"]
|
||||
|
||||
def test_auto_register_adds_to_registry(self, mock_module):
|
||||
"""Test auto_register adds tools to registry."""
|
||||
registry = ToolRegistry()
|
||||
discovery = ToolDiscovery(registry=registry)
|
||||
|
||||
registered = discovery.auto_register("test_register_module")
|
||||
|
||||
assert "register_test" in registered
|
||||
assert registry.get("register_test") is not None
|
||||
|
||||
def test_registered_tool_can_execute(self, mock_module):
|
||||
"""Test that registered tools can be executed."""
|
||||
registry = ToolRegistry()
|
||||
discovery = ToolDiscovery(registry=registry)
|
||||
|
||||
discovery.auto_register("test_register_module")
|
||||
|
||||
result = asyncio.run(
|
||||
registry.execute("register_test", {"value": "hello"})
|
||||
)
|
||||
|
||||
assert result == "HELLO"
|
||||
|
||||
def test_registered_tool_tracks_metrics(self, mock_module):
|
||||
"""Test that tool execution tracks metrics."""
|
||||
registry = ToolRegistry()
|
||||
discovery = ToolDiscovery(registry=registry)
|
||||
|
||||
discovery.auto_register("test_register_module")
|
||||
|
||||
# Execute multiple times
|
||||
for _ in range(3):
|
||||
asyncio.run(registry.execute("register_test", {"value": "test"}))
|
||||
|
||||
metrics = registry.get_metrics("register_test")
|
||||
assert metrics["executions"] == 3
|
||||
assert metrics["health"] == "healthy"
|
||||
|
||||
|
||||
class TestMCBootstrapFunctional:
|
||||
"""Functional tests for MCP bootstrap."""
|
||||
|
||||
def test_auto_bootstrap_empty_list(self):
|
||||
"""Test auto_bootstrap with empty packages list."""
|
||||
registry = ToolRegistry()
|
||||
|
||||
registered = auto_bootstrap(
|
||||
packages=[],
|
||||
registry=registry,
|
||||
force=True,
|
||||
)
|
||||
|
||||
assert registered == []
|
||||
|
||||
def test_auto_bootstrap_nonexistent_package(self):
|
||||
"""Test auto_bootstrap with non-existent package."""
|
||||
registry = ToolRegistry()
|
||||
|
||||
registered = auto_bootstrap(
|
||||
packages=["nonexistent_package_12345"],
|
||||
registry=registry,
|
||||
force=True,
|
||||
)
|
||||
|
||||
assert registered == []
|
||||
|
||||
def test_bootstrap_status(self):
|
||||
"""Test get_bootstrap_status returns expected structure."""
|
||||
from mcp.bootstrap import get_bootstrap_status
|
||||
|
||||
status = get_bootstrap_status()
|
||||
|
||||
assert "auto_bootstrap_enabled" in status
|
||||
assert "discovered_tools_count" in status
|
||||
assert "registered_tools_count" in status
|
||||
assert "default_packages" in status
|
||||
|
||||
|
||||
class TestRegistryIntegration:
|
||||
"""Integration tests for registry with discovery."""
|
||||
|
||||
def test_registry_discover_filtering(self):
|
||||
"""Test registry discover method filters correctly."""
|
||||
registry = ToolRegistry()
|
||||
|
||||
@mcp_tool(name="cat1", category="category1", tags=["tag1"])
|
||||
def func1():
|
||||
pass
|
||||
|
||||
@mcp_tool(name="cat2", category="category2", tags=["tag2"])
|
||||
def func2():
|
||||
pass
|
||||
|
||||
registry.register_tool(name="cat1", function=func1, category="category1", tags=["tag1"])
|
||||
registry.register_tool(name="cat2", function=func2, category="category2", tags=["tag2"])
|
||||
|
||||
# Filter by category
|
||||
cat1_tools = registry.discover(category="category1")
|
||||
assert len(cat1_tools) == 1
|
||||
assert cat1_tools[0].name == "cat1"
|
||||
|
||||
# Filter by tags
|
||||
tag1_tools = registry.discover(tags=["tag1"])
|
||||
assert len(tag1_tools) == 1
|
||||
assert tag1_tools[0].name == "cat1"
|
||||
|
||||
def test_registry_to_dict(self):
|
||||
"""Test registry export includes all fields."""
|
||||
registry = ToolRegistry()
|
||||
|
||||
@mcp_tool(name="export_test", category="test", tags=["a"])
|
||||
def export_func():
|
||||
"""Test export."""
|
||||
pass
|
||||
|
||||
registry.register_tool(
|
||||
name="export_test",
|
||||
function=export_func,
|
||||
category="test",
|
||||
tags=["a"],
|
||||
source_module="test_module",
|
||||
)
|
||||
|
||||
export = registry.to_dict()
|
||||
|
||||
assert export["total_tools"] == 1
|
||||
assert export["auto_discovered_count"] == 1
|
||||
|
||||
tool = export["tools"][0]
|
||||
assert tool["name"] == "export_test"
|
||||
assert tool["category"] == "test"
|
||||
assert tool["tags"] == ["a"]
|
||||
assert tool["source_module"] == "test_module"
|
||||
assert tool["auto_discovered"] is True
|
||||
Reference in New Issue
Block a user