Files
autocad-mcp-pro/tests/test_tool_packs.py
T
Umut CANandClaude Opus 5.5 3b033033f9 feat(mech): six mech_* critique focuses, the mech pack, lean 60, aliases and the counts
The four track-B/G branches are merged; this is the integration that makes the
25 new tools one surface instead of four.

- engineering/mech/critique.py: the six focuses of spec 13, dispatched the way
  the P&ID ones are. One index per critique run, shared through run_critique's
  context, so six focuses cost one read of the drawing; a drawing with no
  ACADMCP_MECH payload yields nothing at all, which is what keeps focus=None
  cheap for a P&ID sheet or a plain DXF. Each focus has a positive and a
  negative fixture.
  Two narrowings are in the module docstring rather than hidden:
  mech_view_misaligned checks the projection axis and not the quadrant,
  because the pinned payload carries no projection angle to check it against,
  and mech_duplicate_dimension catches two dimensions in the same place rather
  than a semantic restatement, because no engine exposes what a dimension
  measured.
- The index reads what the tools actually write, not only the plan's fixture
  shape. mech_part_draw anchors its part payload on a POINT (layer MECH) and
  lists every drawn view under "views"; the plan's index read INSERT/CIRCLE
  only and one "view" per payload, so it was silent on every part the server
  draws. It now reads POINT too, expands "views" into one entry per drawn view
  (the section check looks in the section's own WCS box, the alignment check
  compares frame_bbox centres), and pages entity_list instead of one call.
  Test: a part drawn by draw_part + add_view is read and passes all six; the
  same part with its hatch erased is reported.
- Live type names, measured on AutoCAD 2026 (2026-09-23, scratch document,
  closed): ObjectName AcDbBlockReference -> BLOCKREFERENCE, AcDbRotatedDimension
  -> ROTATEDDIMENSION, AcDbAlignedDimension -> ALIGNEDDIMENSION, AcDbPoint ->
  POINT with GetBoundingBox = the point. The index reads BLOCKREFERENCE as an
  INSERT and any *DIMENSION as a dimension; a wrapper test reports the ezdxf
  entities under exactly those measured names.
- mech_thread_unrepresented uses the view engine's own MINOR_RATIO, and checks
  the thin line ISO 6410 draws: the minor diameter of an external thread, the
  major of an internal one.
- mech_bom_balloon_mismatch: identical parts share one balloon (ISO 6433
  balloons a parts-list row), so a second M12x60 is covered by the first one's
  balloon instead of reported.
- TOOL_PACK_NAMES has "mech" (the 14 tools of SECTIONS 21-23). SECTION 24
  stays core: a frame, a title block, a parts list and an xref are universal
  drafting, not a vertical. Lean gains five and reaches 60 (54 with
  TOOL_PACKS=core).
- _GROUP_TAG_PRIORITY gains sheet directly after mech; the mech group label
  becomes "mechanical". SECTION 24's last four tools (xref_attach,
  xref_manage, image_attach, drawing_export_dwg) now carry the sheet tag, so
  group, pack and SECTION are one list: 229 tools in 25 groups, and
  engineering/blocks/entity_creation/drawing return to their pre-SECTION-24
  sizes.
- Ten golden tuning cases, two of them Turkish (with the diacritic spellings
  added to the alias records next to the ASCII-folded ones). The shared
  AutoCAD command set needed no change: the merge had already resolved TABLE,
  REVCLOUD, SECTIONPLANE, INSERT, ARRAY and SAVEAS, and nothing is stale.
- Inventory regenerated, the frozen group-size snapshot retaken from the live
  registry, and the README release-snapshot / pack / lean figures taken from
  scripts/check_doc_numbers.py rather than typed.

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

258 lines
8.4 KiB
Python

