fix(task-19): a .dxf template really reaches the live seat; the fallback dimvars land on Standard

Documents.Add(<file>) accepts only a genuine DWT. Measured on AutoCAD 2026:
Add(templates/iso_a3_pid.dxf) returned the default acadiso drawing (layers
['0'], Layout1/Layout2) exactly as Add() and Add(<missing.dwt>) did, so
drawing_new(template=<bundled name>) reported ok:true with source
'bundled_dxf' over a blank document, and the explicit .dxf path route was
just as blank. ComBackend.drawing_new now converts any non-.dwt template
through AutoCAD itself (Documents.Open read-only -> SaveAs ac2018_Template,
older constants tried next -> Close(False)), caches the .dwt in the temp
folder keyed on path/size/mtime, hands Add the .dwt, and refuses a missing
template file instead of letting Add default silently. The tool result
carries template.dwt / template.dwt_cached. Re-measured end to end through
server.drawing_new on the real ComBackend: ansi_b_mech -> 11 layers, tab B,
DIMTAD 0 / DIMTIH 1 / DIMEXO 1.5; iso_a3_pid -> 17 layers, tab A3, second
call reuses the cached .dwt (0.8 s conversion once). Fake-ActiveX tests pin
the call shape and that the conversion document never stays open.

Headlessly the build's fallback wrote spec 4.1 only as header DIM variables,
and the renderer folds six of them, so DIMTAD/TIH/TOH/EXE/EXO/GAP/LUNIT/LWD
were a no-op: ansi_b_mech dimensioned with ISO text placement while
system_get_variable and AutoCAD said ANSI. The fallback now also stores each
value on the Standard dimstyle; the five DXFs are rebuilt (--check passes)
and a test reads the effective values off a DIMENSION drawn from each
template and asserts header and style agree.

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:31:44 +03:00
co-authored by Claude Opus 5
parent a76d9e08a3
commit 17f7228fd6
10 changed files with 354 additions and 57 deletions
+92 -4
View File
@@ -169,6 +169,71 @@ def _acad_app():
return _COM_STATE["app"]
#: ``AcSaveAsType`` template constants, newest first: ac2018_Template,
#: ac2013_Template, ac2010_Template. A seat older than the first refuses it
#: with a COM error and the next one is tried.
_TEMPLATE_SAVE_FORMATS = (66, 62, 50)
def _template_cache_dir() -> Path:
import tempfile
return Path(tempfile.gettempdir()) / "acadmcp-templates"
def _template_as_dwt(app, source: Path) -> tuple[Path, bool]:
"""A real ``.dwt`` for ``source``, built once through AutoCAD and cached.
``Documents.Add(<file>)`` accepts only a genuine DWT: measured on AutoCAD
2026, ``Add(templates/iso_a3_mech.dxf)`` returned the default acadiso
drawing (layers ``['0']``, tabs ``Layout1``/``Layout2``) exactly as
``Add()`` and ``Add(<nonexistent.dwt>)`` did, and the DXF bytes renamed
``.dwt`` fared no better. Opening the file and saving it as a template
(``SaveAs(path, ac2018_Template)``) is what yields the eleven layers and
the ``A3`` tab, so that is done here — once per file content, keyed on the
path, size and mtime, under the temp folder — and the ``.dwt`` is what
``Add`` is handed. Returns ``(dwt_path, cached)``.
Must only be called from the COM executor thread. The conversion document
is opened read-only and closed without saving in a ``finally``; the
``.dwt`` on disk is the only thing it leaves behind.
"""
import hashlib
source = Path(source).resolve()
stat = source.stat()
digest = hashlib.sha1(
f"{source}|{stat.st_size}|{stat.st_mtime_ns}".encode("utf-8", "surrogateescape")
).hexdigest()[:12]
cache_dir = _template_cache_dir()
cache_dir.mkdir(parents=True, exist_ok=True)
dwt = cache_dir / f"{source.stem}-{digest}.dwt"
if dwt.is_file() and dwt.stat().st_size > 0:
return dwt, True
doc = app.Documents.Open(str(source), True)
try:
last_exc: Exception | None = None
for fmt in _TEMPLATE_SAVE_FORMATS:
try:
doc.SaveAs(str(dwt), fmt)
break
except _COM_ERROR as exc:
last_exc = exc
log.debug("SaveAs(%s, %d) refused: %s", dwt, fmt, exc)
else:
raise RuntimeError(
f"could not save {source.name} as a .dwt template: {last_exc}"
) from last_exc
finally:
try:
doc.Close(False)
except Exception as exc: # the temp document must never stay open
log.warning("template conversion document did not close: %s", exc)
if not dwt.is_file() or dwt.stat().st_size == 0:
raise RuntimeError(f"AutoCAD reported the template saved but {dwt} is not on disk")
return dwt, False
def _acad_doc():
"""Return active AutoCAD document."""
app = _acad_app()
@@ -993,13 +1058,36 @@ class ComBackend(AutoCADBackend):
return await self._run(_sync)
async def drawing_new(self, template: str | None = None) -> dict:
"""``Documents.Add()``, or ``Documents.Add(<dwt>)`` from a template file.
A template that is not already a ``.dwt`` (the bundled ``.dxf`` files,
an explicit ``.dxf`` path) is first converted into one through AutoCAD
itself — see ``_template_as_dwt`` for the measurement that makes this
necessary — and the result reports ``template_dwt: {path, cached}``.
A template path that is not a file is refused here rather than handed
to ``Add``, which would silently create the default drawing instead.
"""
def _sync():
app = _acad_app()
if template:
doc = app.Documents.Add(template)
else:
if not template:
doc = app.Documents.Add()
return {"ok": True, "name": doc.Name}
return {"ok": True, "name": doc.Name}
source = Path(template)
if not source.is_file():
raise FileNotFoundError(
f"template file not found: {template} (Documents.Add would silently "
"create the default drawing instead)"
)
result: dict[str, Any] = {"ok": True}
if source.suffix.lower() == ".dwt":
dwt = source
else:
dwt, cached = _template_as_dwt(app, source)
result["template_dwt"] = {"path": str(dwt), "cached": cached}
doc = app.Documents.Add(str(dwt))
result["name"] = doc.Name
return result
result = await self._run(_sync)
await self._ensure_document_state()
+7 -4
View File
@@ -158,10 +158,13 @@ def resolve_template(name_or_path: str, engine: str) -> tuple[str, str]:
A catalogue name resolves to ``templates/<name>.dxf`` (headless) or
``templates/<name>.dwt`` (live), falling back to the DXF with
``source="bundled_dxf"`` when the ``.dwt`` twin is missing. Anything with a
path separator or a ``.dwt``/``.dxf`` suffix is a path (``source="path"``,
not checked here). A bare word that is not in the catalogue is refused
with the catalogue names.
``source="bundled_dxf"`` when the ``.dwt`` twin is missing — the live
engine then converts that DXF into a real ``.dwt`` itself before
``Documents.Add`` (``ComBackend.drawing_new``), because AutoCAD ignores a
DXF handed to ``Add`` and silently returns the default drawing. Anything
with a path separator or a ``.dwt``/``.dxf`` suffix is a path
(``source="path"``, not checked here). A bare word that is not in the
catalogue is refused with the catalogue names.
"""
raw = str(name_or_path or "").strip()
if not raw:
+12 -3
View File
@@ -11,9 +11,15 @@ what ships.
Styles: once group S's ``dimstyle_create`` / ``textstyle_create`` are on the
backend they are used (ISO-25 / ISOCP, ANSI / ROMANS, set current). Until then
the same values (spec §4.1) are written as header DIM variables so the template
already dimensions correctly; the final task re-runs this script after the S
merge and re-commits the five files.
the same values (spec §4.1) are written twice — as header DIM variables, which
is what a live seat dimensions with, *and* onto the ``Standard`` dimstyle
entry, which is what the headless renderer reads (it never consults the
header; ``_with_header_dimvars`` folds six of the variables, so the other
eight — DIMTAD, DIMTIH, DIMTOH, DIMEXE, DIMEXO, DIMGAP, DIMLUNIT, DIMLWD/E —
were a header-only no-op: measured, ``ansi_b_mech`` dimensioned with ISO text
placement headlessly while AutoCAD honoured the header). Both engines now
dimension the same template the same way; the final task re-runs this script
after the S merge and re-commits the five files.
``--check`` rebuilds into a temporary folder and compares line by line after
masking the values ezdxf cannot keep constant — measured on 2026-09-16, exactly
@@ -115,11 +121,14 @@ async def _apply_styles(backend, spec: TemplateSpec) -> dict:
)
return {"mode": "styles", "dimstyle": dim["name"], "textstyle": text["name"]}
applied = {}
standard = backend._doc.dimstyles.get("Standard") # what the headless renderer reads
for variable, value in _FALLBACK_DIMVARS[spec.dimstyle_preset].items():
applied[variable] = (await backend.system_set_variable(variable, value))["ok"]
standard.dxf.set(variable.lower(), value)
return {
"mode": "header_dimvars",
"applied": applied,
"dimstyle": standard.dxf.name,
"pending": "dimstyle_create/textstyle_create not on this backend yet (group S)",
}
+11
View File
@@ -1112,6 +1112,13 @@ async def drawing_new(
`source: "bundled_dxf"` when no `.dwt` twin is committed); a path is used
as given. The result carries `template: {name|path, path, source}`.
On the live engine only a genuine `.dwt` reaches `Documents.Add` —
measured on AutoCAD 2026, `Add(<file.dxf>)` silently returns the default
drawing — so a `.dxf` template (bundled or a path) is first converted
through AutoCAD into a cached `.dwt` and `template.dwt` names it
(`template.dwt_cached` says whether the conversion was reused). The
drawing then really carries the template's layers, tabs and page setup.
Refused before anything is replaced: a bare name that is not in the
catalogue (the message lists the five), a template path that does not
exist, and a path outside the allowed directories. With `bootstrap=True`
@@ -1140,7 +1147,11 @@ async def drawing_new(
await ctx.info(f"Creating new drawing (template={template}, bootstrap={bootstrap})")
raw = await backend.drawing_new(template)
result = dict(raw) if isinstance(raw, dict) else {"result": raw}
converted = result.pop("template_dwt", None)
if template_info is not None:
if converted: # the live engine built a .dwt from the .dxf (see ComBackend.drawing_new)
template_info["dwt"] = converted["path"]
template_info["dwt_cached"] = bool(converted.get("cached"))
result["template"] = template_info
if bootstrap:
try:
+14 -14
View File
@@ -489,7 +489,7 @@ $SKPOLY
9
$TDCREATE
40
2461305.9168518516
2461305.9345486113
9
$TDUCREATE
40
@@ -497,7 +497,7 @@ $TDUCREATE
9
$TDUPDATE
40
2461305.9168518516
2461305.9345486113
9
$TDUUPDATE
40
@@ -929,11 +929,11 @@ $PSTYLEMODE
9
$FINGERPRINTGUID
2
{C86D7286-E19E-4B12-95FA-2422D8FA8D21}
{C3A95304-315D-4482-8B26-4A154C8AC81B}
9
$VERSIONGUID
2
{6AD177C1-789E-403D-83B3-B9A1E5C62637}
{020A53F4-F6E6-4A10-988E-58908DDBD5B7}
9
$EXTNAMES
290
@@ -2157,13 +2157,13 @@ Standard
40
1.0
41
2.5
3.0
42
0.625
1.5
43
3.75
44
1.25
1.5
45
0.0
46
@@ -2175,7 +2175,7 @@ Standard
49
2.5
140
2.5
3.0
141
2.5
142
@@ -2189,7 +2189,7 @@ Standard
146
1.0
147
0.625
1.0
148
0.0
69
@@ -2201,15 +2201,15 @@ Standard
72
0
73
0
1
74
0
1
75
0
76
0
77
1
0
78
8
79
@@ -3425,7 +3425,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-21T19:00:16.253966+00:00
1.4.4 @ 2026-09-21T19:25:45.807766+00:00
0
DICTIONARYVAR
5
@@ -3437,7 +3437,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-21T19:00:16.261077+00:00
1.4.4 @ 2026-09-21T19:25:45.814791+00:00
0
ENDSEC
0
+14 -14
View File
@@ -489,7 +489,7 @@ $SKPOLY
9
$TDCREATE
40
2461305.9168518516
2461305.9345486113
9
$TDUCREATE
40
@@ -497,7 +497,7 @@ $TDUCREATE
9
$TDUPDATE
40
2461305.9168518516
2461305.9345486113
9
$TDUUPDATE
40
@@ -929,11 +929,11 @@ $PSTYLEMODE
9
$FINGERPRINTGUID
2
{5DBCBFFD-85BB-4756-97CD-DF14393BA48D}
{EA8DAB3B-221B-4973-A128-0F97D36F69C3}
9
$VERSIONGUID
2
{8BCC5654-8976-41B9-B27D-2DE6DFEDDA75}
{41F085E9-E128-43F7-931E-FEE570A4F621}
9
$EXTNAMES
290
@@ -2157,13 +2157,13 @@ Standard
40
1.0
41
2.5
3.0
42
0.625
1.5
43
3.75
44
1.25
1.5
45
0.0
46
@@ -2175,7 +2175,7 @@ Standard
49
2.5
140
2.5
3.0
141
2.5
142
@@ -2189,7 +2189,7 @@ Standard
146
1.0
147
0.625
1.0
148
0.0
69
@@ -2201,15 +2201,15 @@ Standard
72
0
73
0
1
74
0
1
75
0
76
0
77
1
0
78
8
79
@@ -3425,7 +3425,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-21T19:00:16.265081+00:00
1.4.4 @ 2026-09-21T19:25:45.818106+00:00
0
DICTIONARYVAR
5
@@ -3437,7 +3437,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-21T19:00:16.272301+00:00
1.4.4 @ 2026-09-21T19:25:45.824109+00:00
0
ENDSEC
0
+6 -6
View File
@@ -489,7 +489,7 @@ $SKPOLY
9
$TDCREATE
40
2461305.9168518516
2461305.9345486113
9
$TDUCREATE
40
@@ -497,7 +497,7 @@ $TDUCREATE
9
$TDUPDATE
40
2461305.9168518516
2461305.9345486113
9
$TDUUPDATE
40
@@ -929,11 +929,11 @@ $PSTYLEMODE
9
$FINGERPRINTGUID
2
{06CA53E2-CDAA-439F-9CAD-D12BED7E531C}
{569A5E5D-D8AE-4EE2-8CC8-222F1BC5A1B6}
9
$VERSIONGUID
2
{DFE1833D-4A98-418F-8215-C1956203953D}
{CA16A478-EDAC-4921-A9AD-1AD1114D5AB2}
9
$EXTNAMES
290
@@ -3425,7 +3425,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-21T19:00:16.220288+00:00
1.4.4 @ 2026-09-21T19:25:45.777979+00:00
0
DICTIONARYVAR
5
@@ -3437,7 +3437,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-21T19:00:16.228248+00:00
1.4.4 @ 2026-09-21T19:25:45.783975+00:00
0
ENDSEC
0
+6 -6
View File
@@ -489,7 +489,7 @@ $SKPOLY
9
$TDCREATE
40
2461305.9168518516
2461305.9345486113
9
$TDUCREATE
40
@@ -497,7 +497,7 @@ $TDUCREATE
9
$TDUPDATE
40
2461305.9168518516
2461305.9345486113
9
$TDUUPDATE
40
@@ -929,11 +929,11 @@ $PSTYLEMODE
9
$FINGERPRINTGUID
2
{7664E9B7-DDAC-4E40-A6C4-4BA2274F50FC}
{6791163C-4461-4A1B-8F3F-5AD608DBCCCE}
9
$VERSIONGUID
2
{AA05654D-61E8-4264-879F-0A0EB2E7F4EE}
{9A1CD7BF-015F-4598-B3DB-2BDBFBCC12FA}
9
$EXTNAMES
290
@@ -4333,7 +4333,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-21T19:00:16.186887+00:00
1.4.4 @ 2026-09-21T19:25:45.743145+00:00
0
DICTIONARYVAR
5
@@ -4345,7 +4345,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-21T19:00:16.215553+00:00
1.4.4 @ 2026-09-21T19:25:45.772937+00:00
0
ENDSEC
0
+6 -6
View File
@@ -489,7 +489,7 @@ $SKPOLY
9
$TDCREATE
40
2461305.9168518516
2461305.9345486113
9
$TDUCREATE
40
@@ -497,7 +497,7 @@ $TDUCREATE
9
$TDUPDATE
40
2461305.9168518516
2461305.9345486113
9
$TDUUPDATE
40
@@ -929,11 +929,11 @@ $PSTYLEMODE
9
$FINGERPRINTGUID
2
{A7BD53ED-11FA-44DC-94CD-4208EB90B212}
{B5F8B467-F9F9-4DC6-8A95-A266D8246CA8}
9
$VERSIONGUID
2
{E08D8D46-7798-4863-A73A-C6527F804C81}
{F759C60A-BBAF-4155-A187-77BF78127E81}
9
$EXTNAMES
290
@@ -4501,7 +4501,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-21T19:00:16.231308+00:00
1.4.4 @ 2026-09-21T19:25:45.787707+00:00
0
DICTIONARYVAR
5
@@ -4513,7 +4513,7 @@ DictionaryVariables
280
0
1
1.4.4 @ 2026-09-21T19:00:16.248961+00:00
1.4.4 @ 2026-09-21T19:25:45.803766+00:00
0
ENDSEC
0
+186
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import importlib.util
import types
from pathlib import Path
import pytest
@@ -157,6 +158,53 @@ async def test_drawing_new_from_the_pid_and_ansi_templates(backend):
assert (await backend.drawing_settings())["settings"]["decimal_separator"] == "."
#: Spec §4.1, the eight variables `_with_header_dimvars` does not fold — the
#: ones a header-only write silently loses on the headless renderer.
_PLACEMENT = ("dimtad", "dimtih", "dimtoh", "dimexo", "dimexe", "dimgap", "dimlunit", "dimlwd")
async def _effective_dimvars(backend) -> dict:
"""What the next dimension is really rendered with, read off the entity."""
from ezdxf.entities import DimStyleOverride
info = await backend.dimension_linear(0, 0, 100, 0, 50, 20)
override = DimStyleOverride(backend._doc.entitydb[info.handle])
return {name: override.get(name) for name in _PLACEMENT + ("dimtxt", "dimasz")}
async def test_templates_dimension_with_their_own_preset_headlessly(backend):
"""The build's fallback lands on the ``Standard`` dimstyle, not only the header.
Measured before the fix: ``ansi_b_mech`` reported DIMTAD 0 / DIMTIH 1 from
``system_get_variable`` while the dimension it drew used dimtad=1 / dimtih=0
(the ISO values on ``Standard``), so the same template dimensioned
differently per engine. The header and the style must agree.
"""
await server.drawing_new(template="ansi_b_mech", ctx=_Ctx(backend))
ansi = await _effective_dimvars(backend)
assert ansi == {
"dimtad": 0,
"dimtih": 1,
"dimtoh": 1,
"dimexo": 1.5,
"dimexe": 1.5,
"dimgap": 1.0,
"dimlunit": 2,
"dimlwd": -2,
"dimtxt": 3.0,
"dimasz": 3.0,
}
for name, value in ansi.items():
header = await backend.system_get_variable(name.upper())
assert float(header) == float(value), f"{name}: header {header} != style {value}"
await server.drawing_new(template="iso_a3_mech", ctx=_Ctx(backend))
iso = await _effective_dimvars(backend)
assert (iso["dimtad"], iso["dimtih"], iso["dimtoh"]) == (1, 0, 0)
assert (iso["dimexo"], iso["dimexe"], iso["dimgap"]) == (0.625, 1.25, 0.625)
assert (iso["dimtxt"], iso["dimasz"]) == (2.5, 2.5)
async def test_drawing_new_unknown_template_is_refused_with_the_names(backend):
await backend.entity_create_line(0, 0, 1, 1)
before = (await backend.drawing_info()).entity_count
@@ -185,3 +233,141 @@ async def test_drawing_template_list_tool(backend):
assert result["ok"] and result["count"] == 5
assert [row["name"] for row in result["templates"]] == list(NAMES)
assert result["engine"] == "ezdxf"
# ── COM: only a real .dwt reaches Documents.Add ─────────────────────────────
class _FakeDocuments:
"""Records the ActiveX calls; ``Add`` on a non-``.dwt`` answers blank, as
AutoCAD 2026 does (measured: ``Add(<dxf>)`` == ``Add()`` == ``Add(<missing.dwt>)``)."""
def __init__(self, dwt_bytes: bytes = b"AC1032\x00template"):
self.calls: list[tuple] = []
self.dwt_bytes = dwt_bytes
self.open_docs: list[_FakeDoc] = []
def Add(self, template=None):
self.calls.append(("Add", template))
real = template is not None and str(template).lower().endswith(".dwt")
real = real and Path(template).is_file() and Path(template).read_bytes()[:6] == b"AC1032"
return _FakeDoc(self, "Drawing1.dwg", layers=["0", "GEOMETRY"] if real else ["0"])
def Open(self, path, read_only=False, password=None):
self.calls.append(("Open", path, read_only))
doc = _FakeDoc(self, Path(path).name, layers=["0", "GEOMETRY"])
self.open_docs.append(doc)
return doc
class _FakeDoc:
def __init__(self, documents, name, layers):
self._documents = documents
self.Name = name
self.layers = layers
self.closed = None
def SaveAs(self, path, fmt=None):
self._documents.calls.append(("SaveAs", path, fmt))
if fmt != 66:
raise RuntimeError(f"fake AutoCAD 2026 refuses SaveAs format {fmt}")
Path(path).write_bytes(self._documents.dwt_bytes)
def Close(self, save=True):
self._documents.calls.append(("Close", self.Name, save))
self.closed = save
self._documents.open_docs.remove(self)
@pytest.fixture
def com_new(monkeypatch, tmp_path):
pytest.importorskip("win32com.client", reason="pywin32 not installed")
from backends import com_backend as module
documents = _FakeDocuments()
app = types.SimpleNamespace(Documents=documents)
monkeypatch.setattr(module, "_acad_app", lambda: app)
monkeypatch.setattr(module, "_template_cache_dir", lambda: tmp_path / "cache")
monkeypatch.setattr(module, "_COM_ERROR", (RuntimeError,))
backend = module.ComBackend()
async def _run_inline(func, *args, **kwargs):
return func(*args, **kwargs)
async def _no_state():
pass
monkeypatch.setattr(backend, "_run", _run_inline)
monkeypatch.setattr(backend, "_ensure_document_state", _no_state)
return backend, documents
async def test_com_drawing_new_converts_a_dxf_into_a_real_dwt_once(com_new, tmp_path):
backend, documents = com_new
dxf = TEMPLATES_DIR / "iso_a3_pid.dxf"
result = await backend.drawing_new(str(dxf))
assert result["ok"] and result["name"] == "Drawing1.dwg"
dwt = Path(result["template_dwt"]["path"])
assert result["template_dwt"]["cached"] is False
assert dwt.suffix == ".dwt" and dwt.is_file() and dwt.parent == tmp_path / "cache"
assert documents.calls == [
("Open", str(dxf), True),
("SaveAs", str(dwt), 66),
("Close", "iso_a3_pid.dxf", False),
("Add", str(dwt)),
], "open read-only, save as ac2018_Template, close without saving, then Add the .dwt"
assert documents.open_docs == [], "the conversion document never stays open"
documents.calls.clear()
again = await backend.drawing_new(str(dxf))
assert again["template_dwt"] == {"path": str(dwt), "cached": True}
assert documents.calls == [("Add", str(dwt))], "the second call reuses the cached .dwt"
async def test_com_drawing_new_hands_a_dwt_straight_to_add_and_refuses_a_missing_file(
com_new, tmp_path
):
backend, documents = com_new
mine = tmp_path / "mine.dwt"
mine.write_bytes(documents.dwt_bytes)
result = await backend.drawing_new(str(mine))
assert result == {"ok": True, "name": "Drawing1.dwg"}
assert documents.calls == [("Add", str(mine))]
documents.calls.clear()
with pytest.raises(FileNotFoundError, match="not found"):
await backend.drawing_new(str(tmp_path / "gone.dwt"))
assert documents.calls == [], "Documents.Add(<missing>) would silently create the default"
async def test_com_drawing_new_falls_back_to_an_older_template_format(com_new, monkeypatch):
backend, documents = com_new
accepted = []
def _save_as(self, path, fmt=None):
documents.calls.append(("SaveAs", path, fmt))
if fmt == 66:
raise RuntimeError("older seat: unknown SaveAs type")
accepted.append(fmt)
Path(path).write_bytes(documents.dwt_bytes)
monkeypatch.setattr(_FakeDoc, "SaveAs", _save_as)
result = await backend.drawing_new(str(TEMPLATES_DIR / "ansi_b_mech.dxf"))
assert result["ok"] and accepted == [62]
assert documents.open_docs == []
async def test_com_drawing_new_tool_reports_the_converted_dwt(com_new):
backend, documents = com_new
result = await server.drawing_new(template="iso_a3_pid", bootstrap=False, ctx=_Ctx(backend))
assert result["ok"] is True
assert "template_dwt" not in result
template = result["template"]
assert template["name"] == "iso_a3_pid"
assert template["source"] in ("bundled", "bundled_dxf")
if template["source"] == "bundled_dxf": # no .dwt twin committed: the live engine built one
assert template["path"] == str(TEMPLATES_DIR / "iso_a3_pid.dxf")
assert template["dwt"].endswith(".dwt") and template["dwt_cached"] is False
assert documents.calls[-1] == ("Add", template["dwt"])
else:
assert "dwt" not in template and documents.calls == [("Add", template["path"])]