Files
autocad-mcp-pro/tests/test_understand_topology_tool.py
T
Umut CANandClaude Opus 5.5 2b74bf20a6 feat(understand): drawing_topology_check and the three topo_* critique focuses
engineering/understand/topology.py reads a line network (LINE, polylines
with true bulge arcs, ARC) on the chosen layers and reports dangling
ends, near misses (end-end and end-segment within gap, one row per pair)
and interior crossings of two straight segments of one layer. An end on
another network line is joined; an end on any curve of another layer or
inside a block reference's box is attached; a line drawn twice is not a
join, so a doubled pipe still dangles. gap defaults to 0.2 % of the
network diagonal, never under ten tol.

drawing_topology_check joins drawing_diff in SECTION 26: default layers
are those classify_layer calls piping or electrical at >= 0.9, reported
with their classification; no such layer is a refusal, not a pass.

topo_dangling_endpoint (warning), topo_near_miss (error) and
topo_interior_crossing (info) join the closed enum and run under
focus=None, excluding the P&ID and architectural layer sets, so a
pid_from_spec sheet and an arch_plan_from_spec plan get none of them;
a drawing with no candidate layer is never snapshotted.

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

102 lines
3.9 KiB
Python

"""drawing_topology_check over the MCP wire, on the headless engine."""
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
@pytest.fixture(scope="module")
def pipe_layer(tmp_path_factory) -> str:
truth = build_plant_pair(tmp_path_factory.mktemp("plant"))
return next(
name for name, service in sorted(truth["layer_services"].items()) if service == "product"
)
@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 _line(client, layer, x1, y1, x2, y2):
await client.call_tool(
"entity_create_line", {"x1": x1, "y1": y1, "x2": x2, "y2": y2, "layer": layer}
)
async def _total(client) -> int:
stats = (await client.call_tool("analysis_entity_stats", {})).structured_content
return stats["total_entities"]
async def test_the_default_layers_are_the_classified_network_layers(client, pipe_layer):
await _line(client, pipe_layer, 0, 0, 1000, 0)
await _line(client, pipe_layer, 1003, 0, 2003, 0)
before = await _total(client)
result = (await client.call_tool("drawing_topology_check", {})).structured_content
assert result["layers"] == [pipe_layer]
assert [row["layer"] for row in result["classification"]] == [pipe_layer]
assert result["counts"] == {"dangling": 2, "near_miss": 1, "crossing": 0}
assert result["near_miss"][0]["gap"] == pytest.approx(3.0)
assert await _total(client) == before, "the check never writes"
async def test_a_dxf_path_is_read_without_opening_it(client, pipe_layer, tmp_path):
doc = ezdxf.new()
doc.layers.add(pipe_layer)
msp = doc.modelspace()
msp.add_line((0, 0), (1000, 0), dxfattribs={"layer": pipe_layer})
msp.add_line((500, -500), (500, 500), dxfattribs={"layer": pipe_layer})
path = tmp_path / "crossing.dxf"
doc.saveas(path)
result = (
await client.call_tool("drawing_topology_check", {"path": str(path), "gap": 5.0})
).structured_content
assert result["counts"]["crossing"] == 1
assert result["crossing"][0]["at"] == [500.0, 0.0]
assert await _total(client) == 0, "the current document is untouched"
async def test_an_unknown_layer_is_refused_with_the_drawings_layers(client, pipe_layer):
await _line(client, pipe_layer, 0, 0, 10, 0)
result = await client.call_tool(
"drawing_topology_check", {"layers": ["NOPE"]}, raise_on_error=False
)
assert result.is_error is True
assert "layers: 'NOPE' is not a layer of this drawing" in result.content[0].text
async def test_a_drawing_with_no_network_layer_is_refused_not_passed(client):
await _line(client, "GEOMETRY", 0, 0, 10, 0)
result = await client.call_tool("drawing_topology_check", {}, raise_on_error=False)
assert result.is_error is True
assert "no layer of this drawing is classified piping or electrical" in result.content[0].text
async def test_gap_must_exceed_tol(client, pipe_layer):
await _line(client, pipe_layer, 0, 0, 10, 0)
result = await client.call_tool(
"drawing_topology_check", {"gap": 0.005, "tol": 0.01}, raise_on_error=False
)
assert result.is_error is True
assert "gap (0.005) must exceed tol (0.01)" in result.content[0].text
async def test_the_check_is_advertised_read_only(client):
tools = {tool.name: tool for tool in await client.list_tools()}
assert tools["drawing_topology_check"].annotations.readOnlyHint is True