# tests/test_tool_packs.py
"""TOOL_PACKS: advertise only the vertical packs a client uses; core is always on.
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 21's six part-model tools and
SECTION 22's three standard-parts tools and SECTION 23's five annotation
symbols - 14 in all. SECTION 24 (the sheet: frames, title blocks, revisions,
the parts list, balloons, xrefs, images, DWG) stays in `core` - universal
drafting, not a vertical - and files under its own `sheet` group.
"""
from __future__ import annotations
import logging
import pytest
import pytest_asyncio
import config
import server
pytestmark = pytest.mark.asyncio
PID_TOOLS = {
"pid_symbol_list",
"pid_symbol_insert",
"pid_line_draw",
"pid_tag_parse",
"pid_graph",
"pid_instrument_index",
"pid_line_list",
"pid_equipment_list",
"pid_from_spec",
}
STYLE_TOOLS = {
"dimstyle_list",
"dimstyle_create",
"dimstyle_modify",
"dimstyle_set_current",
"textstyle_list",
"textstyle_create",
"textstyle_set_current",
"mleaderstyle_list",
"mleaderstyle_create",
"drawing_apply_standard",
}
PAGE_SETUP_TOOLS = {
"page_setup_list",
"page_setup_apply",
"plot_style_list",
"batch_plot",
"drawing_template_list",
"drawing_template_save",
}
SETTINGS_TOOLS = {
"document_list",
"document_activate",
"document_close",
"layer_state_save",
"layer_state_restore",
"layer_state_list",
"layer_state_delete",
"view_named_save",
"view_named_restore",
"view_named_list",
"ucs_list",
"ucs_set",
"ucs_restore",
"system_launch",
"system_preferences_get",
"system_preferences_set",
"drawing_properties_get",
"drawing_properties_set",
"system_variable_describe",
"user_pick_point",
"user_select",
"system_prompt_message",
}
MECH_TOOLS = {
"mech_part_draw",
"mech_view_add",
"mech_dimension_part",
"mech_hole_pattern",
"mech_part_from_spec",
"mech_part_inspect",
"std_part_list",
"std_part_insert",
"std_feature_draw",
"surface_texture",
"weld_symbol",
"centre_marks",
"section_line",
"hatch_material",
}
SHEET_TOOLS = {
"sheet_frame",
"titleblock_apply",
"revision_add",
"bom_extract",
"bom_table",
"balloon_add",
"data_extract",
"xref_attach",
"xref_manage",
"image_attach",
"drawing_export_dwg",
}
LEAN_MECH_ESSENTIALS = {
"mech_part_draw",
"mech_view_add",
"mech_dimension_part",
"sheet_frame",
"titleblock_apply",
}
LEAN_SETTINGS_ESSENTIALS = {
"drawing_apply_standard",
"page_setup_apply",
"batch_plot",
"dimstyle_set_current",
"textstyle_set_current",
}
#: 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)
async def _restore(monkeypatch):
yield
monkeypatch.setattr(config.settings, "tool_packs", "all")
await server._apply_tool_profile("full")
def test_pack_registry_names_real_tools_and_only_them():
assert server.TOOL_PACK_NAMES == ("core", "pid", "settings", "mech")
assert server.PACK_TOOL_NAMES["pid"] == frozenset(PID_TOOLS)
assert server.PACK_TOOL_NAMES["mech"] == frozenset(MECH_TOOLS)
assert server.PACK_TOOL_NAMES["settings"] == frozenset(SETTINGS_TOOLS)
assert len(SETTINGS_TOOLS) == 22
assert len(MECH_TOOLS) == 14
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"])
assert not (SHEET_TOOLS & set().union(*server.PACK_TOOL_NAMES.values())), (
"SECTION 24 is core drafting, not a vertical pack"
)
async def test_every_pack_member_is_a_registered_tool():
registered = {tool.name for tool in await server._registered_tools()}
for pack, names in server.PACK_TOOL_NAMES.items():
missing = names - registered
assert not missing, f"pack {pack!r} names unregistered tools: {sorted(missing)}"
async def test_core_only_hides_pid_and_settings_but_keeps_styles_and_page_setup(monkeypatch):
monkeypatch.setattr(config.settings, "tool_packs", "core")
info = await server._apply_tool_profile("full")
disabled = set(info["disabled_tools"])
assert PID_TOOLS <= disabled
assert SETTINGS_TOOLS <= disabled
assert MECH_TOOLS <= disabled
assert not (SHEET_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
assert info["tool_packs"] == {"enabled": ["core"], "available": ALL_PACKS, "ignored": []}
async def test_settings_pack_alone_keeps_pid_hidden(monkeypatch):
monkeypatch.setattr(config.settings, "tool_packs", "settings")
info = await server._apply_tool_profile("full")
disabled = set(info["disabled_tools"])
assert PID_TOOLS <= disabled
assert not (SETTINGS_TOOLS & disabled)
assert info["tool_packs"]["enabled"] == ["core", "settings"]
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 | 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(
monkeypatch, caplog
):
monkeypatch.setattr(config.settings, "tool_packs", "pid,mech3000")
with caplog.at_level(logging.WARNING, logger="autocad_mcp"):
info = await server._apply_tool_profile("full")
assert info["tool_packs"] == {
"enabled": ["core", "pid"],
"available": ALL_PACKS,
"ignored": ["mech3000"],
}
assert "mech3000" in caplog.text
async def test_lean_intersects_with_packs(monkeypatch):
assert {"pid_symbol_insert", "pid_line_draw", "pid_graph"} <= server.LEAN_TOOL_NAMES
monkeypatch.setattr(config.settings, "tool_packs", "core")
info = await server._apply_tool_profile("lean")
assert "pid_graph" in info["disabled_tools"]
assert "entity_create_line" not in info["disabled_tools"]
async def test_lean_carries_the_five_settings_essentials_and_nothing_from_the_settings_pack(
monkeypatch,
):
"""Spec §8.2: lean = 60 (55 + the five mechanical/sheet essentials). The
five settings essentials are core-pack tools, so `TOOL_PACKS=core` leaves
them on a lean surface; no environment tool is lean."""
assert LEAN_SETTINGS_ESSENTIALS <= server.LEAN_TOOL_NAMES
assert LEAN_MECH_ESSENTIALS <= server.LEAN_TOOL_NAMES
assert not (SETTINGS_TOOLS & server.LEAN_TOOL_NAMES)
assert len(server.LEAN_TOOL_NAMES) == 60
monkeypatch.setattr(config.settings, "tool_packs", "core")
info = await server._apply_tool_profile("lean")
disabled = set(info["disabled_tools"])
assert not (LEAN_SETTINGS_ESSENTIALS & disabled)
assert not ({"sheet_frame", "titleblock_apply"} & disabled), "sheet tools are core"
assert info["enabled_count"] == 60 - 6, "lean minus the three pid and three mech tools"
async def test_the_three_track_e_sections_file_under_their_own_groups():
"""Pack, group and SECTION are the same list, so `system_about` and
`docs/tool-inventory.json` describe the surface the packs gate."""
groups = await server._tool_groups()
assert set(groups["styles"]) == STYLE_TOOLS
assert set(groups["page_setup"]) == PAGE_SETUP_TOOLS
assert set(groups["environment"]) == SETTINGS_TOOLS
assert set(groups["mechanical"]) == MECH_TOOLS
assert set(groups["sheet"]) == SHEET_TOOLS
assert len(groups) == 25
async def test_system_about_reports_packs():
from fastmcp import Client
async with Client(server.mcp) as client:
about = (await client.call_tool("system_about", {})).structured_content
assert about["tool_packs"]["available"] == ALL_PACKS