From 8c4306ba2d6eedcc2010bb3bfef3dc6bf052848e Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Wed, 24 Jun 2026 12:52:41 +0800 Subject: [PATCH] Add clipboard_formats: classify + diff the clipboard's available formats The clipboard holds the same content in several formats at once, and nothing reported which formats were present or detected when that set changed. Add classify_format/classify_formats (map standard CF_* ids + registered names to friendly categories), diff_formats (pure monitor primitive returning added/removed/changed), and a Win32 EnumClipboardFormats enumeration. Classifier and diff are pure and headless-tested; only the live enumeration is Win32. --- WHATS_NEW.md | 6 + .../doc/new_features/v186_features_doc.rst | 49 ++++++ .../Zh/doc/new_features/v186_features_doc.rst | 41 +++++ je_auto_control/__init__.py | 7 + .../gui/script_builder/command_schema.py | 18 +++ .../utils/clipboard_formats/__init__.py | 10 ++ .../clipboard_formats/clipboard_formats.py | 140 ++++++++++++++++++ .../utils/executor/action_executor.py | 29 ++++ .../utils/mcp_server/tools/_factories.py | 31 ++++ .../utils/mcp_server/tools/_handlers.py | 15 ++ .../headless/test_clipboard_formats_batch.py | 80 ++++++++++ 11 files changed, 426 insertions(+) create mode 100644 docs/source/Eng/doc/new_features/v186_features_doc.rst create mode 100644 docs/source/Zh/doc/new_features/v186_features_doc.rst create mode 100644 je_auto_control/utils/clipboard_formats/__init__.py create mode 100644 je_auto_control/utils/clipboard_formats/clipboard_formats.py create mode 100644 test/unit_test/headless/test_clipboard_formats_batch.py diff --git a/WHATS_NEW.md b/WHATS_NEW.md index 47ad558c..e9e0adad 100644 --- a/WHATS_NEW.md +++ b/WHATS_NEW.md @@ -1,5 +1,11 @@ # What's New — AutoControl +## What's new (2026-06-24) — Clipboard Format Inspection (classify / diff available formats) + +See which formats are on the clipboard, and detect when its shape changes. Full reference: [`docs/source/Eng/doc/new_features/v186_features_doc.rst`](docs/source/Eng/doc/new_features/v186_features_doc.rst). + +- **`classify_format` / `classify_formats` / `diff_formats` / `list_clipboard_formats` / `clipboard_formats`** (`AC_clipboard_formats`, `AC_classify_formats`, `AC_diff_formats`): the clipboard usually holds the same content in several formats at once (a Word copy = text + HTML + RTF; a file copy = CF_HDROP; a screenshot = CF_DIB). This enumerates the live clipboard (`EnumClipboardFormats`) without consuming anything and classifies each format into a friendly category (text/image/files/html/rtf/csv/audio/…); `diff_formats` is a pure monitor primitive returning `{added, removed, changed}` between two snapshots. The classifier and diff are pure (registered names take priority over dynamic ids); only the live enumeration is Win32. No `PySide6`. + ## What's new (2026-06-24) — Rich Clipboard Formats (RTF and CSV/TSV) Put styled text and tables on the clipboard for cross-app paste into Word and Excel. Full reference: [`docs/source/Eng/doc/new_features/v185_features_doc.rst`](docs/source/Eng/doc/new_features/v185_features_doc.rst). diff --git a/docs/source/Eng/doc/new_features/v186_features_doc.rst b/docs/source/Eng/doc/new_features/v186_features_doc.rst new file mode 100644 index 00000000..7365ce07 --- /dev/null +++ b/docs/source/Eng/doc/new_features/v186_features_doc.rst @@ -0,0 +1,49 @@ +Clipboard Format Inspection (classify / diff available formats) +=============================================================== + +The clipboard usually holds the *same* content in several formats at once — a +copy from Word offers ``CF_UNICODETEXT`` + ``HTML Format`` + ``Rich Text Format``, +a file copy offers ``CF_HDROP``, a screenshot offers ``CF_DIB``. Knowing *which +formats are present* (without consuming any of them) tells an automation what it +can paste, and comparing two snapshots detects when the clipboard's shape +changed. ``clipboard_formats`` adds: + +* :func:`classify_format` / :func:`classify_formats` — map standard ``CF_*`` ids + and registered format names to friendly categories (text / image / files / + html / rtf / csv / audio / …), +* :func:`diff_formats` — a pure monitor primitive: ``{added, removed, changed}`` + between two snapshots, +* :func:`list_clipboard_formats` / :func:`clipboard_formats` — enumerate the live + clipboard (``EnumClipboardFormats``) and classify it. + +The classifier and diff are pure functions (unit-testable on any platform); only +the live enumeration is Win32 (raising ``RuntimeError`` elsewhere). Imports no +``PySide6``. + +Headless API +------------ + +.. code-block:: python + + from je_auto_control import (classify_formats, diff_formats, + clipboard_formats) + + classify_formats([13, {"id": 49383, "name": "HTML Format"}]) + # {"categories": ["html", "text"], "has_text": True, "has_image": False, ...} + + diff_formats([13, 1], [13, 15]) # {"added": [files], "removed": [text], ...} + + clipboard_formats() # live clipboard summary (Windows) + +A descriptor is an id (``13``), an ``{"id": ..., "name": ...}`` dict, or an +``(id, name)`` tuple. A registered ``name`` takes priority over the id, since +registered formats have dynamic ids (``>= 0xC000``). Unrecognised formats are +``"other"``. + +Executor commands +----------------- + +``AC_clipboard_formats`` (live, Windows), ``AC_classify_formats`` (``formats``) +and ``AC_diff_formats`` (``before`` / ``after``) — the latter two are pure and +run anywhere. They are exposed as read-only ``ac_*`` MCP tools and as Script +Builder commands under **Data**. diff --git a/docs/source/Zh/doc/new_features/v186_features_doc.rst b/docs/source/Zh/doc/new_features/v186_features_doc.rst new file mode 100644 index 00000000..7cff1500 --- /dev/null +++ b/docs/source/Zh/doc/new_features/v186_features_doc.rst @@ -0,0 +1,41 @@ +剪貼簿格式檢視(分類 / 比較可用格式) +==================================== + +剪貼簿通常同時以多種格式保存*相同*內容——從 Word 複製會提供 ``CF_UNICODETEXT`` + +``HTML Format`` + ``Rich Text Format``,複製檔案會提供 ``CF_HDROP``,截圖會提供 ``CF_DIB``。 +知道*目前有哪些格式*(且不消耗任何一個)能讓自動化判斷可以貼上什麼,比較兩份快照則能偵測 +剪貼簿的形態何時改變。``clipboard_formats`` 加入: + +* :func:`classify_format` / :func:`classify_formats` ——把標準 ``CF_*`` id 與已註冊格式名稱 + 對應到友善類別(text / image / files / html / rtf / csv / audio……), +* :func:`diff_formats` ——純粹的監看原語:兩份快照之間的 ``{added, removed, changed}``, +* :func:`list_clipboard_formats` / :func:`clipboard_formats` ——列舉存活的剪貼簿 + (``EnumClipboardFormats``)並加以分類。 + +分類器與比較器為純函式(可在任何平台單元測試);只有存活列舉為 Win32(其他平台拋出 +``RuntimeError``)。不匯入 ``PySide6``。 + +無頭 API +-------- + +.. code-block:: python + + from je_auto_control import (classify_formats, diff_formats, + clipboard_formats) + + classify_formats([13, {"id": 49383, "name": "HTML Format"}]) + # {"categories": ["html", "text"], "has_text": True, "has_image": False, ...} + + diff_formats([13, 1], [13, 15]) # {"added": [files], "removed": [text], ...} + + clipboard_formats() # 存活剪貼簿摘要(Windows) + +描述子可為 id(``13``)、``{"id": ..., "name": ...}`` 字典,或 ``(id, name)`` 元組。已註冊的 +``name`` 優先於 id,因為已註冊格式的 id 是動態的(``>= 0xC000``)。未辨識的格式為 ``"other"``。 + +執行器指令 +---------- + +``AC_clipboard_formats``(存活,Windows)、``AC_classify_formats``(``formats``)與 +``AC_diff_formats``(``before`` / ``after``)——後兩者為純函式,可在任何平台執行。皆以唯讀 +``ac_*`` MCP 工具及 Script Builder 指令(位於 **Data** 分類下)形式提供。 diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index edb09043..e9c9e926 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -71,6 +71,11 @@ build_rtf, csv_to_rows, get_clipboard_csv, get_clipboard_rtf, rows_to_csv, rtf_to_text, set_clipboard_csv, set_clipboard_rtf, ) +# Clipboard format inspection (classify / diff available formats) +from je_auto_control.utils.clipboard_formats import ( + classify_format, classify_formats, clipboard_formats, diff_formats, + list_clipboard_formats, +) # VLM element locator (headless) from je_auto_control.utils.vision import ( VLMNotAvailableError, click_by_description, locate_by_description, @@ -1642,6 +1647,8 @@ def start_autocontrol_gui(*args, **kwargs): "build_rtf", "rtf_to_text", "rows_to_csv", "csv_to_rows", "set_clipboard_rtf", "get_clipboard_rtf", "set_clipboard_csv", "get_clipboard_csv", + "classify_format", "classify_formats", "diff_formats", + "list_clipboard_formats", "clipboard_formats", # VLM locator "VLMNotAvailableError", "locate_by_description", "click_by_description", "verify_description", diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index 92aae01f..dc6d60eb 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -1622,6 +1622,24 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None: default=","),), description="Read the clipboard's Csv content as rows (Windows).", )) + specs.append(CommandSpec( + "AC_clipboard_formats", "Data", "List Clipboard Formats", + description="Enumerate + classify the clipboard's formats (Windows).", + )) + specs.append(CommandSpec( + "AC_classify_formats", "Data", "Classify Clipboard Formats", + fields=(FieldSpec("formats", FieldType.STRING, + placeholder='[1, 13, {"id": 49161, "name": "Csv"}]'),), + description="Classify a provided list of clipboard formats (pure).", + )) + specs.append(CommandSpec( + "AC_diff_formats", "Data", "Diff Clipboard Formats", + fields=( + FieldSpec("before", FieldType.STRING, placeholder="[1, 13]"), + FieldSpec("after", FieldType.STRING, placeholder="[1, 13, 15]"), + ), + description="Diff two clipboard-format snapshots (pure).", + )) specs.append(CommandSpec( "AC_watchdog_add", "Flow", "Watchdog: Add Popup Rule", fields=( diff --git a/je_auto_control/utils/clipboard_formats/__init__.py b/je_auto_control/utils/clipboard_formats/__init__.py new file mode 100644 index 00000000..ac7520be --- /dev/null +++ b/je_auto_control/utils/clipboard_formats/__init__.py @@ -0,0 +1,10 @@ +"""Inspect and classify the clipboard's available formats (pure classify/diff + Win32 enum).""" +from je_auto_control.utils.clipboard_formats.clipboard_formats import ( + classify_format, classify_formats, clipboard_formats, diff_formats, + list_clipboard_formats, +) + +__all__ = [ + "classify_format", "classify_formats", "diff_formats", + "list_clipboard_formats", "clipboard_formats", +] diff --git a/je_auto_control/utils/clipboard_formats/clipboard_formats.py b/je_auto_control/utils/clipboard_formats/clipboard_formats.py new file mode 100644 index 00000000..0527b7e2 --- /dev/null +++ b/je_auto_control/utils/clipboard_formats/clipboard_formats.py @@ -0,0 +1,140 @@ +"""Inspect and classify the clipboard's available formats. + +The clipboard usually holds the *same* content in several formats at once — a +copy from Word offers ``CF_UNICODETEXT`` + ``HTML Format`` + ``Rich Text Format``, +a file copy offers ``CF_HDROP``, a screenshot offers ``CF_DIB``. Knowing *which +formats are present* (without consuming any of them) tells an automation what it +can paste and lets it detect when the clipboard's shape changed. + +``clipboard_formats`` enumerates the live clipboard (``EnumClipboardFormats``) and +classifies each format into a friendly category (text / image / files / html / +rtf / csv / audio / …). The classifier and the snapshot diff are pure functions — +unit-testable on any platform — and only the live enumeration is Win32 (raising +``RuntimeError`` elsewhere, like the base ``clipboard`` module). Imports no +``PySide6``. +""" +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union + +# Standard CF_* clipboard format ids → category. +_CF_CATEGORY = { + 1: "text", 7: "text", 13: "text", # CF_TEXT / OEMTEXT / UNICODETEXT + 2: "image", 8: "image", 17: "image", # CF_BITMAP / DIB / DIBV5 + 3: "image", 6: "image", 14: "image", # METAFILEPICT / TIFF / ENHMETAFILE + 9: "palette", # CF_PALETTE + 11: "audio", 12: "audio", # CF_RIFF / CF_WAVE + 15: "files", # CF_HDROP + 16: "locale", # CF_LOCALE +} +# Registered (named) clipboard formats → category (matched case-insensitively). +_NAME_CATEGORY = { + "html format": "html", + "rich text format": "rtf", + "rich text format without objects": "rtf", + "csv": "csv", + "png": "image", "jfif": "image", "gif": "image", "image/png": "image", + "filenamew": "files", "filename": "files", "filegroupdescriptorw": "files", + "filegroupdescriptor": "files", "shell idlist array": "files", +} +_Format = Union[int, Dict[str, Any], Tuple[int, str]] + + +def _coerce(item: _Format) -> Tuple[int, str]: + """Normalise a format descriptor (int / ``{id,name}`` / ``(id,name)``).""" + if isinstance(item, dict): + return int(item.get("id", 0)), str(item.get("name") or "") + if isinstance(item, (tuple, list)): + return int(item[0]), str(item[1] if len(item) > 1 else "") + return int(item), "" + + +def classify_format(format_id: int, name: Optional[str] = None) -> str: + """Return the friendly category for one clipboard format. + + A registered ``name`` (e.g. ``"HTML Format"``) takes priority; otherwise the + standard ``CF_*`` id is mapped. Anything unrecognised is ``"other"``. + """ + if name: + category = _NAME_CATEGORY.get(name.strip().lower()) + if category is not None: + return category + return _CF_CATEGORY.get(int(format_id), "other") + + +def classify_formats(formats: Sequence[_Format]) -> Dict[str, Any]: + """Classify a list of clipboard formats into a summary. + + Returns ``{categories, formats:[{id,name,category}], has_text, has_image, + has_files}``. + """ + items: List[Dict[str, Any]] = [] + for entry in formats: + format_id, name = _coerce(entry) + items.append({"id": format_id, "name": name, + "category": classify_format(format_id, name)}) + categories = sorted({item["category"] for item in items}) + return {"categories": categories, "formats": items, + "has_text": "text" in categories, + "has_image": "image" in categories, + "has_files": "files" in categories} + + +def diff_formats(before: Sequence[_Format], + after: Sequence[_Format]) -> Dict[str, Any]: + """Diff two clipboard-format snapshots into ``{added, removed, changed}``. + + ``added`` / ``removed`` are classified format dicts; ``changed`` is True when + either is non-empty — a pure monitor primitive over two enumerations. + """ + def keyed(formats: Sequence[_Format]) -> Dict[Tuple[int, str], Dict[str, Any]]: + out: Dict[Tuple[int, str], Dict[str, Any]] = {} + for entry in formats: + format_id, name = _coerce(entry) + out[(format_id, name)] = {"id": format_id, "name": name, + "category": classify_format(format_id, name)} + return out + + before_map, after_map = keyed(before), keyed(after) + added = [after_map[k] for k in after_map if k not in before_map] + removed = [before_map[k] for k in before_map if k not in after_map] + return {"added": added, "removed": removed, + "changed": bool(added or removed)} + + +# --- Win32 live enumeration ------------------------------------------------ + +def _format_name(user32, format_id: int) -> str: + import ctypes + buffer = ctypes.create_unicode_buffer(256) + if user32.GetClipboardFormatNameW(format_id, buffer, 256): + return buffer.value + return "" + + +def list_clipboard_formats() -> List[Dict[str, Any]]: + """Return the formats currently on the clipboard as ``[{id, name}]`` (Windows).""" + import sys + if not sys.platform.startswith("win"): + raise RuntimeError("list_clipboard_formats is only supported on Windows") + import ctypes + user32 = ctypes.windll.user32 + if not user32.OpenClipboard(None): + raise RuntimeError("OpenClipboard failed") + try: + formats: List[Dict[str, Any]] = [] + format_id = user32.EnumClipboardFormats(0) + while format_id: + formats.append({"id": int(format_id), + "name": _format_name(user32, format_id)}) + format_id = user32.EnumClipboardFormats(format_id) + return formats + finally: + user32.CloseClipboard() + + +def clipboard_formats() -> Dict[str, Any]: + """Enumerate and classify the live clipboard's formats (Windows). + + Returns the :func:`classify_formats` summary for whatever is on the + clipboard right now, without consuming any format. + """ + return classify_formats(list_clipboard_formats()) diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index f85d4b83..150fb5f7 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -4216,6 +4216,32 @@ def _get_clipboard_csv(delimiter: str = ",") -> Dict[str, Any]: return {"found": rows is not None, "rows": rows or []} +def _clipboard_formats() -> Dict[str, Any]: + """Adapter: enumerate and classify the live clipboard's formats (Windows).""" + from je_auto_control.utils.clipboard_formats import clipboard_formats + return clipboard_formats() + + +def _classify_formats(formats: Any) -> Dict[str, Any]: + """Adapter: classify a provided list of clipboard formats (pure).""" + import json + from je_auto_control.utils.clipboard_formats import classify_formats + if isinstance(formats, str): + formats = json.loads(formats) + return classify_formats(formats) + + +def _diff_formats(before: Any, after: Any) -> Dict[str, Any]: + """Adapter: diff two clipboard-format snapshots (pure).""" + import json + from je_auto_control.utils.clipboard_formats import diff_formats + if isinstance(before, str): + before = json.loads(before) + if isinstance(after, str): + after = json.loads(after) + return diff_formats(before, after) + + def _image_histogram(source: Any = None, bins: Any = 32, space: str = "hsv", region: Any = None) -> Dict[str, Any]: """Adapter: per-channel colour histogram of an image / the screen.""" @@ -6433,6 +6459,9 @@ def __init__(self): "AC_get_clipboard_rtf": _get_clipboard_rtf, "AC_set_clipboard_csv": _set_clipboard_csv, "AC_get_clipboard_csv": _get_clipboard_csv, + "AC_clipboard_formats": _clipboard_formats, + "AC_classify_formats": _classify_formats, + "AC_diff_formats": _diff_formats, "AC_image_histogram": _image_histogram, "AC_histogram_changed": _histogram_changed, "AC_changed_regions": _changed_regions, diff --git a/je_auto_control/utils/mcp_server/tools/_factories.py b/je_auto_control/utils/mcp_server/tools/_factories.py index 8d0188aa..f9899647 100644 --- a/je_auto_control/utils/mcp_server/tools/_factories.py +++ b/je_auto_control/utils/mcp_server/tools/_factories.py @@ -3292,6 +3292,37 @@ def clipboard_files_tools() -> List[MCPTool]: handler=h.get_clipboard_csv, annotations=READ_ONLY, ), + MCPTool( + name="ac_clipboard_formats", + description=("Enumerate and classify the formats currently on the " + "clipboard without consuming them (Windows). Returns " + "{categories, formats:[{id,name,category}], has_text, " + "has_image, has_files}."), + input_schema=schema({}, required=[]), + handler=h.clipboard_formats, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_classify_formats", + description=("Classify a provided list of clipboard formats (pure). " + "'formats' is a list of ids or {id,name}. Returns the " + "same summary as ac_clipboard_formats."), + input_schema=schema({"formats": {"type": "array"}}, + required=["formats"]), + handler=h.classify_formats, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_diff_formats", + description=("Diff two clipboard-format snapshots (pure): " + "{added, removed, changed}. 'before'/'after' are lists " + "of ids or {id,name}."), + input_schema=schema({"before": {"type": "array"}, + "after": {"type": "array"}}, + required=["before", "after"]), + handler=h.diff_formats, + annotations=READ_ONLY, + ), ] diff --git a/je_auto_control/utils/mcp_server/tools/_handlers.py b/je_auto_control/utils/mcp_server/tools/_handlers.py index 55045b29..f45dcdc5 100644 --- a/je_auto_control/utils/mcp_server/tools/_handlers.py +++ b/je_auto_control/utils/mcp_server/tools/_handlers.py @@ -2484,6 +2484,21 @@ def get_clipboard_csv(delimiter=","): return _get_clipboard_csv(delimiter) +def clipboard_formats(): + from je_auto_control.utils.executor.action_executor import _clipboard_formats + return _clipboard_formats() + + +def classify_formats(formats): + from je_auto_control.utils.executor.action_executor import _classify_formats + return _classify_formats(formats) + + +def diff_formats(before, after): + from je_auto_control.utils.executor.action_executor import _diff_formats + return _diff_formats(before, after) + + def image_histogram(source=None, bins=32, space="hsv", region=None): from je_auto_control.utils.executor.action_executor import _image_histogram return _image_histogram(source, bins, space, region) diff --git a/test/unit_test/headless/test_clipboard_formats_batch.py b/test/unit_test/headless/test_clipboard_formats_batch.py new file mode 100644 index 00000000..aa54c8a0 --- /dev/null +++ b/test/unit_test/headless/test_clipboard_formats_batch.py @@ -0,0 +1,80 @@ +"""Headless tests for clipboard-format classification + diff (pure; Win32 enum skipped).""" +import je_auto_control as ac +from je_auto_control.utils.clipboard_formats import ( + classify_format, classify_formats, diff_formats, +) + + +def test_classify_standard_ids(): + assert classify_format(13) == "text" # CF_UNICODETEXT + assert classify_format(1) == "text" # CF_TEXT + assert classify_format(8) == "image" # CF_DIB + assert classify_format(15) == "files" # CF_HDROP + assert classify_format(99999) == "other" + + +def test_classify_named_takes_priority(): + # registered ids are dynamic (>= 0xC000); the name decides the category + assert classify_format(49161, "Csv") == "csv" + assert classify_format(49383, "HTML Format") == "html" + assert classify_format(50000, "Rich Text Format") == "rtf" + assert classify_format(49500, "PNG") == "image" + + +def test_classify_formats_summary(): + summary = classify_formats([13, {"id": 15, "name": ""}, + {"id": 49383, "name": "HTML Format"}]) + assert summary["categories"] == ["files", "html", "text"] + assert summary["has_text"] is True + assert summary["has_files"] is True + assert summary["has_image"] is False + assert {"id": 15, "name": "", "category": "files"} in summary["formats"] + + +def test_diff_formats_added_removed(): + before = [13, 1] + after = [13, 15, {"id": 49383, "name": "HTML Format"}] + diff = diff_formats(before, after) + assert diff["changed"] is True + added_cats = {f["category"] for f in diff["added"]} + removed_cats = {f["category"] for f in diff["removed"]} + assert added_cats == {"files", "html"} + assert removed_cats == {"text"} # CF_TEXT(1) dropped; CF_UNICODETEXT kept + + +def test_diff_formats_no_change(): + diff = diff_formats([13, 15], [15, 13]) + assert diff == {"added": [], "removed": [], "changed": False} + + +def test_tuple_descriptor_accepted(): + assert classify_formats([(49161, "Csv")])["categories"] == ["csv"] + + +# --- wiring (Win32 enumeration not executed in CI) ------------------------- + +def test_executor_pure_paths(): + from je_auto_control.utils.executor.action_executor import ( + _classify_formats, _diff_formats) + assert _classify_formats("[13, 15]")["has_files"] is True + assert _diff_formats("[13]", "[13, 15]")["changed"] is True + + +def test_wiring(): + known = set(ac.executor.known_commands()) + assert {"AC_clipboard_formats", "AC_classify_formats", + "AC_diff_formats"} <= known + from je_auto_control.utils.mcp_server.tools import build_default_tool_registry + names = {t.name for t in build_default_tool_registry()} + assert {"ac_clipboard_formats", "ac_classify_formats", + "ac_diff_formats"} <= names + from je_auto_control.gui.script_builder.command_schema import _build_specs + specs = {s.command for s in _build_specs()} + assert {"AC_clipboard_formats", "AC_classify_formats", + "AC_diff_formats"} <= specs + + +def test_facade_exports(): + for name in ("classify_format", "classify_formats", "diff_formats", + "list_clipboard_formats", "clipboard_formats"): + assert hasattr(ac, name) and name in ac.__all__