feat(task-9): SECTION 22 - std_part_list, std_part_insert, std_feature_draw

engineering/mech/stdparts.py turns the Task 8 tables into real blocks. catalogue()
searches 83 transcribed parts by designation, standard, size or family;
block_spec() emits the typed primitives and DESIG/STD/SIZE/MAT ATTDEFs
block_define takes, in side/top for fasteners and ISO 8826-1 simplified /
ISO 8826-2 detailed for bearings; insert_std_part() defines the block once,
inserts it and writes the std_part ACADMCP_MECH payload the skeleton pins, so
the parts list is a read of the drawing rather than a naming convention.
Threads follow ISO 6410 - minor at 0.8 x major, a thread-length limit line to
the major, a three-quarter minor arc in the end view.

std_feature_draw() adds a thread, a DIN 509 undercut, a DIN 471/472 groove, a
DIN 332 centre hole or an ISO 3601-2 O-ring groove to geometry that already
exists. Its dimensions come from the shared standards registry, never from this
module: an absent table, a size outside coverage or a row missing a dimension is
refused by name, and the DIN 509 relief is drawn from the profile its table
publishes rather than reconstructed here. DIN 332 form R is refused outright.
An unknown designation is refused with the nearest catalogue entries; nothing is
drawn approximately.

SECTION 22 registers the three tools in the new mech pack, with alias records
(INSERT plus shop-floor synonyms in English and Turkish), a regenerated tool
inventory, the README release snapshot at 207 tools in 24 groups, and the frozen
group-size snapshot gaining mech: 3.

Two plan corrections, both verified by running the code:

* the plan's @cad_tool summaries for std_part_insert (118 chars) and
  std_feature_draw (130) overran discovery.serialize.MAX_SUMMARY_CHARS = 110, so
  tests/test_tool_registry.py::test_no_authored_summary_is_long_enough_to_be_truncated
  failed - they would have shipped rendered as "...". Both are shortened to 99
  and 104 characters with the same content.
* the plan's file list omitted tests/test_tool_packs.py, which pins the pack
  registry, the tool_packs report and the group count. It now carries MECH_TOOLS
  and distinguishes ALL_PACKS (declaration order, what "available" reports) from
  ALL_PACKS_ENABLED (sorted, what "enabled" reports) - those two lists stopped
  being the same list when "mech" was appended after "settings".

