* 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
284 lines
10 KiB
Python
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"
|