feat(settings): live COM smoke on AutoCAD 2026 builds the five .dwt twins and records the run

scripts/smoke_settings_com.py --build-dwt exited 0 on AutoCAD 2026 (25.1s),
2026-09-22: five templates/*.dwt written through drawing_template_save
(SaveAs code 66, each file opens with AC1032), page setup plotted and the
PDF's own /MediaBox read back as 420.158 x 297.039 mm, iso_a3_mech.dwt
reopened as source "bundled" with ISO-25 current, layer state / named view /
UCS / preferences / SummaryInfo / prompt line all through the COM engine.
The record is in CHANGELOG.md under Unreleased. The smoke keeps a ledger of
the documents it opens and closes them on the way out, so a failed run never
leaves scratch documents on the operator's seat. Two of its expectations
were corrected to the merged tree: mleaderstyle_create is implemented on COM
(spec: no `mleaderstyle` key), and plot_style_list marks catalogue rows with
`installed`. --interactive was not run unattended on the shared seat (an
unanswered GetPoint holds the command line); Task 23 measured the pick live.
*.dwt / *.dwg are binary in .gitattributes; AutoCAD's plot.log is ignored.

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-22 13:07:31 +03:00
co-authored by Claude Opus 5
parent b54271f4ac
commit 02dbb133b5
10 changed files with 855 additions and 1 deletions
+2
View File
@@ -2,3 +2,5 @@
* text=auto
templates/*.dxf text eol=crlf
templates/*.dwt binary
*.dwt binary
*.dwg binary
+4
View File
@@ -60,3 +60,7 @@ benchmarks/.competitors/
# committed one was stale and unvetted — but CI now installs from it with
# `uv sync --locked`, so ignoring it would silently disarm every locked lane.
# Regenerate with `uv lock` whenever pyproject.toml dependencies change.
# AutoCAD writes its plot-and-publish log into the cwd of the plotting process
# (scripts/smoke_settings_com.py plots from the repo root).
plot.log
+454
View File
@@ -174,6 +174,460 @@ AutoCAD type library (a package directory holding only a `__pycache__`) — an
environment fault, not a backend one; deleting `%TEMP%\gen_py\3.11` cleared
it and no backend change was needed.
### Live COM smoke — settings (track E)
`scripts/smoke_settings_com.py --build-dwt`, AutoCAD 2026 (`25.1s (LMS Tech)`),
Windows 11, 2026-09-22, in a new document (`Drawing27.dwg`) — styles →
page setup → `batch_plot` (the PDF's own `/MediaBox`) → the five `.dwt` twins
built and `iso_a3_mech.dwt` reopened → layer state round trip → named view →
UCS → preferences (one reversible set) → SummaryInfo → prompt line, all
through the COM engine, exit 0:
```json
{
"autocad": "25.1s (LMS Tech)",
"system_launch": {
"launched": false,
"attached": true,
"version": "25.1s (LMS Tech)",
"document": "Drawing9.dwg",
"visible": true,
"progid": "AutoCAD.Application",
"backend": "com"
},
"dwt_built": [
{
"name": "iso_a3_mech",
"path": "templates\\iso_a3_mech.dwt",
"bytes": 18240,
"magic": "AC1032",
"is_dwg_container": true,
"format": "dwt"
},
{
"name": "iso_a1_arch",
"path": "templates\\iso_a1_arch.dwt",
"bytes": 17281,
"magic": "AC1032",
"is_dwg_container": true,
"format": "dwt"
},
{
"name": "iso_a3_pid",
"path": "templates\\iso_a3_pid.dwt",
"bytes": 18528,
"magic": "AC1032",
"is_dwg_container": true,
"format": "dwt"
},
{
"name": "ansi_b_mech",
"path": "templates\\ansi_b_mech.dwt",
"bytes": 17250,
"magic": "AC1032",
"is_dwg_container": true,
"format": "dwt"
},
{
"name": "ansi_d_arch",
"path": "templates\\ansi_d_arch.dwt",
"bytes": 17278,
"magic": "AC1032",
"is_dwg_container": true,
"format": "dwt"
}
],
"document": "Drawing27.dwg",
"styles": {
"dimstyle": {
"ok": true,
"name": "ISO-25-SMOKE",
"values": {
"DIMTXT": 2.5,
"DIMASZ": 2.5,
"DIMEXE": 1.25,
"DIMEXO": 0.625,
"DIMGAP": 0.625,
"DIMTAD": 1,
"DIMTIH": 0,
"DIMTOH": 0,
"DIMDEC": 2,
"DIMDSEP": ",",
"DIMLUNIT": 2,
"DIMZIN": 8,
"DIMBLK": "",
"DIMTXSTY": "ISOCP",
"DIMLWD": -2,
"DIMLWE": -2,
"DIMSCALE": 1.0
},
"written": [
"DIMASZ",
"DIMBLK",
"DIMDEC",
"DIMDSEP",
"DIMEXE",
"DIMEXO",
"DIMGAP",
"DIMLUNIT",
"DIMLWD",
"DIMLWE",
"DIMSCALE",
"DIMTAD",
"DIMTIH",
"DIMTOH",
"DIMTXSTY",
"DIMTXT",
"DIMZIN"
],
"current": true,
"textstyle_created": true
},
"textstyle": {
"ok": true,
"name": "ISOCP-SMOKE",
"font": "isocp.shx",
"font_resolved": true,
"current": false
},
"mleaderstyle": {
"ok": true,
"name": "SMOKE",
"values": {
"arrow_size": 2.5,
"landing_gap": 1.0,
"text_style": "ISOCP",
"text_height": 2.5
},
"textstyle_created": false
},
"mleaderstyles_on_seat": [
"SMOKE",
"Standard"
]
},
"page_setup": {
"applied": {
"paper": "ISO_A3",
"orientation": "landscape",
"size_mm": [
420.0,
297.0
],
"canonical_media_name": "ISO_A3_(420.00_x_297.00_MM)",
"plot_style": "monochrome.ctb",
"plot_style_known": true,
"scale": "1:1",
"scale_factor": 1.0,
"scale_ratio": [
1.0,
1.0
],
"dxf_standard_scale_type": 16,
"activex_standard_scale": 16,
"plot_area": "layout",
"plot_type": 5,
"device": "DWG To PDF.pc3",
"margins_mm": null,
"center": null,
"paper_units": "mm"
},
"changed": {
"paper": [
"",
"ISO_A3"
],
"canonical_media_name": [
"",
"ISO_A3_(420.00_x_297.00_MM)"
],
"size_mm": [
[
0.0,
0.0
],
[
420.0,
297.0
]
],
"plot_style": [
"",
"monochrome.ctb"
],
"device": [
"Microsoft Print to PDF",
"DWG To PDF.pc3"
],
"margins_mm": [
[
0.0,
0.0,
0.0,
0.0
],
[
17.0,
17.0,
5.0,
5.0
]
],
"paper_units": [
"inches",
"mm"
]
},
"pdf": {
"layout": "Layout1",
"ok": true,
"path": "C:\\Users\\VECTOR\\AppData\\Local\\Temp\\acadmcp_settings_smoke_sapms54v\\Drawing27-Layout1.pdf",
"bytes": 2774,
"mediabox_mm": [
420.158,
297.039
],
"paper": "ISO_A3"
},
"installed_plot_styles": [
"Autodesk-Color.stb",
"Autodesk-MONO.stb",
"DWF Virtual Pens.ctb",
"Fill Patterns.ctb",
"Grayscale.ctb",
"Screening 100%.ctb",
"Screening 25%.ctb",
"Screening 50%.ctb",
"Screening 75%.ctb",
"acad.ctb",
"acad.stb",
"monochrome.ctb",
"monochrome.stb"
]
},
"template": {
"path": "C:\\Users\\VECTOR\\Documents\\GitHub\\Autocad-MCP\\templates\\iso_a3_mech.dwt",
"source": "bundled",
"document": "Drawing28.dwg",
"dimstyle_current": [
"ISO-25"
],
"papers": [
"ISO_A3"
]
},
"documents": [
{
"name": "Drawing9.dwg",
"path": null,
"active": false,
"saved": false,
"entity_count": 0
},
{
"name": "Drawing27.dwg",
"path": null,
"active": true,
"saved": false,
"entity_count": 0
}
],
"layer_state": {
"saved": {
"ok": true,
"name": "SMOKE",
"layer_count": 3,
"replaced": false,
"chunks": 2,
"backend": "com"
},
"restored": {
"ok": true,
"name": "SMOKE",
"applied": {
"layers": 3,
"properties": [
"on",
"frozen",
"locked",
"color",
"linetype",
"lineweight",
"plot",
"current"
],
"current_layer": "0"
},
"missing_layers": [],
"new_layers": [],
"backend": "com"
},
"deleted": {
"ok": true,
"deleted": "SMOKE",
"backend": "com"
}
},
"view": {
"saved": {
"ok": true,
"name": "SMOKE_VIEW",
"center": [
100.0,
50.0
],
"height": 80.0,
"width": 160.9964412811388,
"replaced": false,
"backend": "com"
},
"restored": {
"ok": true,
"name": "SMOKE_VIEW",
"center": [
100.0,
50.0
],
"height": 80.0,
"width": 160.9964412811388,
"applied": "zoom_window",
"backend": "com",
"viewctr": [
100.0,
50.0
],
"viewsize": 80.0
}
},
"ucs": {
"set": {
"ok": true,
"name": "SMOKE_UCS",
"origin": [
10.0,
20.0,
0.0
],
"x_axis": [
1.0,
0.0,
0.0
],
"y_axis": [
0.0,
1.0,
0.0
],
"replaced": false,
"current": true,
"backend": "com"
},
"world": {
"ok": true,
"name": "world",
"current": true,
"backend": "com"
}
},
"preferences": {
"cursor_size": 5,
"read_only": [
"Files.SupportPath",
"Files.TemplateDwgPath",
"Files.PrinterStyleSheetPath",
"Files.PrinterConfigPath"
],
"template_dwg_path": "C:\\Users\\VECTOR\\AppData\\Local\\Autodesk\\AutoCAD 2026\\R25.1\\enu\\Template"
},
"properties": {
"set": {
"ok": true,
"summary_written": [
"author",
"title"
],
"custom_written": [
"ACADMCP_SMOKE"
],
"custom_deleted": [],
"backend": "com"
},
"get": {
"summary": {
"title": "ACADMCP settings smoke",
"subject": "",
"author": "smoke_settings_com.py",
"keywords": "",
"comments": ""
},
"summary_available": true,
"custom": {
"ACADMCP_SMOKE": "1"
},
"backend": "com"
}
},
"prompt_message": {
"ok": true,
"text": "ACADMCP settings smoke: done",
"backend": "com"
},
"failures": []
}
```
The headless engine reports the identical `mediabox_mm` (to 1e-9: matplotlib
writes an exact 420 × 297; AutoCAD's PDF driver writes 420.158 × 297.039,
inside spec §9's ±0.5 mm), `layer_state.restored` and `properties.get.custom`
on the same steps; `summary_available` is `false` headlessly and the five
summary fields are `null` (capability `dwgprops`).
What the run refused before the green one, each fixed in the backend and
pinned by a fake-ActiveX test:
- Three runs died with `RPC_E_CALL_REJECTED` (-2147418111) on the read that
follows a document switch — `Documents.Count` straight after
`Documents.Open`, after `Close(False)` inside `document_close`, and in
`_ensure_document_state` — AutoCAD's own message filter refusing the call
while it still switches documents. pywin32 has no
`CoRegisterMessageFilter`, so the backend now waits that window out itself
(`_wait_out_rejected_call`, read-only callables only, 0.25 s → 1 s pauses
inside a 20 s budget) at those sites; any other error is not retried.
- `layer_list` on the reopened template raised `AttributeError: 'IAcadLayer'
object has no attribute 'Color'. Did you mean: 'color'?`. The type library
spells `IAcadEntity` / `IAcadLayer` `color` and `Lineweight`; late-bound
dispatch resolved `.Color` / `.LineWeight` case-insensitively, but once a
makepy cache exists (this machine's was regenerated on 2026-09-17) every
object a method returns is a generated wrapper and the lookup is exact. The
backend now spells both members as the type library does (works on both
wrappers) and the fakes model the measured names.
- `layer_state_restore` raised `'IAcadObject' object has no attribute
'Count'`: `Dictionaries.Item` and `IAcadDictionary.Item` are declared
`IAcadObject*` (the narrowing `_com_unnarrow` already handled for MLEADER
styles); the save before it went through the typed `Dictionaries.Add`, so
only the reopen paths failed. Both now un-narrow.
- A state saved with layer `0` current came back with two warnings and `0`
unapplied: the `Freeze` put on layer `0` raises 'Invalid layer' whatever
the value, even `False` while thawed (measured on a scratch document;
`color` / `Linetype` / `Lineweight` / `Plottable` / `LayerOn` / `Lock` /
`ActiveLayer` all fine on it). `Freeze` is now written only when it would
change the layer.
- Two of the smoke's own expectations predated the merged tree and were
corrected to it: `mleaderstyle_create` is implemented on COM through the
`ACAD_MLEADERSTYLE` dictionary (spec §4: no `mleaderstyle` key exists), so
the smoke now asserts the created style is listed with `arrow_size` 2.5;
and `plot_style_list` marks catalogue rows with `installed: true` (the
`source: "installed"` rows are files the catalogue does not know), so the
`monochrome.ctb` check reads that flag.
`--interactive` (`user_pick_point`) was not run in this session: the seat is
shared with other agents and nobody was confirmed at the keyboard, and an
unanswered `Utility.GetPoint` holds AutoCAD's command line — every COM client
gets `RPC_E_CALL_REJECTED` — until someone presses Esc, past the 60 s timeout.
The pick path was executed live earlier in the track (Task 23's measurement:
`GetPoint` answers in WCS while `GetEntity` answers in the current UCS). Each
plot also dropped AutoCAD's `plot.log` into the repo root; it is ignored now.
## [1.5.1] — 2026-08-06
A patch for three defects found *after* 1.5.0 went to PyPI, two of them by the
+388
View File
@@ -0,0 +1,388 @@
# scripts/smoke_settings_com.py
"""Exercise the settings track once against the live AutoCAD on this machine.
Creates a NEW document (never touches the operator's open drawing) and runs
every SECTION 18-20 path that needs a live seat, through the same backend
methods the tools call:
system_launch (attach) -> {attached, launched, version}
dimstyle_create ISO-25 / textstyle_create ISOCP
/ mleaderstyle_create iso -> listed with arrow_size 2.5
page_setup_apply ISO_A3 -> batch_plot -> the PDF's own /MediaBox (420 x 297)
plot_style_list (installed .ctb files)
drawing_new(template=iso_a3_mech .dwt) -> mech layers, ISO-25 current, ISO_A3
document_list / document_activate
layer_state_save -> change -> layer_state_restore -> layer_state_delete
view_named_save / view_named_list / view_named_restore
ucs_set / ucs_list / ucs_restore("world") + a non-orthogonal refusal
preferences_get + ONE reversible preferences_set (Display.CursorSize)
+ a read-only key refused
drawing_properties_set / get (SummaryInfo + a custom property)
system_prompt_message
--interactive: user_pick_point (needs the operator at the keyboard)
--build-dwt: open each templates/<name>.dxf and save the .dwt twin
through drawing_template_save, before anything else
Prints one JSON report. Exit codes:
0 every assertion held (``failures`` is empty)
1 the seat answered but at least one assertion failed (listed)
2 no live CAD application reachable (pywin32 missing, or the ProgID
cannot be created) -- the COM paths then remain fake-tested only
Reachability is read from ``system_status()`` because ``connect()`` is lazy.
The ``Dispatch`` fallback *launches* the ProgID when nothing is running and a
cold start can exceed ``COM_CALL_TIMEOUT`` (60 s): open AutoCAD first.
AUTOCAD_MCP_BACKEND=com uv run --frozen python scripts/smoke_settings_com.py
uv run --frozen python scripts/smoke_settings_com.py --build-dwt
uv run --frozen python scripts/smoke_settings_com.py --interactive --keep
"""
from __future__ import annotations
import argparse
import asyncio
import json
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
#: The five bundled templates (spec §6). Pinned here rather than read from
#: the catalogue so the smoke also notices a catalogue that lost a row.
TEMPLATE_NAMES = ("iso_a3_mech", "iso_a1_arch", "iso_a3_pid", "ansi_b_mech", "ansi_d_arch")
#: Every DWG/DWT since R13 opens with "AC10xx" (AC1032 for the 2018 format).
DWG_MAGIC = b"AC10"
#: What the ISO A3 landscape sheet must read back as, from the PDF itself.
A3_LANDSCAPE_MM = (420.0, 297.0)
MEDIABOX_TOLERANCE_MM = 0.5
def _check(failures: list[str], label: str, ok: bool) -> None:
if not ok:
failures.append(label)
#: Names of the documents this run opened and has not closed yet. A run that
#: dies half-way closes them in ``main``'s ``finally`` (unless ``--keep``), so
#: a failed smoke never leaves its scratch documents on the operator's seat.
_OPENED: list[str] = []
async def _opened_now(backend) -> str:
"""Record the document a ``drawing_new`` / ``drawing_open`` just made active."""
name = (await backend.system_status()).get("active_document")
if name and name not in _OPENED:
_OPENED.append(name)
return name
async def _close_ours(backend, name: str) -> None:
await backend.document_close(name, discard=True)
if name in _OPENED:
_OPENED.remove(name)
async def _close_leftovers(backend) -> None:
open_names = {row["name"] for row in await backend.document_list()}
for name in list(_OPENED):
if name in open_names:
try:
await backend.document_close(name, discard=True)
except Exception as exc: # best effort on the way out
print(f"could not close {name}: {exc}", file=sys.stderr)
_OPENED.remove(name)
async def build_dwt(backend) -> list[dict]:
"""Open each bundled DXF over COM and save its .dwt twin next to it."""
from engineering.standards.templates import TEMPLATE_CATALOG, TEMPLATES_DIR
rows: list[dict] = []
if len(TEMPLATE_CATALOG) != len(TEMPLATE_NAMES):
raise SystemExit(
f"TEMPLATE_CATALOG has {len(TEMPLATE_CATALOG)} rows, the smoke pins "
f"{len(TEMPLATE_NAMES)}: update TEMPLATE_NAMES deliberately"
)
for name in TEMPLATE_NAMES:
dxf = Path(TEMPLATES_DIR).resolve() / f"{name}.dxf"
dwt = Path(TEMPLATES_DIR).resolve() / f"{name}.dwt"
if not dxf.exists():
raise SystemExit(f"{dxf} is missing: run scripts/build_templates.py first")
await backend.drawing_open(str(dxf))
opened_as = await _opened_now(backend)
saved = await backend.drawing_template_save(
str(dwt), name=name, description=f"AutoCAD MCP Pro bundled template {name}"
)
# SaveAs rebinds the document to the .dwt name (``document_rebound``).
if opened_as in _OPENED:
_OPENED.remove(opened_as)
await _close_ours(backend, await _opened_now(backend))
head = dwt.read_bytes()[:6]
rows.append(
{
"name": name,
"path": str(dwt.relative_to(ROOT)),
"bytes": dwt.stat().st_size,
"magic": head.decode("ascii", "replace"),
"is_dwg_container": head.startswith(DWG_MAGIC),
"format": saved["format"],
}
)
return rows
async def main(args: argparse.Namespace) -> int:
try:
from backends.com_backend import ComBackend
except ImportError as exc:
print(f"pywin32 not available: {exc}")
return 2
backend = ComBackend()
try:
await backend.connect()
except Exception as exc:
print(f"no live AutoCAD reachable: {exc}")
return 2
failures: list[str] = []
report: dict = {}
try:
status = await backend.system_status()
if not status.get("connected"):
print(f"no live AutoCAD reachable: {status.get('error', status)}")
return 2
report["autocad"] = status.get("autocad_version") or status.get("cad_progid")
report["system_launch"] = await backend.system_launch(visible=True)
_check(failures, "system_launch attached", report["system_launch"]["attached"] is True)
if args.build_dwt:
report["dwt_built"] = await build_dwt(backend)
_check(
failures,
"every .dwt is a DWG container",
all(row["is_dwg_container"] for row in report["dwt_built"]),
)
await backend.drawing_new()
smoke_document = await _opened_now(backend)
report["document"] = smoke_document
# ── styles ────────────────────────────────────────────────────────
from engineering.standards.dimstyles import resolve_dimstyle
from engineering.standards.mleaderstyles import resolve_mleaderstyle
created = await backend.dimstyle_create(
"ISO-25-SMOKE", resolve_dimstyle("iso-25", None), set_current=True
)
dimstyles = {row["name"]: row for row in await backend.dimstyle_list()}
smoke_style = dimstyles["ISO-25-SMOKE"]
_check(failures, "dimstyle DIMTXT 2.5", abs(smoke_style["values"]["DIMTXT"] - 2.5) < 1e-6)
_check(failures, "dimstyle DIMDSEP ','", smoke_style["values"]["DIMDSEP"] == ",")
_check(failures, "dimstyle current", smoke_style["current"] is True)
textstyle = await backend.textstyle_create("ISOCP-SMOKE", "isocp.shx")
_check(failures, "isocp.shx resolved on the seat", textstyle["font_resolved"] is True)
# Both engines create MLEADER styles (spec: COM through the
# ACAD_MLEADERSTYLE dictionary's AddObject; no `mleaderstyle` key).
mleader_created = await backend.mleaderstyle_create(
"SMOKE", resolve_mleaderstyle("iso", None)
)
mleader_styles = {row["name"]: row for row in await backend.mleaderstyle_list()}
_check(failures, "mleaderstyle created on COM", mleader_created["ok"] is True)
_check(failures, "mleaderstyle listed", "SMOKE" in mleader_styles)
_check(
failures,
"mleaderstyle ISO arrow 2.5",
abs(float(mleader_styles.get("SMOKE", {}).get("arrow_size", 0.0)) - 2.5) < 1e-6,
)
report["styles"] = {
"dimstyle": created,
"textstyle": textstyle,
"mleaderstyle": mleader_created,
"mleaderstyles_on_seat": sorted(mleader_styles),
}
# ── page setup + the PDF's own mediabox ───────────────────────────
from engineering.standards.papers import resolve_page_setup
from engineering.standards.plot import batch_plot
applied = await backend.page_setup_apply(
"Layout1", resolve_page_setup("ISO_A3", orientation="landscape", scale="1:1")
)
listed = (await backend.page_setup_list("Layout1"))[0]
_check(
failures,
"page_setup_list says 420 x 297",
tuple(float(v) for v in listed["size_mm"]) == A3_LANDSCAPE_MM,
)
out_dir = Path(tempfile.mkdtemp(prefix="acadmcp_settings_smoke_"))
plotted = await batch_plot(backend, ["Layout1"], str(out_dir), "{drawing}-{layout}.pdf")
pdf_row = plotted["sheets"][0]
width_mm, height_mm = (float(v) for v in pdf_row["mediabox_mm"])
_check(
failures,
"PDF /MediaBox is 420 x 297 (+-0.5 mm)",
abs(width_mm - A3_LANDSCAPE_MM[0]) < MEDIABOX_TOLERANCE_MM
and abs(height_mm - A3_LANDSCAPE_MM[1]) < MEDIABOX_TOLERANCE_MM,
)
# Catalogue rows carry ``installed`` (True/False, None when the
# Preferences object is unreachable); ``source: "installed"`` marks
# files the catalogue does not know.
installed = sorted(
row["name"] for row in await backend.plot_style_list() if row.get("installed") is True
)
_check(failures, "monochrome.ctb installed on the seat", "monochrome.ctb" in installed)
report["page_setup"] = {
"applied": applied["applied"],
"changed": applied["changed"],
"pdf": pdf_row,
"installed_plot_styles": installed,
}
# ── bundled template over COM ─────────────────────────────────────
from engineering.standards.templates import resolve_template
template_path, template_source = resolve_template("iso_a3_mech", "com")
opened = await backend.drawing_new(template=template_path)
await _opened_now(backend)
template_layers = {layer.name for layer in await backend.layer_list()}
current_dimstyles = [row["name"] for row in await backend.dimstyle_list() if row["current"]]
template_papers = [row["paper"] for row in await backend.page_setup_list()]
_check(
failures,
"template carries the mech layers",
{"GEOMETRY", "DIM", "CENTER", "HIDDEN", "TITLEBLOCK"} <= template_layers,
)
_check(failures, "template has ISO-25 current", current_dimstyles == ["ISO-25"])
_check(failures, "template has an ISO_A3 page setup", "ISO_A3" in template_papers)
report["template"] = {
"path": str(template_path),
"source": template_source,
"document": opened["name"],
"dimstyle_current": current_dimstyles,
"papers": template_papers,
}
await _close_ours(backend, opened["name"])
activated = await backend.document_activate(smoke_document)
_check(failures, "smoke document re-activated", activated["active"] == smoke_document)
report["documents"] = await backend.document_list()
# ── layer state round trip ────────────────────────────────────────
await backend.layer_create("SMOKE_A", color=1)
await backend.layer_create("SMOKE_B", color=2)
saved = await backend.layer_state_save("SMOKE", description="settings smoke")
await backend.layer_modify("SMOKE_A", color=5)
await backend.layer_freeze("SMOKE_B")
restored = await backend.layer_state_restore("SMOKE")
after = {layer.name: layer for layer in await backend.layer_list()}
_check(failures, "layer state restored the colour", after["SMOKE_A"].color == 1)
_check(failures, "layer state thawed SMOKE_B", after["SMOKE_B"].is_frozen is False)
_check(failures, "restore reports nothing missing", restored["missing_layers"] == [])
_check(
failures,
"restore applied every layer",
restored["applied"]["layers"] == saved["layer_count"],
)
_check(failures, "restore raised no warnings", "warnings" not in restored)
states = await backend.layer_state_list()
_check(failures, "layer state listed", any(row["name"] == "SMOKE" for row in states))
deleted = await backend.layer_state_delete("SMOKE")
report["layer_state"] = {"saved": saved, "restored": restored, "deleted": deleted}
# ── named view ────────────────────────────────────────────────────
view = await backend.view_named_save("SMOKE_VIEW", center=[100.0, 50.0], height=80.0)
views = await backend.view_named_list()
_check(failures, "named view listed", any(row["name"] == "SMOKE_VIEW" for row in views))
report["view"] = {"saved": view, "restored": await backend.view_named_restore("SMOKE_VIEW")}
# ── UCS ───────────────────────────────────────────────────────────
ucs = await backend.ucs_set(
"SMOKE_UCS", [10.0, 20.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]
)
ucs_rows = await backend.ucs_list()
_check(
failures,
"ucs current",
any(row["name"] == "SMOKE_UCS" and row["current"] for row in ucs_rows),
)
try:
await backend.ucs_set("SMOKE_BAD", [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0])
_check(failures, "non-orthogonal UCS refused", False)
except ValueError as exc:
_check(failures, "non-orthogonal UCS refusal names the angle", "45" in str(exc))
report["ucs"] = {"set": ucs, "world": await backend.ucs_restore("world")}
# ── preferences: get, one reversible set, a read-only refusal ─────
prefs = await backend.preferences_get()
cursor = int(prefs["values"]["Display.CursorSize"])
probe = cursor + 1 if cursor < 100 else cursor - 1
bumped = await backend.preferences_set("Display.CursorSize", probe)
reverted = await backend.preferences_set("Display.CursorSize", cursor)
_check(failures, "preference set reports old", bumped["old"] == cursor)
_check(failures, "preference set reports new", bumped["new"] == probe)
_check(failures, "preference reverted", reverted["new"] == cursor)
_check(failures, "read-only keys listed", "Files.SupportPath" in prefs["read_only"])
try:
await backend.preferences_set("Files.SupportPath", "C:/nope")
_check(failures, "read-only preference refused", False)
except ValueError:
_check(failures, "read-only preference refused", True)
report["preferences"] = {
"cursor_size": cursor,
"read_only": prefs["read_only"],
"template_dwg_path": prefs["values"].get("Files.TemplateDwgPath"),
}
# ── SummaryInfo ───────────────────────────────────────────────────
written = await backend.drawing_properties_set(
summary={"title": "ACADMCP settings smoke", "author": "smoke_settings_com.py"},
custom={"ACADMCP_SMOKE": "1"},
)
props = await backend.drawing_properties_get()
_check(failures, "summary available on COM", props["summary_available"] is True)
_check(failures, "title round trip", props["summary"]["title"] == "ACADMCP settings smoke")
_check(failures, "custom property round trip", props["custom"].get("ACADMCP_SMOKE") == "1")
report["properties"] = {"set": written, "get": props}
# ── operator prompt line ──────────────────────────────────────────
report["prompt_message"] = await backend.system_prompt_message(
"ACADMCP settings smoke: done"
)
if args.interactive:
picked = await backend.user_pick_point("ACADMCP smoke: pick any point (Esc cancels)")
report["user_pick_point"] = picked
_check(
failures,
"pick returns a point, cancelled or timed_out",
bool(picked.get("cancelled"))
or bool(picked.get("timed_out"))
or ("x" in picked and "y" in picked),
)
report["failures"] = failures
print(json.dumps(report, indent=2, default=str))
if not args.keep:
await _close_ours(backend, smoke_document)
return 0 if not failures else 1
finally:
if not args.keep:
await _close_leftovers(backend)
await backend.disconnect()
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("--build-dwt", action="store_true", help="build templates/*.dwt first")
parser.add_argument("--interactive", action="store_true", help="also run user_pick_point")
parser.add_argument("--keep", action="store_true", help="leave the smoke document open")
return parser
if __name__ == "__main__":
raise SystemExit(asyncio.run(main(_parser().parse_args())))
+7 -1
View File
@@ -26,7 +26,7 @@ listed by `drawing_template_list`. Start from one with
- `<name>.dwt` — the live twin, used by `drawing_new(template=…)` on the COM
backend. AutoCAD's DWT is a DWG container, which no headless library
writes, so these are produced **on the live machine** by
`uv run --frozen python scripts/smoke_settings_com.py --build-dwt` (Task 24's
`uv run --frozen python scripts/smoke_settings_com.py --build-dwt` (Task 25's
smoke): it opens each DXF over COM and calls `drawing_template_save(…dwt)`
(`SaveAs(path, ac2018_Template)`), then the files are committed once. While
a `.dwt` is missing, `resolve_template` falls back to the DXF and reports
@@ -43,3 +43,9 @@ to CRLF so a checkout on any platform equals a fresh build.
Rebuild after changing anything the build depends on (layer sets, the title
block, the dimension presets, the settings facade) and commit the result
together with the change — the reproducibility test is the gate.
The `.dwt` twins were built on 2026-09-22 on AutoCAD 2026 by
`scripts/smoke_settings_com.py --build-dwt` — each DXF opened over COM and
saved as a template (`AcSaveAsType` 66, `ac2018_Template`). Rebuild them the
same way after changing a DXF; `scripts/build_templates.py --check` gates the
DXF side only.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.