Files
autocad-mcp-pro/tests/test_mech_standards.py
Umut CANandClaude Opus 5 ac6c4075ba feat(task-3): the feature catalogue and its standards tables
engineering/mech/features.py is the nineteen typed features of spec section 5,
each answering the same four questions - is my placement legal, what do I
contribute to this view, to a cut face, and what dimension do I want - plus the
five structural hooks the view engine needs: removal_box, radial_edit,
corner_edit, outline_edit and omission. Pure arithmetic; a test asserts emitted
primitive tuples, not a screenshot.

Nothing restates a table the repository already owns: Keyway takes its width
and depth from engineering.keyway (DIN 6885-1) and is refused by name outside
that table's bore range; GearTeeth takes its outline from engineering.gear's
involute generator and is refused when its tip diameter disagrees with the
segment it sits on.

standards/threads.py transcribes ISO 261 Table 1 and Table 2 - first-choice
M1.6-M64 plus second-choice M14/M18/M22/M27/M33/M39/M45/M52/M60 - and computes
ISO 724's basic minor and pitch diameters from ISO 68-1's own formula, pinned
against the published values (M20x2.5 -> 17.294 / 18.376). The remaining
second-choice diameters, every third-choice diameter and fine pitches below M8
are not transcribed and are refused by name, never interpolated. The drawing
rules are ISO 6410: minor at 0.8 x major, a thread-length limit line across the
major, a three-quarter circle in the end view, and an internal thread whose
section hatch runs to the major diameter.

standards/{undercuts,rings,centres,orings}.py ship DIN 509, DIN 471/472,
DIN 332-1 and ISO 3601-2 with their SOURCE, their row-key contract and an
**empty** row set. That is deliberate and is the track's table rule taken at its
word: a row that could not be verified against the standard is not shipped,
because a wrong relief depth or groove diameter is a wrong workshop drawing
while a narrow table is merely narrow. Every lookup is refused by name and says
what to transcribe and where; the features accept explicit dimensions so the
geometry is fully exercised, and DIN 332 form R is refused outright rather than
approximated by the straight 60 degree cone of forms A and B. Filling any of
these tables later is a data edit with no code change, and a shape test already
fails any row that lands without a column the drawing code reads.

Three deviations from the plan text, each measured rather than assumed
(the merge agent should keep exactly one copy of the first):

1. standards/__init__.py (Task 1, on main) refused a registration with no rows
   ("registered no rows"), which would have made the four not-transcribed
   tables impossible to register at all - and registering them is what keeps a
   later transcription a data edit. register() now refuses a rows argument that
   is not a dict and accepts an empty one; two tests in tests/test_mech_standards.py
   pin both halves. No existing test asserted the old refusal.

2. ORingGroove's housing kind is carried by `housing`, not `kind`. A dataclass
   field named `kind` overwrites the discriminator _Base.to_dict() writes:
   measured, Probe(id='og').to_dict() == {'kind': 'radial', 'id': 'og'}, so
   feature_from_dict would then refuse its own payload as an unknown feature
   kind and no part carrying an O-ring groove could round-trip through XDATA.
   A new test pins the round trip.

3. The plan's end-chamfer test asserted after == (75.0, 10.0) - the corner
   itself - and before == (72.0, 8.268), a point on neither surface. The real
   edit is before (72.0, 10.0) on the cylinder and after (75.0, 8.268) on the
   end face, consistent with the 45-degree start chamfer the same file pins.
   The test was corrected to the measured geometry; the behaviour is unchanged.

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

108 lines
3.3 KiB
Python

"""The standards registry: one row or a named refusal, never an interpolation."""
from __future__ import annotations
import pytest
import engineering.mech.standards as standards_mod
from engineering.mech.standards import (
Coverage,
coverage_of,
lookup,
register,
source_of,
standards,
)
SOURCE = "TEST 1 (2026), table 1 — fixture rows, not a real standard"
@pytest.fixture(autouse=True)
def _fixture_tables():
"""Swap the process-wide registry for three fixture tables, then restore it."""
saved = dict(standards_mod._REGISTRY)
standards_mod._REGISTRY.clear()
register(
"TEST 1",
{10: {"d": 10.0, "w": 3.0}, 20: {"d": 20.0, "w": 6.0}},
Coverage(10.0, 20.0),
SOURCE,
)
register(
"TEST 2",
{18: ({"form": "E", "r": 0.6}, {"form": "F", "r": 0.8})},
Coverage(18.0, 18.0),
SOURCE,
)
register("TEST 3", {"M12": {"pitch": 1.75}}, Coverage(12.0, 12.0), SOURCE)
yield
standards_mod._REGISTRY.clear()
standards_mod._REGISTRY.update(saved)
def test_registered_standards_are_listed_and_carry_their_source():
assert standards() == ("TEST 1", "TEST 2", "TEST 3")
assert source_of("TEST 1") == SOURCE
assert coverage_of("TEST 1") == Coverage(10.0, 20.0, "mm")
def test_a_listed_size_returns_a_copy_of_its_row():
row = lookup("TEST 1", 10)
assert row == {"d": 10.0, "w": 3.0}
row["w"] = 99.0
assert lookup("TEST 1", 10)["w"] == 3.0
def test_a_size_outside_the_coverage_is_refused_by_name():
with pytest.raises(ValueError, match="TEST 1 covers 10-20 mm; 95 mm is outside the table"):
lookup("TEST 1", 95)
def test_a_size_inside_the_coverage_but_not_in_the_table_is_never_interpolated():
with pytest.raises(ValueError, match="no row for 15"):
lookup("TEST 1", 15)
def test_a_variant_is_selected_by_keyword():
assert lookup("TEST 2", 18, form="E")["r"] == 0.6
assert lookup("TEST 2", 18, form="F")["r"] == 0.8
def test_an_unknown_variant_is_refused_and_an_ambiguous_one_too():
with pytest.raises(ValueError, match="no form='G' row"):
lookup("TEST 2", 18, form="G")
with pytest.raises(ValueError, match="ambiguous"):
lookup("TEST 2", 18)
def test_a_designation_key_is_case_insensitive():
assert lookup("TEST 3", "m12")["pitch"] == 1.75
def test_an_unregistered_standard_names_the_ones_that_exist():
with pytest.raises(ValueError, match="DIN 9999.*TEST 1"):
lookup("DIN 9999", 10)
def test_a_source_that_does_not_name_its_table_is_refused():
with pytest.raises(ValueError, match="SOURCE"):
register("TEST 4", {1: {"a": 1}}, Coverage(1.0, 1.0), "made up")
def test_a_table_may_register_its_structure_before_its_rows_are_transcribed():
"""DIN 509, DIN 471/472, DIN 332-1 and ISO 3601-2 ship exactly this way."""
register("TEST 5", {}, Coverage(0.0, 0.0), SOURCE)
assert "TEST 5" in standards()
with pytest.raises(ValueError, match="TEST 5"):
lookup("TEST 5", 12)
def test_a_rows_argument_that_is_not_a_dict_is_refused():
with pytest.raises(ValueError, match="rows dict"):
register("TEST 6", [{"d": 1.0}], Coverage(1.0, 1.0), SOURCE)
def test_registering_the_same_standard_twice_is_refused():
with pytest.raises(ValueError, match="already registered"):
register("TEST 1", {10: {"d": 10.0}}, Coverage(10.0, 10.0), SOURCE)