227 lines
7.4 KiB
Python
227 lines
7.4 KiB
Python
"""Tests for Tool Injection Generator."""
|
|
|
|
import pytest
|
|
import ast
|
|
from unittest.mock import AsyncMock
|
|
from mcp_forge.mcp.injection import ToolInjectionGenerator
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_client_manager():
|
|
"""Create mock MCP client manager with tools."""
|
|
manager = AsyncMock()
|
|
|
|
# Tool schemas
|
|
manager.get_tool_schema = AsyncMock(side_effect=lambda tool_name: {
|
|
"read_file": {
|
|
"type": "object",
|
|
"properties": {
|
|
"path": {"type": "string", "description": "File path to read"},
|
|
"encoding": {"type": "string", "description": "File encoding"}
|
|
},
|
|
"required": ["path"]
|
|
},
|
|
"write_file": {
|
|
"type": "object",
|
|
"properties": {
|
|
"path": {"type": "string", "description": "File path to write"},
|
|
"content": {"type": "string", "description": "Content to write"},
|
|
"mode": {"type": "string", "description": "Write mode"}
|
|
},
|
|
"required": ["path", "content"]
|
|
},
|
|
"calculate": {
|
|
"type": "object",
|
|
"properties": {
|
|
"expression": {"type": "string", "description": "Math expression"},
|
|
"precision": {"type": "integer", "description": "Decimal precision"}
|
|
},
|
|
"required": ["expression"]
|
|
}
|
|
}[tool_name])
|
|
|
|
return manager
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_injection_code_is_valid_python(mock_client_manager):
|
|
"""Test that generated code is valid Python."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
code = await generator.generate_injection_code(
|
|
tool_names=["read_file", "write_file"],
|
|
bridge_socket_path="/tmp/bridge.sock"
|
|
)
|
|
|
|
# Try to parse the generated code
|
|
try:
|
|
ast.parse(code)
|
|
except SyntaxError as e:
|
|
pytest.fail(f"Generated code has syntax error: {e}")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generated_code_includes_bridge_client(mock_client_manager):
|
|
"""Test that generated code includes bridge client."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
code = await generator.generate_injection_code(
|
|
tool_names=["read_file"],
|
|
bridge_socket_path="/tmp/bridge.sock"
|
|
)
|
|
|
|
# Verify bridge client function is included
|
|
assert "_mcp_call" in code
|
|
assert "socket.socket" in code
|
|
assert "socket.AF_UNIX" in code
|
|
assert "/tmp/bridge.sock" in code
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generated_code_includes_tool_functions(mock_client_manager):
|
|
"""Test that generated code includes wrapper functions for each tool."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
code = await generator.generate_injection_code(
|
|
tool_names=["read_file", "write_file", "calculate"],
|
|
bridge_socket_path="/tmp/bridge.sock"
|
|
)
|
|
|
|
# Verify tool functions are defined
|
|
assert "def read_file(" in code
|
|
assert "def write_file(" in code
|
|
assert "def calculate(" in code
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_function_signatures_match_schemas(mock_client_manager):
|
|
"""Test that function signatures match tool schemas."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
code = await generator.generate_injection_code(
|
|
tool_names=["read_file", "write_file"],
|
|
bridge_socket_path="/tmp/bridge.sock"
|
|
)
|
|
|
|
# read_file has path (required) and encoding (optional)
|
|
assert "def read_file(path: str, encoding: str = None)" in code
|
|
|
|
# write_file has path, content (required) and mode (optional)
|
|
assert "def write_file(path: str, content: str, mode: str = None)" in code
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generated_functions_have_docstrings(mock_client_manager):
|
|
"""Test that generated functions have docstrings."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
code = await generator.generate_injection_code(
|
|
tool_names=["read_file"],
|
|
bridge_socket_path="/tmp/bridge.sock"
|
|
)
|
|
|
|
# Verify docstring is present (contains parameter descriptions)
|
|
assert '"""' in code
|
|
assert "File path to read" in code or "path:" in code.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generated_functions_call_bridge(mock_client_manager):
|
|
"""Test that generated functions call _mcp_call."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
code = await generator.generate_injection_code(
|
|
tool_names=["read_file"],
|
|
bridge_socket_path="/tmp/bridge.sock"
|
|
)
|
|
|
|
# Verify function calls _mcp_call with tool name
|
|
lines = code.split('\n')
|
|
in_read_file = False
|
|
found_call = False
|
|
|
|
for line in lines:
|
|
if "def read_file(" in line:
|
|
in_read_file = True
|
|
if in_read_file and "_mcp_call" in line and "read_file" in line:
|
|
found_call = True
|
|
break
|
|
|
|
assert found_call, "Generated function should call _mcp_call with tool name"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_type_hints_from_schema(mock_client_manager):
|
|
"""Test that type hints are generated from schema types."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
code = await generator.generate_injection_code(
|
|
tool_names=["calculate"],
|
|
bridge_socket_path="/tmp/bridge.sock"
|
|
)
|
|
|
|
# calculate has string expression and integer precision
|
|
assert "expression: str" in code
|
|
assert "precision: int" in code or "precision: " in code
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_required_vs_optional_parameters(mock_client_manager):
|
|
"""Test that required and optional parameters are handled correctly."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
code = await generator.generate_injection_code(
|
|
tool_names=["read_file"],
|
|
bridge_socket_path="/tmp/bridge.sock"
|
|
)
|
|
|
|
# path is required (no default), encoding is optional (has default)
|
|
assert "def read_file(path: str, encoding: str = None)" in code or \
|
|
"def read_file(path: str, encoding: " in code
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generated_code_has_imports(mock_client_manager):
|
|
"""Test that generated code includes necessary imports."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
code = await generator.generate_injection_code(
|
|
tool_names=["read_file"],
|
|
bridge_socket_path="/tmp/bridge.sock"
|
|
)
|
|
|
|
# Verify imports
|
|
assert "import socket" in code
|
|
assert "import json" in code
|
|
assert "from typing import Any" in code or "typing" in code
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_tool_list(mock_client_manager):
|
|
"""Test handling of empty tool list."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
code = await generator.generate_injection_code(
|
|
tool_names=[],
|
|
bridge_socket_path="/tmp/bridge.sock"
|
|
)
|
|
|
|
# Should still include bridge client
|
|
assert "_mcp_call" in code
|
|
# But no tool functions
|
|
assert "def read_file(" not in code
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_custom_socket_path(mock_client_manager):
|
|
"""Test that custom socket path is used correctly."""
|
|
generator = ToolInjectionGenerator(mock_client_manager)
|
|
|
|
custom_path = "/custom/path/to/socket.sock"
|
|
code = await generator.generate_injection_code(
|
|
tool_names=["read_file"],
|
|
bridge_socket_path=custom_path
|
|
)
|
|
|
|
# Verify custom path is in generated code
|
|
assert custom_path in code
|