No new ActiveX member is called: the module drives block_list / block_define /
block_insert / entity_set_xdata / entity_create_* only. The COM path was still
executed live once on AutoCAD 2026 in a scratch document created and closed by
the probe: the bolt block defined with layers CENTER+GEOMETRY, attributes read
back as DESIG/STD/SIZE/MAT = ISO 4014 - M12x60 / ISO 4014 / M12x60 / 8.8, the
ACADMCP_MECH std_part payload round-tripped through entity_get_xdata,
STD_ISO_15_6205_SIMPLIFIED inserted, and the M20 thread's three primitives
landing on GEOMETRY.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-22 19:26:49 +03:00
co-authored by Claude Opus 5
parent 6024723d6b
commit 84c1ef42fb
9 changed files with 1923 additions and 12 deletions
+232
View File
@@ -0,0 +1,232 @@
"""std_feature_draw: the ISO 6410 thread, and grooves drawn from registry rows.
The DIN 471 / DIN 509 / DIN 332 / ISO 3601-2 tables themselves land with Task 3
(group M). These tests register **synthetic** tables under obviously fake
standard names, so what is pinned here is that the feature draws exactly the
dimensions the registry publishes - never a number this module invented.
"""
from __future__ import annotations
import math
import pytest
from engineering.mech.standards import Coverage, register, standards
from engineering.mech.stdparts import (
STD_FEATURE_KINDS,
draw_std_feature,
feature_prims,
feature_spec,
place_prims,
)
@pytest.fixture(scope="module", autouse=True)
def synthetic_tables():
"""Round, obviously synthetic rows - NOT DIN or ISO values."""
if "TEST 471" in standards():
return
register(
"TEST 471",
{30.0: {"m": 2.0, "d2": 27.0}},
Coverage(8.0, 100.0),
"synthetic test fixture - the real DIN 471 table lands with Task 3",
)
register(
"TEST 472",
{30.0: {"m": 2.0, "d2": 33.0}},
Coverage(8.0, 100.0),
"synthetic test fixture - the real DIN 472 table lands with Task 3",
)
register(
"TEST 509",
{30.0: {"profile": [[0.0, 0.0], [0.0, 0.4], [2.0, 0.4], [2.5, 0.0]]}},
Coverage(18.0, 80.0),
"synthetic test fixture - the real DIN 509 table lands with Task 3",
)
register(
"TEST 332 A",
{2.5: {"d1": 2.5, "d2": 5.3, "t": 5.0}},
Coverage(1.0, 10.0),
"synthetic test fixture - the real DIN 332-1 table lands with Task 3",
)
register(
"TEST 332 B",
{2.5: {"d1": 2.5, "d2": 5.3, "d3": 6.3, "t": 6.0}},
Coverage(1.0, 10.0),
"synthetic test fixture - the real DIN 332-1 table lands with Task 3",
)
register(
"TEST 3601",
{3.53: {"b": 4.7, "h": 2.7}},
Coverage(1.0, 10.0),
"synthetic test fixture - the real ISO 3601-2 table lands with Task 3",
)
def test_feature_kinds_are_the_five_the_spec_names():
assert STD_FEATURE_KINDS == (
"thread",
"undercut",
"ring_groove",
"centre_hole",
"oring_groove",
)
with pytest.raises(ValueError, match="oring_groove"):
feature_prims("spline", {})
def test_external_thread_is_the_iso6410_representation():
spec = feature_spec("thread", {"d": 20.0, "length": 30.0, "size": "M20"})
assert spec["dims"]["pitch"] == 2.5 # ISO 261 coarse, from the fastener table
assert "ISO 6410" in spec["source"]
assert spec["prims"] == (
{"type": "line", "x1": 0.0, "y1": 8.0, "x2": 30.0, "y2": 8.0, "layer": "GEOMETRY"},
{"type": "line", "x1": 0.0, "y1": -8.0, "x2": 30.0, "y2": -8.0, "layer": "GEOMETRY"},
{"type": "line", "x1": 30.0, "y1": -10.0, "x2": 30.0, "y2": 10.0, "layer": "GEOMETRY"},
)
def test_internal_thread_draws_the_major_diameter_instead():
prims = feature_prims("thread", {"d": 20.0, "length": 30.0, "pitch": 2.5, "internal": True})
assert prims[0]["y1"] == 10.0 and prims[1]["y1"] == -10.0
def test_a_thread_without_a_size_or_a_pitch_is_refused():
with pytest.raises(ValueError, match="never guessed"):
feature_prims("thread", {"d": 20.0, "length": 30.0})
with pytest.raises(ValueError, match="ISO 261"):
feature_prims("thread", {"d": 14.0, "length": 30.0, "size": "M14"})
def test_ring_groove_is_drawn_from_the_registry_row():
prims = feature_prims("ring_groove", {"d": 30.0, "kind": "shaft", "standard": "TEST 471"})
assert len(prims) == 6
assert prims[0] == {
"type": "line",
"x1": 0.0,
"y1": 15.0,
"x2": 0.0,
"y2": 13.5,
"layer": "GEOMETRY",
}
assert prims[1] == {
"type": "line",
"x1": 0.0,
"y1": 13.5,
"x2": 2.0,
"y2": 13.5,
"layer": "GEOMETRY",
}
assert prims[3]["y1"] == -15.0
bore = feature_prims("ring_groove", {"d": 30.0, "kind": "bore", "standard": "TEST 472"})
assert bore[1]["y1"] == 16.5
def test_ring_groove_refuses_a_row_that_is_the_wrong_way_round():
with pytest.raises(ValueError, match="not a shaft groove"):
feature_prims("ring_groove", {"d": 30.0, "kind": "shaft", "standard": "TEST 472"})
with pytest.raises(ValueError, match="DIN 471"):
feature_prims("ring_groove", {"d": 30.0, "kind": "shaft"})
def test_oring_groove_is_a_rectangular_groove_from_the_row():
prims = feature_prims(
"oring_groove", {"d": 30.0, "cord": 3.53, "kind": "shaft", "standard": "TEST 3601"}
)
assert len(prims) == 6
assert prims[0]["y1"] == 15.0 and prims[0]["y2"] == pytest.approx(12.3)
assert prims[1]["x2"] == pytest.approx(4.7)
def test_centre_hole_form_a_is_a_sixty_degree_countersink():
spec = feature_spec("centre_hole", {"size": 2.5, "form": "A", "standard": "TEST 332 A"})
cone = math.sqrt(3.0) * (5.3 - 2.5) / 2.0
assert spec["dims"]["cone"] == pytest.approx(cone)
prims = spec["prims"]
assert len(prims) == 5
assert prims[0] == {
"type": "line",
"x1": 0.0,
"y1": 2.65,
"x2": pytest.approx(cone),
"y2": 1.25,
"layer": "GEOMETRY",
}
assert prims[2]["x2"] == 5.0 and prims[2]["y1"] == 1.25
assert prims[4] == {
"type": "line",
"x1": 5.0,
"y1": -1.25,
"x2": 5.0,
"y2": 1.25,
"layer": "GEOMETRY",
}
def test_centre_hole_form_b_adds_the_protecting_chamfer():
prims = feature_prims("centre_hole", {"size": 2.5, "form": "B", "standard": "TEST 332 B"})
chamfer = (6.3 - 5.3) / (2.0 * math.sqrt(3.0))
assert len(prims) == 7
assert prims[0]["y1"] == 3.15 # d3 / 2
assert prims[0]["x2"] == pytest.approx(chamfer)
def test_centre_hole_form_r_is_refused_not_approximated():
with pytest.raises(ValueError, match="forms A and B"):
feature_prims("centre_hole", {"size": 2.5, "form": "R", "standard": "TEST 332 A"})
def test_undercut_draws_the_profile_the_table_publishes():
prims = feature_prims("undercut", {"d": 30.0, "form": "E", "standard": "TEST 509"})
assert len(prims) == 2
assert prims[0] == {
"type": "polyline",
"points": [[0.0, 15.0], [0.0, 14.6], [2.0, 14.6], [2.5, 15.0]],
"closed": False,
"layer": "GEOMETRY",
}
assert prims[1]["points"][1] == [0.0, -14.6]
def test_undercut_refuses_a_row_without_a_profile():
with pytest.raises(ValueError, match="carries no profile"):
feature_prims("undercut", {"d": 30.0, "form": "E", "standard": "TEST 471"})
with pytest.raises(ValueError, match="DIN 509"):
feature_prims("undercut", {"d": 30.0, "form": "E"})
def test_place_prims_rotates_about_the_insertion_point():
prims = feature_prims("thread", {"d": 20.0, "length": 30.0, "pitch": 2.5})
placed = place_prims(prims, (100.0, 50.0), 90.0)
assert placed[0]["x1"] == pytest.approx(92.0)
assert placed[0]["y1"] == pytest.approx(50.0)
assert placed[0]["x2"] == pytest.approx(92.0)
assert placed[0]["y2"] == pytest.approx(80.0)
with pytest.raises(ValueError, match="finite"):
place_prims(prims, (float("inf"), 0.0))
@pytest.mark.asyncio
async def test_draw_std_feature_writes_the_primitives(backend):
result = await draw_std_feature(
backend, "thread", (100.0, 50.0), {"d": 20.0, "length": 30.0, "size": "M20"}
)
assert result["ok"] is True
assert result["primitive_count"] == 3
assert len(result["handles"]) == 3
assert result["layers"] == ["GEOMETRY"]
assert result["dims"]["pitch"] == 2.5
info = await backend.entity_get(result["handles"][0])
assert info.layer == "GEOMETRY"
@pytest.mark.asyncio
async def test_draw_std_feature_refuses_before_writing_anything(backend):
before = len(await backend.entity_list())
with pytest.raises(ValueError, match="never guessed"):
await draw_std_feature(backend, "thread", (0.0, 0.0), {"d": 20.0, "length": 30.0})
with pytest.raises(ValueError, match="DIN 509"):
await draw_std_feature(backend, "undercut", (0.0, 0.0), {"d": 30.0})
assert len(await backend.entity_list()) == before
+333
View File
@@ -0,0 +1,333 @@
"""The standard-parts catalogue: search, designations, block specs, insertion.
Geometry is asserted as emitted primitive dicts - counts, layers, coordinates -
never as a screenshot.
"""
from __future__ import annotations
import math
import pytest
from backends.block_specs import validate_attdef_specs, validate_entity_specs
from engineering.mech.stdparts import (
FAMILY_STANDARDS,
FAMILY_VIEWS,
THREAD_MINOR_RATIO,
block_spec,
catalogue,
parse_designation,
)
R12 = 2.0 * 18.0 / math.sqrt(3.0) / 2.0 # across-corners radius of an M12 hex head
MINOR12 = THREAD_MINOR_RATIO * 12.0 / 2.0 # ISO 6410 minor radius of an M12 thread
def test_the_iso6410_minor_radius_is_four_point_eight():
"""Pinned as the builder computes it: 0.8 * 12 / 2 is 4.800000000000001 in binary."""
assert MINOR12 == pytest.approx(4.8)
def test_the_catalogue_holds_every_transcribed_size():
rows = catalogue()
assert len(rows) == 83 # 5 fastener standards x 10 sizes + 33 bearings
assert len(catalogue(standard="ISO 15")) == 33
assert len(catalogue("M12")) == 5
assert len(catalogue("6205")) == 1
assert {row["standard"] for row in rows} == set(FAMILY_STANDARDS)
def test_a_catalogue_row_carries_what_a_drawing_needs():
(row,) = catalogue("6205")
assert row["designation"] == "ISO 15 - 6205"
assert row["standard"] == "ISO 15"
assert row["family"] == "deep_groove_bearing"
assert row["views"] == ["simplified", "detailed"]
assert row["length_required"] is False
assert row["dims"] == {"d": 25.0, "D": 52.0, "B": 15.0}
assert "ISO 15:2017" in row["source"]
bolt = next(r for r in catalogue(standard="ISO 4014") if r["size"] == "M12")
assert bolt["length_required"] is True
assert bolt["dims"]["s"] == 18.0 and bolt["dims"]["k"] == 7.5
def test_catalogue_refuses_an_unknown_standard():
with pytest.raises(ValueError, match="ISO 4014"):
catalogue(standard="DIN 931")
def test_parse_designation_normalises_and_splits():
assert parse_designation("iso4014 - m12x60") == {
"designation": "ISO 4014 - M12x60",
"standard": "ISO 4014",
"family": "hex_bolt",
"size": "M12x60",
"thread": "M12",
"length": 60.0,
}
assert parse_designation("6205")["designation"] == "ISO 15 - 6205"
assert parse_designation("ISO 4032 - M12")["length"] is None
def test_parse_designation_refuses_a_missing_or_stray_length():
with pytest.raises(ValueError, match="nominal length"):
parse_designation("ISO 4014 - M12")
with pytest.raises(ValueError, match="takes no length"):
parse_designation("ISO 4032 - M12x60")
with pytest.raises(ValueError, match="catalogue standard"):
parse_designation("ISO 9999 - M12")
def test_an_unknown_size_is_refused_with_the_nearest_entries():
with pytest.raises(ValueError, match=r"ISO 4014 - M12"):
block_spec("ISO 4014 - M13x60", "side")
with pytest.raises(ValueError, match=r"ISO 15 - 6210"):
block_spec("ISO 15 - 6211", "simplified")
with pytest.raises(ValueError, match="approximately"):
block_spec("ISO 4014 - M13x60", "side")
def test_hex_bolt_side_view_is_the_iso6410_representation():
spec = block_spec("ISO 4014 - M12x60", "side")
assert spec["name"] == "STD_ISO_4014_M12X60_SIDE"
assert spec["view"] == "side"
assert spec["dims"]["b"] == pytest.approx(30.0) # 2d + 6 for l <= 125
ents = spec["entities"]
assert len(ents) == 10
assert ents[0]["type"] == "polyline" and ents[0]["closed"] is True
assert ents[0]["points"] == [
[-7.5, -R12],
[0.0, -R12],
[0.0, R12],
[-7.5, R12],
]
assert ents[3] == {
"type": "line",
"x1": 0.0,
"y1": 6.0,
"x2": 60.0,
"y2": 6.0,
"layer": "GEOMETRY",
}
# ISO 6410: minor diameter at 0.8 x major, over the useful thread length.
assert ents[6] == {
"type": "line",
"x1": 30.0,
"y1": MINOR12,
"x2": 60.0,
"y2": MINOR12,
"layer": "GEOMETRY",
}
# ISO 6410: the thread-length limit is a line to the major diameter.
assert ents[8] == {
"type": "line",
"x1": 30.0,
"y1": -6.0,
"x2": 30.0,
"y2": 6.0,
"layer": "GEOMETRY",
}
assert ents[9]["layer"] == "CENTER"
assert spec["bbox"] == pytest.approx((-10.5, -R12, 63.0, R12))
def test_a_fully_threaded_screw_has_no_thread_limit_line():
spec = block_spec("ISO 4017 - M12x60", "side")
assert len(spec["entities"]) == 9
assert spec["dims"]["b"] == pytest.approx(60.0)
minor = [e for e in spec["entities"] if e.get("y1") == MINOR12]
assert minor[0]["x1"] == 0.0 and minor[0]["x2"] == 60.0
def test_hex_top_view_uses_the_three_quarter_minor_arc():
spec = block_spec("ISO 4014 - M12x60", "top")
ents = spec["entities"]
assert len(ents) == 5
assert ents[0]["type"] == "polyline" and len(ents[0]["points"]) == 6
assert ents[1] == {"type": "circle", "cx": 0.0, "cy": 0.0, "r": 6.0, "layer": "GEOMETRY"}
assert ents[2] == {
"type": "arc",
"cx": 0.0,
"cy": 0.0,
"r": MINOR12,
"start_deg": 90.0,
"end_deg": 360.0,
"layer": "GEOMETRY",
}
assert ents[3]["layer"] == "CENTER" and ents[4]["layer"] == "CENTER"
def test_hex_nut_side_view_shows_the_thread_as_hidden_lines():
spec = block_spec("ISO 4032 - M12", "side")
ents = spec["entities"]
assert len(ents) == 8
hidden = [e for e in ents if e["layer"] == "HIDDEN"]
assert len(hidden) == 4
assert {abs(e["y1"]) for e in hidden} == {6.0, MINOR12}
assert ents[0]["points"][1] == [10.8, -R12] # nut height m
def test_washer_views():
side = block_spec("ISO 7089 - M12", "side")
assert len(side["entities"]) == 3
assert side["entities"][0]["points"] == [[0.0, 6.5], [2.5, 6.5], [2.5, 12.0], [0.0, 12.0]]
top = block_spec("ISO 7089 - M12", "top")
assert top["entities"][0] == {
"type": "circle",
"cx": 0.0,
"cy": 0.0,
"r": 12.0,
"layer": "GEOMETRY",
}
assert top["entities"][1]["r"] == 6.5
def test_socket_head_screw_views():
side = block_spec("ISO 4762 - M8x30", "side")
assert len(side["entities"]) == 8
assert side["dims"]["b"] == pytest.approx(28.0)
assert side["entities"][0]["points"][0] == [-8.0, -6.5] # k = 8, dk/2 = 6.5
top = block_spec("ISO 4762 - M8x30", "top")
assert top["entities"][0]["r"] == 6.5
assert len(top["entities"][1]["points"]) == 6 # the hexagon socket, s = 6
def test_bearing_simplified_is_the_iso8826_1_outline_and_cross():
spec = block_spec("ISO 15 - 6205", "simplified")
ents = spec["entities"]
assert len(ents) == 7
assert ents[0]["points"] == [[-7.5, 12.5], [7.5, 12.5], [7.5, 26.0], [-7.5, 26.0]]
assert ents[1]["points"] == [[-7.5, -12.5], [7.5, -12.5], [7.5, -26.0], [-7.5, -26.0]]
assert ents[2] == {
"type": "line",
"x1": -4.5,
"y1": 19.25,
"x2": 4.5,
"y2": 19.25,
"layer": "GEOMETRY",
}
assert ents[3] == {
"type": "line",
"x1": 0.0,
"y1": 15.2,
"x2": 0.0,
"y2": 23.3,
"layer": "GEOMETRY",
}
assert ents[6]["layer"] == "CENTER"
def test_bearing_detailed_draws_the_element_symbol():
spec = block_spec("ISO 15 - 6205", "detailed")
ents = spec["entities"]
assert len(ents) == 5
assert ents[2] == {
"type": "circle",
"cx": 0.0,
"cy": 19.25,
"r": 4.05,
"layer": "GEOMETRY",
}
assert ents[3]["cy"] == -19.25
def test_a_view_the_family_does_not_have_is_refused_and_side_falls_back():
assert block_spec("ISO 15 - 6205", "side")["view"] == "simplified"
with pytest.raises(ValueError, match="simplified"):
block_spec("ISO 15 - 6205", "top")
with pytest.raises(ValueError, match="side"):
block_spec("ISO 4014 - M12x60", "simplified")
@pytest.mark.parametrize(
("designation", "view"),
[
("ISO 4014 - M12x60", "side"),
("ISO 4014 - M12x60", "top"),
("ISO 4017 - M12x60", "side"),
("ISO 4032 - M12", "side"),
("ISO 4032 - M12", "top"),
("ISO 7089 - M12", "side"),
("ISO 7089 - M12", "top"),
("ISO 4762 - M8x30", "side"),
("ISO 4762 - M8x30", "top"),
("ISO 15 - 6205", "simplified"),
("ISO 15 - 6205", "detailed"),
],
)
def test_every_block_spec_validates_against_block_define(designation, view):
spec = block_spec(designation, view)
validate_entity_specs(spec["entities"])
validate_attdef_specs(spec["attdefs"])
assert [att["tag"] for att in spec["attdefs"]] == ["DESIG", "STD", "SIZE", "MAT"]
assert spec["attdefs"][0]["default"] == spec["designation"]
assert spec["attdefs"][0]["invisible"] is False
assert all(att["invisible"] for att in spec["attdefs"][1:])
assert spec["bbox"][0] < spec["bbox"][2] and spec["bbox"][1] < spec["bbox"][3]
assert set(FAMILY_VIEWS[spec["family"]]) >= {spec["view"]}
@pytest.mark.asyncio
async def test_insert_defines_the_block_once_and_writes_the_payload(backend):
from engineering.mech.stdparts import insert_std_part
from engineering.mech.xdata import APP_ID, decode
result = await insert_std_part(backend, "ISO 4014 - M12x60", at=(100.0, 50.0), material="8.8")
assert result["ok"] is True
assert result["block_name"] == "STD_ISO_4014_M12X60_SIDE"
assert result["defined"] is True
assert sorted(result["layers_created"]) == ["CENTER", "GEOMETRY"]
assert result["view"] == "side"
assert result["layer"] == "GEOMETRY"
assert result["dims"]["s"] == 18.0
stored = await backend.entity_get_xdata(result["handle"], APP_ID)
payload = decode([(1001, APP_ID)] + [(1000, value) for value in stored["xdata"][APP_ID]])
assert payload == {
"v": 1,
"kind": "std_part",
"designation": "ISO 4014 - M12x60",
"standard": "ISO 4014",
"size": "M12x60",
"qty": 1,
"material": "8.8",
}
assert await backend.block_get_attributes(result["handle"]) == {
"DESIG": "ISO 4014 - M12x60",
"STD": "ISO 4014",
"SIZE": "M12x60",
"MAT": "8.8",
}
again = await insert_std_part(backend, "ISO 4014 - M12x60", at=(200.0, 50.0))
assert again["defined"] is False and again["layers_created"] == []
assert again["payload"]["material"] == ""
@pytest.mark.asyncio
async def test_bearing_inserts_with_its_own_view_name(backend):
from engineering.mech.stdparts import insert_std_part
result = await insert_std_part(backend, "6205", at=(0.0, 0.0), rotation=90.0)
assert result["block_name"] == "STD_ISO_15_6205_SIMPLIFIED"
assert result["view"] == "simplified"
assert result["rotation"] == 90.0
assert result["dims"]["B"] == 15.0
@pytest.mark.asyncio
async def test_insert_refuses_before_writing_anything(backend):
from engineering.mech.stdparts import insert_std_part
before = len(await backend.entity_list())
with pytest.raises(ValueError, match="Nearest entries"):
await insert_std_part(backend, "ISO 4014 - M13x60", at=(0.0, 0.0))
with pytest.raises(ValueError, match="finite"):
await insert_std_part(backend, "ISO 4014 - M12x60", at=(float("nan"), 0.0))
with pytest.raises(ValueError, match="no attribute"):
await insert_std_part(
backend, "ISO 4014 - M12x60", at=(0.0, 0.0), attributes={"TORQUE": "80"}
)
assert len(await backend.entity_list()) == before
+22 -5
View File
@@ -5,6 +5,8 @@ v1.6 track E adds the `settings` pack: the 22 SECTION 20 environment tools.
Styles (SECTION 18) and page setup / templates (SECTION 19) are drafting
essentials and stay in `core`, so `TOOL_PACKS=core` hides the environment
surface and nothing else.
v1.6 tracks B+G add the `mech` pack: SECTION 22's three standard-parts tools.
"""
from __future__ import annotations
@@ -78,6 +80,12 @@ SETTINGS_TOOLS = {
"system_prompt_message",
}
MECH_TOOLS = {
"std_part_list",
"std_part_insert",
"std_feature_draw",
}
LEAN_SETTINGS_ESSENTIALS = {
"drawing_apply_standard",
"page_setup_apply",
@@ -86,7 +94,10 @@ LEAN_SETTINGS_ESSENTIALS = {
"textstyle_set_current",
}
ALL_PACKS = ["core", "pid", "settings"]
#: Declaration order - what `tool_packs["available"]` reports.
ALL_PACKS = ["core", "pid", "settings", "mech"]
#: `tool_packs["enabled"]` is sorted, which is no longer the same list.
ALL_PACKS_ENABLED = sorted(ALL_PACKS)
@pytest_asyncio.fixture(autouse=True)
@@ -97,11 +108,15 @@ async def _restore(monkeypatch):
def test_pack_registry_names_real_tools_and_only_them():
assert server.TOOL_PACK_NAMES == ("core", "pid", "settings")
assert server.TOOL_PACK_NAMES == ("core", "pid", "settings", "mech")
assert server.PACK_TOOL_NAMES["pid"] == frozenset(PID_TOOLS)
assert server.PACK_TOOL_NAMES["settings"] == frozenset(SETTINGS_TOOLS)
assert server.PACK_TOOL_NAMES["mech"] == frozenset(MECH_TOOLS)
assert len(SETTINGS_TOOLS) == 22
assert len(MECH_TOOLS) == 3
assert not (server.PACK_TOOL_NAMES["settings"] & server.PACK_TOOL_NAMES["pid"])
assert not (server.PACK_TOOL_NAMES["mech"] & server.PACK_TOOL_NAMES["pid"])
assert not (server.PACK_TOOL_NAMES["mech"] & server.PACK_TOOL_NAMES["settings"])
async def test_every_pack_member_is_a_registered_tool():
@@ -117,6 +132,7 @@ async def test_core_only_hides_pid_and_settings_but_keeps_styles_and_page_setup(
disabled = set(info["disabled_tools"])
assert PID_TOOLS <= disabled
assert SETTINGS_TOOLS <= disabled
assert MECH_TOOLS <= disabled
assert not (STYLE_TOOLS & disabled), "styles are core drafting essentials"
assert not (PAGE_SETUP_TOOLS & disabled), "page setup and templates are core"
assert "block_define" not in disabled and "entity_set_xdata" not in disabled
@@ -136,8 +152,8 @@ async def test_all_is_the_default_and_enables_every_pack(monkeypatch):
monkeypatch.setattr(config.settings, "tool_packs", "all")
info = await server._apply_tool_profile("full")
disabled = set(info["disabled_tools"])
assert not ((PID_TOOLS | SETTINGS_TOOLS) & disabled)
assert info["tool_packs"]["enabled"] == ALL_PACKS
assert not ((PID_TOOLS | SETTINGS_TOOLS | MECH_TOOLS) & disabled)
assert info["tool_packs"]["enabled"] == ALL_PACKS_ENABLED
async def test_unknown_pack_is_ignored_with_a_warning_and_core_cannot_be_dropped(
@@ -184,7 +200,8 @@ async def test_the_three_track_e_sections_file_under_their_own_groups():
assert set(groups["styles"]) == STYLE_TOOLS
assert set(groups["page_setup"]) == PAGE_SETUP_TOOLS
assert set(groups["environment"]) == SETTINGS_TOOLS
assert len(groups) == 23
assert set(groups["mech"]) == MECH_TOOLS
assert len(groups) == 24
async def test_system_about_reports_packs():
+9 -1
View File
@@ -529,6 +529,13 @@ async def test_tool_group_sizes_are_unchanged():
their secondary tags; those counts returned to their pre-track-E values
when the section tags landed.) Every other number here has been unchanged
since the snapshot was taken.
`mech` appeared (0 -> 3) when v1.6's SECTION 22 opened with the
standard-parts catalogue (`std_part_list` / `std_part_insert` /
`std_feature_draw`); the `mech` tag is ranked directly after `pid` in
`_GROUP_TAG_PRIORITY`, so those tools file under `mech` rather than under
their secondary `query` / `create` tags. This snapshot is branch-local:
the merge task recomputes it once all four track B+G branches have landed.
"""
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
assert sizes == {
@@ -545,6 +552,7 @@ async def test_tool_group_sizes_are_unchanged():
"environment": 22,
"layers": 14,
"layouts": 12,
"mech": 3,
"page_setup": 6,
"pid": 9,
"premium": 12,
@@ -556,4 +564,4 @@ async def test_tool_group_sizes_are_unchanged():
"validation": 1,
"view": 4,
}
assert sum(sizes.values()) == 204
assert sum(sizes.values()) == 207