forked from Rockachopa/Timmy-time-dashboard
266 lines
8.5 KiB
Python
266 lines
8.5 KiB
Python
|
|
"""Tests for MCP Auto-Bootstrap.
|
||
|
|
|
||
|
|
Tests follow pytest best practices:
|
||
|
|
- No module-level state
|
||
|
|
- Proper fixture cleanup
|
||
|
|
- Isolated tests
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from mcp.bootstrap import (
|
||
|
|
auto_bootstrap,
|
||
|
|
bootstrap_from_directory,
|
||
|
|
get_bootstrap_status,
|
||
|
|
DEFAULT_TOOL_PACKAGES,
|
||
|
|
AUTO_BOOTSTRAP_ENV_VAR,
|
||
|
|
)
|
||
|
|
from mcp.discovery import mcp_tool, ToolDiscovery
|
||
|
|
from mcp.registry import ToolRegistry
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def fresh_registry():
|
||
|
|
"""Create a fresh registry for each test."""
|
||
|
|
return ToolRegistry()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def fresh_discovery(fresh_registry):
|
||
|
|
"""Create a fresh discovery instance for each test."""
|
||
|
|
return ToolDiscovery(registry=fresh_registry)
|
||
|
|
|
||
|
|
|
||
|
|
class TestAutoBootstrap:
|
||
|
|
"""Test auto_bootstrap function."""
|
||
|
|
|
||
|
|
def test_auto_bootstrap_disabled_by_env(self, fresh_registry):
|
||
|
|
"""Test that auto-bootstrap can be disabled via env var."""
|
||
|
|
with patch.dict(os.environ, {AUTO_BOOTSTRAP_ENV_VAR: "0"}):
|
||
|
|
registered = auto_bootstrap(registry=fresh_registry)
|
||
|
|
|
||
|
|
assert len(registered) == 0
|
||
|
|
|
||
|
|
def test_auto_bootstrap_forced_overrides_env(self, fresh_registry):
|
||
|
|
"""Test that force=True overrides env var."""
|
||
|
|
with patch.dict(os.environ, {AUTO_BOOTSTRAP_ENV_VAR: "0"}):
|
||
|
|
# Empty packages list - just test that it runs
|
||
|
|
registered = auto_bootstrap(
|
||
|
|
packages=[],
|
||
|
|
registry=fresh_registry,
|
||
|
|
force=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert len(registered) == 0 # No packages, but didn't abort
|
||
|
|
|
||
|
|
def test_auto_bootstrap_nonexistent_package(self, fresh_registry):
|
||
|
|
"""Test bootstrap from non-existent package."""
|
||
|
|
registered = auto_bootstrap(
|
||
|
|
packages=["nonexistent_package_xyz_12345"],
|
||
|
|
registry=fresh_registry,
|
||
|
|
force=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert len(registered) == 0
|
||
|
|
|
||
|
|
def test_auto_bootstrap_empty_packages(self, fresh_registry):
|
||
|
|
"""Test bootstrap with empty packages list."""
|
||
|
|
registered = auto_bootstrap(
|
||
|
|
packages=[],
|
||
|
|
registry=fresh_registry,
|
||
|
|
force=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert len(registered) == 0
|
||
|
|
|
||
|
|
def test_auto_bootstrap_registers_tools(self, fresh_registry, fresh_discovery):
|
||
|
|
"""Test that auto-bootstrap registers discovered tools."""
|
||
|
|
@mcp_tool(name="bootstrap_tool", category="bootstrap")
|
||
|
|
def bootstrap_func(value: str) -> str:
|
||
|
|
"""A bootstrap test tool."""
|
||
|
|
return value
|
||
|
|
|
||
|
|
# Manually register it
|
||
|
|
fresh_registry.register_tool(
|
||
|
|
name="bootstrap_tool",
|
||
|
|
function=bootstrap_func,
|
||
|
|
category="bootstrap",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Verify it's in the registry
|
||
|
|
record = fresh_registry.get("bootstrap_tool")
|
||
|
|
assert record is not None
|
||
|
|
assert record.auto_discovered is True
|
||
|
|
|
||
|
|
|
||
|
|
class TestBootstrapFromDirectory:
|
||
|
|
"""Test bootstrap_from_directory function."""
|
||
|
|
|
||
|
|
def test_bootstrap_from_directory(self, fresh_registry, tmp_path):
|
||
|
|
"""Test bootstrapping from a directory."""
|
||
|
|
tools_dir = tmp_path / "tools"
|
||
|
|
tools_dir.mkdir()
|
||
|
|
|
||
|
|
tool_file = tools_dir / "my_tools.py"
|
||
|
|
tool_file.write_text('''
|
||
|
|
from mcp.discovery import mcp_tool
|
||
|
|
|
||
|
|
@mcp_tool(name="dir_tool", category="directory")
|
||
|
|
def dir_tool(value: str) -> str:
|
||
|
|
"""A tool from directory."""
|
||
|
|
return value
|
||
|
|
''')
|
||
|
|
|
||
|
|
registered = bootstrap_from_directory(tools_dir, registry=fresh_registry)
|
||
|
|
|
||
|
|
# Function won't be resolved (AST only), so not registered
|
||
|
|
assert len(registered) == 0
|
||
|
|
|
||
|
|
def test_bootstrap_from_nonexistent_directory(self, fresh_registry):
|
||
|
|
"""Test bootstrapping from non-existent directory."""
|
||
|
|
registered = bootstrap_from_directory(
|
||
|
|
Path("/nonexistent/tools"),
|
||
|
|
registry=fresh_registry
|
||
|
|
)
|
||
|
|
|
||
|
|
assert len(registered) == 0
|
||
|
|
|
||
|
|
def test_bootstrap_skips_private_files(self, fresh_registry, tmp_path):
|
||
|
|
"""Test that private files are skipped."""
|
||
|
|
tools_dir = tmp_path / "tools"
|
||
|
|
tools_dir.mkdir()
|
||
|
|
|
||
|
|
private_file = tools_dir / "_private.py"
|
||
|
|
private_file.write_text('''
|
||
|
|
from mcp.discovery import mcp_tool
|
||
|
|
|
||
|
|
@mcp_tool(name="private_tool")
|
||
|
|
def private_tool():
|
||
|
|
pass
|
||
|
|
''')
|
||
|
|
|
||
|
|
registered = bootstrap_from_directory(tools_dir, registry=fresh_registry)
|
||
|
|
assert len(registered) == 0
|
||
|
|
|
||
|
|
|
||
|
|
class TestGetBootstrapStatus:
|
||
|
|
"""Test get_bootstrap_status function."""
|
||
|
|
|
||
|
|
def test_status_default_enabled(self):
|
||
|
|
"""Test status when auto-bootstrap is enabled by default."""
|
||
|
|
with patch.dict(os.environ, {}, clear=True):
|
||
|
|
status = get_bootstrap_status()
|
||
|
|
|
||
|
|
assert status["auto_bootstrap_enabled"] is True
|
||
|
|
assert "discovered_tools_count" in status
|
||
|
|
assert "registered_tools_count" in status
|
||
|
|
assert status["default_packages"] == DEFAULT_TOOL_PACKAGES
|
||
|
|
|
||
|
|
def test_status_disabled(self):
|
||
|
|
"""Test status when auto-bootstrap is disabled."""
|
||
|
|
with patch.dict(os.environ, {AUTO_BOOTSTRAP_ENV_VAR: "0"}):
|
||
|
|
status = get_bootstrap_status()
|
||
|
|
|
||
|
|
assert status["auto_bootstrap_enabled"] is False
|
||
|
|
|
||
|
|
|
||
|
|
class TestIntegration:
|
||
|
|
"""Integration tests for bootstrap + discovery + registry."""
|
||
|
|
|
||
|
|
def test_full_workflow(self, fresh_registry):
|
||
|
|
"""Test the full auto-discovery and registration workflow."""
|
||
|
|
@mcp_tool(name="integration_tool", category="integration")
|
||
|
|
def integration_func(data: str) -> str:
|
||
|
|
"""Integration test tool."""
|
||
|
|
return f"processed: {data}"
|
||
|
|
|
||
|
|
fresh_registry.register_tool(
|
||
|
|
name="integration_tool",
|
||
|
|
function=integration_func,
|
||
|
|
category="integration",
|
||
|
|
source_module="test_module",
|
||
|
|
)
|
||
|
|
|
||
|
|
record = fresh_registry.get("integration_tool")
|
||
|
|
assert record is not None
|
||
|
|
assert record.auto_discovered is True
|
||
|
|
assert record.source_module == "test_module"
|
||
|
|
|
||
|
|
export = fresh_registry.to_dict()
|
||
|
|
assert export["total_tools"] == 1
|
||
|
|
assert export["auto_discovered_count"] == 1
|
||
|
|
|
||
|
|
def test_tool_execution_after_registration(self, fresh_registry):
|
||
|
|
"""Test that registered tools can be executed."""
|
||
|
|
@mcp_tool(name="exec_tool", category="execution")
|
||
|
|
def exec_func(input: str) -> str:
|
||
|
|
"""Executable test tool."""
|
||
|
|
return input.upper()
|
||
|
|
|
||
|
|
fresh_registry.register_tool(
|
||
|
|
name="exec_tool",
|
||
|
|
function=exec_func,
|
||
|
|
category="execution",
|
||
|
|
)
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
result = asyncio.run(fresh_registry.execute("exec_tool", {"input": "hello"}))
|
||
|
|
|
||
|
|
assert result == "HELLO"
|
||
|
|
|
||
|
|
metrics = fresh_registry.get_metrics("exec_tool")
|
||
|
|
assert metrics["executions"] == 1
|
||
|
|
assert metrics["health"] == "healthy"
|
||
|
|
|
||
|
|
def test_discover_filtering(self, fresh_registry):
|
||
|
|
"""Test filtering registered tools."""
|
||
|
|
@mcp_tool(name="cat1_tool", category="category1")
|
||
|
|
def cat1_func():
|
||
|
|
pass
|
||
|
|
|
||
|
|
@mcp_tool(name="cat2_tool", category="category2")
|
||
|
|
def cat2_func():
|
||
|
|
pass
|
||
|
|
|
||
|
|
fresh_registry.register_tool(
|
||
|
|
name="cat1_tool",
|
||
|
|
function=cat1_func,
|
||
|
|
category="category1"
|
||
|
|
)
|
||
|
|
fresh_registry.register_tool(
|
||
|
|
name="cat2_tool",
|
||
|
|
function=cat2_func,
|
||
|
|
category="category2"
|
||
|
|
)
|
||
|
|
|
||
|
|
cat1_tools = fresh_registry.discover(category="category1")
|
||
|
|
assert len(cat1_tools) == 1
|
||
|
|
assert cat1_tools[0].name == "cat1_tool"
|
||
|
|
|
||
|
|
auto_tools = fresh_registry.discover(auto_discovered_only=True)
|
||
|
|
assert len(auto_tools) == 2
|
||
|
|
|
||
|
|
def test_registry_export_includes_metadata(self, fresh_registry):
|
||
|
|
"""Test that registry export includes all metadata."""
|
||
|
|
@mcp_tool(name="meta_tool", category="meta", tags=["tag1", "tag2"])
|
||
|
|
def meta_func():
|
||
|
|
pass
|
||
|
|
|
||
|
|
fresh_registry.register_tool(
|
||
|
|
name="meta_tool",
|
||
|
|
function=meta_func,
|
||
|
|
category="meta",
|
||
|
|
tags=["tag1", "tag2"],
|
||
|
|
)
|
||
|
|
|
||
|
|
export = fresh_registry.to_dict()
|
||
|
|
|
||
|
|
for tool_dict in export["tools"]:
|
||
|
|
assert "tags" in tool_dict
|
||
|
|
assert "source_module" in tool_dict
|
||
|
|
assert "auto_discovered" in tool_dict
|