oraios--serena
c6af9e284a
Tests / catch-all (windows-latest) (push) Has been cancelled
Tests / jvm (macos-latest) (push) Has been cancelled
Tests / jvm (ubuntu-latest) (push) Has been cancelled
Tests / jvm (windows-latest) (push) Has been cancelled
Tests / native (macos-latest) (push) Has been cancelled
Tests / native (ubuntu-latest) (push) Has been cancelled
Tests / native (windows-latest) (push) Has been cancelled
Tests / niche (ubuntu-latest) (push) Has been cancelled
Tests / other-langs (macos-latest) (push) Has been cancelled
Tests / other-langs (ubuntu-latest) (push) Has been cancelled
Tests / other-langs (windows-latest) (push) Has been cancelled
Tests / catch-all (macos-latest) (push) Has been cancelled
Tests / catch-all (ubuntu-latest) (push) Has been cancelled
Docs Build / build (push) Has been cancelled
Docs Build / deploy (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Build and Push Docker Images / build-and-push (push) Has been cancelled
301 行
9.4 KiB
Python
301 行
9.4 KiB
Python
"""Tests for the mcp.py module in serena."""
|
|
|
|
import pytest
|
|
from mcp.server.fastmcp.tools.base import Tool as MCPTool
|
|
|
|
from serena.agent import Tool, ToolRegistry
|
|
from serena.config.context_mode import SerenaAgentContext
|
|
from serena.mcp import SerenaMCPFactory
|
|
|
|
make_tool = SerenaMCPFactory.make_mcp_tool
|
|
|
|
|
|
# Create a mock agent for tool initialization
|
|
class MockAgent:
|
|
def __init__(self):
|
|
self.project_config = None
|
|
self.serena_config = None
|
|
|
|
@staticmethod
|
|
def get_context() -> SerenaAgentContext:
|
|
return SerenaAgentContext.load_default()
|
|
|
|
|
|
class BaseMockTool(Tool):
|
|
"""A mock Tool class for testing."""
|
|
|
|
def __init__(self):
|
|
super().__init__(MockAgent())
|
|
|
|
|
|
class BasicTool(BaseMockTool):
|
|
"""A mock Tool class for testing."""
|
|
|
|
def apply(self, name: str, age: int = 0) -> str:
|
|
"""This is a test function.
|
|
|
|
:param name: The person's name
|
|
:param age: The person's age
|
|
:return: A greeting message
|
|
"""
|
|
return f"Hello {name}, you are {age} years old!"
|
|
|
|
def apply_ex(
|
|
self,
|
|
log_call: bool = True,
|
|
catch_exceptions: bool = True,
|
|
**kwargs,
|
|
) -> str:
|
|
"""Mock implementation of apply_ex."""
|
|
return self.apply(**kwargs)
|
|
|
|
|
|
def test_make_tool_basic() -> None:
|
|
"""Test that make_tool correctly creates an MCP tool from a Tool object."""
|
|
mock_tool = BasicTool()
|
|
|
|
mcp_tool = make_tool(mock_tool)
|
|
|
|
# Test that the MCP tool has the correct properties
|
|
assert isinstance(mcp_tool, MCPTool)
|
|
assert mcp_tool.name == "basic"
|
|
assert "This is a test function. Returns A greeting message." in mcp_tool.description
|
|
|
|
# Test that the parameters were correctly processed
|
|
parameters = mcp_tool.parameters
|
|
assert "properties" in parameters
|
|
assert "name" in parameters["properties"]
|
|
assert "age" in parameters["properties"]
|
|
assert parameters["properties"]["name"]["description"] == "The person's name."
|
|
assert parameters["properties"]["age"]["description"] == "The person's age."
|
|
|
|
|
|
def test_make_tool_execution() -> None:
|
|
"""Test that the execution function created by make_tool works correctly."""
|
|
mock_tool = BasicTool()
|
|
mcp_tool = make_tool(mock_tool)
|
|
|
|
# Execute the MCP tool function
|
|
result = mcp_tool.fn(name="Alice", age=30)
|
|
|
|
assert result == "Hello Alice, you are 30 years old!"
|
|
|
|
|
|
def test_make_tool_no_params() -> None:
|
|
"""Test make_tool with a function that has no parameters."""
|
|
|
|
class NoParamsTool(BaseMockTool):
|
|
def apply(self) -> str:
|
|
"""This is a test function with no parameters.
|
|
|
|
:return: A simple result
|
|
"""
|
|
return "Simple result"
|
|
|
|
def apply_ex(self, *args, **kwargs) -> str:
|
|
return self.apply()
|
|
|
|
tool = NoParamsTool()
|
|
mcp_tool = make_tool(tool)
|
|
|
|
assert mcp_tool.name == "no_params"
|
|
assert "This is a test function with no parameters. Returns A simple result." in mcp_tool.description
|
|
assert mcp_tool.parameters["properties"] == {}
|
|
|
|
|
|
def test_make_tool_no_return_description() -> None:
|
|
"""Test make_tool with a function that has no return description."""
|
|
|
|
class NoReturnTool(BaseMockTool):
|
|
def apply(self, param: str) -> str:
|
|
"""This is a test function.
|
|
|
|
:param param: The parameter
|
|
"""
|
|
return f"Processed: {param}"
|
|
|
|
def apply_ex(self, *args, **kwargs) -> str:
|
|
return self.apply(**kwargs)
|
|
|
|
tool = NoReturnTool()
|
|
mcp_tool = make_tool(tool)
|
|
|
|
assert mcp_tool.name == "no_return"
|
|
assert mcp_tool.description == "This is a test function."
|
|
assert mcp_tool.parameters["properties"]["param"]["description"] == "The parameter."
|
|
|
|
|
|
def test_make_tool_parameter_not_in_docstring() -> None:
|
|
"""Test make_tool when a parameter in properties is not in the docstring."""
|
|
|
|
class MissingParamTool(BaseMockTool):
|
|
def apply(self, name: str, missing_param: str = "") -> str:
|
|
"""This is a test function.
|
|
|
|
:param name: The person's name
|
|
"""
|
|
return f"Hello {name}! Missing param: {missing_param}"
|
|
|
|
def apply_ex(self, *args, **kwargs) -> str:
|
|
return self.apply(**kwargs)
|
|
|
|
tool = MissingParamTool()
|
|
mcp_tool = make_tool(tool)
|
|
|
|
assert "name" in mcp_tool.parameters["properties"]
|
|
assert "missing_param" in mcp_tool.parameters["properties"]
|
|
assert mcp_tool.parameters["properties"]["name"]["description"] == "The person's name."
|
|
assert "description" not in mcp_tool.parameters["properties"]["missing_param"]
|
|
|
|
|
|
def test_make_tool_multiline_docstring() -> None:
|
|
"""Test make_tool with a complex multi-line docstring."""
|
|
|
|
class ComplexDocTool(BaseMockTool):
|
|
def apply(self, project_file_path: str, host: str, port: int) -> str:
|
|
"""Create an MCP server.
|
|
|
|
This function creates and configures a Model Context Protocol server
|
|
with the specified settings.
|
|
|
|
:param project_file_path: The path to the project file, or None
|
|
:param host: The host to bind to
|
|
:param port: The port to bind to
|
|
:return: A configured FastMCP server instance
|
|
"""
|
|
return f"Server config: {project_file_path}, {host}:{port}"
|
|
|
|
def apply_ex(self, *args, **kwargs) -> str:
|
|
return self.apply(**kwargs)
|
|
|
|
tool = ComplexDocTool()
|
|
mcp_tool = make_tool(tool)
|
|
|
|
assert "Create an MCP server" in mcp_tool.description
|
|
assert "Returns A configured FastMCP server instance" in mcp_tool.description
|
|
assert mcp_tool.parameters["properties"]["project_file_path"]["description"] == "The path to the project file, or None."
|
|
assert mcp_tool.parameters["properties"]["host"]["description"] == "The host to bind to."
|
|
assert mcp_tool.parameters["properties"]["port"]["description"] == "The port to bind to."
|
|
|
|
|
|
def test_make_tool_capitalization_and_periods() -> None:
|
|
"""Test that make_tool properly handles capitalization and periods in descriptions."""
|
|
|
|
class FormatTool(BaseMockTool):
|
|
def apply(self, param1: str, param2: str, param3: str) -> str:
|
|
"""Test function.
|
|
|
|
:param param1: lowercase description
|
|
:param param2: description with period.
|
|
:param param3: description with Capitalized word.
|
|
"""
|
|
return f"Formatted: {param1}, {param2}, {param3}"
|
|
|
|
def apply_ex(self, *args, **kwargs) -> str:
|
|
return self.apply(**kwargs)
|
|
|
|
tool = FormatTool()
|
|
mcp_tool = make_tool(tool)
|
|
|
|
assert mcp_tool.parameters["properties"]["param1"]["description"] == "Lowercase description."
|
|
assert mcp_tool.parameters["properties"]["param2"]["description"] == "Description with period."
|
|
assert mcp_tool.parameters["properties"]["param3"]["description"] == "Description with Capitalized word."
|
|
|
|
|
|
def test_make_tool_missing_apply() -> None:
|
|
"""Test make_tool with a tool that doesn't have an apply method."""
|
|
|
|
class BadTool(BaseMockTool):
|
|
pass
|
|
|
|
tool = BadTool()
|
|
|
|
with pytest.raises(AttributeError):
|
|
make_tool(tool)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"docstring, expected_description",
|
|
[
|
|
(
|
|
"""This is a test function.
|
|
|
|
:param param: The parameter
|
|
:return: A result
|
|
""",
|
|
"This is a test function. Returns A result.",
|
|
),
|
|
(
|
|
"""
|
|
:param param: The parameter
|
|
:return: A result
|
|
""",
|
|
"Returns A result.",
|
|
),
|
|
(
|
|
"""
|
|
:param param: The parameter
|
|
""",
|
|
"",
|
|
),
|
|
("Description without params.", "Description without params."),
|
|
],
|
|
)
|
|
def test_make_tool_descriptions(docstring, expected_description) -> None:
|
|
"""Test make_tool with various docstring formats."""
|
|
|
|
class TestTool(BaseMockTool):
|
|
def apply(self, param: str) -> str:
|
|
return f"Result: {param}"
|
|
|
|
def apply_ex(self, *args, **kwargs) -> str:
|
|
return self.apply(**kwargs)
|
|
|
|
# Dynamically set the docstring
|
|
TestTool.apply.__doc__ = docstring
|
|
|
|
tool = TestTool()
|
|
mcp_tool = make_tool(tool)
|
|
|
|
assert mcp_tool.name == "test"
|
|
assert mcp_tool.description == expected_description
|
|
|
|
|
|
def is_test_mock_class(tool_class: type) -> bool:
|
|
"""Check if a class is a test mock class."""
|
|
# Check if the class is defined in a test module
|
|
module_name = tool_class.__module__
|
|
return (
|
|
module_name.startswith(("test.", "tests."))
|
|
or "test_" in module_name
|
|
or tool_class.__name__
|
|
in [
|
|
"BaseMockTool",
|
|
"BasicTool",
|
|
"BadTool",
|
|
"NoParamsTool",
|
|
"NoReturnTool",
|
|
"MissingParamTool",
|
|
"ComplexDocTool",
|
|
"FormatTool",
|
|
"NoDescriptionTool",
|
|
]
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("tool_class", ToolRegistry().get_all_tool_classes())
|
|
def test_make_tool_all_tools(tool_class) -> None:
|
|
"""Test that make_tool works for all tools in the codebase."""
|
|
# Create an instance of the tool
|
|
tool_instance = tool_class(MockAgent())
|
|
|
|
# Try to create an MCP tool from it
|
|
mcp_tool = make_tool(tool_instance)
|
|
|
|
# Basic validation
|
|
assert isinstance(mcp_tool, MCPTool)
|
|
assert mcp_tool.name == tool_class.get_name_from_cls()
|
|
|
|
# The description should be a string (either from docstring or default)
|
|
assert isinstance(mcp_tool.description, str)
|