Files
autocad-mcp-pro/tests/test_sprint2_fixes.py
T
Umutcan EdizsalanandCursor 9ec6c4a989 style: apply ruff format across the repository
ruff format was never run repo-wide; CI now enforces 'ruff format --check', so normalize every module once. No behavior changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:14:51 +03:00

208 lines
7.8 KiB
Python

"""Regression tests for the Sprint-2 audit fixes.
Covers, one block per audit finding:
* NEW-screenshot-1 — headless Agg render returns a valid PNG (no GUI-thread crash)
* I15 — drawing_finalize runs the premium critique as part of the gate
* R4 — dim_overlap critique actually fires (was a guaranteed no-op)
* N4 — dim / construction layers resolve per the active layer set
* NEW-AUTH-1 — the remote-bind guard runs on the run_async launch path
* NEW-gear-roottooth— gear outline never dips inside the root circle (high tooth counts)
* NEW-section-dupbore / NEW-validator-keyway / NEW-gear-baseconstr
"""
from __future__ import annotations
import math
import pytest
pytestmark = pytest.mark.asyncio
class _FakeCtx:
"""Minimal Context stand-in: drawing_finalize only needs lifespan_context + warning."""
def __init__(self, backend):
self.lifespan_context = {"backend": backend}
self.warnings: list[str] = []
async def warning(self, message):
self.warnings.append(message)
# ── NEW-screenshot-1 ────────────────────────────────────────────────────────
async def test_screenshot_renders_png_without_gui_thread_crash(backend):
"""view_screenshot runs in a worker thread; the Agg figure must not crash and
must return valid PNG bytes."""
await backend.entity_create_line(0, 0, 100, 100)
png = await backend.view_screenshot()
assert png is not None
assert png[:4] == b"\x89PNG" # PNG magic
# ── I15 — critique is part of the finalize gate ─────────────────────────────
async def test_finalize_runs_critique_and_blocks_on_construction(backend, tmp_path):
from fastmcp.exceptions import ToolError
import server
await backend.drawing_apply_iso_layers("mech")
await backend.entity_create_line(0, 0, 100, 0, layer="GEOMETRY")
await backend.construction_xline(0, 0, 90) # leftover scaffold -> construction_left ERROR
ctx = _FakeCtx(backend)
save = str(tmp_path / "part.dxf")
with pytest.raises(ToolError) as exc:
await server.drawing_finalize(save_path=save, ctx=ctx)
assert "critique" in str(exc.value).lower()
# Clear the scaffold; finalize now passes and the payload carries the critique.
await backend.construction_clear()
payload = await server.drawing_finalize(save_path=save, ctx=ctx)
assert payload["ok"] is True
assert "critique" in payload
assert payload["critique_summary"]["error"] == 0
# ── R4 — dim_overlap critique fires ─────────────────────────────────────────
async def test_dim_overlap_critique_is_not_a_noop(backend):
await backend.drawing_apply_iso_layers("mech")
# Two dimensions measuring the same segment overlap exactly.
await backend.dimension_linear(0, 0, 100, 0, 50, 20, layer="DIM")
await backend.dimension_linear(0, 0, 100, 0, 50, 20, layer="DIM")
issues = await backend.drawing_critique(focus=["dim_overlap"])
assert len(issues) >= 1
assert all(i.focus == "dim_overlap" for i in issues)
# ── N4 — dim / construction layers follow the active layer set ──────────────
async def test_iso13567_dim_and_construction_layers_resolve(backend):
await backend.drawing_apply_iso_layers("iso13567")
assert backend._active_layer_set_id() == "iso13567"
assert backend._role_layer("dim") == "M-DIMEN-T-N"
assert backend._role_layer("construction") == "M-CONST-E-N"
ln = await backend.entity_create_line(0, 0, 80, 0, layer="M-GEOMET-E-N")
dims = await backend.dimension_auto([ln.handle], style="chain")
assert dims[0].layer == "M-DIMEN-T-N"
xl = await backend.construction_xline(0, 0, 90)
assert xl.layer == "M-CONST-E-N"
# construction_left must catch iso13567 scaffold (layer name independent).
left = await backend.drawing_critique(focus=["construction_left"])
assert len(left) == 1 and left[0].severity == "error"
cleared = await backend.construction_clear()
assert cleared["deleted"] == 1
assert await backend.drawing_critique(focus=["construction_left"]) == []
# ── NEW-AUTH-1 — bind guard on the run_async launch path ────────────────────
async def test_run_async_invokes_bind_guard_for_http(monkeypatch):
import server
seen = {}
def fake_guard(host):
seen["host"] = host
raise SystemExit("blocked before bind")
monkeypatch.setattr(server, "_validate_http_bind", fake_guard)
with pytest.raises(SystemExit):
await server.mcp.run_async(transport="http", host="0.0.0.0")
assert seen["host"] == "0.0.0.0"
async def test_run_async_skips_guard_for_stdio(monkeypatch):
import server
seen = {}
monkeypatch.setattr(server, "_validate_http_bind", lambda h: seen.setdefault("host", h))
async def fake_orig(*args, **kwargs):
return "stdio-ran"
monkeypatch.setattr(server, "_orig_run_async", fake_orig)
out = await server.mcp.run_async(transport="stdio")
assert out == "stdio-ran"
assert seen == {} # guard not invoked for stdio
# ── NEW-gear-roottooth — outline never dips inside the root circle ──────────
async def test_gear_outline_stays_outside_root_circle():
from engineering.gear import generate_full_gear_outline
for module, teeth, pa in [(2, 20, 20.0), (2, 50, 20.0), (3, 60, 20.0), (1, 6, 20.0)]:
pitch_r = module * teeth / 2.0
root_r = pitch_r - 1.25 * module
pts = generate_full_gear_outline(module, teeth, pa)
radii = [math.hypot(x, y) for x, y in pts]
# No point may sit inside the root circle (the self-overlap bug for z>=~42).
assert min(radii) >= root_r - 1e-6, (
f"m={module} z={teeth}: min_r {min(radii)} < root_r {root_r}"
)
# ── NEW-section-dupbore / NEW-validator-keyway / NEW-gear-baseconstr ─────────
async def test_gear_front_view_base_circle_persists_and_keyway_validates(backend, tmp_path):
from engineering.gear import draw_helical_gear_front_view
from engineering.validator import DrawingValidator
await backend.drawing_apply_iso_layers("mech")
res = await draw_helical_gear_front_view(
backend,
module=2,
teeth=20,
helix_angle=0,
center=(0, 0),
bore_diameter=20,
keyway_width=6,
keyway_depth=2.8,
)
# NEW-gear-baseconstr: base circle is a persistent PHANTOM reference, not CONSTRUCTION.
base = await backend.entity_get(res["base_circle"])
assert base.layer == "PHANTOM"
await backend.construction_clear()
assert await backend.entity_get(res["base_circle"]) # survives, handle still valid
# NEW-validator-keyway: the open 4-point keyway polyline is now recognised.
await backend.drawing_save_as(str(tmp_path / "gear.dxf"))
vr = await DrawingValidator().run(
backend,
expected={"must_have_bore": True, "must_have_keyway": True},
)
assert "keyway_section_unverified" not in [f.code for f in vr.findings]
async def test_gear_section_does_not_duplicate_bore_lines(backend):
from engineering.gear import draw_gear_section_aa
await backend.drawing_apply_iso_layers("mech")
await draw_gear_section_aa(
backend,
gear_metadata={
"outer_radius": 22,
"center": [0, 0],
"bore_diameter": 20,
"keyway_width": 6,
"keyway_depth": 2.8,
},
x_offset=100,
face_width=30,
)
dups = await backend.drawing_critique(focus=["duplicate_entities"])
assert dups == []