Files
autocad-mcp-pro/tests/test_understand_scale_tools.py
Umut CANandClaude Opus 5.5 7aaead43d9 feat(understand): drawing_scale_check - is this drawing to scale against that one
Equipment tags found exactly once in each drawing are matched (a tag
found twice - a plan copy, a legend - is reported ambiguous and takes no
part; two plan copies resolve to the cluster that matches the most
tags). For every pair of matched tags at least min_pair_mm apart on the
reference, the ratio of their distances is taken in millimetres by each
drawing's inferred unit; the median, the quartiles, the share within
+/-10 % of the median, the same per room of the reference, and the
verdict: to_scale (>= 80 %, with its factor), schematic (<= 50 %),
partly, or insufficient under three matched tags.

server.py grows SECTION 26 to two tools with drawing_scale_check (core,
read-only, tagged analysis/query); alias record, inventory, README
release snapshot 243, frozen group sizes (analysis 14).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 11:55:57 +03:00

101 lines
3.7 KiB
Python

"""drawing_scale_check over the MCP wire, on a fresh headless document."""
from __future__ import annotations
import ezdxf
import pytest
import pytest_asyncio
from fastmcp import Client
import server
from tests.fixtures.plant_pair import build_plant_pair
pytestmark = pytest.mark.asyncio
#: Six tags of one plant in millimetres; the closest pair is 7 211 apart.
PLAN = {
"T4100": (0.0, 0.0),
"T4200": (12000.0, 0.0),
"T4300": (12000.0, 9000.0),
"T4400": (0.0, 9000.0),
"T4500": (6000.0, 4000.0),
"T4600": (20000.0, 15000.0),
}
@pytest_asyncio.fixture
async def client(monkeypatch):
"""In-memory client on a fresh headless document - never the operator's AutoCAD."""
monkeypatch.setenv("AUTOCAD_MCP_BACKEND", "ezdxf")
async with Client(server.mcp) as connected:
status = (await connected.call_tool("system_status", {})).structured_content or {}
assert status.get("backend") == "ezdxf", "these tests must never touch live AutoCAD"
await connected.call_tool("drawing_new", {})
yield connected
def reference_file(tmp_path):
"""The plan's tags at full size in a millimetre DXF (INSUNITS 4)."""
doc = ezdxf.new(dxfversion="R2010", units=4)
msp = doc.modelspace()
for tag, (x, y) in PLAN.items():
msp.add_text(tag, height=250.0, dxfattribs={"insert": (x, y), "layer": "TAGS"})
path = tmp_path / "reference.dxf"
doc.saveas(path)
return str(path)
async def test_the_synthetic_pid_is_schematic_against_its_layout(client, tmp_path):
truth = build_plant_pair(tmp_path)
result = await client.call_tool(
"drawing_scale_check", {"path": truth["pid"], "reference": truth["layout"]}
)
assert result.structured_content["verdict"] == truth["scale_verdict"]
async def test_the_current_document_at_half_scale_is_to_scale_with_factor_one_half(
client, tmp_path
):
# The same tags at half the coordinates in the current document. Its
# header says metres (drawing_new keeps ezdxf's default INSUNITS 6), but
# 250-unit text and a 10 000-unit spread read as millimetres, and the
# check converts by the inferred unit: the factor stays 0.5.
for tag, (x, y) in PLAN.items():
await client.call_tool(
"entity_create_text",
{"text": tag, "x": x * 0.5, "y": y * 0.5, "height": 250.0, "layer": "TAGS"},
)
before = (await client.call_tool("analysis_entity_stats", {})).structured_content
result = await client.call_tool("drawing_scale_check", {"reference": reference_file(tmp_path)})
after = (await client.call_tool("analysis_entity_stats", {})).structured_content
assert after == before
report = result.structured_content
assert report["units"]["a"]["unit"] == report["units"]["b"]["unit"] == "mm"
assert report["verdict"] == "to_scale"
assert report["factor"] == pytest.approx(0.5, abs=1e-12)
assert report["pair_count"] == 15
assert report["ambiguous"] == []
async def test_a_missing_reference_is_refused_by_name(client, tmp_path):
result = await client.call_tool(
"drawing_scale_check", {"reference": str(tmp_path / "none.dxf")}, raise_on_error=False
)
assert result.is_error is True
assert "reference: no such file" in result.content[0].text
async def test_a_non_positive_floor_is_refused(client, tmp_path):
result = await client.call_tool(
"drawing_scale_check",
{"reference": reference_file(tmp_path), "min_pair_mm": 0.0},
raise_on_error=False,
)
assert result.is_error is True
assert "min_pair_mm" in result.content[0].text
async def test_drawing_scale_check_is_advertised_read_only(client):
tools = {tool.name: tool for tool in await client.list_tools()}
assert tools["drawing_scale_check"].annotations.readOnlyHint is True