engineering/mech/draw.py is the only module in the package that touches a backend: it turns the pure engine's primitive descriptions into entity_create_* and dimension_* calls, places them in WCS, and writes the part model back onto the drawing as ACADMCP_MECH XDATA on an anchor POINT. It is not a contract member and neither are the tools - exactly like gear_draw_* and titleblock_apply_iso_a3, it composes the generic members, so it is written once and works on both engines by construction and no capability key is needed. No new ActiveX call was written, so there was nothing to probe on the live seat; measured instead that every one of the 23 backend members draw.py calls exists on ComBackend and that ComBackend has no abstract methods left. mech_part_draw takes one model - a turned profile or a plate outline plus typed features - and draws its views in first-angle positions (third on request), dimensions them and records the placement. mech_view_add reads the model back out of the drawing and adds a side, top, section (full/half/offset/revolved) or detail view months later without the caller re-describing the part. mech_dimension_part lays out ISO 129 dimensions with no overlapping text, resolves ISO 286 fits against the measured nominal, and moves the holes into a table past a threshold. mech_hole_pattern exposes the same expansion for a drawing that was never built from the model - with its own diameter guard, because Hole.validate needs a part and a standalone pattern has none. mech_part_inspect reads the payload back and never modifies the drawing. mech_part_from_spec draws a whole sheet inside one transaction. Every part is validated before the transaction opens, so a refusal leaves the drawing untouched and names the path (parts[1]: segments[0].d_outer: ...), and the rollback is shielded with the shape engineering/pid/spec.py documents: anyio cancellation is level-triggered, so a bare await in the handler would be cancelled before the rollback body ran and leave half a sheet inside an open transaction. fits goes through dimension_intents(fits=...), the API Task 6 shipped, rather than being stamped on at draw time: that is the path that validates a key by name and refuses a fit on a reference dimension or on a hole that moved into the table. It is scoped per view first - a fit on segment[0] names nothing in the end view, and refusing there would make a correct three-view sheet undrawable - and any key no view claimed is handed back to dimension_intents so the refusal is written in its own vocabulary. SECTION 21 registers the six tools in the new mech pack, with alias records (SECTIONPLANE, ARRAY plus shop-floor synonyms in English and Turkish - mil, kademeli mil, kama, pah, kesit, gorunus), a regenerated tool inventory, the README release snapshot at 210 tools in 24 groups, and the frozen group-size snapshot gaining mech: 6. Lean is deliberately untouched here: spec section 11 puts three of these on the lean surface together with group G's two sheet tools, and Task 15 makes that edit once across all four branches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
207 lines
6.7 KiB
Python
207 lines
6.7 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.
|
|
"""
|
|
|
|
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",
|
|
}
|
|
|
|
LEAN_SETTINGS_ESSENTIALS = {
|
|
"drawing_apply_standard",
|
|
"page_setup_apply",
|
|
"batch_plot",
|
|
"dimstyle_set_current",
|
|
"textstyle_set_current",
|
|
}
|
|
|
|
ALL_PACKS = ["core", "pid", "settings", "mech"]
|
|
ALL_PACKS_SORTED = sorted(ALL_PACKS)
|
|
MECH_TOOLS = {
|
|
"mech_part_draw",
|
|
"mech_view_add",
|
|
"mech_dimension_part",
|
|
"mech_hole_pattern",
|
|
"mech_part_from_spec",
|
|
"mech_part_inspect",
|
|
}
|
|
|
|
|
|
@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 not (server.PACK_TOOL_NAMES["settings"] & server.PACK_TOOL_NAMES["pid"])
|
|
|
|
|
|
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 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) & disabled)
|
|
assert info["tool_packs"]["enabled"] == ALL_PACKS_SORTED
|
|
|
|
|
|
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 = 55. The five 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 not (SETTINGS_TOOLS & server.LEAN_TOOL_NAMES)
|
|
assert len(server.LEAN_TOOL_NAMES) == 55
|
|
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 info["enabled_count"] == 55 - 3, "lean minus the three pid 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["mech"]) == MECH_TOOLS
|
|
assert len(groups) == 24
|
|
|
|
|
|
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
|