feat(server): settings pack — 22 environment tools behind TOOL_PACKS, lean grows to 55, three groups, golden set +10
Task 24 (merge sanity) on main after the five track E groups merged.
- TOOL_PACK_NAMES = ("core", "pid", "settings"); PACK_TOOL_NAMES["settings"]
is exactly the 22 SECTION 20 tools. Styles and page setup stay in core.
- LEAN_TOOL_NAMES += page_setup_apply, batch_plot (53 -> 55).
- _GROUP_TAG_PRIORITY / _GROUP_TAG_LABELS: "style" moved up next to "pid",
"pagesetup" -> page_setup and "environment" -> environment added; the 28
SECTION 19/20 decorators gain their section tag, so _tool_groups() reports
styles 10 / page_setup 6 / environment 22 (23 groups, 204 tools) and
pack == group == section for the settings pack.
- SECTION headers, backends/base.py composition, contracts/__init__.py
exports and the layer_states feature entry were already correct after the
merge (read and confirmed, nothing edited).
- Pack text: config.py, .env.example, README config row; README <sub> line
204 tools in 23 groups; release snapshot 3259 collected tests (measured).
- docs/tool-inventory.json regenerated; registry snapshot re-derived.
- tests: test_tool_packs.py replaced (11 tests), test_capability_contract.py
+3 (composition, the agreed capability flags, layer_states honesty),
golden set +10 tuning cases (81 = 66 tuning + 15 holdout); all ten route
in the top 3 without alias edits.
Deviation from the plan text: TRACK_E_KEYS carries five keys, not six. The
plan's "mleaderstyle" (ezdxf True / com False) contradicts spec §8.1, which
retracted that key because the ActiveX route exists (§4.2); measured on the
tree: capability_keys() == {boundary_trace, chspace, interactive_prompt,
live_application, preferences, revcloud, wipeout}, and
ComBackend.mleaderstyle_create is a native implementation
(backends/com_backend.py:6752, is_capability_default -> False). The test now
also asserts the key is absent.
No new ActiveX call was written in this task, so no live probe was needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
90c1194f7f
commit
70afd57831
@@ -1,6 +1,6 @@
|
||||
"""Golden query set for tool discovery: English in, one right tool out.
|
||||
|
||||
Seventy-one cases (56 tuning + 15 holdout) split into a **tuning** set and a
|
||||
Eighty-one cases (66 tuning + 15 holdout) split into a **tuning** set and a
|
||||
**holdout** set. The split is
|
||||
the point of the file. Ranking work is measured against
|
||||
:data:`TUNING_CASES` only; :data:`HOLDOUT_CASES` were written at the same time,
|
||||
@@ -155,6 +155,25 @@ TUNING_CASES: tuple[GoldenCase, ...] = (
|
||||
),
|
||||
GoldenCase("create a text style that uses the isocp font", "textstyle_create", "paraphrase"),
|
||||
GoldenCase("which dimension styles are defined", "dimstyle_list", "counting", risk="read"),
|
||||
# v1.6 track E — styles, page setup, templates, environment. Written
|
||||
# before the alias records were measured against them; a miss here is
|
||||
# fixed by adding vocabulary to the tool's record in discovery/aliases.py,
|
||||
# never by moving the expectation.
|
||||
GoldenCase("PAGESETUP", "page_setup_apply", "command"),
|
||||
GoldenCase("PUBLISH", "batch_plot", "command"),
|
||||
GoldenCase("MLEADERSTYLE", "mleaderstyle_create", "command"),
|
||||
GoldenCase("STYLE", "textstyle_create", "command"),
|
||||
GoldenCase("iso 25 dimension style", "dimstyle_create", "synonym"),
|
||||
GoldenCase("layer snapshot", "layer_state_save", "synonym"),
|
||||
GoldenCase("save this drawing as a template", "drawing_template_save", "paraphrase"),
|
||||
GoldenCase("switch to the other open drawing", "document_activate", "paraphrase"),
|
||||
GoldenCase("ask the operator to click a point on screen", "user_pick_point", "paraphrase"),
|
||||
GoldenCase(
|
||||
"what does the LTSCALE variable mean",
|
||||
"system_variable_describe",
|
||||
"counting",
|
||||
risk="read",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ import pytest
|
||||
from backends.base import AutoCADBackend, UnsupportedCapabilityError
|
||||
from backends.capability import capability_keys, is_capability_default
|
||||
from backends.com_backend import ComBackend
|
||||
from backends.contracts.environment import EnvironmentContract
|
||||
from backends.contracts.pagesetup import PageSetupContract
|
||||
from backends.contracts.styles import StylesContract
|
||||
from backends.ezdxf_backend import EzdxfBackend
|
||||
|
||||
# Marked per-test rather than module-wide: half of these are plain registry
|
||||
@@ -116,3 +119,50 @@ async def test_an_implementing_backend_is_unaffected(backend):
|
||||
result = await backend.entity_change_space([], "nope")
|
||||
assert result["ok"] is False
|
||||
assert "error" in result, "the real implementation still does its own validation"
|
||||
|
||||
|
||||
# ── track E: composition and the five agreed keys ───────────────────────────
|
||||
|
||||
#: spec §8.1 — the flag each engine must report. `mleaderstyle` is deliberately
|
||||
#: absent: the spec retracted it (the ActiveX route through the
|
||||
#: ``ACAD_MLEADERSTYLE`` dictionary exists, §4.2), and both engines implement
|
||||
#: ``mleaderstyle_create`` natively — `capability_keys()` never registers it.
|
||||
TRACK_E_KEYS = {
|
||||
"dwgprops": {"ezdxf": False, "com": True},
|
||||
"dwt_write": {"ezdxf": False, "com": True},
|
||||
"live_application": {"ezdxf": False, "com": True},
|
||||
"preferences": {"ezdxf": False, "com": True},
|
||||
"interactive_prompt": {"ezdxf": False, "com": True},
|
||||
}
|
||||
|
||||
|
||||
def _feature_keys_full(backend) -> dict:
|
||||
return backend.capabilities().to_dict()["features"]
|
||||
|
||||
|
||||
def test_track_e_contracts_are_composed_into_the_backend():
|
||||
for contract in (StylesContract, PageSetupContract, EnvironmentContract):
|
||||
assert issubclass(AutoCADBackend, contract), f"{contract.__name__} not in AutoCADBackend"
|
||||
assert issubclass(EzdxfBackend, contract) and issubclass(ComBackend, contract)
|
||||
|
||||
|
||||
def test_track_e_capability_keys_carry_the_agreed_flags():
|
||||
assert "mleaderstyle" not in capability_keys(), "spec §8.1: no such engine boundary"
|
||||
ezdxf_features = _feature_keys_full(EzdxfBackend())
|
||||
com_features = _feature_keys_full(ComBackend())
|
||||
for key, flags in TRACK_E_KEYS.items():
|
||||
assert key in ezdxf_features, f"{key} undeclared on ezdxf"
|
||||
assert key in com_features, f"{key} undeclared on com"
|
||||
assert ezdxf_features[key]["supported"] is flags["ezdxf"], key
|
||||
assert com_features[key]["supported"] is flags["com"], key
|
||||
|
||||
|
||||
def test_layer_states_are_declared_as_ours_not_autocads():
|
||||
"""Spec §7.2 / §8.3: the states live in the file and travel with it, and do
|
||||
not appear in AutoCAD's Layer States Manager. Said in the map, not only in
|
||||
a docstring."""
|
||||
for backend in (EzdxfBackend(), ComBackend()):
|
||||
feature = _feature_keys_full(backend)["layer_states"]
|
||||
assert feature["supported"] is True
|
||||
assert feature["mode"] == "xrecord"
|
||||
assert "layer_states_manager" in (feature["reason"] or ""), backend.name
|
||||
|
||||
+122
-14
@@ -1,4 +1,11 @@
|
||||
"""TOOL_PACKS: advertise only the vertical packs a client uses; core is always on."""
|
||||
# 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
|
||||
|
||||
@@ -24,6 +31,63 @@ PID_TOOLS = {
|
||||
"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"]
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def _restore(monkeypatch):
|
||||
@@ -32,29 +96,48 @@ async def _restore(monkeypatch):
|
||||
await server._apply_tool_profile("full")
|
||||
|
||||
|
||||
def test_pack_registry_names_real_tools_and_only_pid_ones():
|
||||
assert server.TOOL_PACK_NAMES == ("core", "pid")
|
||||
def test_pack_registry_names_real_tools_and_only_them():
|
||||
assert server.TOOL_PACK_NAMES == ("core", "pid", "settings")
|
||||
assert server.PACK_TOOL_NAMES["pid"] == frozenset(PID_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_core_only_hides_every_pid_tool_but_keeps_block_define(monkeypatch):
|
||||
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": ["core", "pid"],
|
||||
"ignored": [],
|
||||
}
|
||||
assert info["tool_packs"] == {"enabled": ["core"], "available": ALL_PACKS, "ignored": []}
|
||||
|
||||
|
||||
async def test_all_is_the_default_and_enables_pid(monkeypatch):
|
||||
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")
|
||||
assert not (PID_TOOLS & set(info["disabled_tools"]))
|
||||
assert info["tool_packs"]["enabled"] == ["core", "pid"]
|
||||
disabled = set(info["disabled_tools"])
|
||||
assert not ((PID_TOOLS | SETTINGS_TOOLS) & disabled)
|
||||
assert info["tool_packs"]["enabled"] == ALL_PACKS
|
||||
|
||||
|
||||
async def test_unknown_pack_is_ignored_with_a_warning_and_core_cannot_be_dropped(
|
||||
@@ -65,7 +148,7 @@ async def test_unknown_pack_is_ignored_with_a_warning_and_core_cannot_be_dropped
|
||||
info = await server._apply_tool_profile("full")
|
||||
assert info["tool_packs"] == {
|
||||
"enabled": ["core", "pid"],
|
||||
"available": ["core", "pid"],
|
||||
"available": ALL_PACKS,
|
||||
"ignored": ["mech3000"],
|
||||
}
|
||||
assert "mech3000" in caplog.text
|
||||
@@ -79,9 +162,34 @@ async def test_lean_intersects_with_packs(monkeypatch):
|
||||
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 len(groups) == 23
|
||||
|
||||
|
||||
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"] == ["core", "pid"]
|
||||
assert about["tool_packs"]["available"] == ALL_PACKS
|
||||
|
||||
+21
-36
@@ -495,7 +495,7 @@ async def test_tool_groups_is_byte_identical_to_the_source_declared_grouping():
|
||||
|
||||
|
||||
async def test_tool_group_sizes_are_unchanged():
|
||||
"""Frozen snapshot of the surface (172 tools, 20 groups).
|
||||
"""Frozen snapshot of the surface (204 tools, 23 groups).
|
||||
|
||||
Taken before @cad_tool landed at 131 tools; `batch` moved 2 -> 3 when
|
||||
v1.5.0's `cad_batch` joined `entity_batch_create`/`entity_batch_modify`, and
|
||||
@@ -517,35 +517,18 @@ async def test_tool_group_sizes_are_unchanged():
|
||||
when `pid_tag_parse` (the ISA-5.1 tag grammar as a read-only tool)
|
||||
joined; the `pid`
|
||||
tag is ranked first in `_GROUP_TAG_PRIORITY` so those tools file under
|
||||
`pid` rather than under their secondary `query` / `create` tags. `styles`
|
||||
appeared (0 -> 9) when track E's SECTION 18 opened with the dimension,
|
||||
text and multileader style tools, then 9 -> 10 when
|
||||
`drawing_apply_standard` (ISO or ANSI styles, units and layers in one
|
||||
call) joined; the `style` tag sits ahead of `layer` and `drawing` in
|
||||
`_GROUP_TAG_PRIORITY` so they never file under their secondary
|
||||
`query` / `create` / `modify` / `drawing` tags. `system`
|
||||
moved 7 -> 8 when v1.6's `system_variable_describe` (the sysvar catalogue
|
||||
as a read-only tool) opened SECTION 20. `drawing` moved 11 -> 13 when
|
||||
v1.6's `drawing_properties_get` / `drawing_properties_set` (DWGPROPS on
|
||||
both engines) joined SECTION 20 — tagged `drawing` and `settings`, and
|
||||
`settings` is not a group tag, so they file under drawing.
|
||||
`layouts` moved 12 -> 16 when v1.6's SECTION 19 opened with
|
||||
`page_setup_list` / `page_setup_apply` / `plot_style_list` / `batch_plot`
|
||||
— all tagged `layout`, which `_GROUP_TAG_PRIORITY` ranks above
|
||||
`query`/`export`, so they file with the sheets they set up. `templates`
|
||||
moved 2 -> 3 when v1.6's `drawing_template_list` joined SECTION 19
|
||||
(tagged `template`, which ranks above `drawing`), then 3 -> 4 when
|
||||
`drawing_template_save` joined. `drawing`
|
||||
moved 13 -> 16 when v1.6's `document_list` / `document_activate` /
|
||||
`document_close` (multi-document on both engines) opened SECTION 20;
|
||||
`layers` moved 14 -> 18 with the four `layer_state_*` tools and `view`
|
||||
4 -> 10 with `view_named_*` and `ucs_*` (UCS files under `view`, where
|
||||
AutoCAD's own ribbon keeps it) when v1.6's SECTION 20 grew. `system`
|
||||
moved 8 -> 14 with v1.6's live-only environment tools (`system_launch`,
|
||||
`system_preferences_get/set`, `user_pick_point`, `user_select`,
|
||||
`system_prompt_message`), which refuse headlessly with declared
|
||||
capability keys. Every other number here has been unchanged since the
|
||||
snapshot was taken.
|
||||
`pid` rather than under their secondary `query` / `create` tags.
|
||||
Three groups appeared with v1.6's track E — `styles` (10, SECTION 18),
|
||||
`page_setup` (6, SECTION 19) and `environment` (22, SECTION 20) — each
|
||||
tagged with its section name, ranked directly after `pid` in
|
||||
`_GROUP_TAG_PRIORITY` so a style tool that also carries `dimension` or an
|
||||
environment tool that also carries `layer` files under its section; the
|
||||
`environment` group is exactly `PACK_TOOL_NAMES["settings"]`. (Until the
|
||||
merge task tagged them, the four groups had filed those 28 tools under
|
||||
`layouts` / `templates` / `drawing` / `layers` / `view` / `system` by
|
||||
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.
|
||||
"""
|
||||
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
|
||||
assert sizes == {
|
||||
@@ -554,21 +537,23 @@ async def test_tool_group_sizes_are_unchanged():
|
||||
"blocks": 9,
|
||||
"corner_ops": 4,
|
||||
"dimensions": 5,
|
||||
"drawing": 16,
|
||||
"drawing": 11,
|
||||
"engineering": 10,
|
||||
"entity_creation": 18,
|
||||
"entity_modification": 16,
|
||||
"entity_query": 9,
|
||||
"layers": 18,
|
||||
"layouts": 16,
|
||||
"environment": 22,
|
||||
"layers": 14,
|
||||
"layouts": 12,
|
||||
"page_setup": 6,
|
||||
"pid": 9,
|
||||
"premium": 12,
|
||||
"solids": 5,
|
||||
"styles": 10,
|
||||
"system": 14,
|
||||
"templates": 4,
|
||||
"system": 7,
|
||||
"templates": 2,
|
||||
"transactions": 3,
|
||||
"validation": 1,
|
||||
"view": 10,
|
||||
"view": 4,
|
||||
}
|
||||
assert sum(sizes.values()) == 204
|
||||
|
||||
Reference in New Issue
Block a user