Files
autocad-mcp-pro/tests/test_mech_xdata.py
T
Umut CANandClaude Opus 5 2bb8044144 feat(mech): shared foundation - primitives, XDATA codec, standards registry, ISO 128-50 materials
engineering/mech/ opens with the four modules every track-B/G branch imports,
so the four worktrees can be cut from this commit and none of them has to
invent a name the others will conflict with.

- primitives.py: the typed descriptions the pure modules emit (Line, Arc,
  Circle, Poly, HatchArea, Text, DimIntent) with bbox / translate / rotate /
  scale, and ROLE_LAYER - the one place an ISO 128 line role becomes a layer
  name, so no module hardcodes a layer. The arc bbox sweeps its quadrants: a
  0-180 arc reports y=+r, not its endpoints, which is what keeps the view
  layout from stacking two views on top of each other.
- xdata.py: the ACADMCP_MECH codec in Track A's shape - a 1001 application
  marker, 1000 chunks of at most 255 ASCII characters, the 16 KB per-entity
  ceiling enforced before anything is written, and a foreign application, a
  corrupt chunk stream, a wrong version or an unknown kind refused with
  ValueError instead of decoded into something plausible.
- standards/__init__.py: the table registry. A size outside the coverage is
  refused against the coverage; a size inside it that is not a listed row is
  refused against the rows. Nothing is interpolated, an ambiguous variant is
  never resolved by taking the first row, and a registration whose SOURCE does
  not name the table it was transcribed from is refused.
- standards/materials.py: the section-hatch map for nine materials. ISO
  128-50 standardises only the general hatching (narrow continuous lines at 45
  degrees) - that is acadiso.pat ANSI31 and what engineering/gear.py already
  draws for steel. The distinguishing symbols are ASME Y14.2M's section lining
  as shipped in AutoCAD 2026's acadiso.pat, read off the installed file; cast
  iron's departure from ASME and the absence of any mechanical symbol for
  concrete and wood are stated in the rows themselves. Angles are 0.0 because
  the patterns already carry 45 degrees, and the two architectural patterns'
  scales are derived from the ANSI31 line spacing and recomputed by the test
  rather than chosen.

No tool is registered here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 17:04:59 +03:00

86 lines
2.8 KiB
Python

"""The ACADMCP_MECH chunking codec: Track A's shape, one app, 255-char chunks."""
from __future__ import annotations
import pytest
from backends.xdata_specs import encode_values
from engineering.mech.xdata import APP_ID, KINDS, PAYLOAD_VERSION, decode, encode, to_values
PART = {
"v": 1,
"kind": "part",
"id": "1A2B",
"view": "front",
"part": {"name": "shaft", "segments": [{"length": 60.0, "d_outer": 40.0}]},
}
def test_app_id_version_and_kinds_are_the_pinned_ones():
assert APP_ID == "ACADMCP_MECH"
assert PAYLOAD_VERSION == 1
assert KINDS == ("part", "std_part", "balloon")
def test_encode_opens_with_the_app_id_and_chunks_at_255():
rows = encode({"v": 1, "kind": "balloon", "item": 3, "targets": ["1A2B"] * 60})
assert rows[0] == (1001, APP_ID)
assert all(code == 1000 for code, _value in rows[1:])
assert len(rows) > 2, "the payload must actually have been split"
assert all(len(value) <= 255 for _code, value in rows[1:])
def test_roundtrip_is_exact():
assert decode(encode(PART)) == PART
def test_decode_reads_the_bare_value_list_a_backend_returns():
"""`entity_get_xdata` hands back decoded values, not (code, value) rows."""
assert decode(to_values(PART)) == PART
def test_the_version_is_injected_when_absent_and_a_wrong_one_is_refused():
payload = {
"kind": "std_part",
"designation": "ISO 4014 - M12x60",
"standard": "ISO 4014",
"size": "M12x60",
"qty": 1,
"material": "8.8",
}
assert decode(encode(payload))["v"] == PAYLOAD_VERSION
with pytest.raises(ValueError, match="version"):
encode({**payload, "v": 99})
def test_a_foreign_application_is_refused():
with pytest.raises(ValueError, match="ACADMCP_PID"):
decode([(1001, "ACADMCP_PID"), (1000, '{"v":1,"kind":"part"}')])
def test_a_corrupt_payload_is_refused_not_silently_dropped():
with pytest.raises(ValueError, match="not JSON"):
decode([(1001, APP_ID), (1000, '{"v":1,"kind":')])
def test_an_unknown_kind_is_refused_on_both_sides():
with pytest.raises(ValueError, match="kind"):
encode({"v": 1, "kind": "sheet_frame"})
with pytest.raises(ValueError, match="kind"):
decode([(1001, APP_ID), (1000, '{"v":1,"kind":"sheet_frame"}')])
def test_the_16_kb_entity_ceiling_is_enforced_before_writing():
huge = {"v": 1, "kind": "part", "id": "1", "view": "front", "part": {"n": "x" * 20000}}
with pytest.raises(ValueError, match="16 KB"):
encode(huge)
def test_every_chunk_is_a_value_the_backend_will_accept():
"""`entity_set_xdata` runs the values through `encode_values`; ASCII-only
JSON means no line separator can reach a 1000 tag and split the DXF."""
rows = encode(PART)
tags = encode_values(to_values(PART))
assert all(code == 1000 for code, _value in tags)
assert len(tags) == len(rows) - 1