From c210b00233c45e34ff9100c4d327f4dbdda23161 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 26 Jun 2026 06:12:21 +0800 Subject: [PATCH] Add verify_field: read a field back and confirm the typed value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit field_entry types and hopes — a slow IME, focus steal, input mask or auto-format can silently drop characters and nothing reads the field back. Distinct from action_effect (any near-target change) and postcondition.text_present (text anywhere). compare_field_value is the pure comparator (exact/trim/ci/normalized/contains); verify_field_value reads via an injectable reader; fill_and_verify types, reads back and retries until it matches. --- WHATS_NEW.md | 6 + .../doc/new_features/v210_features_doc.rst | 57 +++++++++ .../Zh/doc/new_features/v210_features_doc.rst | 49 +++++++ je_auto_control/__init__.py | 5 + .../gui/script_builder/command_schema.py | 27 ++++ .../utils/executor/action_executor.py | 24 ++++ .../utils/mcp_server/tools/_factories.py | 29 +++++ .../utils/mcp_server/tools/_handlers.py | 16 +++ .../utils/verify_field/__init__.py | 11 ++ .../utils/verify_field/verify_field.py | 101 +++++++++++++++ .../headless/test_verify_field_batch.py | 121 ++++++++++++++++++ 11 files changed, 446 insertions(+) create mode 100644 docs/source/Eng/doc/new_features/v210_features_doc.rst create mode 100644 docs/source/Zh/doc/new_features/v210_features_doc.rst create mode 100644 je_auto_control/utils/verify_field/__init__.py create mode 100644 je_auto_control/utils/verify_field/verify_field.py create mode 100644 test/unit_test/headless/test_verify_field_batch.py diff --git a/WHATS_NEW.md b/WHATS_NEW.md index 29571847..d0a40e5a 100644 --- a/WHATS_NEW.md +++ b/WHATS_NEW.md @@ -2,6 +2,12 @@ ## What's new (2026-06-26) +### Verify a Field After Typing + +Read the field back and confirm the value actually landed — don't type and hope. Full reference: [`docs/source/Eng/doc/new_features/v210_features_doc.rst`](docs/source/Eng/doc/new_features/v210_features_doc.rst). + +- **`compare_field_value` / `verify_field_value` / `fill_and_verify`** (`AC_compare_field_value`, `AC_verify_field_value`): `field_entry` types into a control and *hopes* — a slow IME, focus steal, input mask or auto-format can silently mangle or drop characters, and nothing reads the field back. This is distinct from `action_effect` (did *anything* change near the target?) and `postcondition.text_present` (does the text appear *anywhere*?) — neither confirms *this* field equals *this* value. `compare_field_value` is the pure comparator (`exact`/`trim`/`ci`/`normalized` NFKC/`contains`); `verify_field_value` reads through an injectable `reader` (native accessibility value in the executor); `fill_and_verify` types via an injectable `filler`, reads back, and retries (optionally clearing first) until it matches or attempts run out. Every comparison and retry decision is pure and unit-tested without a real control. Second feature of the ROUND-15 input-fidelity lane. No `PySide6`. + ### Retry Budget — Deadline + Jitter Retry a flaky step bounded by a total time budget, with jittered backoff. Full reference: [`docs/source/Eng/doc/new_features/v209_features_doc.rst`](docs/source/Eng/doc/new_features/v209_features_doc.rst). diff --git a/docs/source/Eng/doc/new_features/v210_features_doc.rst b/docs/source/Eng/doc/new_features/v210_features_doc.rst new file mode 100644 index 00000000..0883308a --- /dev/null +++ b/docs/source/Eng/doc/new_features/v210_features_doc.rst @@ -0,0 +1,57 @@ +Verify a Field After Typing +=========================== + +``field_entry`` types into a control and *hopes* it landed. A slow IME, a focus +steal, an input mask or an auto-format can silently mangle or drop characters, +and nothing reads the field back to notice. This is distinct from +``action_effect`` (did *anything* change near the target?) and +``postcondition.text_present`` (does the text appear *anywhere* on screen?) — +neither confirms *this* field now equals *this* value. ``verify_field`` closes +the read-back gap. + +* :func:`compare_field_value` — pure: compare an expected and actual value under + a match ``mode`` — ``exact`` / ``trim`` / ``ci`` (case-insensitive) / + ``normalized`` (Unicode NFKC + case-fold + whitespace) / ``contains``. +* :func:`verify_field_value` — read the field through an injectable ``reader`` + and compare. +* :func:`fill_and_verify` — type through an injectable ``filler``, read back, and + retry (optionally clearing first) until it matches or attempts run out. + +In the executor the reader is the native accessibility value, but every +comparison and retry decision is pure and testable without a real control. +Imports no ``PySide6``. + +Headless API +------------ + +.. code-block:: python + + from je_auto_control import ( + compare_field_value, verify_field_value, fill_and_verify, + ) + + compare_field_value("café", "café", mode="normalized")["match"] # True + + # Read a control back and assert it took the value + ok = verify_field_value("invoice.pdf", + reader=lambda: read_control_value())["match"] + + # Type, read back, and retry up to 3 times (clearing before each retry) + fill_and_verify("2026-06-26", filler=type_into_field, + reader=read_control_value, attempts=3, clear=select_all_del) + +``fill_and_verify`` returns the final :func:`compare_field_value` result plus an +``attempts`` count, so a flow can branch on a persistent mismatch instead of +typing blind. ``filler`` / ``reader`` / ``clear`` are injectable, so the retry +logic is fully unit-tested without a real field. + +Executor commands +----------------- + +``AC_compare_field_value`` (``expected`` / ``actual`` / ``mode`` → ``{match, +mode, expected, actual}``, pure) and ``AC_verify_field_value`` (``expected`` + +``name`` / ``role`` / ``app_name`` / ``automation_id`` / ``mode`` → the match +result, reading the control's value through the accessibility backend). They are +the matching read-only ``ac_*`` MCP tools and Script Builder commands under +**Flow**. :func:`fill_and_verify` (which wraps a typing callable) is the +Python-API surface. diff --git a/docs/source/Zh/doc/new_features/v210_features_doc.rst b/docs/source/Zh/doc/new_features/v210_features_doc.rst new file mode 100644 index 00000000..b62f6912 --- /dev/null +++ b/docs/source/Zh/doc/new_features/v210_features_doc.rst @@ -0,0 +1,49 @@ +輸入後驗證欄位 +============== + +``field_entry`` 對控制項輸入後就*指望*它生效了。緩慢的 IME、焦點被搶、輸入遮罩或自動格式化都可能 +悄悄竄改或漏掉字元,而沒有任何東西讀回欄位來察覺。這有別於 ``action_effect``(目標附近是否有*任何* +變化?)與 ``postcondition.text_present``(該文字是否出現在畫面*某處*?)——兩者都無法確認*這個*欄位 +現在等於*這個*值。``verify_field`` 補上讀回這道缺口。 + +* :func:`compare_field_value` ——純函式:在某個比對 ``mode`` 下比較預期與實際值—— + ``exact`` / ``trim`` / ``ci``(不分大小寫)/ ``normalized``(Unicode NFKC + 大小寫摺疊 + 空白)/ + ``contains``。 +* :func:`verify_field_value` ——透過可注入的 ``reader`` 讀回欄位並比較。 +* :func:`fill_and_verify` ——透過可注入的 ``filler`` 輸入、讀回、並重試(可選擇先清空), + 直到相符或用完次數。 + +在執行器中,reader 即原生無障礙值,但每個比較與重試決策都是純函式,可在沒有真實控制項的情況下測試。 +不匯入 ``PySide6``。 + +無頭 API +-------- + +.. code-block:: python + + from je_auto_control import ( + compare_field_value, verify_field_value, fill_and_verify, + ) + + compare_field_value("café", "café", mode="normalized")["match"] # True + + # 讀回控制項並斷言它取得了該值 + ok = verify_field_value("invoice.pdf", + reader=lambda: read_control_value())["match"] + + # 輸入、讀回、最多重試 3 次(每次重試前先清空) + fill_and_verify("2026-06-26", filler=type_into_field, + reader=read_control_value, attempts=3, clear=select_all_del) + +``fill_and_verify`` 回傳最終的 :func:`compare_field_value` 結果加上 ``attempts`` 次數, +讓流程能在持續不符時分支處理,而非盲目輸入。``filler`` / ``reader`` / ``clear`` 皆可注入, +故重試邏輯能在沒有真實欄位的情況下完整測試。 + +執行器指令 +---------- + +``AC_compare_field_value``(``expected`` / ``actual`` / ``mode`` → ``{match, +mode, expected, actual}``,純函式)與 ``AC_verify_field_value``(``expected`` 加上 +``name`` / ``role`` / ``app_name`` / ``automation_id`` / ``mode`` → 比對結果, +透過無障礙後端讀取控制項的值)。皆以對應的唯讀 ``ac_*`` MCP 工具及 Script Builder 指令 +(位於 **Flow** 分類下)形式提供。:func:`fill_and_verify`(包裹一個輸入 callable)則是 Python API 介面。 diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index eb8c3e56..e25642fd 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -119,6 +119,10 @@ from je_auto_control.utils.retry_budget import ( RetryBudget, backoff_delay, jittered_delay, run_with_budget, ) +# Read a field back after typing and confirm the intended value landed +from je_auto_control.utils.verify_field import ( + compare_field_value, fill_and_verify, verify_field_value, +) # Rich clipboard formats — RTF + CSV/TSV codecs and Windows get / set from je_auto_control.utils.clipboard_rich_formats import ( build_rtf, csv_to_rows, get_clipboard_csv, get_clipboard_rtf, rows_to_csv, @@ -1739,6 +1743,7 @@ def start_autocontrol_gui(*args, **kwargs): "ime_state", "is_composing", "wait_for_composition_commit", "decode_conversion_mode", "RetryBudget", "run_with_budget", "backoff_delay", "jittered_delay", + "compare_field_value", "verify_field_value", "fill_and_verify", "build_rtf", "rtf_to_text", "rows_to_csv", "csv_to_rows", "set_clipboard_rtf", "get_clipboard_rtf", "set_clipboard_csv", "get_clipboard_csv", diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index 580bc14f..17221686 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -4431,6 +4431,33 @@ def _add_work_queue_specs(specs: List[CommandSpec]) -> None: ), description="The backoff delay schedule for the first N retries.", )) + specs.append(CommandSpec( + "AC_compare_field_value", "Flow", "Compare Field Value", + fields=( + FieldSpec("expected", FieldType.STRING, placeholder="expected"), + FieldSpec("actual", FieldType.STRING, placeholder="actual"), + FieldSpec("mode", FieldType.STRING, optional=True, default="exact", + placeholder="exact / trim / ci / normalized / contains"), + ), + description="Compare expected vs actual field value under a mode.", + )) + specs.append(CommandSpec( + "AC_verify_field_value", "Flow", "Verify Field Value", + fields=( + FieldSpec("expected", FieldType.STRING, placeholder="expected"), + FieldSpec("name", FieldType.STRING, optional=True, + placeholder="control name"), + FieldSpec("role", FieldType.STRING, optional=True, + placeholder="control role"), + FieldSpec("app_name", FieldType.STRING, optional=True, + placeholder="app name"), + FieldSpec("automation_id", FieldType.STRING, optional=True, + placeholder="automation id"), + FieldSpec("mode", FieldType.STRING, optional=True, default="exact", + placeholder="exact / trim / ci / normalized / contains"), + ), + description="Read a control's value back and confirm it equals expected.", + )) specs.append(CommandSpec( "AC_normalize_ext", "Shell", "Normalize Extension", fields=( diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index 2e33e9ee..b44416b6 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -2761,6 +2761,28 @@ def _plan_retry_delays(attempts: Any, base: Any = 0.1, max_delay: Any = 5.0, return {"delays": [float(d) for d in budget.plan(int(attempts))]} +def _compare_field_value(expected: Any, actual: Any, + mode: Any = "exact") -> Dict[str, Any]: + """Adapter: compare an expected vs actual field value under a mode (pure).""" + from je_auto_control.utils.verify_field import compare_field_value + return compare_field_value(expected, actual, mode=str(mode)) + + +def _verify_field_value(expected: Any, name: Optional[str] = None, + role: Optional[str] = None, + app_name: Optional[str] = None, + automation_id: Optional[str] = None, + mode: Any = "exact") -> Dict[str, Any]: + """Adapter: read a native control's value back and compare to expected.""" + from je_auto_control.utils.verify_field import verify_field_value + return verify_field_value( + expected, + reader=lambda: _control_get_value(name=name, role=role, + app_name=app_name, + automation_id=automation_id), + mode=str(mode)) + + def _normalize_ext(target: str) -> Dict[str, Any]: """Adapter: the lowercased extension of a path / bare ext (pure).""" from je_auto_control.utils.file_assoc import normalize_ext @@ -6785,6 +6807,8 @@ def __init__(self): "AC_decode_conversion_mode": _decode_conversion_mode, "AC_retry_delay": _retry_delay, "AC_plan_retry_delays": _plan_retry_delays, + "AC_compare_field_value": _compare_field_value, + "AC_verify_field_value": _verify_field_value, "AC_normalize_ext": _normalize_ext, "AC_file_association": _file_association, "AC_get_control_text": _get_control_text, diff --git a/je_auto_control/utils/mcp_server/tools/_factories.py b/je_auto_control/utils/mcp_server/tools/_factories.py index 253acce4..7995e9fd 100644 --- a/je_auto_control/utils/mcp_server/tools/_factories.py +++ b/je_auto_control/utils/mcp_server/tools/_factories.py @@ -1797,6 +1797,35 @@ def smart_wait_tools() -> List[MCPTool]: handler=h.plan_retry_delays, annotations=READ_ONLY, ), + MCPTool( + name="ac_compare_field_value", + description=("Compare an 'expected' vs 'actual' field value under a " + "match 'mode' (exact / trim / ci / normalized / " + "contains). Pure. Returns {match, mode, expected, " + "actual}."), + input_schema=schema({"expected": {"type": "string"}, + "actual": {"type": "string"}, + "mode": {"type": "string"}}, + required=["expected", "actual"]), + handler=h.compare_field_value, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_verify_field_value", + description=("Read a native control's value back (accessibility) " + "and confirm it equals 'expected' under match 'mode'. " + "Identify the control by name / role / app_name / " + "automation_id. Returns {match, expected, actual}."), + input_schema=schema({"expected": {"type": "string"}, + "name": {"type": "string"}, + "role": {"type": "string"}, + "app_name": {"type": "string"}, + "automation_id": {"type": "string"}, + "mode": {"type": "string"}}, + required=["expected"]), + handler=h.verify_field_value, + 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 e0755250..42ce390a 100644 --- a/je_auto_control/utils/mcp_server/tools/_handlers.py +++ b/je_auto_control/utils/mcp_server/tools/_handlers.py @@ -682,6 +682,22 @@ def plan_retry_delays(attempts, base=0.1, max_delay=5.0, multiplier=2.0, return _plan_retry_delays(attempts, base, max_delay, multiplier, jitter) +def compare_field_value(expected, actual, mode="exact"): + from je_auto_control.utils.executor.action_executor import ( + _compare_field_value, + ) + return _compare_field_value(expected, actual, mode) + + +def verify_field_value(expected, name=None, role=None, app_name=None, + automation_id=None, mode="exact"): + from je_auto_control.utils.executor.action_executor import ( + _verify_field_value, + ) + return _verify_field_value(expected, name, role, app_name, automation_id, + mode) + + def normalize_ext(target): from je_auto_control.utils.executor.action_executor import _normalize_ext return _normalize_ext(target) diff --git a/je_auto_control/utils/verify_field/__init__.py b/je_auto_control/utils/verify_field/__init__.py new file mode 100644 index 00000000..9eefae69 --- /dev/null +++ b/je_auto_control/utils/verify_field/__init__.py @@ -0,0 +1,11 @@ +"""Read a field back after typing and confirm it holds the intended value.""" +from je_auto_control.utils.verify_field.verify_field import ( + MATCH_CI, MATCH_CONTAINS, MATCH_EXACT, MATCH_NORMALIZED, MATCH_TRIM, + compare_field_value, fill_and_verify, verify_field_value, +) + +__all__ = [ + "compare_field_value", "verify_field_value", "fill_and_verify", + "MATCH_EXACT", "MATCH_TRIM", "MATCH_CI", "MATCH_NORMALIZED", + "MATCH_CONTAINS", +] diff --git a/je_auto_control/utils/verify_field/verify_field.py b/je_auto_control/utils/verify_field/verify_field.py new file mode 100644 index 00000000..73e27fa6 --- /dev/null +++ b/je_auto_control/utils/verify_field/verify_field.py @@ -0,0 +1,101 @@ +"""Read a field back after typing and confirm it holds the intended value. + +``field_entry`` types into a control and *hopes* it landed: a slow IME, a focus +steal, an input mask or an auto-format can silently mangle or drop characters, +and nothing reads the field back to notice. This is distinct from +``action_effect`` (did *anything* change near the target?) and +``postcondition.text_present`` (does the text appear *anywhere* on screen?) — +neither confirms *this* field now equals *this* value. + +* :func:`compare_field_value` — pure: compare an expected and actual value under + a match ``mode`` (``exact`` / ``trim`` / ``ci`` / ``normalized`` / + ``contains``). +* :func:`verify_field_value` — read the field through an injectable ``reader`` + and compare. +* :func:`fill_and_verify` — type through an injectable ``filler``, read back, and + retry (optionally clearing first) until it matches or attempts run out. + +The reader / filler seams default to the native accessibility value in the +executor, but every comparison and retry decision is pure and testable without a +real control. Imports no ``PySide6``. +""" +from typing import Any, Callable, Dict, Optional + +# Match modes. +MATCH_EXACT = "exact" +MATCH_TRIM = "trim" +MATCH_CI = "ci" +MATCH_NORMALIZED = "normalized" +MATCH_CONTAINS = "contains" + +# A reader returns the field's current value; a filler types a value into it. +FieldReader = Callable[[], Optional[str]] +FieldFiller = Callable[[str], None] + + +def _canonical(text: str, mode: str) -> str: + """Canonicalize ``text`` for comparison under ``mode`` (pure).""" + if mode in (MATCH_TRIM, MATCH_CI, MATCH_CONTAINS): + text = text.strip() + if mode in (MATCH_CI, MATCH_CONTAINS): + text = text.casefold() + if mode == MATCH_NORMALIZED: + from je_auto_control.utils.text_normalize import normalize_text + return normalize_text(text) + return text + + +def _as_text(value: Any) -> str: + """Coerce a value to a string, treating ``None`` as empty.""" + return "" if value is None else str(value) + + +def compare_field_value(expected: Any, actual: Any, *, + mode: str = MATCH_EXACT) -> Dict[str, Any]: + """Compare ``expected`` against ``actual`` under ``mode`` (pure). + + Returns ``{match, mode, expected, actual}``. ``contains`` is a (trimmed, + case-insensitive) substring test; the others compare canonical equality. + """ + expected_text = _as_text(expected) + actual_text = _as_text(actual) + if mode == MATCH_CONTAINS: + match = _canonical(expected_text, mode) in _canonical(actual_text, mode) + else: + match = _canonical(expected_text, mode) == _canonical(actual_text, mode) + return {"match": bool(match), "mode": mode, + "expected": expected_text, "actual": actual_text} + + +def verify_field_value(expected: Any, *, reader: FieldReader, + mode: str = MATCH_EXACT) -> Dict[str, Any]: + """Read the field via ``reader`` and compare it to ``expected``. + + Returns the :func:`compare_field_value` result for the value read back. + """ + return compare_field_value(expected, reader(), mode=mode) + + +def fill_and_verify(value: Any, *, filler: FieldFiller, reader: FieldReader, + attempts: int = 2, mode: str = MATCH_EXACT, + clear: Optional[Callable[[], None]] = None + ) -> Dict[str, Any]: + """Type ``value`` via ``filler``, read it back, and retry until it matches. + + Up to ``attempts`` tries; before each retry (not the first) ``clear`` is + called if supplied. Returns the final :func:`compare_field_value` result + with an added ``attempts`` count. + """ + total = max(1, int(attempts)) + result: Dict[str, Any] = compare_field_value(value, None, mode=mode) + used = 0 + for used in range(1, total + 1): + if clear is not None and used > 1: + clear() + filler(value) + result = compare_field_value(value, reader(), mode=mode) + if result["match"]: + break + result = dict(result) + result["attempts"] = used + return result diff --git a/test/unit_test/headless/test_verify_field_batch.py b/test/unit_test/headless/test_verify_field_batch.py new file mode 100644 index 00000000..b00dc3ac --- /dev/null +++ b/test/unit_test/headless/test_verify_field_batch.py @@ -0,0 +1,121 @@ +"""Headless tests for verify_field (pure compare + injected reader / filler).""" +import unicodedata + +import je_auto_control as ac +from je_auto_control.utils.verify_field import ( + MATCH_CI, MATCH_CONTAINS, MATCH_EXACT, MATCH_NORMALIZED, MATCH_TRIM, + compare_field_value, fill_and_verify, verify_field_value, +) + + +# --- pure compare --------------------------------------------------------- + +def test_compare_exact(): + assert compare_field_value("abc", "abc")["match"] is True + assert compare_field_value("abc", "abd")["match"] is False + + +def test_compare_trim_and_ci(): + assert compare_field_value("hi", " hi ", mode=MATCH_TRIM)["match"] is True + assert compare_field_value("Hi", "hi", mode=MATCH_EXACT)["match"] is False + assert compare_field_value("Hi", " HI ", mode=MATCH_CI)["match"] is True + + +def test_compare_contains(): + assert compare_field_value("ell", "Hello", mode=MATCH_CONTAINS)["match"] + assert not compare_field_value("xyz", "Hello", + mode=MATCH_CONTAINS)["match"] + + +def test_compare_normalized_unicode(): + # Precomposed (U+00E9) vs decomposed (e + U+0301) "cafe" differ byte-wise + # but match once NFKC-normalized. + base = "café" + nfc = unicodedata.normalize("NFC", base) + nfd = unicodedata.normalize("NFD", base) + assert nfc != nfd + assert compare_field_value(nfc, nfd, mode=MATCH_NORMALIZED)["match"] is True + assert compare_field_value(nfc, nfd, mode=MATCH_EXACT)["match"] is False + + +def test_compare_none_is_empty(): + assert compare_field_value(None, "")["match"] is True + assert compare_field_value("", None)["match"] is True + result = compare_field_value("x", None) + assert result["match"] is False + assert result["actual"] == "" + + +# --- verify via injected reader ------------------------------------------- + +def test_verify_field_value_reads_back(): + result = verify_field_value("save.txt", reader=lambda: "save.txt") + assert result["match"] is True + assert result["actual"] == "save.txt" + + +def test_verify_field_value_mismatch(): + result = verify_field_value("save.txt", reader=lambda: "sve.txt") + assert result["match"] is False + + +# --- fill_and_verify retry ------------------------------------------------ + +def test_fill_and_verify_succeeds_first_try(): + typed = [] + result = fill_and_verify("hello", filler=typed.append, + reader=lambda: "hello") + assert result["match"] is True + assert result["attempts"] == 1 + assert typed == ["hello"] + + +def test_fill_and_verify_retries_until_match(): + reads = iter(["wrong", "hello"]) # first read fails, second succeeds + cleared = [] + typed = [] + result = fill_and_verify("hello", filler=typed.append, + reader=lambda: next(reads), + clear=lambda: cleared.append(True), attempts=3) + assert result["match"] is True + assert result["attempts"] == 2 + assert cleared == [True] # cleared once before the retry + assert typed == ["hello", "hello"] + + +def test_fill_and_verify_gives_up_after_attempts(): + typed = [] + result = fill_and_verify("hello", filler=typed.append, + reader=lambda: "nope", attempts=2) + assert result["match"] is False + assert result["attempts"] == 2 + assert len(typed) == 2 + + +# --- wiring --------------------------------------------------------------- + +def test_executor_pure_compare_path(): + from je_auto_control.utils.executor.action_executor import ( + _compare_field_value, + ) + out = _compare_field_value("a b", "a b", mode="normalized") + assert out["match"] is True + + +def test_wiring(): + known = set(ac.executor.known_commands()) + assert {"AC_compare_field_value", "AC_verify_field_value"} <= 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_compare_field_value", "ac_verify_field_value"} <= names + from je_auto_control.gui.script_builder.command_schema import _build_specs + specs = {s.command for s in _build_specs()} + assert {"AC_compare_field_value", "AC_verify_field_value"} <= specs + + +def test_facade_exports(): + for name in ("compare_field_value", "verify_field_value", + "fill_and_verify"): + assert hasattr(ac, name) and name in ac.__all__