feat(templates): drawing_template_save on both engines — dwt_write is a declared capability

Live AutoCAD writes a real .dwt through SaveAs(path, ac2018_Template = 66) and
puts the description into the summary Comments; headlessly a .dxf template is
written through drawing_save_as and a .dwt request is refused with capability
dwt_write, naming the DXF route and the COM route. drawing_save_as's "dwt"
entry was 5 (acR13_dxf): a DWT request wrote an R13 DXF under a .dwt name.

Probed live on AutoCAD 2026 (typelib: ac2018_Template = 66, acR13_dxf = 5,
acTemplateDwg absent; the saved file starts AC1032, the document is rebound,
Comments takes). SECTION 19 is now 6 tools; SAVEAS joins SHARED_ACAD_COMMANDS;
templates/README.md documents the two files per template.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-21 22:48:31 +03:00
co-authored by Claude Opus 5
parent 17f7228fd6
commit b458923e19
9 changed files with 322 additions and 20 deletions
+114
View File
@@ -10,6 +10,7 @@ import pytest
from fastmcp.exceptions import ToolError
import server
from backends.base import UnsupportedCapabilityError
from engineering.standards.templates import (
TEMPLATE_CATALOG,
TEMPLATES_DIR,
@@ -371,3 +372,116 @@ async def test_com_drawing_new_tool_reports_the_converted_dwt(com_new):
assert documents.calls[-1] == ("Add", template["dwt"])
else:
assert "dwt" not in template and documents.calls == [("Add", template["path"])]
# ── drawing_template_save ───────────────────────────────────────────────────
async def test_template_save_dxf_headless_round_trips(backend, tmp_path):
await backend.layer_create("MINE", color=3)
out = tmp_path / "mine_template.dxf"
result = await backend.drawing_template_save(str(out), name="Mine", description="my sheet")
assert result == {
"ok": True,
"path": str(out),
"format": "dxf",
"backend": "ezdxf",
"name": "Mine",
"description_written": False,
"description_note": (
"DXF has no template description field; it is a DWG/DWT summary property "
"(capability 'dwgprops')"
),
}
assert out.is_file()
await server.drawing_new(template=str(out), ctx=_Ctx(backend))
assert "MINE" in {layer.name for layer in await backend.layer_list()}
async def test_template_save_dwt_headless_is_refused_with_the_capability(backend, tmp_path):
out = tmp_path / "mine.dwt"
with pytest.raises(UnsupportedCapabilityError) as excinfo:
await backend.drawing_template_save(str(out))
assert excinfo.value.capability == "dwt_write"
message = str(excinfo.value)
assert ".dxf" in message and "com" in message.lower()
assert not out.exists()
async def test_template_save_refuses_other_suffixes(backend, tmp_path):
with pytest.raises(ValueError, match=r"\.dxf"):
await backend.drawing_template_save(str(tmp_path / "mine.txt"))
async def test_template_save_tool_validates_the_path(backend, tmp_path):
with pytest.raises(ToolError):
await server.drawing_template_save("../../outside.dxf", ctx=_Ctx(backend))
result = await server.drawing_template_save(str(tmp_path / "t.dxf"), ctx=_Ctx(backend))
assert result["ok"] and result["name"] == "t"
class _SaveRecorder:
def __init__(self):
self.calls = []
self.SummaryInfo = types.SimpleNamespace()
self.Name = "Drawing1.dwg"
def SaveAs(self, path, fmt=None):
self.calls.append(("SaveAs", path, fmt))
self.Name = Path(path).name
@pytest.fixture
def com_backend(monkeypatch):
pytest.importorskip("win32com.client", reason="pywin32 not installed")
from backends import com_backend as module
document = _SaveRecorder()
monkeypatch.setattr(module, "_acad_doc", lambda: document)
backend = module.ComBackend()
async def _run_inline(func, *args, **kwargs):
return func(*args, **kwargs)
monkeypatch.setattr(backend, "_run", _run_inline)
return backend, document
async def test_com_template_save_dwt_uses_ac2018_template(com_backend, tmp_path):
backend, document = com_backend
out = str(tmp_path / "mine.dwt")
result = await backend.drawing_template_save(out, description="A3 mech")
assert document.calls == [("SaveAs", out, 66)]
assert document.SummaryInfo.Comments == "A3 mech"
assert result == {
"ok": True,
"path": out,
"format": "dwt",
"backend": "com",
"name": "mine",
"description_written": True,
"document_rebound": True,
}
async def test_com_template_save_dxf_uses_the_dxf_code(com_backend, tmp_path):
backend, document = com_backend
out = str(tmp_path / "mine.dxf")
result = await backend.drawing_template_save(out)
assert document.calls == [("SaveAs", out, 61)]
assert result["format"] == "dxf" and result["description_written"] is False
async def test_com_save_as_dwt_no_longer_writes_an_r13_dxf(com_backend, tmp_path):
"""drawing_save_as mapped 'dwt' to 5, which is AcSaveAsType.acR13_dxf."""
backend, document = com_backend
out = str(tmp_path / "x.dwt")
await backend.drawing_save_as(out, "dwt")
assert document.calls == [("SaveAs", out, 66)]
async def test_com_template_save_refuses_other_suffixes_before_saving(com_backend, tmp_path):
backend, document = com_backend
with pytest.raises(ValueError, match=r"\.dwt"):
await backend.drawing_template_save(str(tmp_path / "x.dwg"))
assert document.calls == []
+5 -4
View File
@@ -495,7 +495,7 @@ async def test_tool_groups_is_byte_identical_to_the_source_declared_grouping():
async def test_tool_group_sizes_are_unchanged():
"""Frozen snapshot of the surface (171 tools, 20 groups).
"""Frozen snapshot of the surface (172 tools, 20 groups).
Taken before @cad_tool landed at 131 tools; `batch` moved 2 -> 3 when
v1.5.0's `cad_batch` joined `entity_batch_create`/`entity_batch_modify`, and
@@ -523,7 +523,8 @@ async def test_tool_group_sizes_are_unchanged():
— all tagged `layout`, which `_GROUP_TAG_PRIORITY` ranks above
`query`/`export`, so they file with the sheets they set up. `templates`
moved 2 -> 3 when v1.6's `drawing_template_list` joined SECTION 19
(tagged `template`, which ranks above `drawing`). Every other number
(tagged `template`, which ranks above `drawing`), then 3 -> 4 when
`drawing_template_save` joined. Every other number
here has been unchanged since the snapshot was taken.
"""
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
@@ -544,9 +545,9 @@ async def test_tool_group_sizes_are_unchanged():
"premium": 12,
"solids": 5,
"system": 7,
"templates": 3,
"templates": 4,
"transactions": 3,
"validation": 1,
"view": 4,
}
assert sum(sizes.values()) == 171
assert sum(sizes.values()) == 172