Files
freecad-robust-mcp-fc111/tests/unit/test_tools_execution.py
T
Sean P. KaneandGitHub 6a6cad9e65 feat: Add workbench and major cleanup, refactor, and updates (#21)
* FreeCAD addon support for Workbench and Plugins

* docs: refactor docs and clean up linters, etc.

* Remove mdformat

* test: improve test coverage

* test:Lots of general fixes

* chore: more general fixes
2026-01-06 15:44:27 -08:00

276 lines
9.3 KiB
Python

"""Tests for execution tools module."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from freecad_mcp.bridge.base import ConnectionStatus, ExecutionResult
class TestExecutionTools:
"""Tests for Python execution tools."""
@pytest.fixture
def mock_mcp(self):
"""Create a mock MCP server that captures tool registrations."""
mcp = MagicMock()
mcp._registered_tools = {}
def tool_decorator():
def wrapper(func):
mcp._registered_tools[func.__name__] = func
return func
return wrapper
mcp.tool = tool_decorator
return mcp
@pytest.fixture
def mock_bridge(self):
"""Create a mock FreeCAD bridge."""
return AsyncMock()
@pytest.fixture
def register_tools(self, mock_mcp, mock_bridge):
"""Register execution tools and return the registered functions."""
from freecad_mcp.tools.execution import register_execution_tools
async def get_bridge():
return mock_bridge
register_execution_tools(mock_mcp, get_bridge)
return mock_mcp._registered_tools
@pytest.mark.asyncio
async def test_execute_python_success(self, register_tools, mock_bridge):
"""execute_python should return success result."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result={"value": 42, "type": "int"},
stdout="",
stderr="",
execution_time_ms=10.5,
)
)
execute_python = register_tools["execute_python"]
result = await execute_python(code="_result_ = {'value': 42, 'type': 'int'}")
assert result["success"] is True
assert result["result"] == {"value": 42, "type": "int"}
assert result["execution_time_ms"] == 10.5
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_execute_python_with_timeout(self, register_tools, mock_bridge):
"""execute_python should pass timeout to bridge."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result=True,
stdout="",
stderr="",
execution_time_ms=5.0,
)
)
execute_python = register_tools["execute_python"]
await execute_python(code="_result_ = True", timeout_ms=60000)
mock_bridge.execute_python.assert_called_once()
call_args = mock_bridge.execute_python.call_args
assert call_args.kwargs.get("timeout_ms") == 60000 or call_args.args[1] == 60000
@pytest.mark.asyncio
async def test_execute_python_failure(self, register_tools, mock_bridge):
"""execute_python should return error on failure."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=False,
result=None,
stdout="",
stderr="NameError: name 'foo' is not defined",
execution_time_ms=2.0,
error_type="NameError",
error_traceback="Traceback...\nNameError: name 'foo' is not defined",
)
)
execute_python = register_tools["execute_python"]
result = await execute_python(code="foo")
assert result["success"] is False
assert result["error_type"] == "NameError"
assert "foo" in result["error_traceback"]
@pytest.mark.asyncio
async def test_execute_python_with_stdout(self, register_tools, mock_bridge):
"""execute_python should capture stdout."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result=None,
stdout="Hello, World!\n",
stderr="",
execution_time_ms=1.0,
)
)
execute_python = register_tools["execute_python"]
result = await execute_python(code="print('Hello, World!')")
assert result["success"] is True
assert result["stdout"] == "Hello, World!\n"
@pytest.mark.asyncio
async def test_get_freecad_version(self, register_tools, mock_bridge):
"""get_freecad_version should return version info."""
mock_bridge.get_freecad_version = AsyncMock(
return_value={
"version": "1.0.0",
"version_tuple": [1, 0, 0],
"build_date": "2024-01-15",
"python_version": "3.11.6",
"gui_available": True,
}
)
get_freecad_version = register_tools["get_freecad_version"]
result = await get_freecad_version()
assert result["version"] == "1.0.0"
assert result["gui_available"] is True
mock_bridge.get_freecad_version.assert_called_once()
@pytest.mark.asyncio
async def test_get_connection_status_connected(self, register_tools, mock_bridge):
"""get_connection_status should return connected status."""
mock_bridge.get_status = AsyncMock(
return_value=ConnectionStatus(
connected=True,
mode="xmlrpc",
freecad_version="1.0.0",
gui_available=True,
last_ping_ms=5.5,
error=None,
)
)
get_connection_status = register_tools["get_connection_status"]
result = await get_connection_status()
assert result["connected"] is True
assert result["mode"] == "xmlrpc"
assert result["last_ping_ms"] == 5.5
@pytest.mark.asyncio
async def test_get_connection_status_disconnected(
self, register_tools, mock_bridge
):
"""get_connection_status should return disconnected status with error."""
mock_bridge.get_status = AsyncMock(
return_value=ConnectionStatus(
connected=False,
mode="xmlrpc",
error="Connection refused",
)
)
get_connection_status = register_tools["get_connection_status"]
result = await get_connection_status()
assert result["connected"] is False
assert result["error"] == "Connection refused"
@pytest.mark.asyncio
async def test_get_console_output(self, register_tools, mock_bridge):
"""get_console_output should return console lines."""
mock_bridge.get_console_output = AsyncMock(
return_value=[
"FreeCAD started",
"Document created: TestDoc",
"Box created",
]
)
get_console_output = register_tools["get_console_output"]
result = await get_console_output()
# Returns a list directly, not a dict
assert result == [
"FreeCAD started",
"Document created: TestDoc",
"Box created",
]
mock_bridge.get_console_output.assert_called_once()
@pytest.mark.asyncio
async def test_get_console_output_with_lines_param(
self, register_tools, mock_bridge
):
"""get_console_output should pass lines parameter."""
mock_bridge.get_console_output = AsyncMock(return_value=["Line 1"])
get_console_output = register_tools["get_console_output"]
await get_console_output(lines=50)
mock_bridge.get_console_output.assert_called_once_with(50)
@pytest.mark.asyncio
async def test_get_mcp_server_environment(self, register_tools, mock_bridge):
"""get_mcp_server_environment should return environment info."""
mock_bridge.get_status = AsyncMock(
return_value=ConnectionStatus(
connected=True,
mode="xmlrpc",
freecad_version="1.0.0",
gui_available=True,
last_ping_ms=5.0,
error=None,
)
)
get_env = register_tools["get_mcp_server_environment"]
result = await get_env()
# Should have standard fields
assert "instance_id" in result
assert "hostname" in result
assert "os_name" in result
assert "python_version" in result
assert "in_docker" in result
# Should have freecad status
assert "freecad" in result
assert result["freecad"]["connected"] is True
assert result["freecad"]["mode"] == "xmlrpc"
assert result["freecad"]["is_headless"] is False
# Should have env vars
assert "env_vars" in result
mock_bridge.get_status.assert_called_once()
@pytest.mark.asyncio
async def test_get_mcp_server_environment_headless(
self, register_tools, mock_bridge
):
"""get_mcp_server_environment should detect headless mode."""
mock_bridge.get_status = AsyncMock(
return_value=ConnectionStatus(
connected=True,
mode="embedded",
freecad_version="1.0.0",
gui_available=False,
last_ping_ms=0.0,
error=None,
)
)
get_env = register_tools["get_mcp_server_environment"]
result = await get_env()
assert result["freecad"]["gui_available"] is False
assert result["freecad"]["is_headless"] is True