Files
autocad-mcp-pro/tests/test_sprint4_validator.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

129 lines
5.0 KiB
Python

"""Sprint-4 R27 regression tests for engineering.validator step 5.
R27: title/helix consistency must match SPUR / HELICAL as whole words (not
substrings) and scope the scan to title-block text where possible.
"""
import pytest
from engineering.layers import ensure_engineering_layers, ensure_standard_linetypes
from engineering.validator import DrawingValidator
pytestmark = pytest.mark.asyncio
def _codes(result) -> set[str]:
return {f.code for f in result.findings}
async def _ensure_hidden_linetype(backend) -> None:
"""ezdxf doesn't ship a HIDDEN pattern; add a synthetic one."""
doc = backend._require_doc()
if "HIDDEN" not in doc.linetypes:
doc.linetypes.add(
"HIDDEN",
pattern=[6.35, 5.0, -1.27],
description="Hidden _ _ _ _ _ _",
)
async def _bootstrap(backend):
await ensure_standard_linetypes(backend)
await _ensure_hidden_linetype(backend)
await ensure_engineering_layers(backend)
async def test_spur_as_substring_does_not_false_match(backend, tmp_path):
"""R27: 'SPURIOUS' contains 'spur' but must NOT trip a SPUR mismatch."""
await _bootstrap(backend)
await backend.entity_create_text(
text="SPURIOUS HELICAL GEAR", x=0, y=0, height=5, layer="TEXT",
)
await backend.drawing_save_as(str(tmp_path / "spurious.dxf"))
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
codes = _codes(result)
assert "title_says_spur_for_helical" not in codes
assert "title_helical_spur" not in codes
async def test_genuine_spur_title_flags_when_helical_expected(backend, tmp_path):
"""R27: a real 'SPUR GEAR' title still flags the mismatch when helix declared."""
await _bootstrap(backend)
await backend.entity_create_text(
text="SPUR GEAR", x=0, y=0, height=5, layer="TEXT",
)
await backend.drawing_save_as(str(tmp_path / "spur.dxf"))
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
codes = _codes(result)
assert "title_says_spur_for_helical" in codes
assert result.ok is False
async def test_spur_word_in_titleblock_layer_flags(backend, tmp_path):
"""R27: title-block-scoped scan — SPUR on TITLEBLOCK layer still flags."""
await _bootstrap(backend)
await backend.entity_create_text(
text="SPUR GEAR", x=0, y=0, height=5, layer="TITLEBLOCK",
)
await backend.drawing_save_as(str(tmp_path / "spur_tb.dxf"))
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
assert "title_says_spur_for_helical" in _codes(result)
async def test_substring_in_titleblock_layer_does_not_false_match(backend, tmp_path):
"""R27: 'SPURIOUS' on TITLEBLOCK layer must not trip a SPUR match either."""
await _bootstrap(backend)
await backend.entity_create_text(
text="SPURIOUS NOTE", x=0, y=0, height=5, layer="TITLEBLOCK",
)
await backend.drawing_save_as(str(tmp_path / "spurious_tb.dxf"))
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
codes = _codes(result)
assert "title_says_spur_for_helical" not in codes
assert "title_helical_spur" not in codes
async def test_helical_spur_combo_still_flags_invalid(backend, tmp_path):
"""R27: both whole words present -> invalid combo error preserved."""
await _bootstrap(backend)
await backend.entity_create_text(
text="HELICAL SPUR GEAR", x=0, y=0, height=5, layer="TEXT",
)
await backend.drawing_save_as(str(tmp_path / "combo.dxf"))
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
assert "title_helical_spur" in _codes(result)
assert result.ok is False
async def test_no_helix_expected_skips_title_check(backend, tmp_path):
"""Gating preserved: without helix_angle, a SPUR title raises no title finding."""
await _bootstrap(backend)
await backend.entity_create_text(
text="SPUR GEAR", x=0, y=0, height=5, layer="TEXT",
)
await backend.drawing_save_as(str(tmp_path / "nohelix.dxf"))
result = await DrawingValidator().run(backend)
codes = _codes(result)
assert "title_says_spur_for_helical" not in codes
assert "title_helical_spur" not in codes
async def test_titleblock_layer_takes_precedence_over_text_layer(backend, tmp_path):
"""R27 scoping: when title-block text exists, the TEXT-layer scan is not used.
SPUR sits on the generic TEXT layer (a body note), while the title block on
TITLEBLOCK is clean — the scoped scan should ignore the body note.
"""
await _bootstrap(backend)
await backend.entity_create_text(
text="HELICAL GEAR", x=0, y=0, height=5, layer="TITLEBLOCK",
)
await backend.entity_create_text(
text="SPUR TOOTH DETAIL NOTE", x=0, y=-20, height=3.5, layer="TEXT",
)
await backend.drawing_save_as(str(tmp_path / "scope.dxf"))
result = await DrawingValidator().run(backend, expected={"helix_angle": 15})
codes = _codes(result)
assert "title_says_spur_for_helical" not in codes
assert "title_helical_spur" not in codes