Files
freecad-robust-mcp-fc111/tests/unit/test_tools_documents.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

284 lines
10 KiB
Python

"""Tests for document tools module."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from freecad_mcp.bridge.base import DocumentInfo, ExecutionResult
class TestDocumentTools:
"""Tests for document management 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 document tools and return the registered functions."""
from freecad_mcp.tools.documents import register_document_tools
async def get_bridge():
return mock_bridge
register_document_tools(mock_mcp, get_bridge)
return mock_mcp._registered_tools
@pytest.mark.asyncio
async def test_list_documents_empty(self, register_tools, mock_bridge):
"""list_documents should return empty list when no documents."""
mock_bridge.get_documents = AsyncMock(return_value=[])
list_documents = register_tools["list_documents"]
result = await list_documents()
assert result == []
mock_bridge.get_documents.assert_called_once()
@pytest.mark.asyncio
async def test_list_documents_with_docs(self, register_tools, mock_bridge):
"""list_documents should return document info."""
mock_docs = [
DocumentInfo(
name="Doc1",
label="Document 1",
path="/tmp/doc1.FCStd",
objects=["Box", "Cylinder"],
is_modified=False,
active_object="Box",
),
DocumentInfo(
name="Doc2",
label="Document 2",
path=None,
objects=["Sphere"],
is_modified=True,
active_object=None,
),
]
mock_bridge.get_documents = AsyncMock(return_value=mock_docs)
list_documents = register_tools["list_documents"]
result = await list_documents()
assert len(result) == 2
assert result[0]["name"] == "Doc1"
assert result[0]["object_count"] == 2
assert result[0]["is_modified"] is False
assert result[1]["name"] == "Doc2"
assert result[1]["object_count"] == 1
assert result[1]["is_modified"] is True
@pytest.mark.asyncio
async def test_get_active_document_none(self, register_tools, mock_bridge):
"""get_active_document should return None when no active document."""
mock_bridge.get_active_document = AsyncMock(return_value=None)
get_active_document = register_tools["get_active_document"]
result = await get_active_document()
assert result is None
mock_bridge.get_active_document.assert_called_once()
@pytest.mark.asyncio
async def test_get_active_document_returns_info(self, register_tools, mock_bridge):
"""get_active_document should return document info when available."""
mock_doc = DocumentInfo(
name="ActiveDoc",
label="Active Document",
path="/tmp/active.FCStd",
objects=["Part1", "Part2"],
is_modified=True,
active_object="Part1",
)
mock_bridge.get_active_document = AsyncMock(return_value=mock_doc)
get_active_document = register_tools["get_active_document"]
result = await get_active_document()
assert result["name"] == "ActiveDoc"
assert result["label"] == "Active Document"
assert result["objects"] == ["Part1", "Part2"]
assert result["is_modified"] is True
@pytest.mark.asyncio
async def test_create_document_default_name(self, register_tools, mock_bridge):
"""create_document should create with default name."""
mock_doc = DocumentInfo(
name="Unnamed",
label="Unnamed",
path=None,
objects=[],
is_modified=False,
)
mock_bridge.create_document = AsyncMock(return_value=mock_doc)
create_document = register_tools["create_document"]
result = await create_document()
assert result["name"] == "Unnamed"
mock_bridge.create_document.assert_called_once_with("Unnamed", None)
@pytest.mark.asyncio
async def test_create_document_with_name_and_label(
self, register_tools, mock_bridge
):
"""create_document should use provided name and label."""
mock_doc = DocumentInfo(
name="MyPart",
label="My Part Design",
path=None,
objects=[],
is_modified=False,
)
mock_bridge.create_document = AsyncMock(return_value=mock_doc)
create_document = register_tools["create_document"]
result = await create_document(name="MyPart", label="My Part Design")
assert result["name"] == "MyPart"
assert result["label"] == "My Part Design"
mock_bridge.create_document.assert_called_once_with("MyPart", "My Part Design")
@pytest.mark.asyncio
async def test_open_document(self, register_tools, mock_bridge):
"""open_document should open and return document info."""
mock_doc = DocumentInfo(
name="OpenedDoc",
label="Opened Document",
path="/tmp/test.FCStd",
objects=["Box", "Fillet"],
is_modified=False,
)
mock_bridge.open_document = AsyncMock(return_value=mock_doc)
open_document = register_tools["open_document"]
result = await open_document(path="/tmp/test.FCStd")
assert result["name"] == "OpenedDoc"
assert result["path"] == "/tmp/test.FCStd"
assert result["objects"] == ["Box", "Fillet"]
mock_bridge.open_document.assert_called_once_with("/tmp/test.FCStd")
@pytest.mark.asyncio
async def test_save_document_default(self, register_tools, mock_bridge):
"""save_document should save active document."""
mock_bridge.save_document = AsyncMock(return_value="/tmp/saved.FCStd")
save_document = register_tools["save_document"]
result = await save_document()
assert result["success"] is True
assert result["path"] == "/tmp/saved.FCStd"
mock_bridge.save_document.assert_called_once_with(None, None)
@pytest.mark.asyncio
async def test_save_document_with_path(self, register_tools, mock_bridge):
"""save_document should save to specified path."""
mock_bridge.save_document = AsyncMock(return_value="/new/path.FCStd")
save_document = register_tools["save_document"]
result = await save_document(doc_name="MyDoc", path="/new/path.FCStd")
assert result["success"] is True
assert result["path"] == "/new/path.FCStd"
mock_bridge.save_document.assert_called_once_with("MyDoc", "/new/path.FCStd")
@pytest.mark.asyncio
async def test_close_document_without_save(self, register_tools, mock_bridge):
"""close_document should close without saving by default."""
mock_bridge.close_document = AsyncMock()
close_document = register_tools["close_document"]
result = await close_document(doc_name="TestDoc")
assert result["success"] is True
assert result["saved"] is False
mock_bridge.close_document.assert_called_once_with("TestDoc")
@pytest.mark.asyncio
async def test_close_document_with_save(self, register_tools, mock_bridge):
"""close_document should save before closing when requested."""
mock_bridge.save_document = AsyncMock(return_value="/tmp/doc.FCStd")
mock_bridge.close_document = AsyncMock()
close_document = register_tools["close_document"]
result = await close_document(doc_name="TestDoc", save_changes=True)
assert result["success"] is True
assert result["saved"] is True
mock_bridge.save_document.assert_called_once_with("TestDoc")
mock_bridge.close_document.assert_called_once_with("TestDoc")
@pytest.mark.asyncio
async def test_close_document_save_failure(self, register_tools, mock_bridge):
"""close_document should still close even if save fails."""
mock_bridge.save_document = AsyncMock(side_effect=Exception("Save failed"))
mock_bridge.close_document = AsyncMock()
close_document = register_tools["close_document"]
result = await close_document(doc_name="TestDoc", save_changes=True)
assert result["success"] is True
assert result["saved"] is False
mock_bridge.close_document.assert_called_once()
@pytest.mark.asyncio
async def test_recompute_document_success(self, register_tools, mock_bridge):
"""recompute_document should return success on recompute."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=True,
result=True,
stdout="",
stderr="",
execution_time_ms=5.0,
)
)
recompute_document = register_tools["recompute_document"]
result = await recompute_document(doc_name="TestDoc")
assert result["success"] is True
assert result.get("error") is None
mock_bridge.execute_python.assert_called_once()
@pytest.mark.asyncio
async def test_recompute_document_failure(self, register_tools, mock_bridge):
"""recompute_document should return error on failure."""
mock_bridge.execute_python = AsyncMock(
return_value=ExecutionResult(
success=False,
result=None,
stdout="",
stderr="",
execution_time_ms=5.0,
error_type="ValueError",
error_traceback="No document found",
)
)
recompute_document = register_tools["recompute_document"]
result = await recompute_document(doc_name="NonExistent")
assert result["success"] is False
assert result["error"] == "No document found"