Files
autocad-mcp-pro/tests/test_sprint4_ezdxf.py
T
Umutcan Edizsalan 32a68be919 fix: correctness, cross-backend parity & enforced quality gate (audit Sprints 2-4)
Audit-driven fixes across the dual backend, the premium workflow, and the
engineering layer. 318 tests, ruff-clean.

Quality gate (the moat):
- drawing_finalize now runs the premium ISO-128 critique in addition to the
  8-step validator; leftover construction, non-ISO lineweights, untrimmed
  corners, duplicates and dim overlap block the gate (strict_critique fails on
  warnings too).
- dim_overlap critique no longer a no-op (dim reference point + bbox fallback);
  dim/construction layers resolve from the active layer set (iso13567 too).

Stability / correctness:
- view_screenshot & drawing_export_pdf render headless via Agg (no more
  worker-thread SIGSEGV).
- gear tooth profile no longer self-overlaps at high tooth counts; section view
  no longer duplicates bore lines; validator keyway heuristic fixed.
- ezdxf aligned/angular dim API, full-circle polar array duplicate already
  covered by Sprint 1; this bundle adds save-format-from-extension,
  entity_offset side selection, ARC length, properties parity (bounding_box /
  rotation / char_height), mtext rotation.

COM robustness (mocked-COM tested):
- CoUninitialize on teardown; transaction commit/rollback try/finally + UNDO via
  the CMDACTIVE guard; system_run_lisp guard + USERS1 value capture;
  set_variable type coercion; HWND via Application.HWND; BlockInfo.description.

Security:
- HTTP remote-bind guard fires on every launch path (incl. `fastmcp run`);
  StaticTokenVerifier wired into FastMCP; AutoLISP allowlist bypass-vector tests.

Hygiene: linetype-on-demand in _apply_attrs, honest view_zoom returns, snap-on-arc
sweep handling, point_tangent acos clamp, dynamic tool_groups / tool count,
word-boundary SPUR/HELICAL, undo-stack locking.

Deferred: R22 (undo/transaction stack isolation) lands with the I1 refiner (1.2),
which depends on it.
2026-06-19 08:36:58 +03:00

137 lines
5.1 KiB
Python

"""Regression tests for the Sprint-4 ezdxf-backend fixes.
* R24 — _apply_attrs loads CENTER/HIDDEN on demand so entity_create_line(
linetype=...) actually renders the dashed linetype instead of
silently falling back to Continuous.
* S3 — block_list surfaces the block definition's description instead of "".
* R26/N9 — view_zoom_extents / view_zoom_window return a consistent
{"ok": True, "applied": False, ...} shape (no fake framing success).
* NEW-undo-1 — drawing_undo / transaction_* serialize _undo_stack access
(no read/mutate outside self._lock); behaviour still correct.
All tests are backend-agnostic ezdxf and use the shared `backend` fixture.
"""
from __future__ import annotations
import pytest
pytestmark = pytest.mark.asyncio
# ── R24 — CENTER/HIDDEN linetype actually loaded + applied ──────────────────
async def test_create_line_loads_center_linetype(backend):
# Fresh doc: CENTER is not loaded yet.
assert "CENTER" not in backend._doc.linetypes
line = await backend.entity_create_line(0, 0, 100, 0, linetype="CENTER")
# The linetype must now be defined in the document (a real dashed pattern),
# not silently dropped — otherwise it renders as Continuous.
assert "CENTER" in backend._doc.linetypes
# And it must be assigned to the entity.
info = await backend.entity_get(line.handle)
assert info.linetype == "CENTER"
async def test_create_line_loads_hidden_linetype(backend):
assert "HIDDEN" not in backend._doc.linetypes
line = await backend.entity_create_line(0, 0, 50, 50, linetype="HIDDEN")
assert "HIDDEN" in backend._doc.linetypes
info = await backend.entity_get(line.handle)
assert info.linetype == "HIDDEN"
async def test_create_line_continuous_does_not_error(backend):
# Built-in linetypes are a no-op for the loader — must not raise.
line = await backend.entity_create_line(0, 0, 10, 0, linetype="Continuous")
info = await backend.entity_get(line.handle)
assert info.linetype == "Continuous"
# ── S3 — block description surfaced from the definition ─────────────────────
async def test_block_list_surfaces_description(backend):
# Create a block from an entity, then stamp a description on its definition.
seg = await backend.entity_create_line(0, 0, 10, 0)
await backend.block_create_from_entities("WIDGET", [seg.handle], 0.0, 0.0)
backend._doc.blocks.get("WIDGET").block.dxf.description = "Mechanical widget"
blocks = await backend.block_list()
widget = next(b for b in blocks if b.name == "WIDGET")
assert widget.description == "Mechanical widget"
async def test_block_list_description_defaults_empty(backend):
# A block with no description must still surface "" (no crash, default).
seg = await backend.entity_create_line(0, 0, 5, 0)
await backend.block_create_from_entities("PLAIN", [seg.handle], 0.0, 0.0)
blocks = await backend.block_list()
plain = next(b for b in blocks if b.name == "PLAIN")
assert plain.description == ""
# ── R26 / N9 — honest zoom payload shape ────────────────────────────────────
async def test_zoom_extents_reports_not_applied(backend):
res = await backend.view_zoom_extents()
assert res["ok"] is True
assert res["applied"] is False
assert "message" in res
async def test_zoom_window_reports_not_applied(backend):
res = await backend.view_zoom_window(0, 0, 100, 100)
assert res["ok"] is True
assert res["applied"] is False
assert "message" in res
# ── NEW-undo-1 — undo/transaction stack behaviour under lock ────────────────
async def test_undo_empty_stack_returns_error(backend):
res = await backend.drawing_undo()
assert res["ok"] is False
assert "undo" in res["error"].lower()
async def test_commit_without_transaction_returns_error(backend):
res = await backend.transaction_commit()
assert res["ok"] is False
assert "transaction" in res["error"].lower()
async def test_rollback_without_transaction_returns_error(backend):
res = await backend.transaction_rollback()
assert res["ok"] is False
assert "transaction" in res["error"].lower()
async def test_transaction_begin_commit_roundtrip(backend):
begin = await backend.transaction_begin()
assert begin["ok"] is True
assert len(backend._undo_stack) == 1
commit = await backend.transaction_commit()
assert commit["ok"] is True
assert len(backend._undo_stack) == 0
async def test_transaction_rollback_restores_snapshot(backend):
# Snapshot an empty doc, add an entity, then roll back to the snapshot.
await backend.transaction_begin()
line = await backend.entity_create_line(0, 0, 10, 0)
before = await backend.entity_get(line.handle)
assert before.handle # entity exists
res = await backend.transaction_rollback()
assert res["ok"] is True
assert len(backend._undo_stack) == 0
# The line is gone after rollback.
listed = await backend.entity_list(type_filter="LINE")
assert listed == []