A drawing somebody else made, read back as one report in which every claim carries its evidence and a confidence; the drawing is never modified. - units.py: $INSUNITS against the unit the geometry implies (text heights, wall thickness, door swings, labelled room areas, overall size). The header is overruled only when the evidence outweighs it, and then with a warning that quotes the numbers. Robust extents grow from the quartile box of entity centres; what never joins is an outlier, named by handle. - clusters.py: separate bodies of model-space content by raster, 8-connectivity and box absorption - two copies of one plan are two clusters. - describe.py: layers with drawn length and the vocabulary's meaning (name keyword, else the texts on the layer), equipment tags (one tag lettered twice beside its symbol merged, repeats flagged ambiguous), room labels in the wall-layer faces of engineering.arch.faces, text languages by character class, blocks, layouts, the title block. - server.py opens SECTION 26 (Understanding & QA) with drawing_understand: core, read-only, tagged analysis/query. Alias record, inventory, README release snapshot 242, frozen group sizes (analysis 13). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
83 lines
3.5 KiB
Python
83 lines
3.5 KiB
Python
"""drawing_understand over the MCP wire, on a fresh headless document."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from fastmcp import Client
|
|
|
|
import server
|
|
from tests.fixtures.plant_pair import build_plant_pair
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
@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
|
|
|
|
|
|
async def test_drawing_understand_reads_the_synthetic_layout_file(client, tmp_path):
|
|
truth = build_plant_pair(tmp_path)
|
|
result = await client.call_tool("drawing_understand", {"path": truth["layout"]})
|
|
report = result.structured_content
|
|
assert report["units"]["declared"]["code"] == truth["layout_insunits"]
|
|
assert report["units"]["inferred"] == truth["layout_true_unit"]
|
|
assert report["extents"]["outliers"][0]["handle"] == truth["outlier_handle"]
|
|
assert len(report["clusters"]) == truth["cluster_count"]
|
|
|
|
|
|
async def test_drawing_understand_reads_the_current_document_and_leaves_it_alone(client):
|
|
# A 6400 x 4400 outline on WALL (perimeter 2 x (6400 + 4400) = 21 600) and
|
|
# one tag.
|
|
corners = [(0.0, 0.0), (6400.0, 0.0), (6400.0, 4400.0), (0.0, 4400.0)]
|
|
for (x1, y1), (x2, y2) in zip(corners, corners[1:] + corners[:1], strict=True):
|
|
await client.call_tool(
|
|
"entity_create_line", {"x1": x1, "y1": y1, "x2": x2, "y2": y2, "layer": "WALL"}
|
|
)
|
|
await client.call_tool(
|
|
"entity_create_text",
|
|
{"text": "T4100", "x": 1000.0, "y": 1000.0, "height": 250.0, "layer": "TAGS"},
|
|
)
|
|
before = (await client.call_tool("analysis_entity_stats", {})).structured_content
|
|
report = (await client.call_tool("drawing_understand", {})).structured_content
|
|
after = (await client.call_tool("analysis_entity_stats", {})).structured_content
|
|
assert after == before
|
|
layers = {row["name"]: row for row in report["layers"]}
|
|
assert layers["WALL"]["entities"] == 4
|
|
assert layers["WALL"]["length"] == pytest.approx(21600.0, abs=1e-9)
|
|
assert [t["tag"] for t in report["equipment_tags"]] == ["T4100"]
|
|
|
|
|
|
async def test_a_missing_file_is_refused_by_path(client, tmp_path):
|
|
missing = tmp_path / "nowhere.dxf"
|
|
result = await client.call_tool(
|
|
"drawing_understand", {"path": str(missing)}, raise_on_error=False
|
|
)
|
|
assert result.is_error is True
|
|
assert "path: no such file" in result.content[0].text
|
|
assert "nowhere.dxf" in result.content[0].text
|
|
|
|
|
|
async def test_a_file_the_engine_cannot_parse_is_refused(client, tmp_path):
|
|
# Not a DXF: the snapshot's parse error comes back as a tool error that
|
|
# names the file, never as a traceback.
|
|
garbage = tmp_path / "not_a_drawing.dxf"
|
|
garbage.write_text("this is not a drawing", encoding="utf-8")
|
|
result = await client.call_tool(
|
|
"drawing_understand", {"path": str(garbage)}, raise_on_error=False
|
|
)
|
|
assert result.is_error is True
|
|
assert "not_a_drawing.dxf" in result.content[0].text
|
|
|
|
|
|
async def test_drawing_understand_is_advertised_read_only(client):
|
|
tools = {tool.name: tool for tool in await client.list_tools()}
|
|
assert tools["drawing_understand"].annotations.readOnlyHint is True
|