diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index fb4ff1cd4..90f072a30 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -30,10 +30,10 @@ jobs: fail-fast: false matrix: include: - - { os: windows-2022, python-version: "3.10" } + - { os: windows-2022, python-version: "3.11" } - { os: windows-2022, python-version: "3.14" } - { os: ubuntu-22.04, python-version: "3.14" } - - { os: macos-15, python-version: "3.10" } + - { os: macos-15, python-version: "3.11" } steps: - uses: actions/checkout@v5 with: diff --git a/.github/workflows/platform-smoke.yml b/.github/workflows/platform-smoke.yml index cde4a6a4c..7744112c6 100644 --- a/.github/workflows/platform-smoke.yml +++ b/.github/workflows/platform-smoke.yml @@ -43,12 +43,7 @@ jobs: # # When both resolve, drop the markers from pyproject.toml. os: [windows-2022, ubuntu-22.04, macos-15, ubuntu-22.04-arm, windows-11-arm] - python-version: ["3.10", "3.14"] - exclude: - # CPython's official Windows arm64 builds start at 3.11, so - # setup-python has no 3.10 interpreter to fetch on this runner. - - os: windows-11-arm - python-version: "3.10" + python-version: ["3.14"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c85212e5e..a45ba6d95 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -68,20 +68,19 @@ jobs: # systems the project also claims to support. Linux and macOS are # here to measure them; they carry the two ends of the supported # Python range rather than all five, because what differs between - # 3.10 and 3.14 is Python and what differs here is the OS. + # 3.11 and 3.14 is Python and what differs here is the OS. runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - - { os: windows-2022, python-version: "3.10" } - { os: windows-2022, python-version: "3.11" } - { os: windows-2022, python-version: "3.12" } - { os: windows-2022, python-version: "3.13" } - { os: windows-2022, python-version: "3.14" } - - { os: ubuntu-22.04, python-version: "3.10" } + - { os: ubuntu-22.04, python-version: "3.11" } - { os: ubuntu-22.04, python-version: "3.14" } - - { os: macos-15, python-version: "3.10" } + - { os: macos-15, python-version: "3.11" } - { os: macos-15, python-version: "3.14" } steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml index 6c1534fbf..2f48fbb63 100644 --- a/.github/workflows/stable.yml +++ b/.github/workflows/stable.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] + python-version: [ "3.11", "3.12", "3.13", "3.14" ] steps: - uses: actions/checkout@v5 diff --git a/CHANGELOG.md b/CHANGELOG.md index a3148b81e..78b704952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.2.0] - 2026-11-02 + +Python 3.11 is now the minimum. Pillow 13.0.0 and dnspython 2.9.0, the releases that fix the advisories +Dependabot reports, need 3.11. While 3.10 was supported, the package had to pin Pillow 12.3.0 for it, and the +lock kept dnspython 2.8.0. `pip` on Python 3.10 keeps installing 1.1.0. + +- `requires-python` is `>=3.11`. The dependency is a single `pillow==13.0.0`, and the dnspython constraint + applies on every interpreter. +- The CI cells that ran 3.10 to cover Windows, Ubuntu and macOS now run 3.11, and mypy checks against 3.11. +- `je_auto_control.api` is unchanged. + ## [1.1.0] - 2026-11-02 1.1.0 merges the development line into the release line. The development line re-implemented several diff --git a/CLAUDE.md b/CLAUDE.md index b4331dbfd..c125f5d80 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ AutoControl (`je_auto_control`) is a cross-platform GUI automation framework: mouse and keyboard control, image recognition, OCR, accessibility-tree and VLM element location, action scripting, and report generation behind one API. Backends: Windows (Win32 ctypes), macOS (pyobjc/Quartz), Linux X11 (python-Xlib), Linux Wayland (libei / ydotool), Android (adb), iOS (WebDriverAgent). -- **Package**: `je_auto_control` · **Python** ≥ 3.10 · **License**: MIT · **Author**: JE-Chen +- **Package**: `je_auto_control` · **Python** ≥ 3.11 · **License**: MIT · **Author**: JE-Chen - **[architecture_explore.md](architecture_explore.md)** is the per-module map — read it before changing structure; it lists all 312 `utils/` subpackages, every GUI tab, and file-level tables for the large subsystems. ## Architecture diff --git a/Progress.md b/Progress.md index fcffc3b1d..03b970b83 100644 --- a/Progress.md +++ b/Progress.md @@ -561,6 +561,7 @@ U-20261009-18 的每一個裝置面呼叫都只對假的 ADB/uiautomator2/WD `TODO` — `test/unit_test/headless/test_usb_acl_prompt.py::test_bridge_remember_persists_acl_rule` 在 Python 3.10 的 Linux/macOS `coverage run -m pytest` 中仍有間歇 SIGSEGV(exit 139)紀錄。 需在對應環境開啟 `faulthandler` 重跑並定位原生呼叫;單次重跑通過不足以證明已修復。 +2026-11-02 起不再支援 Python 3.10,CI 不再跑 3.10;同一機制(分配時觸發循環回收)在 3.11 也存在,改看 3.11 的方格。 --- diff --git a/README.md b/README.md index 0f66b334e..3532f6259 100644 --- a/README.md +++ b/README.md @@ -786,8 +786,7 @@ management, the accessibility tree, the action executor, the MCP/REST/TCP servers and the GUI all work — measured, not assumed. Every other platform is unaffected. -**Requirements:** Python ≥ 3.10 (≥ 3.11 on Windows arm64, which is where -CPython's official builds for it start). On Linux, install build +**Requirements:** Python ≥ 3.11. On Linux, install build prerequisites first: ```bash diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index dacc86d6b..259a84b56 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -634,7 +634,7 @@ ACME/TLS 与加密录影会抛出指名缺哪个 wheel 的错误,而不是 鼠标、键盘、屏幕尺寸、窗口管理、无障碍树、动作执行器、MCP/REST/TCP 服务器 与 GUI 都正常——这是实测的,不是推论的。其他平台不受影响。 -**系统需求:** Python ≥ 3.10(Windows arm64≥ 3.11,CPython 官方构建从那里开始)。 +**系统需求:** Python ≥ 3.11。 Linux 请先安装构建依赖: ```bash diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 4ea84c709..9e684a4ee 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -633,7 +633,7 @@ ACME/TLS 與加密錄影會拋出指名缺哪個 wheel 的錯誤,而不是 滑鼠、鍵盤、螢幕尺寸、視窗管理、無障礙樹、動作執行器、MCP/REST/TCP 伺服器 與 GUI 都正常——這是實測的,不是推論的。其他平台不受影響。 -**系統需求:** Python ≥ 3.10(Windows arm64≥ 3.11,CPython 官方建置從那裡開始)。 +**系統需求:** Python ≥ 3.11。 Linux 請先安裝建置前置套件: ```bash diff --git a/Third_Party_License.md b/Third_Party_License.md index 796cee612..6eaa9b5dd 100644 --- a/Third_Party_License.md +++ b/Third_Party_License.md @@ -11,7 +11,7 @@ Full copies of the upstream license texts are archived under `LICENSEs/`. | Package | Version | License | Purpose | |---|---|---|---| | [je_open_cv](https://pypi.org/project/je-open-cv/) | 0.0.22 | MIT | OpenCV-based image recognition helpers (`locate_all_image`, template match) | -| [Pillow](https://pypi.org/project/Pillow/) | 13.0.0 (12.3.0 on Python 3.10) | MIT-CMU (HPND) | Screenshot encoding, image I/O | +| [Pillow](https://pypi.org/project/Pillow/) | 13.0.0 | MIT-CMU (HPND) | Screenshot encoding, image I/O | | [mss](https://pypi.org/project/mss/) | 10.1.0 | MIT | Fast multi-monitor screenshot backend | | [pyobjc-core](https://pypi.org/project/pyobjc-core/) | 12.1 | MIT | macOS backend — Python/Objective-C bridge *(Darwin only)* | | [pyobjc](https://pypi.org/project/pyobjc/) | 12.1 | MIT | macOS backend — Cocoa / Quartz bindings *(Darwin only)* | diff --git a/architecture_explore.md b/architecture_explore.md index c7567dbe6..7dda09cf4 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -25,7 +25,7 @@ iOS(WebDriverAgent)。核心能力是滑鼠/鍵盤控制、影像辨識、 | 指標 | 數值 | | --- | ---: | | Python 模組總數(含周邊子專案) | 1,494 | -| 程式碼總行數 | 227,738 | +| 程式碼總行數 | 228,248 | | `je_auto_control/utils/` 子套件數 | 311 | | `AC_*` 動作指令數(`known_commands()` 實測) | 911 | | 套件門面 `__all__` 公開名稱數 | 1504 | @@ -34,7 +34,7 @@ iOS(WebDriverAgent)。核心能力是滑鼠/鍵盤控制、影像辨識、 | `test_*.py` 測試檔/測試函式 | 846 / 8,825 | | 範例腳本 | 45 | -**技術基線**:Python ≥ 3.10、MIT 授權、必要相依只有 `je_open_cv`/`opencv-python`/`pillow`/`mss`/ +**技術基線**:Python ≥ 3.11、MIT 授權、必要相依只有 `je_open_cv`/`opencv-python`/`pillow`/`mss`/ `defusedxml`/`cryptography`(加上各平台專屬的 pyobjc、python-Xlib);其餘全部是選用 extras (`gui`、`webrtc`、`signaling`、`discovery`、`pdf`、`office`、`fuzzy`、`s3`、`locale`、`audio`)。 大量子系統刻意只用標準庫實作(REST 伺服器、JSON Schema、JWT、TOTP、WebSocket 框架、ACME、 @@ -164,7 +164,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | --- | ---: | --- | | `je_auto_control/__init__.py` | 2,738 | **套件門面**。按首次存取解析 1504 個公開名稱,具體型別宣告與 `__all__` 保留原始來源及順序。 | | `je_auto_control/_lazy_exports.py` | 29 | 公開名稱的來源屬性/模組繫結與首次存取解析;保留並行的明確指派。 | -| `je_auto_control/_export_bindings.py` | 1,580 | 由具體型別宣告產生的公開名稱來源查表;CI 檢查產生結果與順序。 | +| `je_auto_control/_export_bindings.py` | 1,839 | 由具體型別宣告產生的公開名稱來源查表;CI 檢查產生結果與順序。 | | `je_auto_control/__main__.py` | 106 | 舊版 argparse 進入點:`-e` 執行單檔、`-d` 執行整個目錄、`--execute_str` 執行 JSON 字串、`-c` 建立專案。 | | `je_auto_control/cli.py` | 455 | **主 CLI**(`je_auto_control` console script)。子命令:`run`(含 `--var`/`--dry-run`/`--journal`)、`journal`(依 run 篩選與有界遮罩快照)、`validate`/`lint`、`list-commands`、`fmt`、`record`、`codegen`、`failure-bundle`、`list-jobs`、`start-server`、`start-rest`、`version`。所有子命令延遲匯入,確保不碰 Qt。 | | `je_auto_control/api/__init__.py` | 100 | 版本化整合進入點。 | @@ -183,7 +183,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | --- | ---: | --- | | `wrapper/platform_wrapper.py` | 137 | **Strategy 樞紐**。依 `sys.platform` 匯入唯一後端並匯出 `keyboard`、`keyboard_check`、`keyboard_keys_table`、`mouse`、`mouse_keys_table`、`special_mouse_keys_table`、`screen`、`recorder`;八個名稱都帶著 `backend_contract` 的型別出去,其中 `keyboard`/`mouse` 因為四個分支綁的是三種互不相容的形狀,先落在私有的 `_keyboard`/`_mouse`(`Any`)上再標合約;載入失敗直接拋 `AutoControlException`(fail fast);X11 的 `keyboard_keys_table` 透過明確存取才取得真正 dict,其他後端保留原有表。 | | `wrapper/backend_contract.py` | 249 | 平台縫的型別合約:`ScreenBackend`/`KeyboardCheckBackend`/`RecorderBackend` 三個跨平台 Protocol,加上 `keyboard`/`mouse` 各自的三份——`Win32*`(SendInput 與 Interception)、`Darwin*`(Quartz)、`X11Unix*`(XTest/uinput/Wayland/BSD),因為這兩個名稱的呼叫形狀真的因平台而異;`KeyboardBackend`/`MouseBackend` 依 `sys.platform` 別名到其中一組,所以呼叫端被檢查的是它真的會走到的簽章。四個 `_platform_*` 組裝模組各自標注自己綁的是什麼,少一個成員就在該後端自己的檔案裡紅掉,而不是在三層之上的呼叫點。 | -| `wrapper/capabilities.py` | 283 | 無副作用的 immutable context/input-capture metadata;不載入原生依賴或要求授權,XWayland 範圍明示。 | +| `wrapper/capabilities.py` | 295 | 無副作用的 immutable context/input-capture metadata;不載入原生依賴或要求授權,XWayland 範圍明示。 | | `wrapper/_platform_windows.py` | 376 | Windows 後端組裝:Win32 ctypes 模組 + 虛擬鍵表(含 OEM 鍵與 `keyboard_key_aliases` 別名表)+ 選用 Interception 驅動。 | | `wrapper/_platform_osx.py` | 162 | macOS 後端組裝(Quartz 事件 + osx 虛擬鍵表)。 | | `wrapper/_platform_linux.py` | 155 | X11 後端組裝(python-Xlib + 選用 uinput)。鍵盤/滑鼠共用延後選擇的輸入後端;匯入不探測 uinput,鍵碼表只在明確存取時解析成共用 dict。 | @@ -285,7 +285,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.1 執行引擎與腳本資產 -> 25 個套件、約 19,487 行。 +> 25 個套件、約 19,488 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -296,7 +296,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/dag/` | 1,352 | 跨主機動作/受管任務 DAG;權限及停止傳遞、目的地完成/釋放驗證、去重與未知結果審閱 | | `utils/decision_table/` | 112 | DMN 風格決策表:規則 + 命中策略,把分支外部化 | | `utils/deterministic/` | 116 | 決定性執行控制:固定亂數種子 + 凍結時鐘 | -| `utils/action_journal/` | 855 | 結構化動作日誌:schema v1、寫入前遮罩、跨行程排序/尾端復原、executor 明確 scope 擷取與本機 parallel/DAG parent 傳遞;門面、AC/MCP、Script Builder 與 CLI 擷取/有界查閱;observer/路徑政策隨巢狀根與 workers 保留,Run History 查閱與手動 history 關聯共用相同來源。 | +| `utils/action_journal/` | 856 | 結構化動作日誌:schema v1、寫入前遮罩、跨行程排序/尾端復原、executor 明確 scope 擷取與本機 parallel/DAG parent 傳遞;門面、AC/MCP、Script Builder 與 CLI 擷取/有界查閱;observer/路徑政策隨巢狀根與 workers 保留,Run History 查閱與手動 history 關聯共用相同來源。 | | `utils/executor/` | 10,702 | **核心**。`Executor` 指令分派表(774 個 `AC_*`)、參數插值、乾跑、逐步 callback;`flow_control` 提供 34 個區塊指令(迴圈/分支/try/巨集/變數) | | `utils/flow_debugger/` | 166 | action list 的單步除錯器與追蹤器 | | `utils/input_macro/` | 469 | 定時輸入事件:錄製結果的整形(`timeline`/`InputRecorder`,Windows 與 macOS 共用)、重播與宣告式輸入序列 DSL | @@ -317,7 +317,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.2 框架基礎設施 -> 14 個套件、約 3,791 行。 +> 14 個套件、約 3,796 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -329,9 +329,9 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/exception/` | 377 | **例外階層根**。框架錯誤繼承 `AutoControlException`;`control_signals.py` 共用無 executor 相依的取消載體/分類,保留 MCP 與 history 舊匯入別名;`cancellation.py` 提供 Qt-free 協作式 token 與 immutable progress;另有集中式錯誤訊息(`exception_tags`)。 | | `utils/failure_bundle/` | 229 | 可攜、已遮蔽的失敗診斷 ZIP(截圖 + 診斷 + log 尾段) | | `utils/file_process/` | 40 | 目錄檔案列舉(`execute_dir` 的後端) | -| `utils/logging/` | 354 | `autocontrol_logger`、共用 handler、executor/CLI/REST/MCP/背景 run 的 vault 值遮罩與持久化診斷快照 | +| `utils/logging/` | 358 | `autocontrol_logger`、共用 handler、executor/CLI/REST/MCP/背景 run 的 vault 值遮罩與持久化診斷快照 | | `utils/package_manager/` | 193 | 動態載入套件並把 executor 注入其中;載入前先過套件閘門(允許清單加上只能從 Python 呼叫的開關,工作區 X-12) | -| `utils/path_guard/` | 266 | 命令列及伺服器的路徑邊界、客戶端根目錄隔離與受管任務來源政策紀錄 | +| `utils/path_guard/` | 267 | 命令列及伺服器的路徑邊界、客戶端根目錄隔離與受管任務來源政策紀錄 | | `utils/platform_id/` | 62 | 作業系統家族的單一判定點。`sys.platform` 原本在一百多處跟字面清單比對,而那些清單都沒有 BSD;`is_x11_unix()` 問的是「這是不是 X11 unix」,這才是守衛一直想問的問題 | | `utils/shell_process/` | 279 | `ShellManager`:以 argv list 執行外部命令(禁用 `shell=True`) | | `utils/start_exe/` | 36 | 啟動另一個執行檔行程 | @@ -478,7 +478,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.8 元素定位、自我修復與智慧等待 -> 23 個套件、約 6,336 行。 +> 23 個套件、約 6,339 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -501,20 +501,20 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/observation_delta/` | 122 | token 預算內的觀察差異:兩個 UI 影格之間變了什麼 | | `utils/screen_state/` | 191 | 語義畫面狀態:快照/差異與結構化畫面描述 | | `utils/scroll_find/` | 103 | 捲動直到目標影像/文字可見 | -| `utils/self_healing/` | 2,233 | 執行時自癒定位與固定 PNG 版本比較、明確母體/延遲/來源報告 | +| `utils/self_healing/` | 2,236 | 執行時自癒定位與固定 PNG 版本比較、明確母體/延遲/來源報告 | | `utils/semantic_recording/` | 498 | 為錄製內容加上語義錨點,支援換機重播與自癒重播 | | `utils/settle_detector/` | 79 | 以純函式介面判定 UI 是否已靜止 | | `utils/smart_waits/` | 663 | 智慧等待:以影格差異取代 `time.sleep` | ### 5.4.9 AI / Agent / LLM -> 14 個套件、約 52,059 行。 +> 14 個套件、約 52,131 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | | `utils/a2a/` | 92 | A2A(agent-to-agent)agent card 產生 | | `utils/agent/` | 2,253 | 閉環 Computer-Use Agent 主迴圈 + Anthropic/OpenAI/Computer-Use 三後端 | -| `utils/agent_runtime/` | 25,831 | 受管任務、服務、控制權世代、結果驗證與本機推論;所有入口共用執行層。 | +| `utils/agent_runtime/` | 25,891 | 受管任務、服務、控制權世代、結果驗證與本機推論;所有入口共用執行層。 | | `utils/agent_memory/` | 166 | agent 的持久化情節記憶(goal → trajectory → outcome) | | `utils/agent_replay/` | 67 | 可攜的 agent 軌跡追蹤(記錄 observation→action 並重播) | | `utils/agent_trace/` | 172 | agent 可觀測性:OpenTelemetry GenAI 慣例的 LLM span | @@ -522,21 +522,21 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/cua_action/` | 204 | 標準化 computer-use 動作結構(Anthropic/OpenAI → `AC_*`) | | `utils/llm/` | 369 | 自然語言 → action list 規劃器 + Anthropic/null 後端;規劃執行隔離頂層變數並沿用巢狀範圍 | | `utils/mcp_registry/` | 97 | MCP registry `server.json` 資訊清單產生(可被發現) | -| `utils/mcp_server/` | 21,478 | **無頭 MCP 伺服器**(預設註冊 820 個工具=801 個 `ac_*` + 19 個別名):stdio + HTTP 傳輸、工具工廠與處理器、資源、prompt、稽核、限流、外掛熱重載 | +| `utils/mcp_server/` | 21,490 | **無頭 MCP 伺服器**(預設註冊 820 個工具=801 個 `ac_*` + 19 個別名):stdio + HTTP 傳輸、工具工廠與處理器、資源、prompt、稽核、限流、外掛熱重載 | | `utils/tool_use_schema/` | 191 | 把 `AC_*` 指令匯出成 Claude/OpenAI 的 tool-use schema | | `utils/trajectory_eval/` | 132 | agent 軌跡評估:依評分規準為一次執行打分 | | `utils/vision/` | 660 | VLM 元素定位器(依描述找元素)+ Anthropic/OpenAI/null 後端 | ### 5.4.10 遠端桌面與 USB -> 6 個套件、約 30,418 行。 +> 6 個套件、約 30,477 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | | `utils/admin/` | 423 | 多主機管理主控台:平行輪詢 N 個 AutoControl REST 端點 | -| `utils/config_sync/` | 8,496 | 持久化設定與條件交易:`contract.py` 管理有限 JSON 與協定例外;`store.py` 以 SQLite 原子比較版本/記錄重送收據;`routes.py` 提供第二版 HTTP 與明確盲寫相容;`commands.py` 共用 AC/MCP/Builder;`client.py` 條件提交,另有因果模型、peer ledger 與本機 SQLite outbox;第三版 /causal 交易已接上確認/退休/回收與刪除憑證;client/outbox 已接上原請求重送、有界 CAS 重合併及原子快照確認;持久化控制、原子本機 full-resync/舊待送退役與歷史收據已接上。本機資產工具另提供 manifest、整版雜湊驗證與不可變目錄發布;七種來源 adapters 保存可攜因果 profiles;workspace INI 與 GUI 共用偏好正規化。來源/outbox 與原子 full-resync 基準已由 SyncWorkspace 協調;引用資產以不可變分塊/完整 hash 收據、具驗證 HTTP 與完整 profile 串流發布接上;Sync UI 仍屬 C3。 | +| `utils/config_sync/` | 8,542 | 持久化設定與條件交易:`contract.py` 管理有限 JSON 與協定例外;`store.py` 以 SQLite 原子比較版本/記錄重送收據;`routes.py` 提供第二版 HTTP 與明確盲寫相容;`commands.py` 共用 AC/MCP/Builder;`client.py` 條件提交,另有因果模型、peer ledger 與本機 SQLite outbox;第三版 /causal 交易已接上確認/退休/回收與刪除憑證;client/outbox 已接上原請求重送、有界 CAS 重合併及原子快照確認;持久化控制、原子本機 full-resync/舊待送退役與歷史收據已接上。本機資產工具另提供 manifest、整版雜湊驗證與不可變目錄發布;七種來源 adapters 保存可攜因果 profiles;workspace INI 與 GUI 共用偏好正規化。來源/outbox 與原子 full-resync 基準已由 SyncWorkspace 協調;引用資產以不可變分塊/完整 hash 收據、具驗證 HTTP 與完整 profile 串流發布接上;Sync UI 仍屬 C3。 | | `utils/device_matrix/` | 201 | 行動裝置矩陣:同一 action list 於多台裝置平行執行 | -| `utils/remote_desktop/` | 15,230 | **遠端桌面子系統**:TCP/WebSocket/WebRTC 三條傳輸路徑、主機與檢視端、訊令伺服器、TURN/中繼、多檢視者、錄影、信任清單、TOTP、稽核鏈;私有選用功能模組保留整組匯入結果。 | +| `utils/remote_desktop/` | 15,243 | **遠端桌面子系統**:TCP/WebSocket/WebRTC 三條傳輸路徑、主機與檢視端、訊令伺服器、TURN/中繼、多檢視者、錄影、信任清單、TOTP、稽核鏈;私有選用功能模組保留整組匯入結果。 | | `utils/usb/` | 5,060 | 跨平台 USB 列舉/熱插拔/裝置直通(WinUSB、IOKit、libusb 後端 + ACL + WebRTC DataChannel 通道) | | `utils/usbip/` | 1,008 | USB/IP 線路協定主機端(協定封包、TCP 伺服器、libusb URB 後端) | @@ -554,7 +554,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.11 伺服器、網路協定與外部整合 -> 24 個套件、約 7,570 行。 +> 24 個套件、約 7,571 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -576,7 +576,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/otp/` | 37 | TOTP 一次性密碼產生(自動化 2FA 登入) | | `utils/outbox/` | 107 | 交易式 outbox,保證至少一次的事件投遞 | | `utils/pytest_plugin/` | 283 | pytest 外掛 + BDD step library(`pytest11` entry point) | -| `utils/rest_api/` | 2,225 | 純標準庫 REST 前端:路由、Bearer 驗證、限流、Prometheus 指標、OpenAPI 3.1 產生 | +| `utils/rest_api/` | 2,226 | 純標準庫 REST 前端:路由、Bearer 驗證、限流、Prometheus 指標、OpenAPI 3.1 產生 | | `utils/socket_server/` | 163 | 執行 action JSON 的執行緒式 TCP 指令伺服器(預設綁 127.0.0.1) | | `utils/sse_client/` | 142 | Server-Sent Events 用戶端解析 | | `utils/tls_acme/` | 475 | TLS 自動化:HTTP-01 挑戰伺服器、金鑰/CSR、自動續期 | @@ -735,7 +735,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `mouse_aliases.py` | 39 | 單鍵點擊別名(`AC_click_left` 等),executor 與 callback executor 共用。 | | `flags.py` | 19 | 旗標讀取 `as_bool`:依拼法(`"true"`/`"yes"`/`"on"`/`"1"`)而非 Python 真值判斷,所以 JSON、CLI、MCP 傳來的 `"false"` 是關;所有 adapter 與流程指令共用。 | -#### `utils/mcp_server/`(21,478 行,820 個工具)— 最大子系統 +#### `utils/mcp_server/`(21,490 行,820 個工具)— 最大子系統 | 檔案 | 行數 | 職責 | | --- | ---: | --- | @@ -752,7 +752,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `tools/_healing_evaluation.py` | 40 | 固定影像比較工具與報告匯出。 | | `tools/_handlers_journal.py` | 75 | 日誌擷取與查閱 adapter:進度、合作式取消及保留執行資料的受控失敗。 | | `_tool_notifications.py` | 44 | Publish tool-list changes to the live HTTP session streams owned by one transport. | -| `disclosure.py` | 212 | Session-local availability and bounded opaque cursors; disclosure never grants execution authority. | +| `disclosure.py` | 213 | Session-local availability and bounded opaque cursors; disclosure never grants execution authority. | | `_disclosure_server.py` | 161 | Bind tool availability to transport lifetimes while retaining the existing dispatcher gates. | | `stdio.py` | 23 | Lazy stdio entry point with optional host-configured tool availability. | | `discovery.py` | 140 | Bounded, authority-filtered tool summaries and detached schemas from one registry snapshot. | @@ -788,7 +788,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `rate_limit.py` | 48 | 工具呼叫的 token bucket 限流。 | | `__main__.py` | 126 | `je_auto_control_mcp` console script 進入點。 | -#### `utils/remote_desktop/`(15,230 行/72 檔) +#### `utils/remote_desktop/`(15,243 行/72 檔) 三條傳輸路徑並存:**TCP**(JPEG 影格)、**WebSocket**(同協定換傳輸)、**WebRTC**(aiortc 視訊 + DataChannel)。 `_export_bindings.py` 保存產生的來源對照;`_optional/` 分別管理 WebRTC、錄影、多檢視者、 @@ -806,7 +806,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `registry.py` | 564 | `AC_remote_*` 指令使用的行程級單例。 | | `webrtc_transport.py` | 421 | 共用 WebRTC 管線:asyncio 橋接執行緒、螢幕視訊軌、設定。 | | `multi_viewer.py` | 357 | 每個連入檢視端各跑一個 `WebRTCDesktopHost` 的協調器。 | -| `signaling_server.py` | 392 | WebRTC SDP rendezvous、持久化設定及具驗證的資產 chunk/blob 路由;CLI 提供 config/asset store 路徑與明確舊版寫入相容。 | +| `signaling_server.py` | 393 | WebRTC SDP rendezvous、持久化設定及具驗證的資產 chunk/blob 路由;CLI 提供 config/asset store 路徑與明確舊版寫入相容。 | | `audit_log.py` | 355 | SQLite 雜湊鏈稽核記錄。 | | `host_capture.py` | 323 | TCP 主機的影格與游標產生:螢幕列舉、監視器索引轉擷取區域、預設 JPEG/游標 provider,以及 `FrameProductionMixin`(游標輪詢、擷取迴圈、上線編碼)。 | | `ws_protocol.py` | 318 | 最小 RFC 6455 WebSocket 框架與握手。 | @@ -818,9 +818,9 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `jpeg_recorder_encrypted.py` | 239 | AES-GCM 加密版 session 錄影。 | | `address_book.py` | 229 | 檢視端的主機通訊錄。 | | `audio.py` / `webrtc_audio.py` / `webrtc_mic.py` | 243 / 207 / 155 | 音訊擷取播放、音訊軌、麥克風上行。 | -| `webrtc_file_commit.py` | 354 | Confirm native file publication with bounded authenticated probes and transfer/hash/size receipt matching. | +| `webrtc_file_commit.py` | 355 | Confirm native file publication with bounded authenticated probes and transfer/hash/size receipt matching. | | `webrtc_file_callbacks.py` | 38 | Notify native file path and frozen receipt listeners while preserving original interruption objects. | -| `webrtc_file_receive.py` | 89 | Bind file receivers to one authenticated peer and channel, retiring partial bytes when ownership ends. | +| `webrtc_file_receive.py` | 101 | Bind file receivers to one authenticated peer and channel, retiring partial bytes when ownership ends. | | `webrtc_file_write.py` | 209 | Bounded observable DataChannel writes preserve caller cancellation and serialize file producers. | | `webrtc_file_tasks.py` | 117 | Run remote inbox pushes outside the event loop while preserving connection ownership and cooperative cancellation. | | `webrtc_files.py` | 416 | 專屬 DataChannel 的分塊檔案傳輸、可選校驗與原子發布確認。 | @@ -872,11 +872,11 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `usbip/libusb_backend.py` | 224 | 以 PyUSB/libusb 執行 URB 的正式後端。 | | `usbip/backend.py` | 87 | 可插拔 URB 執行後端。 | -#### `utils/rest_api/`(2,225 行) +#### `utils/rest_api/`(2,226 行) | 檔案 | 行數 | 職責 | | --- | ---: | --- | -| `rest_server.py` | 710 | HTTP 前端主體;相容 check-only gate,RBAC 必須帶使用者身分。 | +| `rest_server.py` | 711 | HTTP 前端主體;相容 check-only gate,RBAC 必須帶使用者身分。 | | `rest_handlers.py` | 539 | 端點實作。 | | `rest_openapi.py` | 503 | 走訪路由表產生 OpenAPI 3.1 規格。 | | `rest_auth.py` | 227 | Bearer token 驗證 + 逐 client 限流閘門。 | @@ -901,22 +901,22 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/cv2_utils/template_detection.py` | 182 | Locate a template image on screen, in the coordinates the mouse takes. | | `utils/cv2_utils/video_recording.py` | 76 | Internal image/capture utilities. | -#### `utils/config_sync/`(8,496 行)— 持久化、因果模型與本機重送 +#### `utils/config_sync/`(8,542 行)— 持久化、因果模型與本機重送 | 模組 | 行數 | 職責 | | --- | ---: | --- | | `utils/config_sync/__init__.py` | 81 | 設定、因果模型與本機 outbox 的無頭匯出;HTTP 整合範圍明確分開。 | -| `utils/config_sync/client.py` | 351 | 既有 HTTP/第二版 CAS 與時間戳合併;提供因果 transport 共用的覆寫式衝突分類掛鉤。 | +| `utils/config_sync/client.py` | 357 | 既有 HTTP/第二版 CAS 與時間戳合併;提供因果 transport 共用的覆寫式衝突分類掛鉤。 | | `utils/config_sync/contract.py` | 57 | 共用有限 JSON、識別碼/版本與平坦例外契約。 | | `utils/config_sync/database.py` | 49 | 延後解析路徑、按操作持有與釋放的可選 SQLite 連線。 | | `utils/config_sync/store.py` | 177 | bucket CAS 交易、歷史操作收據、帳號容量與明確盲寫相容。 | -| `utils/config_sync/routes.py` | 79 | 可選 signaling 的私有第二版設定路由、專用 commit 與第三版路由註冊;舊讀取拒絕因果帳號。 | +| `utils/config_sync/routes.py` | 89 | 可選 signaling 的私有第二版設定路由、專用 commit 與第三版路由註冊;舊讀取拒絕因果帳號。 | | `utils/config_sync/commands.py` | 37 | 本機設定庫 v2/因果 v3 get/commit 共用 AC/MCP/Builder,保留根目錄與取消檢查。 | | `utils/config_sync/versions.py` | 155 | 因果版本向量、雙方衝突保存、封存 JSON 與明確觀察後的解決。 | | `utils/config_sync/peers.py` | 132 | 可序列化 peer 確認/退休/full-resync epoch 與刪除回收資格判斷。 | | `utils/config_sync/outbox_models.py` | 193 | 操作身分/來源封存、端點正規化、重試預算與投遞報告。 | | `utils/config_sync/outbox_schema.py` | 98 | 辨識自有 outbox SQLite application/schema,驗證並原子遷移版本 1/2→3,拒絕修改外來資料庫。 | -| `utils/config_sync/outbox.py` | 365 | 本機持久化佇列、固定準備請求、有界退避/取消與 lease;成功收據與完整伺服器快照原子確認。 | +| `utils/config_sync/outbox.py` | 373 | 本機持久化佇列、固定準備請求、有界退避/取消與 lease;成功收據與完整伺服器快照原子確認。 | | `utils/config_sync/causal_commands.py` | 79 | 純資料合併/peer 轉換與私有佇列 enqueue/status;不連線或套用設定。 | | `utils/config_sync/causal_models.py` | 145 | 明確 actor、完整衝突 alternatives 與不保留舊 payload 的永久刪除憑證。 | | `utils/config_sync/causal_merge.py` | 68 | 確定性多值 frontier、因果支配、操作分叉拒絕與刪除憑證合併。 | @@ -927,14 +927,14 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/config_sync/causal_routes.py` | 53 | 可選 signaling 的獨立第三版 GET/PUT 路由與 full-resync 拒絕回應。 | | `utils/config_sync/causal_wire.py` | 65 | 共用既有 HTTP egress/redirect 限制的第三版 transport,嚴格區分過期版本與操作 ID 衝突。 | | `utils/config_sync/asset_models.py` | 130 | 可攜資產名稱、精確大小/SHA-256 與第一版不可變 manifest。 | -| `utils/config_sync/assets.py` | 269 | 整版暫存驗證後一次發布不可變目錄;精確重驗快取與取消清理。 | +| `utils/config_sync/assets.py` | 270 | 整版暫存驗證後一次發布不可變目錄;精確重驗快取與取消清理。 | | `utils/config_sync/asset_commands.py` | 52 | 私有本機 manifest/資產版本命令,共用即時 roots 與取消檢查。 | | `utils/config_sync/outbox_batch.py` | 106 | Atomically enqueue a bounded namespace batch while checking current generation and pending resync barriers. | | `utils/config_sync/blob_models.py` | 86 | Bounded content-addressed chunk descriptors and portable profile asset references contain no local paths. | | `utils/config_sync/blob_schema.py` | 42 | Identify an owned immutable asset-chunk database before creating account quotas or verified blob receipts. | -| `utils/config_sync/blob_store.py` | 217 | Durable account-isolated chunks publish a blob receipt only after every byte, size and full hash verify. | -| `utils/config_sync/blob_codec.py` | 26 | Canonical bounded base64 chunks keep binary asset traffic inside the existing authenticated JSON body limit. | -| `utils/config_sync/blob_binding.py` | 139 | Capture explicitly named source assets and stream complete inert profile bundles from verified content caches. | +| `utils/config_sync/blob_store.py` | 219 | Durable account-isolated chunks publish a blob receipt only after every byte, size and full hash verify. | +| `utils/config_sync/blob_codec.py` | 25 | Canonical bounded base64 chunks keep binary asset traffic inside the existing authenticated JSON body limit. | +| `utils/config_sync/blob_binding.py` | 148 | Capture explicitly named source assets and stream complete inert profile bundles from verified content caches. | | `utils/config_sync/blob_http.py` | 139 | Resumable asset HTTP reuses bounded egress, refreshes live credentials and preserves cancellation. | | `utils/config_sync/blob_routes.py` | 66 | Authenticated bounded asset routes expose account-isolated chunks and complete hash-verified blob receipts. | | `utils/config_sync/workspace_assets.py` | 72 | Bind causal definitions to immutable asset uploads and durable profile downloads under live authority. | @@ -952,20 +952,20 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/config_sync/workspace_review.py` | 35 | Resolve canonical workspace preferences only from the exact conflict-free heads explicitly reviewed. | | `utils/config_sync/profile_load.py` | 91 | Load an explicitly reviewed immutable generation into a separate native working copy with no runtime activation. | | `utils/config_sync/profile_native.py` | 184 | Validate complete native profile generations against sealed portable heads before native working-copy loading. | -| `utils/config_sync/profile_bindings.py` | 91 | Match native generation values to exact portable heads and rebind verified asset references to a working root. | +| `utils/config_sync/profile_bindings.py` | 96 | Match native generation values to exact portable heads and rebind verified asset references to a working root. | | `utils/config_sync/profile_runtime.py` | 88 | Construct disabled native hotkey and trigger objects without registering listeners or polling a backend. | | `utils/config_sync/profile_models.py` | 123 | Own an explicitly loaded native working snapshot without changing global engines, sources or active preferences. | | `utils/config_sync/profile_commands.py` | 28 | Private native-profile loading commands share exact manifest review and current path/cancellation authority. | | `utils/config_sync/adapter_sources.py` | 129 | Capture actual domain sources as data, excluding runtime enablement, counters, credentials and device handles. | | `utils/config_sync/adapter_ledger.py` | 171 | Atomic adapter head merging preserves source baselines, parallel branches and compact operation receipts. | -| `utils/config_sync/adapter_state.py` | 282 | Lazy owned SQLite adapter profiles keep causal heads and private source baselines across restarts. | +| `utils/config_sync/adapter_state.py` | 283 | Lazy owned SQLite adapter profiles keep causal heads and private source baselines across restarts. | | `utils/config_sync/adapter_protocol.py` | 32 | Data-only adapter protocol shared by source capture and inert profile materialization. | | `utils/config_sync/adapters.py` | 277 | Concrete source adapters persist causal inbox profiles without changing a live script, locator or daemon. | | `utils/config_sync/adapter_profiles.py` | 121 | Materialize complete inert adapter profiles through the verified asset-generation publisher. | | `utils/config_sync/adapter_factory.py` | 202 | Build private local adapter profiles from explicit JSON source descriptors with live path/cancellation authority. | | `utils/config_sync/adapter_commands.py` | 59 | Private JSON command adapters capture, merge, resolve and materialize inert causal definition profiles. | | `utils/config_sync/control_models.py` | 127 | 固定控制意圖、穩定 RPC 操作 ID 與控制結果。 | -| `utils/config_sync/control_store.py` | 262 | 持久化控制 lease、固定請求與精簡歷史收據。 | +| `utils/config_sync/control_store.py` | 267 | 持久化控制 lease、固定請求與精簡歷史收據。 | | `utils/config_sync/control_adoption.py` | 85 | 收據確認時原子採用本機世代、快照並封存待送舊編輯。 | | `utils/config_sync/control_delivery.py` | 156 | 明確 peer 控制投遞、有界過期重合併與原請求重送。 | | `utils/config_sync/outbox_lease.py` | 15 | 共用持久化 due/lease 到期判定。 | @@ -989,7 +989,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/codegen/journal_commands.py` | 13 | 日誌候選的 JSON executor 轉接與明確匯出。 | | `utils/codegen/journal_cli.py` | 26 | CLI --from-log 模式、JSON 審閱輸出與可選 ZIP。 | -#### `utils/self_healing/`(2,233 行)— 執行時定位與固定影像比較 +#### `utils/self_healing/`(2,236 行)— 執行時定位與固定影像比較 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -1000,18 +1000,18 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/self_healing/commands.py` | 74 | 巢狀 JSON 請求、路徑政策與明確失敗的指令轉接。 | | `utils/self_healing/source_inspection.py` | 31 | 原始日誌步驟的有界唯讀查閱與 JSON 指令轉接。 | | `utils/self_healing/template_models.py` | 86 | 不可變候選狀態、縮圖、驗證與檔案轉換結果。 | -| `utils/self_healing/template_images.py` | 62 | 有界 PNG/JPEG/BMP 解碼、目標格式編碼與預覽縮圖。 | -| `utils/self_healing/template_bundle.py` | 163 | 不可變原始/候選版本包、雜湊驗證與目前內容狀態。 | +| `utils/self_healing/template_images.py` | 61 | 有界 PNG/JPEG/BMP 解碼、目標格式編碼與預覽縮圖。 | +| `utils/self_healing/template_bundle.py` | 167 | 不可變原始/候選版本包、雜湊驗證與目前內容狀態。 | | `utils/self_healing/template_candidates.py` | 164 | 固定樣本驗證、明確接受與 byte-exact 回復;鎖與內容比較保護變更。 | | `utils/self_healing/template_commands.py` | 55 | 有界候選 PNG 檔案輸入與五個 JSON 指令轉接。 | | `utils/self_healing/runtime_evidence.py` | 108 | 不觸發截圖的執行時身份、版本、操作及策略遙測。 | | `utils/self_healing/heal_log.py` | 141 | 相容既有格式的 JSONL 記錄與執行中秘密遮蔽。 | -| `utils/self_healing/evaluation_models.py` | 235 | 不可變影像樣本、候選、母體與來源模型。 | +| `utils/self_healing/evaluation_models.py` | 236 | 不可變影像樣本、候選、母體與來源模型。 | | `utils/self_healing/evaluation.py` | 175 | 固定 frame 比較;標註、驗證、誤判、成本及延遲分開統計。 | -| `utils/self_healing/frame_locators.py` | 142 | 純影像比對與明確 VLM backend;共用區域、原點與縮放換算。 | +| `utils/self_healing/frame_locators.py` | 141 | 純影像比對與明確 VLM backend;共用區域、原點與縮放換算。 | | `utils/self_healing/evaluation_io.py` | 188 | 有界且受路徑政策限制的本機資料集、原子 JSON/HTML 報告。 | -#### `utils/agent_runtime/`(25,831 行/182 檔) +#### `utils/agent_runtime/`(25,891 行/182 檔) | 檔案 | 行數 | 職責 | | --- | ---: | --- | @@ -1025,7 +1025,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `android_control.py` | 56 | Trusted Android control leases require real device fencing and independent per-command settlement. | | `android_geometry.py` | 99 | Exact half-open Android touch-region coverage for whole pointer paths, including swipe interiors. | | `android_hierarchy.py` | 287 | Parse bounded Android app hierarchies with explicit host text grants and private freshness signatures. | -| `android_input_evidence.py` | 222 | Bind current Android dispatcher geometry and keyguard evidence without granting input authority. | +| `android_input_evidence.py` | 223 | Bind current Android dispatcher geometry and keyguard evidence without granting input authority. | | `android_input_windows.py` | 127 | Decode pinned AOSP input-window records without retaining foreign names or raw window dumps. | | `android_scope.py` | 87 | Validate bounded Android device, boot and focused-app evidence without publishing raw dumps. | | `android_session.py` | 143 | Pin Android identity, propose finite commands and dispatch only exact checked proposals. | @@ -1034,47 +1034,47 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `browser_commands.py` | 168 | Closed browser commands operate only inside an owned WebRunner helper session. | | `browser_environment.py` | 80 | Closed browser Python dependency reports; probing never starts Playwright or a browser. | | `browser_readonly_scope.py` | 35 | Narrow a frozen browser to a host-admitted verification scope without replacing input authority. | -| `browser_retirement.py` | 49 | Exact live SDK retirement descriptions; serialized context IDs never grant authority. | -| `browser_target.py` | 206 | Own a WebRunner SDK session inside a managed helper, without importing browser engines. | +| `browser_retirement.py` | 51 | Exact live SDK retirement descriptions; serialized context IDs never grant authority. | +| `browser_target.py` | 208 | Own a WebRunner SDK session inside a managed helper, without importing browser engines. | | `browser_transfers.py` | 101 | Rooted file snapshots and atomic publication around WebRunner-owned browser transfers. | | `browser_validation.py` | 35 | Pure browser text and URL bounds shared without loading SDK or command dispatch. | | `browser_verification.py` | 91 | Pinned browser completion clauses use fresh bounded SDK reads, never agent claims or previews. | -| `browser_workflows.py` | 179 | Compile rooted CSV forms and captured field transfers into reviewed WebRunner task actions. | +| `browser_workflows.py` | 181 | Compile rooted CSV forms and captured field transfers into reviewed WebRunner task actions. | | `budget_clock.py` | 35 | Persistent monotonic task budgets that cannot be extended by wall-clock rollback or restart. | | `cli.py` | 132 | Managed-task CLI parsing; the existing CLI owns terminal output. | -| `client.py` | 91 | Shared local service client for GUI, CLI, MCP and Python task operations. | +| `client.py` | 93 | Shared local service client for GUI, CLI, MCP and Python task operations. | | `commands.py` | 135 | One command catalogue shared by executor, MCP and the visual script editor. | | `completion_contract.py` | 62 | Shared completion evidence and retirement checks without service or DAG transport dependencies. | -| `completion_recovery.py` | 656 | Explicit post-effect completion review with private lineage and permanent read-only targets. | +| `completion_recovery.py` | 660 | Explicit post-effect completion review with private lineage and permanent read-only targets. | | `context.py` | 74 | Host-owned operation context shared with local workers, never serialized model authority. | | `control_admission.py` | 29 | Closed runtime stop requests share a bounded reserve separate from ordinary HTTP polling. | -| `dag_admission.py` | 79 | Trusted local DAG admission records bind original ownership, caller authority and a live attempt. | +| `dag_admission.py` | 80 | Trusted local DAG admission records bind original ownership, caller authority and a live attempt. | | `dag_batch_recording.py` | 93 | Durable dispatch claims and independent destination verification for one managed DAG batch. | | `dag_batches.py` | 366 | Owner-scoped graph lifecycle and runtime-owned task/artifact host adapters. | | `dag_continuation.py` | 339 | Whole-graph review and one-shot continuation approval never replay a possible delivery. | | `dag_continuation_evidence.py` | 169 | Bounded read-only destination evidence for explicit whole-batch continuation decisions. | | `dag_continuation_plan.py` | 89 | Explicit per-node decisions preserve graph scope and never reset a possible delivery claim. | -| `dag_jobs.py` | 141 | Service-owned durable DAG deliveries never replay an interrupted coordinator attempt. | +| `dag_jobs.py` | 140 | Service-owned durable DAG deliveries never replay an interrupted coordinator attempt. | | `dag_local.py` | 97 | Closed local-node transport admits claimed DAG work under the original owner's authority. | | `dag_retention.py` | 127 | Expire settled DAG records while retaining unresolved input and referenced recovery evidence. | -| `dag_service_client.py` | 54 | Runtime-bound batch coordination and destination calls refresh both approving and original owners. | +| `dag_service_client.py` | 53 | Runtime-bound batch coordination and destination calls refresh both approving and original owners. | | `delegation.py` | 43 | Short-lived, authenticated principal forwarding across the local service boundary. | | `dialog_scope.py` | 54 | Pause managed desktop work at unsaved-content or unrecognized modal windows. | | `document_launch.py` | 46 | Bounded document handoff runs outside the disposable native input helper's process tree. | | `errors.py` | 21 | Typed failures for managed tasks, observations and control ownership. | -| `evidence.py` | 212 | Service-owned task provenance, redacted artifacts and thirty-day metadata retention. | +| `evidence.py` | 215 | Service-owned task provenance, redacted artifacts and thirty-day metadata retention. | | `evidence_cache.py` | 92 | Database-bound evidence paths never turn historical journal references into filesystem ownership. | -| `evidence_handoff.py` | 365 | Explicit revocable evidence-only grants preserve ordinary task ownership and input isolation. | +| `evidence_handoff.py` | 368 | Explicit revocable evidence-only grants preserve ordinary task ownership and input isolation. | | `file_dialog_scope.py` | 62 | Bind the existing file-dialog executor to one task-owned window and rooted absolute path. | | `file_target.py` | 35 | A noninteractive target for rooted archive workflows, separate from desktop input. | | `handoff_records.py` | 115 | Fixed-family handoff records and host-only access to explicitly granted evidence. | | `helper_environment.py` | 56 | Host-reviewed browser Python environments and bounded owned dependency-probe helpers. | | `helper_owner_linux.py` | 452 | Linux owner-service channel: one lease per helper, relayed JSON lines, fenced close. | -| `images.py` | 43 | Bounded redacted JPEG validation is shared by point grounding, previews and persistence. | +| `images.py` | 42 | Bounded redacted JPEG validation is shared by point grounding, previews and persistence. | | `input_journal.py` | 96 | Durable resource witnesses prevent input transfer after an unreviewed host crash. | | `input_ledger.py` | 88 | Host-owned press announcements and acknowledged releases survive helper termination. | | `jobs.py` | 109 | Durable background dispatch over the existing fenced WorkQueue implementation. | -| `local_evidence.py` | 28 | Bind server-side batch evidence to its owning runtime without global client discovery. | +| `local_evidence.py` | 29 | Bind server-side batch evidence to its owning runtime without global client discovery. | | `manifest_evidence.py` | 51 | Independently re-read archive manifests and their scoped files at task completion. | | `model_tools.py` | 65 | Local-model schemas expose observation references without changing legacy executor signatures. | | `models.py` | 214 | Detached JSON contracts for managed computer tasks and their evidence. | @@ -1083,7 +1083,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `native_job.py` | 170 | Own a Windows helper process tree through a kill-on-close Job Object. | | `native_recovery.py` | 36 | Host-only input recovery checks never replay persisted keys or signal persisted PIDs. | | `native_scope.py` | 111 | Scope native window and control targets before their existing backend performs an action. | -| `native_target.py` | 302 | Native desktop proxy with private IPC, host input ledger and cross-database ownership. | +| `native_target.py` | 304 | Native desktop proxy with private IPC, host input ledger and cross-database ownership. | | `native_worker.py` | 339 | Private native helper entry point; stdout is a bounded owner-only JSON protocol. | | `observation_refs.py` | 151 | Resolve observation-local elements and thumbnail points into host-checked native coordinates. | | `ollama.py` | 82 | Local-only Ollama decisions over bounded HTTP, without an SDK or remote fallback. | @@ -1109,7 +1109,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `presence_peer_linux.py` | 218 | Retain Linux connected-process evidence only; no desktop session or input authority. | | `presence_pipe_linux.py` | 211 | Linux presence transport behind the Windows pipe's interface; its endpoint arrives only through the owned launch. | | `presence_pipe_windows.py` | 209 | Secured local byte pipes for owned helpers; no desktop observation or runtime binding. | -| `presence_pool.py` | 380 | Host-only shared presence ownership; task cancellation never cancels another runtime's source. | +| `presence_pool.py` | 381 | Host-only shared presence ownership; task cancellation never cancels another runtime's source. | | `presence_protocol.py` | 74 | Per-connection sequences and issued-token ownership; shared source lifetime remains separate. | | `presence_reader.py` | 219 | Fresh presence checks inside owned input helpers, with bounded callers and an independent watchdog. | | `presence_security_windows.py` | 220 | Query-only endpoint identity and noninheritable current-user/System security descriptors. | @@ -1119,8 +1119,8 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `presence_platform.py` | 67 | Pick the physical presence API for this helper's platform; a platform without a trusted source refuses. | | `presence_quartz.py` | 165 | macOS physical presence: a listen-only session event tap that counts hardware events, plus console lock state. | | `presence_win32.py` | 255 | Header-only Raw Input observation in the dedicated helper, with fresh supported-console checks. | -| `presence_windows.py` | 252 | Fresh bounded source actor with retained native resources, independent of input-helper lifetimes. | -| `presence_x11.py` | 181 | X11 physical presence: XInput2 raw events from real devices, never from XTEST, plus logind lock state. | +| `presence_windows.py` | 254 | Fresh bounded source actor with retained native resources, independent of input-helper lifetimes. | +| `presence_x11.py` | 185 | X11 physical presence: XInput2 raw events from real devices, never from XTEST, plus logind lock state. | | `presence_worker.py` | 88 | Fixed contained source child; bootstrap and endpoint ownership precede observation. | | `readonly_target.py` | 157 | Fresh isolated completion observations with live scope checks and permanent no-input helper contracts. | | `readonly_worker.py` | 70 | Permanently read-only owned helper: observation never upgrades to native input or task execution. | @@ -1132,8 +1132,8 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `remote_worker.py` | 213 | Owned remote SDK worker with closed RPC, refreshed authority and ACK-gated finite effects. | | `retention.py` | 71 | Thirty-day cleanup of stopped tasks and exclusively owned runtime artifacts. | | `routes.py` | 165 | Authenticated REST adapters for the runtime explicitly owned by a server. | -| `runner.py` | 111 | Autonomous skill/local-model driver over exactly the external task operations. | -| `retained_browser.py` | 147 | Bounded effect-task ownership of actual permanently read-only browser helper processes. | +| `runner.py` | 117 | Autonomous skill/local-model driver over exactly the external task operations. | +| `retained_browser.py` | 148 | Bounded effect-task ownership of actual permanently read-only browser helper processes. | | `retained_browser_runtime.py` | 91 | Connect actual browser retirement to existing runtime leases and private completion bindings. | | `retained_browser_target.py` | 98 | Closed transient read borrowers; the runtime collection owns the original helper lifetime. | | `runtime.py` | 750 | Shared managed-task lifecycle with fenced dispatch and evidence-based completion. | @@ -1149,24 +1149,24 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `target_config.py` | 198 | Immutable host-reviewed target configuration, distinct from untrusted task options. | | `target_continuity.py` | 16 | Require a live UI target context before trusting a finite skill's verified prefix. | | `target_factory.py` | 27 | Create inert managed adapters from host profiles; SDKs and native resources stay lazy. | -| `target_profiles.py` | 204 | Durable target profiles, live controls and explicit dependency diagnostics separate from native evidence. | +| `target_profiles.py` | 205 | Durable target profiles, live controls and explicit dependency diagnostics separate from native evidence. | | `targets.py` | 193 | Explicit target adapters; native imports occur only during requested operations. | -| `task_authority.py` | 312 | Trusted source-owner recovery admission and live owner/reviewer authority checks. | +| `task_authority.py` | 316 | Trusted source-owner recovery admission and live owner/reviewer authority checks. | | `task_handoff.py` | 81 | Real non-DAG source identity for explicit completion evidence, without fabricated admission. | -| `task_recovery.py` | 483 | Review stopped tasks and create one explicitly approved successor without replay. | +| `task_recovery.py` | 484 | Review stopped tasks and create one explicitly approved successor without replay. | | `task_skills.py` | 373 | Immutable managed skill candidates require a verified trial and explicit version publication. | -| `task_triggers.py` | 272 | Durable preauthorized task bindings with owner refresh, bounded backlog and event deduplication. | +| `task_triggers.py` | 271 | Durable preauthorized task bindings with owner refresh, bounded backlog and event deduplication. | | `transfer_destination_scope.py` | 33 | Injected destination profiles and live origin ceilings confine reviewed import directories. | | `transfer_import_contract.py` | 83 | Closed destination management envelopes carry opaque foreign paths without forwarding local authority. | | `transfer_import.py` | 342 | Destination-owned consent and fenced recovery bind source manifests to immutable local files profiles. | | `transfer_import_files.py` | 127 | Verified import chunks and owned generations reuse existing asset publication and verification. | | `transfer_import_jobs.py` | 98 | An explicitly started runtime service owns import workers independently of GUI observers. | -| `transfer_import_ledger.py` | 201 | Destination delivery generations bind durable import intents to exact existing WorkQueue claims. | +| `transfer_import_ledger.py` | 200 | Destination delivery generations bind durable import intents to exact existing WorkQueue claims. | | `transfer_staging.py` | 171 | Journal exclusive staging identities and retire only recognized files and empty directories. | | `transfer_cache.py` | 34 | One runtime-derived private blob cache never accepts a public path or configuration account selector. | | `transfer_files.py` | 111 | Bounded source-file descriptors and verified chunks remain private until export consent commits. | -| `transfer_host.py` | 201 | Host-injected source authority and destination scope keep task policy out of transfer leaves. | -| `transfer_lineage.py` | 147 | Bind artifact authority to real bounded recovery ancestry and original accepted DAG admission. | +| `transfer_host.py` | 202 | Host-injected source authority and destination scope keep task policy out of transfer leaves. | +| `transfer_lineage.py` | 148 | Bind artifact authority to real bounded recovery ancestry and original accepted DAG admission. | | `transfer_models.py` | 191 | Portable criterion selections and immutable descriptors for reviewed outcome transfers. | | `transfer_privacy.py` | 53 | Exclude actual host private stores and evidence roots without reserving ordinary output basenames. | | `transfer_protocol.py` | 171 | Runtime-owned transfer serialization coordinates source consent and service-owned destination imports. | @@ -1174,21 +1174,21 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `transfer_management.py` | 103 | Closed management envelopes validate foreign source and destination replies before retaining consent. | | `transfer_evidence_management.py` | 85 | Closed management projections validate portable evidence consent without importing foreign authority. | | `transfer_routing.py` | 133 | Coordinator-owned routed consent persists exact approval before forwarding source or destination creation. | -| `transfer_records.py` | 291 | Private transfer reservations bind consent and retain capacity until owned cleanup is confirmed. | +| `transfer_records.py` | 292 | Private transfer reservations bind consent and retain capacity until owned cleanup is confirmed. | | `transfer_selection.py` | 134 | Resolve only source-owned passed file criteria and their explicitly selected manifest names. | | `transfer_source.py` | 273 | Explicitly reviewed source artifacts publish immutable blobs under live host-derived authority. | -| `transport.py` | 117 | Bounded HTTP requests; cancellation discards late inference replies before any input. | +| `transport.py` | 128 | Bounded HTTP requests; cancellation discards late inference replies before any input. | | `trigger_config.py` | 69 | Validate durable trigger schedules and rooted file-version notifications without side effects. | | `trigger_history.py` | 89 | Renewed trigger lineages share accepted event tombstones and outstanding-task limits. | | `trigger_reauthorization.py` | 149 | Explicitly replace a trigger's unavailable origins while retaining its event history and template. | | `trigger_registration.py` | 21 | Insert normal and explicitly renewed trigger templates through one bounded transaction contract. | -| `verification.py` | 150 | Deterministic completion evidence with explicit passed, failed and unknown states. | +| `verification.py` | 153 | Deterministic completion evidence with explicit passed, failed and unknown states. | | `window_pixels.py` | 118 | Capture only an identified Windows window, with no desktop-pixel fallback. | | `workflow_desktop.py` | 70 | Reviewed semantic control and document operations within the current task's application scope. | | `wire_data.py` | 46 | Resource-free bounded JSON and budget primitives shared by host models and owned transports. | | `workflow_files.py` | 128 | Rooted, bounded and idempotent download/archive operations for managed workflows. | -| `workflow_specs.py` | 243 | Compile four reviewed workflows into inspectable, bounded managed-task specifications. | -| `workflow_values.py` | 88 | Resolve receipt-backed workflow variables and compare explicit per-step expectations. | +| `workflow_specs.py` | 245 | Compile four reviewed workflows into inspectable, bounded managed-task specifications. | +| `workflow_values.py` | 90 | Resolve receipt-backed workflow variables and compare explicit per-step expectations. | | `workflow_wait.py` | 31 | Bounded, cancellable semantic read-back polling never repeats an input operation. | | `workspace_scope.py` | 36 | Bounded workspace bootstrap only focuses explicitly approved application windows. | @@ -1291,7 +1291,7 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,只需要 PySide6 | `journal_browser.py` | 250 | Run History 與 Self-Healing 共用的唯讀日誌檢視:來源連結/路徑/run/step 篩選、有界快照表與明細,舊結果以 request ID 隔離。 | | `journal_browser_worker.py` | 85 | 背景查閱與逐筆取消;帶入 context 並在開檔前重查 deferred 擁有者,以 queued counts 回報進度。 | | `manual_run_controls.py` | 72 | 手動執行的 history 勾選與獨立 journal 路徑;以共用無頭 scope 包住既有 GUI 執行,不改結果與變數。 | -| `language_wrapper/` | 7,934 | 四語系字典(英/日/簡中/繁中)+ `multi_language_wrapper` 執行期切換器與監聽註冊表。 | +| `language_wrapper/` | 7,935 | 四語系字典(英/日/簡中/繁中)+ `multi_language_wrapper` 執行期切換器與監聽註冊表。 | | `selector/` | 216 | 拖曳選取螢幕區域的半透明全螢幕覆蓋層與樣板裁切工具(互動式,但都有對應的程式化 API)。 | > **分頁指令一律走 Actions 選單**:分頁本身只放輸入、表格與結果檢視,指令由視窗層選單暴露。 @@ -1306,20 +1306,20 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,只需要 PySide6 | screenshot | `_screenshot_tab.py` | 138 | 截圖、選區、螢幕尺寸、取像素色。 | | image_detect | `_image_detect_tab.py` | 117 | 樣板裁切、定位、定位全部、定位並點擊。 | | record | `_record_tab.py` | 144 | 錄製/停止/回放/存檔/載入。 | -| script_builder | `script_builder/` | 6,823 | **視覺化腳本編輯器**:`command_schema.py`(5,470 行 `AC_*` 參數綱要)、`step_model.py`(步驟模型與 AC JSON 序列化)、`step_list_view.py`(含巢狀 body 的樹狀檢視)、`step_form_view.py`(綱要驅動表單)、`builder_tab.py`(含日誌候選預覽與明確匯入,保留來源)。 | +| script_builder | `script_builder/` | 6,902 | **視覺化腳本編輯器**:`command_schema.py`(5,549 行 `AC_*` 參數綱要)、`step_model.py`(步驟模型與 AC JSON 序列化)、`step_list_view.py`(含巢狀 body 的樹狀檢視)、`step_form_view.py`(綱要驅動表單)、`builder_tab.py`(含日誌候選預覽與明確匯入,保留來源)。 | | flow_editor | `flow_editor/` | 490 | 節點式流程圖檢視:`layout.py`(純 Python 佈局演算法,可單測)、`scene.py`(Qt 場景繪製)、`tab.py`。 | | script | `_script_tab.py` | 158 | 載入/執行單檔或整個目錄、內建編輯器執行。 | | recording_editor | `recording_editor_tab.py` | 262 | 裁切、過濾、重新縮放錄製內容;Actions 提供背景日誌候選審閱。 | | variables | `variables_tab.py` | 169 | 檢視、灌入、清除 executor 執行期作用域。 | | secrets | `secrets_tab.py` | 187 | 解鎖保險庫並管理 `${secrets.NAME}`。 | | vlm | `vlm_tab.py` | 125 | 用文字描述 UI 元素,交由模型定位。 | -| self_healing | `self_healing_tab.py`、`healing_evaluation_panel.py`、`healing_evaluation_worker.py`、`template_candidate_panel.py`、`template_candidate_worker.py` | 935 | 執行時定位/記錄、固定影像版本比較、原始步驟與候選樣板生命週期;背景工作可取消並丟棄過期結果。 | +| self_healing | `self_healing_tab.py`、`healing_evaluation_panel.py`、`healing_evaluation_worker.py`、`template_candidate_panel.py`、`template_candidate_worker.py` | 941 | 執行時定位/記錄、固定影像版本比較、原始步驟與候選樣板生命週期;背景工作可取消並丟棄過期結果。 | | ocr_reader | `ocr_tab.py` | 195 | 傾印區域文字或以 regex 搜尋。 | | accessibility | `accessibility_tab.py` | 192 | 瀏覽 OS UI 樹並依 role/name 點擊。 | | live_hud | `live_hud_tab.py` | 136 | 即時 HUD:滑鼠位置、游標下像素色、log tail。 | | llm_planner | `llm_planner_tab.py` | 200 | 自然語言描述 → 預覽 → 執行。 | | computer_use | `computer_use_tab.py` | 179 | 從 GUI 啟動 Anthropic 閉環 agent。 | -| agent_tasks | `agent_tasks_tab.py` | 606 | 受管任務輸入、狀態與 Actions menu;服務獨立於視圖生命週期。 | +| agent_tasks | `agent_tasks_tab.py` | 609 | 受管任務輸入、狀態與 Actions menu;服務獨立於視圖生命週期。 | | scheduler | `scheduler_tab.py` | 173 | 註冊間隔式 action JSON 執行。 | | hotkeys | `hotkeys_tab.py` | 165 | 將全域熱鍵綁到 action 檔。 | | triggers | `triggers_tab.py` | 374 | 影像/視窗/像素/檔案事件監看。 | @@ -1338,10 +1338,10 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,只需要 PySide6 | window_manager | `window_tab.py` | 137 | 列出、聚焦、關閉視窗。 | | plugins | `plugins_tab.py` | 82 | 從使用者目錄載入額外 `AC_` 指令。 | | webrunner | `webrunner_tab.py` | 204 | 從 GUI 驅動 `je_web_runner`。 | -| dag_runner | `dag_tab.py` | 364 | 編輯、驗證、執行跨主機 DAG。 | +| dag_runner | `dag_tab.py` | 369 | 編輯、驗證、執行跨主機 DAG。 | | chatops | `chatops_tab.py` | 144 | 在接上 Slack 前先本機測試 slash 指令。 | | trace_replay | `trace_replay_tab.py` | 192 | 拖曳捲動時光回溯錄製內容。 | -| remote_desktop | `remote_desktop/`(17 檔) | 7,977 | 見下。 | +| remote_desktop | `remote_desktop/`(17 檔) | 7,979 | 見下。 | | presence | `presence_tab.py` | 164 | 多檢視者在場名單。 | | rest_api | `rest_api_tab.py` | 260 | 啟停 HTTP 前端並顯示 URL、實際驗證模式與可用 token。 | | admin_console | `admin_console_tab.py` | 318 | 管理多個遠端 AutoControl REST 端點。 | @@ -1359,12 +1359,12 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,只需要 PySide6 | --- | ---: | --- | | `gui/diagnostics_capabilities.py` | 60 | 只呈現既有不可變能力快照與四語狀態;不探測、不連線,不持有原生物件。 | -#### 遠端桌面 GUI(`gui/remote_desktop/`,35 檔/7,977 行) +#### 遠端桌面 GUI(`gui/remote_desktop/`,35 檔/7,979 行) | 模組 | 行數 | 職責 | | --- | ---: | --- | | `folder_sync_controls.py` | 70 | Translate the existing folder toggle into owned start, reconnect handoff and explicit baseline reset. | -| `folder_sync_io.py` | 128 | Bind confirmed folder sends, cancellation and frozen receipts to one captured GUI owner. | +| `folder_sync_io.py` | 130 | Bind confirmed folder sends, cancellation and frozen receipts to one captured GUI owner. | | `webrtc_panel.py` | 417 | WebRTC 子分頁主體。 | | `webrtc_dialogs.py` | 520 | WebRTC GUI 用的自訂對話框與清單元件(待審檢視者、信任清單、通訊錄、遠端檔案表、稽核記錄、LAN 瀏覽)。 | | `advanced_group.py` | 98 | 兩個 WebRTC 面板共用的 Advanced STUN/TURN(含選用硬體編碼器)群組,含它寫回面板的 Protocol。 | @@ -1435,7 +1435,7 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,只需要 PySide6 | 檔案 | 用途 | | --- | --- | | `stable.yml` | 每次 push/PR 到 `main` 與每日排程跑 Windows 五版本的示範腳本;合併到 main 後由 `scripts/stable_release.py` 決定版本(`pyproject.toml` 宣告的版本已有 tag 就加一個 patch,還沒有 tag 就照寫的發,minor/major 版就是這樣發的)並上傳 PyPI(使用 `PYPI_API_TOKEN`)。`publish` job 只安裝雜湊鎖定的 `.github/requirements/publish.txt`(`build`、`twine` 與建置後端 `setuptools`,由同目錄的 `publish.in` 用 `uv pip compile` 產生),並以 `python -m build --no-isolation` 建置,所以建置後端也是鎖定的那一版。 | -| `dev.yml` | 每次 push/PR 到 `dev` 跑 headless pytest(四格:Windows 的 3.10 與 3.14、Ubuntu 3.14、macOS 3.10,都是 `quality.yml` 九格裡的格子,不量 coverage)。push 通過後由 `publish-dev` job 用 `dev.toml` 建置並上傳 `je_auto_control_dev`(同一個 `PYPI_API_TOKEN`);只有這個 commit 仍是 `dev` 的最新一筆、而且 wheel 內容跟 PyPI 最新一版不同時才上傳,版本由 `scripts/dev_release.py` 向 PyPI 查,不回寫 repo。`publish-dev` 跟 `stable.yml` 的 `publish` 一樣只安裝 `.github/requirements/publish.txt` 並以 `python -m build --no-isolation` 建置;`test_publish_tooling_lock.py` 守住這兩個 job 的安裝行、建置指令,以及鎖定版本滿足 `pyproject.toml`/`dev.toml` 的 `build-system.requires`。 | +| `dev.yml` | 每次 push/PR 到 `dev` 跑 headless pytest(四格:Windows 的 3.11 與 3.14、Ubuntu 3.14、macOS 3.11,都是 `quality.yml` 九格裡的格子,不量 coverage)。push 通過後由 `publish-dev` job 用 `dev.toml` 建置並上傳 `je_auto_control_dev`(同一個 `PYPI_API_TOKEN`);只有這個 commit 仍是 `dev` 的最新一筆、而且 wheel 內容跟 PyPI 最新一版不同時才上傳,版本由 `scripts/dev_release.py` 向 PyPI 查,不回寫 repo。`publish-dev` 跟 `stable.yml` 的 `publish` 一樣只安裝 `.github/requirements/publish.txt` 並以 `python -m build --no-isolation` 建置;`test_publish_tooling_lock.py` 守住這兩個 job 的安裝行、建置指令,以及鎖定版本滿足 `pyproject.toml`/`dev.toml` 的 `build-system.requires`。 | | `release.yml` | 發佈流程(上傳步驟目前關閉)。 | | `quality.yml` | ruff、bandit、dependency review、九格矩陣的 headless pytest(含 coverage 地板)與 mypy。 | | `platform-smoke.yml` | 跨平台煙霧測試。 | @@ -1493,16 +1493,16 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。 | 層/子系統 | 檔案數 | 行數 | | --- | ---: | ---: | -| `gui/` | 144 | 39,748 | -| `utils/mcp_server/` | 55 | 21,478 | -| `utils/remote_desktop/` | 72 | 15,230 | +| `gui/` | 144 | 39,844 | +| `utils/mcp_server/` | 55 | 21,490 | +| `utils/remote_desktop/` | 72 | 15,243 | | `utils/executor/` | 11 | 10,702 | | `utils/usb/` | 20 | 5,060 | -| `je_auto_control/`(頂層 3 檔) | 5 | 4,908 | +| `je_auto_control/`(頂層 3 檔) | 5 | 5,167 | | `utils/accessibility/` | 15 | 3,292 | -| `wrapper/` | 25 | 5,980 | +| `wrapper/` | 25 | 5,992 | | `windows/` | 24 | 2,265 | -| `utils/rest_api/` | 8 | 2,225 | +| `utils/rest_api/` | 8 | 2,226 | | `utils/agent/` | 13 | 2,253 | | `linux_with_x11/` | 19 | 1,369 | | `linux_wayland/` | 20 | 3,844 | @@ -1513,8 +1513,8 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。 | `osx/` | 17 | 981 | | `autocontrol-lsp/` | 8 | 744 | | `utils/hotkey/` | 7 | 866 | -| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 997 | 101,371 | -| **總計** | **1,482** | **226,995** | +| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 997 | 101,486 | +| **總計** | **1,482** | **227,503** | `gui/agent_observation_view.py` provides the bounded read-only agent element table and DPR-1 image selection preview. `gui/agent_tasks_tab.py` owns only request editing and Actions-menu translation; business validation remains in the headless runtime. diff --git a/autocontrol-lsp/pyproject.toml b/autocontrol-lsp/pyproject.toml index 6707a60d6..e54d0b609 100644 --- a/autocontrol-lsp/pyproject.toml +++ b/autocontrol-lsp/pyproject.toml @@ -11,7 +11,7 @@ authors = [ ] readme = "README.md" license = { text = "MIT" } -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ # Only stdlib — but discovery imports je_auto_control at runtime # to pull the AC_* command list out of the executor. So a working diff --git a/benchmarks/mcp_discovery.py b/benchmarks/mcp_discovery.py index 4f494ddc0..0bd018865 100644 --- a/benchmarks/mcp_discovery.py +++ b/benchmarks/mcp_discovery.py @@ -11,6 +11,8 @@ from je_auto_control.utils.mcp_server.server import MCPServer from je_auto_control.utils.mcp_server.tools import MCPTool, build_default_tool_registry +_TOOLS_CALL = "tools/call" + def _rpc(server: MCPServer, method: str, params: dict | None = None) -> tuple[dict, float, int]: request = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}}) @@ -42,14 +44,14 @@ def _mode(tools: list[MCPTool], mode: str, repeats: int) -> dict[str, Any]: search, enable = [], [] search_bytes = 0 for _ in range(repeats): - _, elapsed, search_bytes = _rpc(server, "tools/call", { + _, elapsed, search_bytes = _rpc(server, _TOOLS_CALL, { "name": "ac_mcp_tool_search", "arguments": {"query": "screen", "limit": 10}}) search.append(elapsed) if mode == "progressive": - _, elapsed, _ = _rpc(server, "tools/call", { + _, elapsed, _ = _rpc(server, _TOOLS_CALL, { "name": "ac_mcp_tools_enable", "arguments": {"names": ["ac_screen_size"]}}) enable.append(elapsed) - _rpc(server, "tools/call", { + _rpc(server, _TOOLS_CALL, { "name": "ac_mcp_tools_disable", "arguments": {"names": ["ac_screen_size"]}}) return {"count": len(initial["tools"]), "response_json_bytes": wire_bytes, "descriptor_json_bytes": len(json.dumps(initial["tools"], ensure_ascii=False, diff --git a/dev.toml b/dev.toml index 796180b0b..2a6375fc2 100644 --- a/dev.toml +++ b/dev.toml @@ -17,16 +17,14 @@ authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ] description = "GUI Automation Framework" -requires-python = ">=3.10" +requires-python = ">=3.11" license-files = ["LICENSE"] dependencies = [ "je_open_cv==0.0.22; sys_platform != 'win32' or platform_machine != 'ARM64'", "opencv-python>=4.8,<6; sys_platform != 'win32' or platform_machine != 'ARM64'", # Pillow 13.0.0 fixes the 12.x advisories (critical: GHSA-v9pv-5gc8-w2m6, - # GHSA-5x8w-8jj5-q52h) but needs Python >=3.11; 3.10 keeps 12.3.0, the - # newest release it can install. - "pillow==12.3.0; python_version < '3.11'", - "pillow==13.0.0; python_version >= '3.11'", + # GHSA-5x8w-8jj5-q52h). + "pillow==13.0.0", "pyobjc-core==12.2.2;platform_system=='Darwin'", "pyobjc==12.2.2;platform_system=='Darwin'", "python-Xlib==0.33;platform_system=='Linux' or platform_system=='FreeBSD' or platform_system=='OpenBSD' or platform_system=='NetBSD'", @@ -35,7 +33,6 @@ dependencies = [ "cryptography>=50.0.0; sys_platform != 'win32' or platform_machine != 'ARM64'" ] classifiers = [ - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", diff --git a/docs/source/Eng/doc/installation/installation_doc.rst b/docs/source/Eng/doc/installation/installation_doc.rst index dc27cc1be..084a83d58 100644 --- a/docs/source/Eng/doc/installation/installation_doc.rst +++ b/docs/source/Eng/doc/installation/installation_doc.rst @@ -12,7 +12,7 @@ Install from PyPI Requirements ============ -* Python 3.10 or higher +* Python 3.11 or higher * pip 19.3 or higher With GUI Support diff --git a/docs/source/Eng/doc/operations_layer/usb_passthrough_operator_guide.rst b/docs/source/Eng/doc/operations_layer/usb_passthrough_operator_guide.rst index 3f4bedf26..05adddd80 100644 --- a/docs/source/Eng/doc/operations_layer/usb_passthrough_operator_guide.rst +++ b/docs/source/Eng/doc/operations_layer/usb_passthrough_operator_guide.rst @@ -23,7 +23,7 @@ Prerequisites On the **host** (the machine with the physical USB device): -- Python 3.10+ with AutoControl installed. +- Python 3.11+ with AutoControl installed. - The optional ``webrtc`` extra: ``pip install je_auto_control[webrtc]``. - ``pyusb`` installed if you want the libusb backend: ``pip install pyusb``. @@ -32,7 +32,7 @@ On the **host** (the machine with the physical USB device): On the **viewer** (the remote machine that will use the device): -- Python 3.10+ with AutoControl installed. +- Python 3.11+ with AutoControl installed. - Network reach to the host's REST API port (default 9939) **and** to the WebRTC signalling / TURN endpoints if the viewer is behind NAT. - The host's bearer token (operator hands it over out-of-band). diff --git a/docs/source/Zh/doc/installation/installation_doc.rst b/docs/source/Zh/doc/installation/installation_doc.rst index 0d1fb5059..5c1e0481e 100644 --- a/docs/source/Zh/doc/installation/installation_doc.rst +++ b/docs/source/Zh/doc/installation/installation_doc.rst @@ -12,7 +12,7 @@ 系統需求 ======== -* Python 3.10 以上 +* Python 3.11 以上 * pip 19.3 以上 安裝 GUI 支援 diff --git a/docs/source/Zh/doc/operations_layer/usb_passthrough_operator_guide.rst b/docs/source/Zh/doc/operations_layer/usb_passthrough_operator_guide.rst index 6e95a775a..7604b94ab 100644 --- a/docs/source/Zh/doc/operations_layer/usb_passthrough_operator_guide.rst +++ b/docs/source/Zh/doc/operations_layer/usb_passthrough_operator_guide.rst @@ -21,7 +21,7 @@ production。``default_passthrough_backend()`` 會依當前 OS 自動挑 backend 在 **host**\ (有實體 USB 裝置的機器)上: -- Python 3.10+ 並安裝 AutoControl。 +- Python 3.11+ 並安裝 AutoControl。 - 選用的 ``webrtc`` 套件:``pip install je_auto_control[webrtc]``\ 。 - 如要使用 libusb backend 需安裝 ``pyusb``\ : ``pip install pyusb``\ 。 @@ -30,7 +30,7 @@ production。``default_passthrough_backend()`` 會依當前 OS 自動挑 backend 在 **viewer**\ (將使用該裝置的遠端機器)上: -- Python 3.10+ 並安裝 AutoControl。 +- Python 3.11+ 並安裝 AutoControl。 - 能連到 host 的 REST API port(預設 9939),**且** 在 NAT 後方時 能連到 WebRTC signalling / TURN 端點。 - host 的 bearer token(操作員以帶外管道交付)。 diff --git a/docs/source/getting_started/installation.rst b/docs/source/getting_started/installation.rst index 17514290f..8a2f64514 100644 --- a/docs/source/getting_started/installation.rst +++ b/docs/source/getting_started/installation.rst @@ -5,7 +5,7 @@ Installation Requirements ============ -* **Python** >= 3.10 +* **Python** >= 3.11 * **pip** >= 19.3 Basic Installation diff --git a/docs/updates/2026-11.md b/docs/updates/2026-11.md index b154d74c4..cc9ca94b7 100644 --- a/docs/updates/2026-11.md +++ b/docs/updates/2026-11.md @@ -588,3 +588,69 @@ Index and query commands: [README.md](README.md). New entries go at the end. variable, some reads need `drive_input`, MCP refusals are terser, and the state machine runs on the calling executor. `CHANGELOG.md` 1.1.0 lists them. - **Open items**: none. + +## U-20261102-02 · 2026-11-02 · Clear the SonarCloud findings the 1.1.0 release PR reported · #ci #test #quality + +- **What**: SonarCloud listed 2,087 open findings in the new code of PR #538 + (Release 1.1.0). All are maintainability findings; the quality gate passed. + 1,616 were in tests, 471 in the package, scripts, examples and the Android + helper. +- **Result / numbers**: + - S9073 (1,208): every flagged `assert a and b` is one assert per condition, + so a failure names the condition that broke. + - S5778 (400): each `pytest.raises` block holds only the call under test; + setup calls are hoisted, and branching bodies moved into a local function. + - S1192 (292): `scripts/generate_facade_exports.py` emits one constant per + owner module the generated bindings repeat (249 findings); the rest are + named constants. A generated owner named like a credential carries a + `nosec B105` with its reason. + - The remaining package findings are fixed in place: redundant classes in + except tuples (checked against the real MRO), `\d`/`\w` regexes with + `re.ASCII` so they still match ASCII only, nested conditionals and `with` + statements, cognitive-complexity splits, empty blocks given a reason. + The Script Builder table's 34 append runs became `extend`; its built spec + list is byte-identical. + - 33 findings are left as intended and closed on SonarCloud with a reason: + 15 S5754 (BaseException handlers that re-raise through a helper or hand + the error to another thread), 12 S5886 (`dataclasses.replace` inferred as + `DataclassInstance`), 4 S7504 (`list()` snapshots of a dict the loop + mutates), 1 S5655 (a writable ctypes buffer) and 1 kotlin S6624 (no + version catalog for the Android helper; the version is locked by + `gradle.lockfile`). +- **Verification**: ruff, bandit, `radon cc -nc`, `generate_facade_exports.py + --check`, `typing_contract_verify.py` (both modes) and the doc-count and + budget tests pass; `architecture_explore.md` line counts re-measured. + +## U-20261102-03 · 2026-11-02 · Require Python 3.11; stop supporting 3.10 · #migration #deps #ci + +- **Why**: every package in the workspace moves its floor to Python 3.11 together. Security releases such as Pillow 13.0.0 need 3.11, and supporting 3.10 kept older, vulnerable versions in the locks. +- **What**: `requires-python` is `>=3.11` and the 3.10 classifier is gone; the CI matrices start at 3.11; the docs that gave 3.10 as the floor now give 3.11. +- **Locks**: + - `uv.lock`: the 3.10-only branches are gone; removed: `exceptiongroup`, `tomli`; one version left instead of a 3.10 one beside it: `dnspython`, `numpy`, `pillow`. +- **Also**: The Pillow dependency is a single `pillow==13.0.0`, and the dnspython constraint applies on every interpreter. The Pillow and dnspython Dependabot alerts were open only because of the 3.10 entries. The CI cells that ran 3.10 to cover Windows, Ubuntu and macOS now run 3.11, and mypy targets 3.11. The version is declared as 1.2.0, and CHANGELOG.md says what changed. +- **Open items**: none. + +## U-20261103-01 · 2026-11-03 · Hold the collector off around the presence reader's timed windows · #incident #agent #ci #test + +- **Run.** Quality run 41476383837 on the 1.2.0 release PR (#548). Ubuntu 22.04 3.14 failed twice, the + first attempt and its rerun, on the same test and in the same place: + `test_agent_presence_reader.py::test_reader_loss_does_not_block_worker_owned_release_only_ack_cleanup`. + Its first `harness.reader.ready()` raised `physical presence reader deadline exhausted`. In the same + run, the other 17,546 tests passed, and so did the other eight squares. +- **Why only there.** The same commit passed the Ubuntu 3.14 square of Dev CI (run 41476340230). The two + jobs differ only in `coverage run`, and on Linux both run under gdb (`ci_native_diagnostics.py + --native`). `ready()` must finish connect, handshake and reply within 0.25 s. On the 1.1.0 release PR + (#538) the same test passed at 0.455 s, against 0.254 s for its neighbours. +- **Cause (inferred, not observed).** Allocation counts trigger the collector, so a fixed test order + puts a collection in the same place on every run. After 17,000 tests under coverage and gdb, one + collection of the heap takes longer than the 0.25 s window. #546 and #547 shifted the allocation + pattern enough to move a collection into it. The CI log has no direct evidence of a collection. The + reasons are that the failure repeats at one point, that only the slowest configuration fails, and + that the reader's code did not change. +- **Fix.** The `harness` fixture collects once, disables the collector for the test and always + re-enables it, even when `Harness()` raises. The product deadline is unchanged. A collection pause + in production still fails closed and requires an explicit resume. The file's 34 tests run in 8.3 s, + as before. +- **Check.** If the cause is something else, the quality run on #548 will fail again in the same place. + It then needs a gc callback that logs long collections, not a wider deadline. +- **Open items:** none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 93049f040..0f58b8f0e 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,10 @@ In the same commit: delete the item from `Progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20261103-01 | 2026-11-03 | Hold the collector off around the presence reader's timed windows | #incident #agent #ci #test | [2026-11](2026-11.md) | +| U-20261102-03 | 2026-11-02 | Require Python 3.11; stop supporting 3.10 | #migration #deps #ci | [2026-11](2026-11.md) | +| U-20261102-02 | 2026-11-02 | Clear the SonarCloud findings the 1.1.0 release PR reported | #ci #test #quality | [2026-11](2026-11.md) | +| U-20261102-01 | 2026-11-02 | Merge the release line into dev for 1.1.0 | #release #migration | [2026-11](2026-11.md) | | U-20261101-17 | 2026-11-01 | Patch the dependencies named in Dependabot security alerts | #bugfix #deps #security #ci | [2026-11](2026-11.md) | | U-20261101-16 | 2026-11-01 | Accept either refusal when a revocation races the lease watch | #incident #ci #test | [2026-11](2026-11.md) | | U-20261101-15 | 2026-11-01 | Observe only the maintenance call's own writes in the receipt test | #incident #ci #test | [2026-11](2026-11.md) | @@ -604,7 +608,7 @@ In the same commit: delete the item from `Progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| -| [2026-11.md](2026-11.md) | 2026-11 | 17 | +| [2026-11.md](2026-11.md) | 2026-11 | 21 | | [2026-10-f.md](2026-10-f.md) | 2026-10 | 5 | | [2026-10-e.md](2026-10-e.md) | 2026-10 | 91 | | [2026-10-d.md](2026-10-d.md) | 2026-10 | 15 | diff --git a/examples/sync_assets.py b/examples/sync_assets.py index 539a76c5e..8781ae12e 100644 --- a/examples/sync_assets.py +++ b/examples/sync_assets.py @@ -5,6 +5,8 @@ from je_auto_control import DirectoryAssetTransport, build_asset_manifest, sync_assets +_TEMPLATE = 'template.bin' + def main() -> None: """Round-trip a script and referenced template as inert files with no device or network calls.""" @@ -12,10 +14,10 @@ def main() -> None: root = Path(temporary) source = root / 'source' source.mkdir() - (source / 'template.bin').write_bytes(b'controlled-template-bytes') - actions = [['AC_locate', {'template': 'template.bin'}]] + (source / _TEMPLATE).write_bytes(b'controlled-template-bytes') + actions = [['AC_locate', {'template': _TEMPLATE}]] (source / 'script.json').write_text(json.dumps(actions), encoding='utf-8') - manifest = build_asset_manifest(source, ['script.json', 'template.bin']) + manifest = build_asset_manifest(source, ['script.json', _TEMPLATE]) transport = DirectoryAssetTransport(source, root / 'cache') result = sync_assets(manifest, transport) repeated = sync_assets(manifest, transport) diff --git a/examples/sync_profile_load.py b/examples/sync_profile_load.py index d25bbe35b..532e3774c 100644 --- a/examples/sync_profile_load.py +++ b/examples/sync_profile_load.py @@ -8,13 +8,16 @@ from examples.sync_referenced_assets import _template_png from je_auto_control import LoadedSyncProfile, SyncReference, SyncWorkspace, load_sync_profile +_SCRIPT = 'demo.json' +_TEMPLATE_ASSET = 'assets/template.png' + def configuration(root: Path) -> dict[str, object]: """Select seven temporary native definition domains and bind one explicit image asset.""" image = root / 'selected-template.png' image.write_bytes(_template_png()) definitions: dict[str, object] = { - 'scripts': {'demo.json': {'actions': [['AC_locate_image_center', {'image': str(image)}]]}}, + 'scripts': {_SCRIPT: {'actions': [['AC_locate_image_center', {'image': str(image)}]]}}, 'locators': {'login': {'name': 'Sign in', 'role': 'button'}}, 'hotkeys': {'demo-key': {'combo': 'ctrl+alt+1', 'script_path': 'scripts/demo.json'}}, 'triggers': {'demo-watch': {'kind': 'file', 'script_path': 'scripts/demo.json', 'repeat': False, @@ -28,8 +31,8 @@ def configuration(root: Path) -> dict[str, object]: {'kind': kind, 'state_path': str(root / 'adapters.sqlite3'), 'source': {'records': values}} for kind, values in definitions.items()] profiles[0]['bindings'] = { - 'fields': {'demo.json': {'/actions/0/1/image': SyncReference('local', 'button').to_dict()}}, - 'local_values': {'button': str(image)}, 'assets': {'button': 'assets/template.png'}} + 'fields': {_SCRIPT: {'/actions/0/1/image': SyncReference('local', 'button').to_dict()}}, + 'local_values': {'button': str(image)}, 'assets': {'button': _TEMPLATE_ASSET}} return {'schema_version': 1, 'endpoint': 'https://controlled.invalid', 'user_id': 'example', 'outbox_path': str(root / 'outbox.sqlite3'), 'profiles': profiles} @@ -42,16 +45,16 @@ def workflow(root: Path) -> dict[str, object]: original = _digests(generation.directory) loaded: LoadedSyncProfile = load_sync_profile(generation.directory, root / 'workspaces', expected_digest=generation.manifest_digest) - actions = loaded.script_actions('demo.json') + actions = loaded.script_actions(_SCRIPT) arguments = cast(dict[str, object], cast(list[object], actions[0])[1]) - assert arguments['image'] == str(loaded.directory / 'assets/template.png') + assert arguments['image'] == str(loaded.directory / _TEMPLATE_ASSET) assert loaded.hotkeys[0].enabled is False and loaded.triggers[0].enabled is False assert loaded.address_book.list_entries()[0]['host_id'] == 'example-host' loaded.locators.save('cancel', name='Cancel') assert 'cancel' in loaded.locators.all() assert original == _digests(generation.directory) return dict(loaded.to_dict(), immutable_source_unchanged=True, executed=0, listeners_started=0, - external_calls=0, image_bytes=(loaded.directory / 'assets/template.png').stat().st_size) + external_calls=0, image_bytes=(loaded.directory / _TEMPLATE_ASSET).stat().st_size) def _digests(directory: Path) -> dict[str, str]: diff --git a/examples/sync_referenced_assets.py b/examples/sync_referenced_assets.py index c0273db4c..58e9e9abf 100644 --- a/examples/sync_referenced_assets.py +++ b/examples/sync_referenced_assets.py @@ -10,6 +10,8 @@ from je_auto_control import CausalConfigStore, SyncAssetStore, SyncReference, SyncWorkspace from je_auto_control.utils.path_guard import PathPolicy +_SCRIPT_NAME = 'demo.json' + def configuration(root: Path, endpoint: str, *, sender: bool) -> dict[str, object]: """Select an exact native action file and bind one image argument to a private asset alias.""" @@ -20,9 +22,9 @@ def configuration(root: Path, endpoint: str, *, sender: bool) -> dict[str, objec image = root / 'template.png' image.write_bytes(_template_png()) actions = [['AC_locate_image_center', {'image': str(image)}]] - (root / 'demo.json').write_text(json.dumps(actions), encoding='utf-8') - source = {'path': str(root), 'paths': ['demo.json']} - bindings = {'fields': {'demo.json': {'/actions/0/1/image': SyncReference('local', 'button').to_dict()}}, + (root / _SCRIPT_NAME).write_text(json.dumps(actions), encoding='utf-8') + source = {'path': str(root), 'paths': [_SCRIPT_NAME]} + bindings = {'fields': {_SCRIPT_NAME: {'/actions/0/1/image': SyncReference('local', 'button').to_dict()}}, 'local_values': {'button': str(image)}, 'assets': {'button': 'assets/template.png'}} return {'schema_version': 1, 'endpoint': endpoint, 'user_id': 'example', 'outbox_path': str(root / 'outbox.sqlite3'), 'asset_cache_path': str(root / 'cache.sqlite3'), diff --git a/je_auto_control/_export_bindings.py b/je_auto_control/_export_bindings.py index 6dabf4a35..f38bd6f7a 100644 --- a/je_auto_control/_export_bindings.py +++ b/je_auto_control/_export_bindings.py @@ -4,1496 +4,1761 @@ PUBLIC_ORDER_SHA256 = 'c109d2a2016c994303368885f610e9332773cd0d5694404d4400efd976fd4266' +_LINUX_WAYLAND_GLOBAL_SHORTCUTS = 'je_auto_control.linux_wayland.global_shortcuts' +_LINUX_WAYLAND_INPUT_EVENTS = 'je_auto_control.linux_wayland.input_events' +_UTILS_A11Y_AUDIT = 'je_auto_control.utils.a11y_audit' +_UTILS_A2A = 'je_auto_control.utils.a2a' +_UTILS_AB_LOCATOR = 'je_auto_control.utils.ab_locator' +_UTILS_ACCESSIBILITY = 'je_auto_control.utils.accessibility' +_UTILS_ACTIONABILITY = 'je_auto_control.utils.actionability' +_UTILS_ACTION_EFFECT = 'je_auto_control.utils.action_effect' +_UTILS_ACTION_GROUNDING = 'je_auto_control.utils.action_grounding' +_UTILS_ACTION_JOURNAL = 'je_auto_control.utils.action_journal' +_UTILS_ACTION_SIGNING = 'je_auto_control.utils.action_signing' +_UTILS_ACT_IN_VIEW = 'je_auto_control.utils.act_in_view' +_UTILS_ADAPTIVE_TIMEOUT = 'je_auto_control.utils.adaptive_timeout' +_UTILS_ADMIN = 'je_auto_control.utils.admin' +_UTILS_AGENT_AGENT_LOOP = 'je_auto_control.utils.agent.agent_loop' +_UTILS_AGENT_BACKENDS = 'je_auto_control.utils.agent.backends' +_UTILS_AGENT_COMPUTER_USE = 'je_auto_control.utils.agent.computer_use' +_UTILS_AGENT_MEMORY = 'je_auto_control.utils.agent_memory' +_UTILS_AGENT_REPLAY = 'je_auto_control.utils.agent_replay' +_UTILS_AGENT_RUNTIME = 'je_auto_control.utils.agent_runtime' +_UTILS_AGENT_TRACE = 'je_auto_control.utils.agent_trace' +_UTILS_ANCHOR_LOCATOR = 'je_auto_control.utils.anchor_locator' +_UTILS_ANOMALY = 'je_auto_control.utils.anomaly' +_UTILS_APPROVAL = 'je_auto_control.utils.approval' +_UTILS_APP_IDLE = 'je_auto_control.utils.app_idle' +_UTILS_ARTIFACT_STORE = 'je_auto_control.utils.artifact_store' +_UTILS_ASSERTION = 'je_auto_control.utils.assertion' +_UTILS_ASSETS = 'je_auto_control.utils.assets' +_UTILS_AX_PROPS = 'je_auto_control.utils.ax_props' +_UTILS_AX_TEXT = 'je_auto_control.utils.ax_text' +_UTILS_AX_TREE_WALK = 'je_auto_control.utils.ax_tree_walk' +_UTILS_BAGGAGE = 'je_auto_control.utils.baggage' +_UTILS_BIDI_CHECK = 'je_auto_control.utils.bidi_check' +_UTILS_BULKHEAD = 'je_auto_control.utils.bulkhead' +_UTILS_CANONICAL_LOG = 'je_auto_control.utils.canonical_log' +_UTILS_CHANGE_LOCALIZE = 'je_auto_control.utils.change_localize' +_UTILS_CHAOS = 'je_auto_control.utils.chaos' +_UTILS_CHATOPS = 'je_auto_control.utils.chatops' +_UTILS_CHECKPOINT = 'je_auto_control.utils.checkpoint' +_UTILS_CHECKSUM = 'je_auto_control.utils.checksum' +_UTILS_CI_ANNOTATIONS = 'je_auto_control.utils.ci_annotations' +_UTILS_CLIPBOARD_CLIPBOARD = 'je_auto_control.utils.clipboard.clipboard' +_UTILS_CLIPBOARD_FILES = 'je_auto_control.utils.clipboard_files' +_UTILS_CLIPBOARD_FORMATS = 'je_auto_control.utils.clipboard_formats' +_UTILS_CLIPBOARD_HISTORY = 'je_auto_control.utils.clipboard_history' +_UTILS_CLIPBOARD_RICH_FORMATS = 'je_auto_control.utils.clipboard_rich_formats' +_UTILS_CODEGEN_CANDIDATE_MODELS = 'je_auto_control.utils.codegen.candidate_models' +_UTILS_CODEGEN_CODEGEN = 'je_auto_control.utils.codegen.codegen' +_UTILS_CODEGEN_ROBOT_CHECK = 'je_auto_control.utils.codegen.robot_check' +_UTILS_COLOR_MATCH = 'je_auto_control.utils.color_match' +_UTILS_COLOR_REGION = 'je_auto_control.utils.color_region' +_UTILS_COLOR_STATS = 'je_auto_control.utils.color_stats' +_UTILS_COLUMN_LAYOUT = 'je_auto_control.utils.column_layout' +_UTILS_COMPLIANCE = 'je_auto_control.utils.compliance' +_UTILS_CONFIG_BUNDLE = 'je_auto_control.utils.config_bundle' +_UTILS_CONFIG_REDACTION = 'je_auto_control.utils.config_redaction' +_UTILS_CONFIG_SCHEMA = 'je_auto_control.utils.config_schema' +_UTILS_CONFIG_SYNC_ADAPTERS = 'je_auto_control.utils.config_sync.adapters' +_UTILS_CONFIG_SYNC_ADAPTER_REFERENCES = 'je_auto_control.utils.config_sync.adapter_references' +_UTILS_CONFIG_SYNC_ASSETS = 'je_auto_control.utils.config_sync.assets' +_UTILS_CONFIG_SYNC_ASSET_MODELS = 'je_auto_control.utils.config_sync.asset_models' +_UTILS_CONFIG_SYNC_BLOB_MODELS = 'je_auto_control.utils.config_sync.blob_models' +_UTILS_CONFIG_SYNC_CAUSAL_MODELS = 'je_auto_control.utils.config_sync.causal_models' +_UTILS_CONFIG_SYNC_CLIENT = 'je_auto_control.utils.config_sync.client' +_UTILS_CONFIG_SYNC_CONTROL_MODELS = 'je_auto_control.utils.config_sync.control_models' +_UTILS_CONFIG_SYNC_OUTBOX = 'je_auto_control.utils.config_sync.outbox' +_UTILS_CONFIG_SYNC_PEERS = 'je_auto_control.utils.config_sync.peers' +_UTILS_CONFIG_SYNC_STORE = 'je_auto_control.utils.config_sync.store' +_UTILS_CONFIG_SYNC_VERSIONS = 'je_auto_control.utils.config_sync.versions' +_UTILS_CONFIG_SYNC_WORKSPACE_MODELS = 'je_auto_control.utils.config_sync.workspace_models' +_UTILS_CONFUSABLES = 'je_auto_control.utils.confusables' +_UTILS_CONTRAST_MAP = 'je_auto_control.utils.contrast_map' +_UTILS_CONTROL_PATTERNS = 'je_auto_control.utils.control_patterns' +_UTILS_COOKIE_JAR = 'je_auto_control.utils.cookie_jar' +_UTILS_COORDINATE_SPACE = 'je_auto_control.utils.coordinate_space' +_UTILS_COST_TELEMETRY = 'je_auto_control.utils.cost_telemetry' +_UTILS_CRITIC_FEATURES = 'je_auto_control.utils.critic_features' +_UTILS_CUA_ACTION = 'je_auto_control.utils.cua_action' +_UTILS_CVD_SIMULATE = 'je_auto_control.utils.cvd_simulate' +_UTILS_DAG = 'je_auto_control.utils.dag' +_UTILS_DATASET_DIFF = 'je_auto_control.utils.dataset_diff' +_UTILS_DATA_DRIFT = 'je_auto_control.utils.data_drift' +_UTILS_DATA_PROFILE = 'je_auto_control.utils.data_profile' +_UTILS_DATA_QUALITY = 'je_auto_control.utils.data_quality' +_UTILS_DATA_SOURCE = 'je_auto_control.utils.data_source' +_UTILS_DECISION_TABLE = 'je_auto_control.utils.decision_table' +_UTILS_DETERMINISTIC = 'je_auto_control.utils.deterministic' +_UTILS_DEVICE_MATRIX = 'je_auto_control.utils.device_matrix' +_UTILS_DIAGNOSTICS = 'je_auto_control.utils.diagnostics' +_UTILS_DOTENV = 'je_auto_control.utils.dotenv' +_UTILS_EDGE_LINES = 'je_auto_control.utils.edge_lines' +_UTILS_EDGE_MATCH = 'je_auto_control.utils.edge_match' +_UTILS_EGRESS = 'je_auto_control.utils.egress' +_UTILS_ELEMENT_DIFF = 'je_auto_control.utils.element_diff' +_UTILS_ELEMENT_PARSE = 'je_auto_control.utils.element_parse' +_UTILS_ELEMENT_PROPOSAL = 'je_auto_control.utils.element_proposal' +_UTILS_ELEMENT_SCORING = 'je_auto_control.utils.element_scoring' +_UTILS_ENSURE_STATE = 'je_auto_control.utils.ensure_state' +_UTILS_EVENTS = 'je_auto_control.utils.events' +_UTILS_EXCEPTION_CANCELLATION = 'je_auto_control.utils.exception.cancellation' +_UTILS_EXCEPTION_EXCEPTIONS = 'je_auto_control.utils.exception.exceptions' +_UTILS_EXECUTOR_ACTION_EXECUTOR = 'je_auto_control.utils.executor.action_executor' +_UTILS_EXECUTOR_RUN_CONTROL = 'je_auto_control.utils.executor.run_control' +_UTILS_EXPECT_POLL = 'je_auto_control.utils.expect_poll' +_UTILS_FAILURE_HOOKS = 'je_auto_control.utils.failure_hooks' +_UTILS_FAILURE_SIGNATURE = 'je_auto_control.utils.failure_signature' +_UTILS_FEATURE_FLAGS = 'je_auto_control.utils.feature_flags' +_UTILS_FEATURE_MATCH = 'je_auto_control.utils.feature_match' +_UTILS_FIELD_ENTRY = 'je_auto_control.utils.field_entry' +_UTILS_FILE_ASSOC = 'je_auto_control.utils.file_assoc' +_UTILS_FILE_DIALOG = 'je_auto_control.utils.file_dialog' +_UTILS_FILE_DROP = 'je_auto_control.utils.file_drop' +_UTILS_FLAKE_CLUSTER = 'je_auto_control.utils.flake_cluster' +_UTILS_FLAKINESS = 'je_auto_control.utils.flakiness' +_UTILS_FLOW_DEBUGGER = 'je_auto_control.utils.flow_debugger' +_UTILS_FOCUS_ORDER = 'je_auto_control.utils.focus_order' +_UTILS_FORM_FIELDS = 'je_auto_control.utils.form_fields' +_UTILS_FUZZY = 'je_auto_control.utils.fuzzy' +_UTILS_GAMEPAD = 'je_auto_control.utils.gamepad' +_UTILS_GENERATE_REPORT_GENERATE_HTML_REPORT = 'je_auto_control.utils.generate_report.generate_html_report' +_UTILS_GENERATE_REPORT_GENERATE_JSON_REPORT = 'je_auto_control.utils.generate_report.generate_json_report' +_UTILS_GENERATE_REPORT_GENERATE_XML_REPORT = 'je_auto_control.utils.generate_report.generate_xml_report' +_UTILS_GETTEXT_CATALOG = 'je_auto_control.utils.gettext_catalog' +_UTILS_GOVERNANCE = 'je_auto_control.utils.governance' +_UTILS_GRID_LOCATOR = 'je_auto_control.utils.grid_locator' +_UTILS_GROUNDING_CONSENSUS = 'je_auto_control.utils.grounding_consensus' +_UTILS_GUARDRAIL = 'je_auto_control.utils.guardrail' +_UTILS_HEADING_SEGMENT = 'je_auto_control.utils.heading_segment' +_UTILS_HEAL_ANALYTICS = 'je_auto_control.utils.heal_analytics' +_UTILS_HOTKEY_HOTKEY_DAEMON = 'je_auto_control.utils.hotkey.hotkey_daemon' +_UTILS_HSV_SEGMENT = 'je_auto_control.utils.hsv_segment' +_UTILS_HTTP_CASSETTE = 'je_auto_control.utils.http_cassette' +_UTILS_HTTP_CONDITIONAL = 'je_auto_control.utils.http_conditional' +_UTILS_HTTP_CONTENT = 'je_auto_control.utils.http_content' +_UTILS_HTTP_PROBLEM = 'je_auto_control.utils.http_problem' +_UTILS_HUMANIZE_MOTION = 'je_auto_control.utils.humanize.motion' +_UTILS_HUMANIZE_TYPING = 'je_auto_control.utils.humanize.typing' +_UTILS_I18N_TEST = 'je_auto_control.utils.i18n_test' +_UTILS_ICON_CLASSIFY = 'je_auto_control.utils.icon_classify' +_UTILS_IDEMPOTENCY = 'je_auto_control.utils.idempotency' +_UTILS_IDLE_KEEPAWAKE = 'je_auto_control.utils.idle_keepawake' +_UTILS_IMAGE_DEDUP = 'je_auto_control.utils.image_dedup' +_UTILS_IMAGE_QUALITY = 'je_auto_control.utils.image_quality' +_UTILS_IME_STATE = 'je_auto_control.utils.ime_state' +_UTILS_IMG_HISTOGRAM = 'je_auto_control.utils.img_histogram' +_UTILS_INPUT_MACRO = 'je_auto_control.utils.input_macro' +_UTILS_INPUT_REACH = 'je_auto_control.utils.input_reach' +_UTILS_JSONPATH = 'je_auto_control.utils.jsonpath' +_UTILS_JSON_CONTRACT = 'je_auto_control.utils.json_contract' +_UTILS_JSON_JSON_FILE = 'je_auto_control.utils.json.json_file' +_UTILS_JSON_PATCH = 'je_auto_control.utils.json_patch' +_UTILS_JSON_SCHEMA = 'je_auto_control.utils.json_schema' +_UTILS_JWT = 'je_auto_control.utils.jwt' +_UTILS_KEYBOARD_LAYOUT = 'je_auto_control.utils.keyboard_layout' +_UTILS_KEY_HOLD = 'je_auto_control.utils.key_hold' +_UTILS_LAYERED_CONFIG = 'je_auto_control.utils.layered_config' +_UTILS_LEGACY_ACCESSIBLE = 'je_auto_control.utils.legacy_accessible' +_UTILS_LICENSE_POLICY = 'je_auto_control.utils.license_policy' +_UTILS_LINK_HEADER = 'je_auto_control.utils.link_header' +_UTILS_LLM = 'je_auto_control.utils.llm' +_UTILS_LOCALE_COLLATION = 'je_auto_control.utils.locale_collation' +_UTILS_LOCALE_PARSE = 'je_auto_control.utils.locale_parse' +_UTILS_LOCATOR_CHAIN = 'je_auto_control.utils.locator_chain' +_UTILS_LOCATOR_REPAIR = 'je_auto_control.utils.locator_repair' +_UTILS_LOCK_SESSION = 'je_auto_control.utils.lock_session' +_UTILS_LOOP_GUARD = 'je_auto_control.utils.loop_guard' +_UTILS_MARKS_LAYOUT = 'je_auto_control.utils.marks_layout' +_UTILS_MATCH_AUTOTHRESH = 'je_auto_control.utils.match_autothresh' +_UTILS_MATCH_ENSEMBLE = 'je_auto_control.utils.match_ensemble' +_UTILS_MATCH_STABILITY = 'je_auto_control.utils.match_stability' +_UTILS_MATCH_TRUST = 'je_auto_control.utils.match_trust' +_UTILS_MCP_REGISTRY = 'je_auto_control.utils.mcp_registry' +_UTILS_MCP_SERVER = 'je_auto_control.utils.mcp_server' +_UTILS_MCP_SERVER_DISCLOSURE = 'je_auto_control.utils.mcp_server.disclosure' +_UTILS_MCP_SERVER_DISCOVERY = 'je_auto_control.utils.mcp_server.discovery' +_UTILS_MEDIA_ASSERT = 'je_auto_control.utils.media_assert' +_UTILS_MESSAGE_FORMAT = 'je_auto_control.utils.message_format' +_UTILS_MODIFIER_STATE = 'je_auto_control.utils.modifier_state' +_UTILS_MONITOR_LAYOUT = 'je_auto_control.utils.monitor_layout' +_UTILS_MOTION_REGIONS = 'je_auto_control.utils.motion_regions' +_UTILS_MOUSE_PATH = 'je_auto_control.utils.mouse_path' +_UTILS_MOUSE_RELATIVE = 'je_auto_control.utils.mouse_relative' +_UTILS_MULTIPART = 'je_auto_control.utils.multipart' +_UTILS_NEAR_DUP = 'je_auto_control.utils.near_dup' +_UTILS_NOTIFY = 'je_auto_control.utils.notify' +_UTILS_NOTIFY_CHANNELS = 'je_auto_control.utils.notify_channels' +_UTILS_OBSERVABILITY = 'je_auto_control.utils.observability' +_UTILS_OBSERVATION = 'je_auto_control.utils.observation' +_UTILS_OBSERVATION_DELTA = 'je_auto_control.utils.observation_delta' +_UTILS_OBSERVER = 'je_auto_control.utils.observer' +_UTILS_OCR_OCR_ENGINE = 'je_auto_control.utils.ocr.ocr_engine' +_UTILS_OCR_STRUCTURE = 'je_auto_control.utils.ocr.structure' +_UTILS_OCR_TESSERACT_SETUP = 'je_auto_control.utils.ocr.tesseract_setup' +_UTILS_OCR_TEXT_SPAN = 'je_auto_control.utils.ocr.text_span' +_UTILS_OFFICE = 'je_auto_control.utils.office' +_UTILS_OPTIMISTIC = 'je_auto_control.utils.optimistic' +_UTILS_OTLP_EXPORT = 'je_auto_control.utils.otlp_export' +_UTILS_OTP = 'je_auto_control.utils.otp' +_UTILS_PATH_GUARD = 'je_auto_control.utils.path_guard' +_UTILS_PDF_PDF_READER = 'je_auto_control.utils.pdf.pdf_reader' +_UTILS_PERCENTILES = 'je_auto_control.utils.percentiles' +_UTILS_PERCEPTUAL_DIFF = 'je_auto_control.utils.perceptual_diff' +_UTILS_PII_TEXT = 'je_auto_control.utils.pii_text' +_UTILS_PLUGIN_LOADER_PLUGIN_LOADER = 'je_auto_control.utils.plugin_loader.plugin_loader' +_UTILS_PLUGIN_SDK = 'je_auto_control.utils.plugin_sdk' +_UTILS_POSTCONDITION = 'je_auto_control.utils.postcondition' +_UTILS_PREPROCESS = 'je_auto_control.utils.preprocess' +_UTILS_PROCESS_DOC = 'je_auto_control.utils.process_doc' +_UTILS_PROCESS_MINING = 'je_auto_control.utils.process_mining' +_UTILS_PROFILER = 'je_auto_control.utils.profiler' +_UTILS_PROVENANCE = 'je_auto_control.utils.provenance' +_UTILS_QUARANTINE = 'je_auto_control.utils.quarantine' +_UTILS_RATE_LIMIT = 'je_auto_control.utils.rate_limit' +_UTILS_RBAC = 'je_auto_control.utils.rbac' +_UTILS_RBAC_ADMIN = 'je_auto_control.utils.rbac.admin' +_UTILS_READABILITY = 'je_auto_control.utils.readability' +_UTILS_READING_FLOW = 'je_auto_control.utils.reading_flow' +_UTILS_RECORDING_EDIT_EDITOR = 'je_auto_control.utils.recording_edit.editor' +_UTILS_RECURRENCE = 'je_auto_control.utils.recurrence' +_UTILS_REDACTION = 'je_auto_control.utils.redaction' +_UTILS_REFERENTIAL = 'je_auto_control.utils.referential' +_UTILS_REMOTE_DESKTOP = 'je_auto_control.utils.remote_desktop' +_UTILS_REMOTE_DESKTOP_CONNECT_COORDINATOR = 'je_auto_control.utils.remote_desktop.connect_coordinator' +_UTILS_REMOTE_DESKTOP_PRESENCE = 'je_auto_control.utils.remote_desktop.presence' +_UTILS_REMOTE_DESKTOP_WEBRTC_INSPECTOR = 'je_auto_control.utils.remote_desktop.webrtc_inspector' +_UTILS_RESILIENCE = 'je_auto_control.utils.resilience' +_UTILS_REST_API_REST_SERVER = 'je_auto_control.utils.rest_api.rest_server' +_UTILS_RETRY_BUDGET = 'je_auto_control.utils.retry_budget' +_UTILS_RICH_CLIPBOARD = 'je_auto_control.utils.rich_clipboard' +_UTILS_ROTATED_MATCH = 'je_auto_control.utils.rotated_match' +_UTILS_RUN_DIFF = 'je_auto_control.utils.run_diff' +_UTILS_RUN_HISTORY = 'je_auto_control.utils.run_history' +_UTILS_RUN_HISTORY_HISTORY_STORE = 'je_auto_control.utils.run_history.history_store' +_UTILS_SAGA = 'je_auto_control.utils.saga' +_UTILS_SALIENCY = 'je_auto_control.utils.saliency' +_UTILS_SARIF = 'je_auto_control.utils.sarif' +_UTILS_SBOM = 'je_auto_control.utils.sbom' +_UTILS_SCALE_DETECT = 'je_auto_control.utils.scale_detect' +_UTILS_SCHEDULER_SCHEDULER = 'je_auto_control.utils.scheduler.scheduler' +_UTILS_SCHEMA_COMPAT = 'je_auto_control.utils.schema_compat' +_UTILS_SCREEN_GRID = 'je_auto_control.utils.screen_grid' +_UTILS_SCREEN_STATE = 'je_auto_control.utils.screen_state' +_UTILS_SCRIPT_VARS_INTERPOLATE = 'je_auto_control.utils.script_vars.interpolate' +_UTILS_SEARCH_INDEX = 'je_auto_control.utils.search_index' +_UTILS_SECRETS = 'je_auto_control.utils.secrets' +_UTILS_SECRET_REF = 'je_auto_control.utils.secret_ref' # nosec B105 # reason: an owner module path, not a credential +_UTILS_SELECTION_VIEW = 'je_auto_control.utils.selection_view' +_UTILS_SELF_HEALING = 'je_auto_control.utils.self_healing' +_UTILS_SESSION_GUARD = 'je_auto_control.utils.session_guard' +_UTILS_SETTLE_DETECTOR = 'je_auto_control.utils.settle_detector' +_UTILS_SET_OF_MARKS = 'je_auto_control.utils.set_of_marks' +_UTILS_SHAPE_LOCATOR = 'je_auto_control.utils.shape_locator' +_UTILS_SHELL_OPEN = 'je_auto_control.utils.shell_open' +_UTILS_SHELL_PROCESS_SHELL_EXEC = 'je_auto_control.utils.shell_process.shell_exec' +_UTILS_SKILL_LIBRARY = 'je_auto_control.utils.skill_library' +_UTILS_SLO = 'je_auto_control.utils.slo' +_UTILS_SMART_WAITS = 'je_auto_control.utils.smart_waits' +_UTILS_SMOOTHING = 'je_auto_control.utils.smoothing' +_UTILS_SOFT_ASSERT = 'je_auto_control.utils.soft_assert' +_UTILS_SSE_CLIENT = 'je_auto_control.utils.sse_client' +_UTILS_SSIM = 'je_auto_control.utils.ssim' +_UTILS_STATE_MACHINE = 'je_auto_control.utils.state_machine' +_UTILS_STATS = 'je_auto_control.utils.stats' +_UTILS_STEP_REPAIR = 'je_auto_control.utils.step_repair' +_UTILS_STEP_TIMELINE = 'je_auto_control.utils.step_timeline' +_UTILS_SUBPIXEL_MATCH = 'je_auto_control.utils.subpixel_match' +_UTILS_SYSTEM_VOLUME = 'je_auto_control.utils.system_volume' +_UTILS_TABLE_GRID_FILL = 'je_auto_control.utils.table_grid_fill' +_UTILS_TABLE_PATTERN = 'je_auto_control.utils.table_pattern' +_UTILS_TEST_DATA = 'je_auto_control.utils.test_data' +_UTILS_TEST_SELECT = 'je_auto_control.utils.test_select' +_UTILS_TEST_SHARD = 'je_auto_control.utils.test_shard' +_UTILS_TEST_SUITE = 'je_auto_control.utils.test_suite' +_UTILS_TEXT_BLOCKS = 'je_auto_control.utils.text_blocks' +_UTILS_TEXT_DIFF = 'je_auto_control.utils.text_diff' +_UTILS_TEXT_NORMALIZE = 'je_auto_control.utils.text_normalize' +_UTILS_TEXT_REGIONS = 'je_auto_control.utils.text_regions' +_UTILS_TEXT_SIMILARITY = 'je_auto_control.utils.text_similarity' +_UTILS_TEXT_UNICODE = 'je_auto_control.utils.text_unicode' +_UTILS_THEME_NORMALIZE = 'je_auto_control.utils.theme_normalize' +_UTILS_TIMESERIES = 'je_auto_control.utils.timeseries' +_UTILS_TRACE_CONTEXT = 'je_auto_control.utils.trace_context' +_UTILS_TRANSFORM_WINDOW = 'je_auto_control.utils.transform_window' +_UTILS_TRIGGERS_EMAIL_TRIGGER = 'je_auto_control.utils.triggers.email_trigger' +_UTILS_TRIGGERS_TRIGGER_ENGINE = 'je_auto_control.utils.triggers.trigger_engine' +_UTILS_TRIGGERS_WEBHOOK_SERVER = 'je_auto_control.utils.triggers.webhook_server' +_UTILS_TWEEN_DRAG = 'je_auto_control.utils.tween_drag' +_UTILS_URL_CANON = 'je_auto_control.utils.url_canon' +_UTILS_USB = 'je_auto_control.utils.usb' +_UTILS_VERIFY_FIELD = 'je_auto_control.utils.verify_field' +_UTILS_VEX = 'je_auto_control.utils.vex' +_UTILS_VIDEO_REPORT = 'je_auto_control.utils.video_report' +_UTILS_VISION = 'je_auto_control.utils.vision' +_UTILS_VISUAL_MATCH = 'je_auto_control.utils.visual_match' +_UTILS_VISUAL_REGRESSION = 'je_auto_control.utils.visual_regression' +_UTILS_VOICE = 'je_auto_control.utils.voice' +_UTILS_VULN_SCAN = 'je_auto_control.utils.vuln_scan' +_UTILS_WATCHDOG = 'je_auto_control.utils.watchdog' +_UTILS_WATCHER_WATCHER = 'je_auto_control.utils.watcher.watcher' +_UTILS_WEBRUNNER_BRIDGE = 'je_auto_control.utils.webrunner_bridge' +_UTILS_WINDOW_CAPTURE = 'je_auto_control.utils.window_capture' +_UTILS_WINDOW_GEOMETRY = 'je_auto_control.utils.window_geometry' +_UTILS_WINDOW_LAYOUT = 'je_auto_control.utils.window_layout' +_UTILS_WINDOW_ZORDER = 'je_auto_control.utils.window_zorder' +_UTILS_WORK_QUEUE = 'je_auto_control.utils.work_queue' +_WRAPPER_AUTO_CONTROL_IMAGE = 'je_auto_control.wrapper.auto_control_image' +_WRAPPER_AUTO_CONTROL_KEYBOARD = 'je_auto_control.wrapper.auto_control_keyboard' +_WRAPPER_AUTO_CONTROL_MOUSE = 'je_auto_control.wrapper.auto_control_mouse' +_WRAPPER_AUTO_CONTROL_RECORD = 'je_auto_control.wrapper.auto_control_record' +_WRAPPER_AUTO_CONTROL_SCREEN = 'je_auto_control.wrapper.auto_control_screen' +_WRAPPER_AUTO_CONTROL_WINDOW = 'je_auto_control.wrapper.auto_control_window' +_WRAPPER_CAPABILITIES = 'je_auto_control.wrapper.capabilities' +_WRAPPER_DEVICE_CONTEXT = 'je_auto_control.wrapper.device_context' +_WRAPPER_MOBILE_COMMANDS = 'je_auto_control.wrapper.mobile_commands' +_WRAPPER_MOBILE_EXTENSIONS = 'je_auto_control.wrapper.mobile_extensions' + EXPORTS: dict[str, ExportBinding] = { - 'AgentTargetProfile': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentTargetProfile'), - 'manage_agent_target': AttributeBinding('je_auto_control.utils.agent_runtime', 'manage_agent_target'), - 'list_agent_targets': AttributeBinding('je_auto_control.utils.agent_runtime', 'list_agent_targets'), - 'draft_agent_workflow': AttributeBinding('je_auto_control.utils.agent_runtime', 'draft_agent_workflow'), - 'workflow_download': AttributeBinding('je_auto_control.utils.agent_runtime', 'workflow_download'), - 'workflow_manifest': AttributeBinding('je_auto_control.utils.agent_runtime', 'workflow_manifest'), - 'workflow_control': AttributeBinding('je_auto_control.utils.agent_runtime', 'workflow_control'), - 'workflow_open': AttributeBinding('je_auto_control.utils.agent_runtime', 'workflow_open'), - 'workflow_wait': AttributeBinding('je_auto_control.utils.agent_runtime', 'workflow_wait'), - 'browser_navigate': AttributeBinding('je_auto_control.utils.agent_runtime', 'browser_navigate'), - 'browser_click': AttributeBinding('je_auto_control.utils.agent_runtime', 'browser_click'), - 'browser_fill': AttributeBinding('je_auto_control.utils.agent_runtime', 'browser_fill'), - 'browser_select': AttributeBinding('je_auto_control.utils.agent_runtime', 'browser_select'), - 'browser_check': AttributeBinding('je_auto_control.utils.agent_runtime', 'browser_check'), - 'browser_read': AttributeBinding('je_auto_control.utils.agent_runtime', 'browser_read'), - 'browser_wait': AttributeBinding('je_auto_control.utils.agent_runtime', 'browser_wait'), - 'browser_link': AttributeBinding('je_auto_control.utils.agent_runtime', 'browser_link'), - 'browser_download': AttributeBinding('je_auto_control.utils.agent_runtime', 'browser_download'), - 'browser_upload': AttributeBinding('je_auto_control.utils.agent_runtime', 'browser_upload'), - 'AgentActionRequest': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentActionRequest'), - 'AgentActionResult': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentActionResult'), - 'AgentControlBusy': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentControlBusy'), - 'AgentObservation': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentObservation'), - 'AgentRuntime': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentRuntime'), - 'AgentTaskBudget': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentTaskBudget'), - 'AgentTaskError': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentTaskError'), - 'AgentTaskHandle': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentTaskHandle'), - 'AgentTaskResult': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentTaskResult'), - 'AgentTaskSpec': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentTaskSpec'), - 'get_agent_task_receipts': AttributeBinding('je_auto_control.utils.agent_runtime', 'get_agent_task_receipts'), - 'reconcile_agent_task': AttributeBinding('je_auto_control.utils.agent_runtime', 'reconcile_agent_task'), - 'prune_agent_tasks': AttributeBinding('je_auto_control.utils.agent_runtime', 'prune_agent_tasks'), - 'recover_agent_target': AttributeBinding('je_auto_control.utils.agent_runtime', 'recover_agent_target'), - 'AgentObservationStale': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentObservationStale'), - 'AgentHumanReview': AttributeBinding('je_auto_control.utils.agent_runtime', 'AgentHumanReview'), - 'manage_agent_transfer': AttributeBinding('je_auto_control.utils.agent_runtime', 'manage_agent_transfer'), - 'manage_agent_skill': AttributeBinding('je_auto_control.utils.agent_runtime', 'manage_agent_skill'), - 'list_agent_skills': AttributeBinding('je_auto_control.utils.agent_runtime', 'list_agent_skills'), - 'manage_agent_recovery': AttributeBinding('je_auto_control.utils.agent_runtime', 'manage_agent_recovery'), - 'manage_agent_trigger': AttributeBinding('je_auto_control.utils.agent_runtime', 'manage_agent_trigger'), - 'list_agent_triggers': AttributeBinding('je_auto_control.utils.agent_runtime', 'list_agent_triggers'), - 'act_agent_task': AttributeBinding('je_auto_control.utils.agent_runtime', 'act_agent_task'), - 'agent_runtime_readiness': AttributeBinding('je_auto_control.utils.agent_runtime', 'agent_runtime_readiness'), - 'agent_runtime_status': AttributeBinding('je_auto_control.utils.agent_runtime', 'agent_runtime_status'), - 'cancel_agent_task': AttributeBinding('je_auto_control.utils.agent_runtime', 'cancel_agent_task'), - 'finish_agent_task': AttributeBinding('je_auto_control.utils.agent_runtime', 'finish_agent_task'), - 'get_agent_task': AttributeBinding('je_auto_control.utils.agent_runtime', 'get_agent_task'), - 'lookup_agent_task': AttributeBinding('je_auto_control.utils.agent_runtime', 'lookup_agent_task'), - 'inspect_agent_dag': AttributeBinding('je_auto_control.utils.agent_runtime', 'inspect_agent_dag'), - 'manage_agent_dag': AttributeBinding('je_auto_control.utils.agent_runtime', 'manage_agent_dag'), - 'list_agent_tasks': AttributeBinding('je_auto_control.utils.agent_runtime', 'list_agent_tasks'), - 'observe_agent_task': AttributeBinding('je_auto_control.utils.agent_runtime', 'observe_agent_task'), - 'pause_agent_task': AttributeBinding('je_auto_control.utils.agent_runtime', 'pause_agent_task'), - 'resume_agent_task': AttributeBinding('je_auto_control.utils.agent_runtime', 'resume_agent_task'), - 'start_agent_runtime': AttributeBinding('je_auto_control.utils.agent_runtime', 'start_agent_runtime'), - 'submit_agent_task': AttributeBinding('je_auto_control.utils.agent_runtime', 'submit_agent_task'), - 'stop_agent_runtime': AttributeBinding('je_auto_control.utils.agent_runtime', 'stop_agent_runtime'), - 'stop_all_agent_tasks': AttributeBinding('je_auto_control.utils.agent_runtime', 'stop_all_agent_tasks'), + 'AgentTargetProfile': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentTargetProfile'), + 'manage_agent_target': AttributeBinding(_UTILS_AGENT_RUNTIME, 'manage_agent_target'), + 'list_agent_targets': AttributeBinding(_UTILS_AGENT_RUNTIME, 'list_agent_targets'), + 'draft_agent_workflow': AttributeBinding(_UTILS_AGENT_RUNTIME, 'draft_agent_workflow'), + 'workflow_download': AttributeBinding(_UTILS_AGENT_RUNTIME, 'workflow_download'), + 'workflow_manifest': AttributeBinding(_UTILS_AGENT_RUNTIME, 'workflow_manifest'), + 'workflow_control': AttributeBinding(_UTILS_AGENT_RUNTIME, 'workflow_control'), + 'workflow_open': AttributeBinding(_UTILS_AGENT_RUNTIME, 'workflow_open'), + 'workflow_wait': AttributeBinding(_UTILS_AGENT_RUNTIME, 'workflow_wait'), + 'browser_navigate': AttributeBinding(_UTILS_AGENT_RUNTIME, 'browser_navigate'), + 'browser_click': AttributeBinding(_UTILS_AGENT_RUNTIME, 'browser_click'), + 'browser_fill': AttributeBinding(_UTILS_AGENT_RUNTIME, 'browser_fill'), + 'browser_select': AttributeBinding(_UTILS_AGENT_RUNTIME, 'browser_select'), + 'browser_check': AttributeBinding(_UTILS_AGENT_RUNTIME, 'browser_check'), + 'browser_read': AttributeBinding(_UTILS_AGENT_RUNTIME, 'browser_read'), + 'browser_wait': AttributeBinding(_UTILS_AGENT_RUNTIME, 'browser_wait'), + 'browser_link': AttributeBinding(_UTILS_AGENT_RUNTIME, 'browser_link'), + 'browser_download': AttributeBinding(_UTILS_AGENT_RUNTIME, 'browser_download'), + 'browser_upload': AttributeBinding(_UTILS_AGENT_RUNTIME, 'browser_upload'), + 'AgentActionRequest': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentActionRequest'), + 'AgentActionResult': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentActionResult'), + 'AgentControlBusy': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentControlBusy'), + 'AgentObservation': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentObservation'), + 'AgentRuntime': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentRuntime'), + 'AgentTaskBudget': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentTaskBudget'), + 'AgentTaskError': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentTaskError'), + 'AgentTaskHandle': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentTaskHandle'), + 'AgentTaskResult': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentTaskResult'), + 'AgentTaskSpec': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentTaskSpec'), + 'get_agent_task_receipts': AttributeBinding(_UTILS_AGENT_RUNTIME, 'get_agent_task_receipts'), + 'reconcile_agent_task': AttributeBinding(_UTILS_AGENT_RUNTIME, 'reconcile_agent_task'), + 'prune_agent_tasks': AttributeBinding(_UTILS_AGENT_RUNTIME, 'prune_agent_tasks'), + 'recover_agent_target': AttributeBinding(_UTILS_AGENT_RUNTIME, 'recover_agent_target'), + 'AgentObservationStale': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentObservationStale'), + 'AgentHumanReview': AttributeBinding(_UTILS_AGENT_RUNTIME, 'AgentHumanReview'), + 'manage_agent_transfer': AttributeBinding(_UTILS_AGENT_RUNTIME, 'manage_agent_transfer'), + 'manage_agent_skill': AttributeBinding(_UTILS_AGENT_RUNTIME, 'manage_agent_skill'), + 'list_agent_skills': AttributeBinding(_UTILS_AGENT_RUNTIME, 'list_agent_skills'), + 'manage_agent_recovery': AttributeBinding(_UTILS_AGENT_RUNTIME, 'manage_agent_recovery'), + 'manage_agent_trigger': AttributeBinding(_UTILS_AGENT_RUNTIME, 'manage_agent_trigger'), + 'list_agent_triggers': AttributeBinding(_UTILS_AGENT_RUNTIME, 'list_agent_triggers'), + 'act_agent_task': AttributeBinding(_UTILS_AGENT_RUNTIME, 'act_agent_task'), + 'agent_runtime_readiness': AttributeBinding(_UTILS_AGENT_RUNTIME, 'agent_runtime_readiness'), + 'agent_runtime_status': AttributeBinding(_UTILS_AGENT_RUNTIME, 'agent_runtime_status'), + 'cancel_agent_task': AttributeBinding(_UTILS_AGENT_RUNTIME, 'cancel_agent_task'), + 'finish_agent_task': AttributeBinding(_UTILS_AGENT_RUNTIME, 'finish_agent_task'), + 'get_agent_task': AttributeBinding(_UTILS_AGENT_RUNTIME, 'get_agent_task'), + 'lookup_agent_task': AttributeBinding(_UTILS_AGENT_RUNTIME, 'lookup_agent_task'), + 'inspect_agent_dag': AttributeBinding(_UTILS_AGENT_RUNTIME, 'inspect_agent_dag'), + 'manage_agent_dag': AttributeBinding(_UTILS_AGENT_RUNTIME, 'manage_agent_dag'), + 'list_agent_tasks': AttributeBinding(_UTILS_AGENT_RUNTIME, 'list_agent_tasks'), + 'observe_agent_task': AttributeBinding(_UTILS_AGENT_RUNTIME, 'observe_agent_task'), + 'pause_agent_task': AttributeBinding(_UTILS_AGENT_RUNTIME, 'pause_agent_task'), + 'resume_agent_task': AttributeBinding(_UTILS_AGENT_RUNTIME, 'resume_agent_task'), + 'start_agent_runtime': AttributeBinding(_UTILS_AGENT_RUNTIME, 'start_agent_runtime'), + 'submit_agent_task': AttributeBinding(_UTILS_AGENT_RUNTIME, 'submit_agent_task'), + 'stop_agent_runtime': AttributeBinding(_UTILS_AGENT_RUNTIME, 'stop_agent_runtime'), + 'stop_all_agent_tasks': AttributeBinding(_UTILS_AGENT_RUNTIME, 'stop_all_agent_tasks'), 'SyncWorkspace': AttributeBinding('je_auto_control.utils.config_sync.workspace_sync', 'SyncWorkspace'), - 'SyncWorkspaceError': AttributeBinding('je_auto_control.utils.config_sync.workspace_models', 'SyncWorkspaceError'), - 'WorkspaceSyncResult': - AttributeBinding('je_auto_control.utils.config_sync.workspace_models', 'WorkspaceSyncResult'), + 'SyncWorkspaceError': AttributeBinding(_UTILS_CONFIG_SYNC_WORKSPACE_MODELS, 'SyncWorkspaceError'), + 'WorkspaceSyncResult': AttributeBinding(_UTILS_CONFIG_SYNC_WORKSPACE_MODELS, 'WorkspaceSyncResult'), 'load_sync_profile': AttributeBinding('je_auto_control.utils.config_sync.profile_load', 'load_sync_profile'), 'LoadedSyncProfile': AttributeBinding('je_auto_control.utils.config_sync.profile_models', 'LoadedSyncProfile'), - 'SyncAssetBlob': AttributeBinding('je_auto_control.utils.config_sync.blob_models', 'SyncAssetBlob'), - 'SyncAssetReference': AttributeBinding('je_auto_control.utils.config_sync.blob_models', 'SyncAssetReference'), + 'SyncAssetBlob': AttributeBinding(_UTILS_CONFIG_SYNC_BLOB_MODELS, 'SyncAssetBlob'), + 'SyncAssetReference': AttributeBinding(_UTILS_CONFIG_SYNC_BLOB_MODELS, 'SyncAssetReference'), 'SyncAssetStore': AttributeBinding('je_auto_control.utils.config_sync.blob_store', 'SyncAssetStore'), 'SyncAssetHTTPClient': AttributeBinding('je_auto_control.utils.config_sync.blob_http', 'SyncAssetHTTPClient'), - 'SyncControlOperation': - AttributeBinding('je_auto_control.utils.config_sync.control_models', 'SyncControlOperation'), - 'SyncControlRequest': AttributeBinding('je_auto_control.utils.config_sync.control_models', 'SyncControlRequest'), - 'SyncControlResult': AttributeBinding('je_auto_control.utils.config_sync.control_models', 'SyncControlResult'), - 'AssetFile': AttributeBinding('je_auto_control.utils.config_sync.asset_models', 'AssetFile'), - 'AssetManifest': AttributeBinding('je_auto_control.utils.config_sync.asset_models', 'AssetManifest'), - 'AssetSyncResult': AttributeBinding('je_auto_control.utils.config_sync.asset_models', 'AssetSyncResult'), - 'AssetTransport': AttributeBinding('je_auto_control.utils.config_sync.assets', 'AssetTransport'), - 'DirectoryAssetTransport': AttributeBinding('je_auto_control.utils.config_sync.assets', 'DirectoryAssetTransport'), - 'SyncBindings': AttributeBinding('je_auto_control.utils.config_sync.adapter_references', 'SyncBindings'), - 'SyncReference': AttributeBinding('je_auto_control.utils.config_sync.adapter_references', 'SyncReference'), + 'SyncControlOperation': AttributeBinding(_UTILS_CONFIG_SYNC_CONTROL_MODELS, 'SyncControlOperation'), + 'SyncControlRequest': AttributeBinding(_UTILS_CONFIG_SYNC_CONTROL_MODELS, 'SyncControlRequest'), + 'SyncControlResult': AttributeBinding(_UTILS_CONFIG_SYNC_CONTROL_MODELS, 'SyncControlResult'), + 'AssetFile': AttributeBinding(_UTILS_CONFIG_SYNC_ASSET_MODELS, 'AssetFile'), + 'AssetManifest': AttributeBinding(_UTILS_CONFIG_SYNC_ASSET_MODELS, 'AssetManifest'), + 'AssetSyncResult': AttributeBinding(_UTILS_CONFIG_SYNC_ASSET_MODELS, 'AssetSyncResult'), + 'AssetTransport': AttributeBinding(_UTILS_CONFIG_SYNC_ASSETS, 'AssetTransport'), + 'DirectoryAssetTransport': AttributeBinding(_UTILS_CONFIG_SYNC_ASSETS, 'DirectoryAssetTransport'), + 'SyncBindings': AttributeBinding(_UTILS_CONFIG_SYNC_ADAPTER_REFERENCES, 'SyncBindings'), + 'SyncReference': AttributeBinding(_UTILS_CONFIG_SYNC_ADAPTER_REFERENCES, 'SyncReference'), 'ApplyReport': AttributeBinding('je_auto_control.utils.config_sync.adapter_schema', 'ApplyReport'), 'SyncAdapterState': AttributeBinding('je_auto_control.utils.config_sync.adapter_state', 'SyncAdapterState'), - 'SyncAdapter': AttributeBinding('je_auto_control.utils.config_sync.adapters', 'SyncAdapter'), - 'ScriptSyncAdapter': AttributeBinding('je_auto_control.utils.config_sync.adapters', 'ScriptSyncAdapter'), - 'LocatorSyncAdapter': AttributeBinding('je_auto_control.utils.config_sync.adapters', 'LocatorSyncAdapter'), - 'HotkeySyncAdapter': AttributeBinding('je_auto_control.utils.config_sync.adapters', 'HotkeySyncAdapter'), - 'TriggerSyncAdapter': AttributeBinding('je_auto_control.utils.config_sync.adapters', 'TriggerSyncAdapter'), - 'AddressBookSyncAdapter': AttributeBinding('je_auto_control.utils.config_sync.adapters', 'AddressBookSyncAdapter'), - 'SettingsSyncAdapter': AttributeBinding('je_auto_control.utils.config_sync.adapters', 'SettingsSyncAdapter'), - 'WorkspaceSettingsSyncAdapter': - AttributeBinding('je_auto_control.utils.config_sync.adapters', 'WorkspaceSettingsSyncAdapter'), + 'SyncAdapter': AttributeBinding(_UTILS_CONFIG_SYNC_ADAPTERS, 'SyncAdapter'), + 'ScriptSyncAdapter': AttributeBinding(_UTILS_CONFIG_SYNC_ADAPTERS, 'ScriptSyncAdapter'), + 'LocatorSyncAdapter': AttributeBinding(_UTILS_CONFIG_SYNC_ADAPTERS, 'LocatorSyncAdapter'), + 'HotkeySyncAdapter': AttributeBinding(_UTILS_CONFIG_SYNC_ADAPTERS, 'HotkeySyncAdapter'), + 'TriggerSyncAdapter': AttributeBinding(_UTILS_CONFIG_SYNC_ADAPTERS, 'TriggerSyncAdapter'), + 'AddressBookSyncAdapter': AttributeBinding(_UTILS_CONFIG_SYNC_ADAPTERS, 'AddressBookSyncAdapter'), + 'SettingsSyncAdapter': AttributeBinding(_UTILS_CONFIG_SYNC_ADAPTERS, 'SettingsSyncAdapter'), + 'WorkspaceSettingsSyncAdapter': AttributeBinding(_UTILS_CONFIG_SYNC_ADAPTERS, 'WorkspaceSettingsSyncAdapter'), 'materialize_sync_profile': AttributeBinding('je_auto_control.utils.config_sync.adapter_profiles', 'materialize_sync_profile'), 'adapter_from_profile': AttributeBinding('je_auto_control.utils.config_sync.adapter_factory', 'adapter_from_profile'), - 'build_asset_manifest': AttributeBinding('je_auto_control.utils.config_sync.assets', 'build_asset_manifest'), - 'sync_assets': AttributeBinding('je_auto_control.utils.config_sync.assets', 'sync_assets'), + 'build_asset_manifest': AttributeBinding(_UTILS_CONFIG_SYNC_ASSETS, 'build_asset_manifest'), + 'sync_assets': AttributeBinding(_UTILS_CONFIG_SYNC_ASSETS, 'sync_assets'), 'CausalSyncClient': AttributeBinding('je_auto_control.utils.config_sync.causal_client', 'CausalSyncClient'), 'CausalReceipt': AttributeBinding('je_auto_control.utils.config_sync.causal_receipt', 'CausalReceipt'), 'CausalBucket': AttributeBinding('je_auto_control.utils.config_sync.causal_state', 'CausalBucket'), 'CausalConfigStore': AttributeBinding('je_auto_control.utils.config_sync.causal_store', 'CausalConfigStore'), - 'CausalConflict': AttributeBinding('je_auto_control.utils.config_sync.causal_models', 'CausalConflict'), - 'DeletionFence': AttributeBinding('je_auto_control.utils.config_sync.causal_models', 'DeletionFence'), - 'SyncActor': AttributeBinding('je_auto_control.utils.config_sync.causal_models', 'SyncActor'), - 'MergeDecision': AttributeBinding('je_auto_control.utils.config_sync.versions', 'MergeDecision'), - 'SyncConflict': AttributeBinding('je_auto_control.utils.config_sync.versions', 'SyncConflict'), - 'SyncEntry': AttributeBinding('je_auto_control.utils.config_sync.versions', 'SyncEntry'), - 'merge_entries': AttributeBinding('je_auto_control.utils.config_sync.versions', 'merge_entries'), - 'FullResyncRequired': AttributeBinding('je_auto_control.utils.config_sync.peers', 'FullResyncRequired'), - 'SyncPeer': AttributeBinding('je_auto_control.utils.config_sync.peers', 'SyncPeer'), - 'SyncPeers': AttributeBinding('je_auto_control.utils.config_sync.peers', 'SyncPeers'), - 'SyncDrainResult': AttributeBinding('je_auto_control.utils.config_sync.outbox', 'SyncDrainResult'), - 'SyncLocalIdentity': AttributeBinding('je_auto_control.utils.config_sync.outbox', 'SyncLocalIdentity'), - 'SyncOperation': AttributeBinding('je_auto_control.utils.config_sync.outbox', 'SyncOperation'), - 'SyncOutbox': AttributeBinding('je_auto_control.utils.config_sync.outbox', 'SyncOutbox'), - 'SyncRetryPolicy': AttributeBinding('je_auto_control.utils.config_sync.outbox', 'SyncRetryPolicy'), - 'ConfigBucket': AttributeBinding('je_auto_control.utils.config_sync.client', 'ConfigBucket'), - 'ConfigSyncClient': AttributeBinding('je_auto_control.utils.config_sync.client', 'ConfigSyncClient'), - 'ConfigSyncError': AttributeBinding('je_auto_control.utils.config_sync.client', 'ConfigSyncError'), - 'ConfigStore': AttributeBinding('je_auto_control.utils.config_sync.store', 'ConfigStore'), - 'ConfigRevisionConflict': AttributeBinding('je_auto_control.utils.config_sync.store', 'ConfigRevisionConflict'), - 'ConfigStoreError': AttributeBinding('je_auto_control.utils.config_sync.store', 'ConfigStoreError'), - 'ConfigStoreFull': AttributeBinding('je_auto_control.utils.config_sync.store', 'ConfigStoreFull'), - 'click_mouse': AttributeBinding('je_auto_control.wrapper.auto_control_mouse', 'click_mouse'), - 'mouse_keys_table': AttributeBinding('je_auto_control.wrapper.auto_control_mouse', 'mouse_keys_table'), - 'get_mouse_position': AttributeBinding('je_auto_control.wrapper.auto_control_mouse', 'get_mouse_position'), - 'press_mouse': AttributeBinding('je_auto_control.wrapper.auto_control_mouse', 'press_mouse'), - 'release_mouse': AttributeBinding('je_auto_control.wrapper.auto_control_mouse', 'release_mouse'), - 'mouse_scroll': AttributeBinding('je_auto_control.wrapper.auto_control_mouse', 'mouse_scroll'), - 'mouse_scroll_error_message': - AttributeBinding('je_auto_control.wrapper.auto_control_mouse', 'mouse_scroll_error_message'), - 'set_mouse_position': AttributeBinding('je_auto_control.wrapper.auto_control_mouse', 'set_mouse_position'), - 'special_mouse_keys_table': - AttributeBinding('je_auto_control.wrapper.auto_control_mouse', 'special_mouse_keys_table'), - 'HumanizedMotion': AttributeBinding('je_auto_control.utils.humanize.motion', 'HumanizedMotion'), - 'humanized_path': AttributeBinding('je_auto_control.utils.humanize.motion', 'humanized_path'), - 'move_mouse_humanized': AttributeBinding('je_auto_control.utils.humanize.motion', 'move_mouse_humanized'), - 'humanized_key_delays': AttributeBinding('je_auto_control.utils.humanize.typing', 'humanized_key_delays'), - 'type_text_humanized': AttributeBinding('je_auto_control.utils.humanize.typing', 'type_text_humanized'), - 'press_keyboard_key': AttributeBinding('je_auto_control.wrapper.auto_control_keyboard', 'press_keyboard_key'), - 'release_keyboard_key': AttributeBinding('je_auto_control.wrapper.auto_control_keyboard', 'release_keyboard_key'), - 'type_keyboard': AttributeBinding('je_auto_control.wrapper.auto_control_keyboard', 'type_keyboard'), - 'check_key_is_press': AttributeBinding('je_auto_control.wrapper.auto_control_keyboard', 'check_key_is_press'), - 'write': AttributeBinding('je_auto_control.wrapper.auto_control_keyboard', 'write'), - 'write_secret': AttributeBinding('je_auto_control.wrapper.auto_control_keyboard', 'write_secret'), - 'hotkey': AttributeBinding('je_auto_control.wrapper.auto_control_keyboard', 'hotkey'), + 'CausalConflict': AttributeBinding(_UTILS_CONFIG_SYNC_CAUSAL_MODELS, 'CausalConflict'), + 'DeletionFence': AttributeBinding(_UTILS_CONFIG_SYNC_CAUSAL_MODELS, 'DeletionFence'), + 'SyncActor': AttributeBinding(_UTILS_CONFIG_SYNC_CAUSAL_MODELS, 'SyncActor'), + 'MergeDecision': AttributeBinding(_UTILS_CONFIG_SYNC_VERSIONS, 'MergeDecision'), + 'SyncConflict': AttributeBinding(_UTILS_CONFIG_SYNC_VERSIONS, 'SyncConflict'), + 'SyncEntry': AttributeBinding(_UTILS_CONFIG_SYNC_VERSIONS, 'SyncEntry'), + 'merge_entries': AttributeBinding(_UTILS_CONFIG_SYNC_VERSIONS, 'merge_entries'), + 'FullResyncRequired': AttributeBinding(_UTILS_CONFIG_SYNC_PEERS, 'FullResyncRequired'), + 'SyncPeer': AttributeBinding(_UTILS_CONFIG_SYNC_PEERS, 'SyncPeer'), + 'SyncPeers': AttributeBinding(_UTILS_CONFIG_SYNC_PEERS, 'SyncPeers'), + 'SyncDrainResult': AttributeBinding(_UTILS_CONFIG_SYNC_OUTBOX, 'SyncDrainResult'), + 'SyncLocalIdentity': AttributeBinding(_UTILS_CONFIG_SYNC_OUTBOX, 'SyncLocalIdentity'), + 'SyncOperation': AttributeBinding(_UTILS_CONFIG_SYNC_OUTBOX, 'SyncOperation'), + 'SyncOutbox': AttributeBinding(_UTILS_CONFIG_SYNC_OUTBOX, 'SyncOutbox'), + 'SyncRetryPolicy': AttributeBinding(_UTILS_CONFIG_SYNC_OUTBOX, 'SyncRetryPolicy'), + 'ConfigBucket': AttributeBinding(_UTILS_CONFIG_SYNC_CLIENT, 'ConfigBucket'), + 'ConfigSyncClient': AttributeBinding(_UTILS_CONFIG_SYNC_CLIENT, 'ConfigSyncClient'), + 'ConfigSyncError': AttributeBinding(_UTILS_CONFIG_SYNC_CLIENT, 'ConfigSyncError'), + 'ConfigStore': AttributeBinding(_UTILS_CONFIG_SYNC_STORE, 'ConfigStore'), + 'ConfigRevisionConflict': AttributeBinding(_UTILS_CONFIG_SYNC_STORE, 'ConfigRevisionConflict'), + 'ConfigStoreError': AttributeBinding(_UTILS_CONFIG_SYNC_STORE, 'ConfigStoreError'), + 'ConfigStoreFull': AttributeBinding(_UTILS_CONFIG_SYNC_STORE, 'ConfigStoreFull'), + 'click_mouse': AttributeBinding(_WRAPPER_AUTO_CONTROL_MOUSE, 'click_mouse'), + 'mouse_keys_table': AttributeBinding(_WRAPPER_AUTO_CONTROL_MOUSE, 'mouse_keys_table'), + 'get_mouse_position': AttributeBinding(_WRAPPER_AUTO_CONTROL_MOUSE, 'get_mouse_position'), + 'press_mouse': AttributeBinding(_WRAPPER_AUTO_CONTROL_MOUSE, 'press_mouse'), + 'release_mouse': AttributeBinding(_WRAPPER_AUTO_CONTROL_MOUSE, 'release_mouse'), + 'mouse_scroll': AttributeBinding(_WRAPPER_AUTO_CONTROL_MOUSE, 'mouse_scroll'), + 'mouse_scroll_error_message': AttributeBinding(_WRAPPER_AUTO_CONTROL_MOUSE, 'mouse_scroll_error_message'), + 'set_mouse_position': AttributeBinding(_WRAPPER_AUTO_CONTROL_MOUSE, 'set_mouse_position'), + 'special_mouse_keys_table': AttributeBinding(_WRAPPER_AUTO_CONTROL_MOUSE, 'special_mouse_keys_table'), + 'HumanizedMotion': AttributeBinding(_UTILS_HUMANIZE_MOTION, 'HumanizedMotion'), + 'humanized_path': AttributeBinding(_UTILS_HUMANIZE_MOTION, 'humanized_path'), + 'move_mouse_humanized': AttributeBinding(_UTILS_HUMANIZE_MOTION, 'move_mouse_humanized'), + 'humanized_key_delays': AttributeBinding(_UTILS_HUMANIZE_TYPING, 'humanized_key_delays'), + 'type_text_humanized': AttributeBinding(_UTILS_HUMANIZE_TYPING, 'type_text_humanized'), + 'press_keyboard_key': AttributeBinding(_WRAPPER_AUTO_CONTROL_KEYBOARD, 'press_keyboard_key'), + 'release_keyboard_key': AttributeBinding(_WRAPPER_AUTO_CONTROL_KEYBOARD, 'release_keyboard_key'), + 'type_keyboard': AttributeBinding(_WRAPPER_AUTO_CONTROL_KEYBOARD, 'type_keyboard'), + 'check_key_is_press': AttributeBinding(_WRAPPER_AUTO_CONTROL_KEYBOARD, 'check_key_is_press'), + 'write': AttributeBinding(_WRAPPER_AUTO_CONTROL_KEYBOARD, 'write'), + 'write_secret': AttributeBinding(_WRAPPER_AUTO_CONTROL_KEYBOARD, 'write_secret'), + 'hotkey': AttributeBinding(_WRAPPER_AUTO_CONTROL_KEYBOARD, 'hotkey'), 'start_exe': AttributeBinding('je_auto_control.utils.start_exe.start_another_process', 'start_exe'), - 'get_keyboard_keys_table': - AttributeBinding('je_auto_control.wrapper.auto_control_keyboard', 'get_keyboard_keys_table'), - 'keyboard_key_name': AttributeBinding('je_auto_control.wrapper.auto_control_keyboard', 'keyboard_key_name'), - 'screen_size': AttributeBinding('je_auto_control.wrapper.auto_control_screen', 'screen_size'), - 'screenshot': AttributeBinding('je_auto_control.wrapper.auto_control_screen', 'screenshot'), - 'locate_all_image': AttributeBinding('je_auto_control.wrapper.auto_control_image', 'locate_all_image'), - 'locate_image_center': AttributeBinding('je_auto_control.wrapper.auto_control_image', 'locate_image_center'), - 'locate_and_click': AttributeBinding('je_auto_control.wrapper.auto_control_image', 'locate_and_click'), + 'get_keyboard_keys_table': AttributeBinding(_WRAPPER_AUTO_CONTROL_KEYBOARD, 'get_keyboard_keys_table'), + 'keyboard_key_name': AttributeBinding(_WRAPPER_AUTO_CONTROL_KEYBOARD, 'keyboard_key_name'), + 'screen_size': AttributeBinding(_WRAPPER_AUTO_CONTROL_SCREEN, 'screen_size'), + 'screenshot': AttributeBinding(_WRAPPER_AUTO_CONTROL_SCREEN, 'screenshot'), + 'locate_all_image': AttributeBinding(_WRAPPER_AUTO_CONTROL_IMAGE, 'locate_all_image'), + 'locate_image_center': AttributeBinding(_WRAPPER_AUTO_CONTROL_IMAGE, 'locate_image_center'), + 'locate_and_click': AttributeBinding(_WRAPPER_AUTO_CONTROL_IMAGE, 'locate_and_click'), 'CriticalExit': AttributeBinding('je_auto_control.utils.critical_exit.critical_exit', 'CriticalExit'), - 'AutoControlException': AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlException'), - 'AutoControlKeyboardException': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlKeyboardException'), - 'AutoControlMouseException': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlMouseException'), - 'AutoControlCantFindKeyException': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlCantFindKeyException'), - 'AutoControlScreenException': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlScreenException'), + 'AutoControlException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlException'), + 'AutoControlKeyboardException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlKeyboardException'), + 'AutoControlMouseException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlMouseException'), + 'AutoControlCantFindKeyException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlCantFindKeyException'), + 'AutoControlScreenException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlScreenException'), 'AutoControlFlatTemplateException': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlFlatTemplateException'), - 'ImageNotFoundException': AttributeBinding('je_auto_control.utils.exception.exceptions', 'ImageNotFoundException'), - 'AutoControlJsonActionException': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlJsonActionException'), - 'AutoControlRecordException': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlRecordException'), - 'AutoControlActionNullException': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlActionNullException'), - 'AutoControlActionException': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlActionException'), - 'record': AttributeBinding('je_auto_control.wrapper.auto_control_record', 'record'), - 'stop_record': AttributeBinding('je_auto_control.wrapper.auto_control_record', 'stop_record'), - 'stop_record_timeline': AttributeBinding('je_auto_control.wrapper.auto_control_record', 'stop_record_timeline'), - 'read_action_json': AttributeBinding('je_auto_control.utils.json.json_file', 'read_action_json'), - 'read_executable_action_json': - AttributeBinding('je_auto_control.utils.json.json_file', 'read_executable_action_json'), - 'write_action_json': AttributeBinding('je_auto_control.utils.json.json_file', 'write_action_json'), - 'format_action_json': AttributeBinding('je_auto_control.utils.json.json_file', 'format_action_json'), - 'execute_action': AttributeBinding('je_auto_control.utils.executor.action_executor', 'execute_action'), - 'execute_files': AttributeBinding('je_auto_control.utils.executor.action_executor', 'execute_files'), - 'executor': AttributeBinding('je_auto_control.utils.executor.action_executor', 'executor'), - 'execute_action_with_vars': - AttributeBinding('je_auto_control.utils.executor.action_executor', 'execute_action_with_vars'), - 'record_to_json': AttributeBinding('je_auto_control.wrapper.auto_control_record', 'record_to_json'), - 'generate_code': AttributeBinding('je_auto_control.utils.codegen.codegen', 'generate_code'), - 'generate_code_file': AttributeBinding('je_auto_control.utils.codegen.codegen', 'generate_code_file'), + AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlFlatTemplateException'), + 'ImageNotFoundException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'ImageNotFoundException'), + 'AutoControlJsonActionException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlJsonActionException'), + 'AutoControlRecordException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlRecordException'), + 'AutoControlActionNullException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlActionNullException'), + 'AutoControlActionException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlActionException'), + 'record': AttributeBinding(_WRAPPER_AUTO_CONTROL_RECORD, 'record'), + 'stop_record': AttributeBinding(_WRAPPER_AUTO_CONTROL_RECORD, 'stop_record'), + 'stop_record_timeline': AttributeBinding(_WRAPPER_AUTO_CONTROL_RECORD, 'stop_record_timeline'), + 'read_action_json': AttributeBinding(_UTILS_JSON_JSON_FILE, 'read_action_json'), + 'read_executable_action_json': AttributeBinding(_UTILS_JSON_JSON_FILE, 'read_executable_action_json'), + 'write_action_json': AttributeBinding(_UTILS_JSON_JSON_FILE, 'write_action_json'), + 'format_action_json': AttributeBinding(_UTILS_JSON_JSON_FILE, 'format_action_json'), + 'execute_action': AttributeBinding(_UTILS_EXECUTOR_ACTION_EXECUTOR, 'execute_action'), + 'execute_files': AttributeBinding(_UTILS_EXECUTOR_ACTION_EXECUTOR, 'execute_files'), + 'executor': AttributeBinding(_UTILS_EXECUTOR_ACTION_EXECUTOR, 'executor'), + 'execute_action_with_vars': AttributeBinding(_UTILS_EXECUTOR_ACTION_EXECUTOR, 'execute_action_with_vars'), + 'record_to_json': AttributeBinding(_WRAPPER_AUTO_CONTROL_RECORD, 'record_to_json'), + 'generate_code': AttributeBinding(_UTILS_CODEGEN_CODEGEN, 'generate_code'), + 'generate_code_file': AttributeBinding(_UTILS_CODEGEN_CODEGEN, 'generate_code_file'), 'generate_candidate_from_log': AttributeBinding('je_auto_control.utils.codegen.journal_import', 'generate_candidate_from_log'), - 'CandidateGenerationError': - AttributeBinding('je_auto_control.utils.codegen.candidate_models', 'CandidateGenerationError'), - 'CandidateScript': AttributeBinding('je_auto_control.utils.codegen.candidate_models', 'CandidateScript'), + 'CandidateGenerationError': AttributeBinding(_UTILS_CODEGEN_CANDIDATE_MODELS, 'CandidateGenerationError'), + 'CandidateScript': AttributeBinding(_UTILS_CODEGEN_CANDIDATE_MODELS, 'CandidateScript'), 'write_candidate_archive': AttributeBinding('je_auto_control.utils.codegen.candidate_archive', 'write_candidate_archive'), 'http_request': AttributeBinding('je_auto_control.utils.http_client.http_client', 'http_request'), 'query_sqlite': AttributeBinding('je_auto_control.utils.sql.sql_query', 'query_sqlite'), 'send_email': AttributeBinding('je_auto_control.utils.email_send.email_sender', 'send_email'), - 'assert_pdf_text': AttributeBinding('je_auto_control.utils.pdf.pdf_reader', 'assert_pdf_text'), - 'extract_pdf_text': AttributeBinding('je_auto_control.utils.pdf.pdf_reader', 'extract_pdf_text'), - 'pdf_metadata': AttributeBinding('je_auto_control.utils.pdf.pdf_reader', 'pdf_metadata'), - 'pdf_page_count': AttributeBinding('je_auto_control.utils.pdf.pdf_reader', 'pdf_page_count'), - 'add_command_to_executor': - AttributeBinding('je_auto_control.utils.executor.action_executor', 'add_command_to_executor'), + 'assert_pdf_text': AttributeBinding(_UTILS_PDF_PDF_READER, 'assert_pdf_text'), + 'extract_pdf_text': AttributeBinding(_UTILS_PDF_PDF_READER, 'extract_pdf_text'), + 'pdf_metadata': AttributeBinding(_UTILS_PDF_PDF_READER, 'pdf_metadata'), + 'pdf_page_count': AttributeBinding(_UTILS_PDF_PDF_READER, 'pdf_page_count'), + 'add_command_to_executor': AttributeBinding(_UTILS_EXECUTOR_ACTION_EXECUTOR, 'add_command_to_executor'), 'test_record_instance': AttributeBinding('je_auto_control.utils.test_record.record_test_class', 'test_record_instance'), 'pil_screenshot': AttributeBinding('je_auto_control.utils.cv2_utils.screenshot', 'pil_screenshot'), - 'TextMatch': AttributeBinding('je_auto_control.utils.ocr.ocr_engine', 'TextMatch'), - 'find_text_matches': AttributeBinding('je_auto_control.utils.ocr.ocr_engine', 'find_text_matches'), - 'locate_text_center': AttributeBinding('je_auto_control.utils.ocr.ocr_engine', 'locate_text_center'), - 'wait_for_text': AttributeBinding('je_auto_control.utils.ocr.ocr_engine', 'wait_for_text'), - 'click_text': AttributeBinding('je_auto_control.utils.ocr.ocr_engine', 'click_text'), - 'set_tesseract_cmd': AttributeBinding('je_auto_control.utils.ocr.ocr_engine', 'set_tesseract_cmd'), - 'read_text_in_region': AttributeBinding('je_auto_control.utils.ocr.ocr_engine', 'read_text_in_region'), - 'find_text_regex': AttributeBinding('je_auto_control.utils.ocr.ocr_engine', 'find_text_regex'), - 'find_spans': AttributeBinding('je_auto_control.utils.ocr.text_span', 'find_spans'), - 'group_lines': AttributeBinding('je_auto_control.utils.ocr.text_span', 'group_lines'), - 'OCRStatus': AttributeBinding('je_auto_control.utils.ocr.tesseract_setup', 'OCRStatus'), - 'find_tesseract_cmd': AttributeBinding('je_auto_control.utils.ocr.tesseract_setup', 'find_tesseract_cmd'), - 'ocr_languages': AttributeBinding('je_auto_control.utils.ocr.tesseract_setup', 'ocr_languages'), - 'ocr_status': AttributeBinding('je_auto_control.utils.ocr.tesseract_setup', 'ocr_status'), - 'set_tessdata_dir': AttributeBinding('je_auto_control.utils.ocr.tesseract_setup', 'set_tessdata_dir'), - 'char_table': AttributeBinding('je_auto_control.utils.keyboard_layout', 'char_table'), - 'foreground_keyboard_layout': - AttributeBinding('je_auto_control.utils.keyboard_layout', 'foreground_keyboard_layout'), - 'layout_char_table': AttributeBinding('je_auto_control.utils.keyboard_layout', 'layout_char_table'), - 'vk_to_char': AttributeBinding('je_auto_control.utils.keyboard_layout', 'vk_to_char'), - 'input_desktop_available': AttributeBinding('je_auto_control.utils.input_reach', 'input_desktop_available'), - 'input_reaches_system': AttributeBinding('je_auto_control.utils.input_reach', 'input_reaches_system'), - 'trim_actions': AttributeBinding('je_auto_control.utils.recording_edit.editor', 'trim_actions'), - 'insert_action': AttributeBinding('je_auto_control.utils.recording_edit.editor', 'insert_action'), - 'remove_action': AttributeBinding('je_auto_control.utils.recording_edit.editor', 'remove_action'), - 'filter_actions': AttributeBinding('je_auto_control.utils.recording_edit.editor', 'filter_actions'), - 'adjust_delays': AttributeBinding('je_auto_control.utils.recording_edit.editor', 'adjust_delays'), - 'scale_coordinates': AttributeBinding('je_auto_control.utils.recording_edit.editor', 'scale_coordinates'), - 'dedupe_moves': AttributeBinding('je_auto_control.utils.recording_edit.editor', 'dedupe_moves'), - 'merge_sleeps': AttributeBinding('je_auto_control.utils.recording_edit.editor', 'merge_sleeps'), - 'Scheduler': AttributeBinding('je_auto_control.utils.scheduler.scheduler', 'Scheduler'), - 'ScheduledJob': AttributeBinding('je_auto_control.utils.scheduler.scheduler', 'ScheduledJob'), - 'default_scheduler': AttributeBinding('je_auto_control.utils.scheduler.scheduler', 'default_scheduler'), - 'interpolate_actions': AttributeBinding('je_auto_control.utils.script_vars.interpolate', 'interpolate_actions'), - 'interpolate_value': AttributeBinding('je_auto_control.utils.script_vars.interpolate', 'interpolate_value'), - 'load_vars_from_json': AttributeBinding('je_auto_control.utils.script_vars.interpolate', 'load_vars_from_json'), + 'TextMatch': AttributeBinding(_UTILS_OCR_OCR_ENGINE, 'TextMatch'), + 'find_text_matches': AttributeBinding(_UTILS_OCR_OCR_ENGINE, 'find_text_matches'), + 'locate_text_center': AttributeBinding(_UTILS_OCR_OCR_ENGINE, 'locate_text_center'), + 'wait_for_text': AttributeBinding(_UTILS_OCR_OCR_ENGINE, 'wait_for_text'), + 'click_text': AttributeBinding(_UTILS_OCR_OCR_ENGINE, 'click_text'), + 'set_tesseract_cmd': AttributeBinding(_UTILS_OCR_OCR_ENGINE, 'set_tesseract_cmd'), + 'read_text_in_region': AttributeBinding(_UTILS_OCR_OCR_ENGINE, 'read_text_in_region'), + 'find_text_regex': AttributeBinding(_UTILS_OCR_OCR_ENGINE, 'find_text_regex'), + 'find_spans': AttributeBinding(_UTILS_OCR_TEXT_SPAN, 'find_spans'), + 'group_lines': AttributeBinding(_UTILS_OCR_TEXT_SPAN, 'group_lines'), + 'OCRStatus': AttributeBinding(_UTILS_OCR_TESSERACT_SETUP, 'OCRStatus'), + 'find_tesseract_cmd': AttributeBinding(_UTILS_OCR_TESSERACT_SETUP, 'find_tesseract_cmd'), + 'ocr_languages': AttributeBinding(_UTILS_OCR_TESSERACT_SETUP, 'ocr_languages'), + 'ocr_status': AttributeBinding(_UTILS_OCR_TESSERACT_SETUP, 'ocr_status'), + 'set_tessdata_dir': AttributeBinding(_UTILS_OCR_TESSERACT_SETUP, 'set_tessdata_dir'), + 'char_table': AttributeBinding(_UTILS_KEYBOARD_LAYOUT, 'char_table'), + 'foreground_keyboard_layout': AttributeBinding(_UTILS_KEYBOARD_LAYOUT, 'foreground_keyboard_layout'), + 'layout_char_table': AttributeBinding(_UTILS_KEYBOARD_LAYOUT, 'layout_char_table'), + 'vk_to_char': AttributeBinding(_UTILS_KEYBOARD_LAYOUT, 'vk_to_char'), + 'input_desktop_available': AttributeBinding(_UTILS_INPUT_REACH, 'input_desktop_available'), + 'input_reaches_system': AttributeBinding(_UTILS_INPUT_REACH, 'input_reaches_system'), + 'trim_actions': AttributeBinding(_UTILS_RECORDING_EDIT_EDITOR, 'trim_actions'), + 'insert_action': AttributeBinding(_UTILS_RECORDING_EDIT_EDITOR, 'insert_action'), + 'remove_action': AttributeBinding(_UTILS_RECORDING_EDIT_EDITOR, 'remove_action'), + 'filter_actions': AttributeBinding(_UTILS_RECORDING_EDIT_EDITOR, 'filter_actions'), + 'adjust_delays': AttributeBinding(_UTILS_RECORDING_EDIT_EDITOR, 'adjust_delays'), + 'scale_coordinates': AttributeBinding(_UTILS_RECORDING_EDIT_EDITOR, 'scale_coordinates'), + 'dedupe_moves': AttributeBinding(_UTILS_RECORDING_EDIT_EDITOR, 'dedupe_moves'), + 'merge_sleeps': AttributeBinding(_UTILS_RECORDING_EDIT_EDITOR, 'merge_sleeps'), + 'Scheduler': AttributeBinding(_UTILS_SCHEDULER_SCHEDULER, 'Scheduler'), + 'ScheduledJob': AttributeBinding(_UTILS_SCHEDULER_SCHEDULER, 'ScheduledJob'), + 'default_scheduler': AttributeBinding(_UTILS_SCHEDULER_SCHEDULER, 'default_scheduler'), + 'interpolate_actions': AttributeBinding(_UTILS_SCRIPT_VARS_INTERPOLATE, 'interpolate_actions'), + 'interpolate_value': AttributeBinding(_UTILS_SCRIPT_VARS_INTERPOLATE, 'interpolate_value'), + 'load_vars_from_json': AttributeBinding(_UTILS_SCRIPT_VARS_INTERPOLATE, 'load_vars_from_json'), 'VariableScope': AttributeBinding('je_auto_control.utils.script_vars.scope', 'VariableScope'), 'execution_scope': AttributeBinding('je_auto_control.utils.script_vars.execution', 'execution_scope'), - 'MouseWatcher': AttributeBinding('je_auto_control.utils.watcher.watcher', 'MouseWatcher'), - 'PixelWatcher': AttributeBinding('je_auto_control.utils.watcher.watcher', 'PixelWatcher'), - 'LogTail': AttributeBinding('je_auto_control.utils.watcher.watcher', 'LogTail'), - 'list_windows': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'list_windows'), - 'find_window': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'find_window'), - 'focus_window': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'focus_window'), - 'wait_for_window': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'wait_for_window'), - 'close_window_by_title': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'close_window_by_title'), - 'show_window_by_title': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'show_window_by_title'), - 'minimize_window_by_title': - AttributeBinding('je_auto_control.wrapper.auto_control_window', 'minimize_window_by_title'), - 'foreground_window': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'foreground_window'), - 'window_rect': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'window_rect'), - 'move_window_by_title': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'move_window_by_title'), - 'foreground_window_process_id': - AttributeBinding('je_auto_control.wrapper.auto_control_window', 'foreground_window_process_id'), - 'window_process_id': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'window_process_id'), - 'post_key_to_window': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'post_key_to_window'), - 'post_click_to_window': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'post_click_to_window'), - 'windows_for_process_id': AttributeBinding('je_auto_control.wrapper.auto_control_window', 'windows_for_process_id'), - 'minimize_windows_for_process': - AttributeBinding('je_auto_control.wrapper.auto_control_window', 'minimize_windows_for_process'), - 'get_clipboard': AttributeBinding('je_auto_control.utils.clipboard.clipboard', 'get_clipboard'), - 'set_clipboard': AttributeBinding('je_auto_control.utils.clipboard.clipboard', 'set_clipboard'), - 'get_clipboard_image': AttributeBinding('je_auto_control.utils.clipboard.clipboard', 'get_clipboard_image'), - 'set_clipboard_image': AttributeBinding('je_auto_control.utils.clipboard.clipboard', 'set_clipboard_image'), - 'HotkeyDaemon': AttributeBinding('je_auto_control.utils.hotkey.hotkey_daemon', 'HotkeyDaemon'), - 'HotkeyBinding': AttributeBinding('je_auto_control.utils.hotkey.hotkey_daemon', 'HotkeyBinding'), - 'default_hotkey_daemon': AttributeBinding('je_auto_control.utils.hotkey.hotkey_daemon', 'default_hotkey_daemon'), - 'PopupWatchdog': AttributeBinding('je_auto_control.utils.watchdog', 'PopupWatchdog'), - 'WatchdogRule': AttributeBinding('je_auto_control.utils.watchdog', 'WatchdogRule'), - 'default_popup_watchdog': AttributeBinding('je_auto_control.utils.watchdog', 'default_popup_watchdog'), - 'generate_totp': AttributeBinding('je_auto_control.utils.otp', 'generate_totp'), - 'verify_totp': AttributeBinding('je_auto_control.utils.otp', 'verify_totp'), - 'generate_secret': AttributeBinding('je_auto_control.utils.otp', 'generate_secret'), - 'TOTPError': AttributeBinding('je_auto_control.utils.otp', 'TOTPError'), - 'handle_file_dialog': AttributeBinding('je_auto_control.utils.file_dialog', 'handle_file_dialog'), - 'FileDialogDriver': AttributeBinding('je_auto_control.utils.file_dialog', 'FileDialogDriver'), - 'ensure_interactive_session': AttributeBinding('je_auto_control.utils.session_guard', 'ensure_interactive_session'), - 'is_session_locked': AttributeBinding('je_auto_control.utils.session_guard', 'is_session_locked'), - 'WorkQueue': AttributeBinding('je_auto_control.utils.work_queue', 'WorkQueue'), - 'WorkItem': AttributeBinding('je_auto_control.utils.work_queue', 'WorkItem'), - 'BusinessError': AttributeBinding('je_auto_control.utils.work_queue', 'BusinessError'), - 'WorkQueueError': AttributeBinding('je_auto_control.utils.work_queue', 'WorkQueueError'), - 'generate_rows': AttributeBinding('je_auto_control.utils.test_data', 'generate_rows'), - 'write_dataset': AttributeBinding('je_auto_control.utils.test_data', 'write_dataset'), - 'rank_flows': AttributeBinding('je_auto_control.utils.test_select', 'rank_flows'), - 'select_flows': AttributeBinding('je_auto_control.utils.test_select', 'select_flows'), - 'build_server_manifest': AttributeBinding('je_auto_control.utils.mcp_registry', 'build_server_manifest'), - 'write_server_manifest': AttributeBinding('je_auto_control.utils.mcp_registry', 'write_server_manifest'), + 'MouseWatcher': AttributeBinding(_UTILS_WATCHER_WATCHER, 'MouseWatcher'), + 'PixelWatcher': AttributeBinding(_UTILS_WATCHER_WATCHER, 'PixelWatcher'), + 'LogTail': AttributeBinding(_UTILS_WATCHER_WATCHER, 'LogTail'), + 'list_windows': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'list_windows'), + 'find_window': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'find_window'), + 'focus_window': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'focus_window'), + 'wait_for_window': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'wait_for_window'), + 'close_window_by_title': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'close_window_by_title'), + 'show_window_by_title': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'show_window_by_title'), + 'minimize_window_by_title': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'minimize_window_by_title'), + 'foreground_window': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'foreground_window'), + 'window_rect': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'window_rect'), + 'move_window_by_title': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'move_window_by_title'), + 'foreground_window_process_id': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'foreground_window_process_id'), + 'window_process_id': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'window_process_id'), + 'post_key_to_window': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'post_key_to_window'), + 'post_click_to_window': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'post_click_to_window'), + 'windows_for_process_id': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'windows_for_process_id'), + 'minimize_windows_for_process': AttributeBinding(_WRAPPER_AUTO_CONTROL_WINDOW, 'minimize_windows_for_process'), + 'get_clipboard': AttributeBinding(_UTILS_CLIPBOARD_CLIPBOARD, 'get_clipboard'), + 'set_clipboard': AttributeBinding(_UTILS_CLIPBOARD_CLIPBOARD, 'set_clipboard'), + 'get_clipboard_image': AttributeBinding(_UTILS_CLIPBOARD_CLIPBOARD, 'get_clipboard_image'), + 'set_clipboard_image': AttributeBinding(_UTILS_CLIPBOARD_CLIPBOARD, 'set_clipboard_image'), + 'HotkeyDaemon': AttributeBinding(_UTILS_HOTKEY_HOTKEY_DAEMON, 'HotkeyDaemon'), + 'HotkeyBinding': AttributeBinding(_UTILS_HOTKEY_HOTKEY_DAEMON, 'HotkeyBinding'), + 'default_hotkey_daemon': AttributeBinding(_UTILS_HOTKEY_HOTKEY_DAEMON, 'default_hotkey_daemon'), + 'PopupWatchdog': AttributeBinding(_UTILS_WATCHDOG, 'PopupWatchdog'), + 'WatchdogRule': AttributeBinding(_UTILS_WATCHDOG, 'WatchdogRule'), + 'default_popup_watchdog': AttributeBinding(_UTILS_WATCHDOG, 'default_popup_watchdog'), + 'generate_totp': AttributeBinding(_UTILS_OTP, 'generate_totp'), + 'verify_totp': AttributeBinding(_UTILS_OTP, 'verify_totp'), + 'generate_secret': AttributeBinding(_UTILS_OTP, 'generate_secret'), + 'TOTPError': AttributeBinding(_UTILS_OTP, 'TOTPError'), + 'handle_file_dialog': AttributeBinding(_UTILS_FILE_DIALOG, 'handle_file_dialog'), + 'FileDialogDriver': AttributeBinding(_UTILS_FILE_DIALOG, 'FileDialogDriver'), + 'ensure_interactive_session': AttributeBinding(_UTILS_SESSION_GUARD, 'ensure_interactive_session'), + 'is_session_locked': AttributeBinding(_UTILS_SESSION_GUARD, 'is_session_locked'), + 'WorkQueue': AttributeBinding(_UTILS_WORK_QUEUE, 'WorkQueue'), + 'WorkItem': AttributeBinding(_UTILS_WORK_QUEUE, 'WorkItem'), + 'BusinessError': AttributeBinding(_UTILS_WORK_QUEUE, 'BusinessError'), + 'WorkQueueError': AttributeBinding(_UTILS_WORK_QUEUE, 'WorkQueueError'), + 'generate_rows': AttributeBinding(_UTILS_TEST_DATA, 'generate_rows'), + 'write_dataset': AttributeBinding(_UTILS_TEST_DATA, 'write_dataset'), + 'rank_flows': AttributeBinding(_UTILS_TEST_SELECT, 'rank_flows'), + 'select_flows': AttributeBinding(_UTILS_TEST_SELECT, 'select_flows'), + 'build_server_manifest': AttributeBinding(_UTILS_MCP_REGISTRY, 'build_server_manifest'), + 'write_server_manifest': AttributeBinding(_UTILS_MCP_REGISTRY, 'write_server_manifest'), 'ElementRepository': AttributeBinding('je_auto_control.utils.element_repository', 'ElementRepository'), - 'FlowDebugger': AttributeBinding('je_auto_control.utils.flow_debugger', 'FlowDebugger'), - 'trace_actions': AttributeBinding('je_auto_control.utils.flow_debugger', 'trace_actions'), - 'Skill': AttributeBinding('je_auto_control.utils.skill_library', 'Skill'), - 'SkillLibrary': AttributeBinding('je_auto_control.utils.skill_library', 'SkillLibrary'), - 'assess_text': AttributeBinding('je_auto_control.utils.guardrail', 'assess_text'), - 'redact_text': AttributeBinding('je_auto_control.utils.guardrail', 'redact_text'), - 'scan_text': AttributeBinding('je_auto_control.utils.guardrail', 'scan_text'), - 'build_agent_card': AttributeBinding('je_auto_control.utils.a2a', 'build_agent_card'), - 'write_agent_card': AttributeBinding('je_auto_control.utils.a2a', 'write_agent_card'), - 'read_workbook': AttributeBinding('je_auto_control.utils.office', 'read_workbook'), - 'write_workbook': AttributeBinding('je_auto_control.utils.office', 'write_workbook'), - 'read_document': AttributeBinding('je_auto_control.utils.office', 'read_document'), - 'write_document': AttributeBinding('je_auto_control.utils.office', 'write_document'), - 'read_presentation': AttributeBinding('je_auto_control.utils.office', 'read_presentation'), - 'write_presentation': AttributeBinding('je_auto_control.utils.office', 'write_presentation'), - 'AgentMemory': AttributeBinding('je_auto_control.utils.agent_memory', 'AgentMemory'), - 'Episode': AttributeBinding('je_auto_control.utils.agent_memory', 'Episode'), - 'DeterministicRun': AttributeBinding('je_auto_control.utils.deterministic', 'DeterministicRun'), - 'seed_everything': AttributeBinding('je_auto_control.utils.deterministic', 'seed_everything'), - 'ScreenObserver': AttributeBinding('je_auto_control.utils.observer', 'ScreenObserver'), - 'WatchRule': AttributeBinding('je_auto_control.utils.observer', 'WatchRule'), - 'default_observer': AttributeBinding('je_auto_control.utils.observer', 'default_observer'), - 'image_predicate': AttributeBinding('je_auto_control.utils.observer', 'image_predicate'), - 'pixel_predicate': AttributeBinding('je_auto_control.utils.observer', 'pixel_predicate'), - 'text_predicate': AttributeBinding('je_auto_control.utils.observer', 'text_predicate'), - 'build_sbom': AttributeBinding('je_auto_control.utils.sbom', 'build_sbom'), - 'write_sbom': AttributeBinding('je_auto_control.utils.sbom', 'write_sbom'), - 'merge_results': AttributeBinding('je_auto_control.utils.test_shard', 'merge_results'), - 'shard_flows': AttributeBinding('je_auto_control.utils.test_shard', 'shard_flows'), - 'extract_fields': AttributeBinding('je_auto_control.utils.data_quality', 'extract_fields'), - 'mask_rows': AttributeBinding('je_auto_control.utils.data_quality', 'mask_rows'), - 'validate_rows': AttributeBinding('je_auto_control.utils.data_quality', 'validate_rows'), - 'infer_schema': AttributeBinding('je_auto_control.utils.data_profile', 'infer_schema'), - 'profile_rows': AttributeBinding('je_auto_control.utils.data_profile', 'profile_rows'), - 'categorical_drift': AttributeBinding('je_auto_control.utils.data_drift', 'categorical_drift'), - 'detect_drift': AttributeBinding('je_auto_control.utils.data_drift', 'detect_drift'), - 'ks_two_sample': AttributeBinding('je_auto_control.utils.data_drift', 'ks_two_sample'), - 'psi': AttributeBinding('je_auto_control.utils.data_drift', 'psi'), - 'SchemaChange': AttributeBinding('je_auto_control.utils.schema_compat', 'SchemaChange'), - 'check_compatibility': AttributeBinding('je_auto_control.utils.schema_compat', 'check_compatibility'), - 'diff_schemas': AttributeBinding('je_auto_control.utils.schema_compat', 'diff_schemas'), - 'is_backward_compatible': AttributeBinding('je_auto_control.utils.schema_compat', 'is_backward_compatible'), - 'is_forward_compatible': AttributeBinding('je_auto_control.utils.schema_compat', 'is_forward_compatible'), - 'is_full_compatible': AttributeBinding('je_auto_control.utils.schema_compat', 'is_full_compatible'), - 'cell_changes': AttributeBinding('je_auto_control.utils.dataset_diff', 'cell_changes'), - 'diff_rows': AttributeBinding('je_auto_control.utils.dataset_diff', 'diff_rows'), - 'summarize_diff': AttributeBinding('je_auto_control.utils.dataset_diff', 'summarize_diff'), - 'check_accepted_values': AttributeBinding('je_auto_control.utils.referential', 'check_accepted_values'), - 'check_foreign_key': AttributeBinding('je_auto_control.utils.referential', 'check_foreign_key'), - 'check_row_count': AttributeBinding('je_auto_control.utils.referential', 'check_row_count'), - 'check_unique_key': AttributeBinding('je_auto_control.utils.referential', 'check_unique_key'), - 'check_catalog': AttributeBinding('je_auto_control.utils.i18n_test', 'check_catalog'), - 'check_overflow': AttributeBinding('je_auto_control.utils.i18n_test', 'check_overflow'), - 'pseudo_localize': AttributeBinding('je_auto_control.utils.i18n_test', 'pseudo_localize'), - 'pseudo_localize_catalog': AttributeBinding('je_auto_control.utils.i18n_test', 'pseudo_localize_catalog'), - 'Checkpoint': AttributeBinding('je_auto_control.utils.checkpoint', 'Checkpoint'), - 'CheckpointStore': AttributeBinding('je_auto_control.utils.checkpoint', 'CheckpointStore'), - 'CheckpointStoreError': AttributeBinding('je_auto_control.utils.checkpoint', 'CheckpointStoreError'), - 'run_resumable': AttributeBinding('je_auto_control.utils.checkpoint', 'run_resumable'), - 'mark_click': AttributeBinding('je_auto_control.utils.set_of_marks', 'mark_click'), - 'mark_elements': AttributeBinding('je_auto_control.utils.set_of_marks', 'mark_elements'), - 'mark_screen': AttributeBinding('je_auto_control.utils.set_of_marks', 'mark_screen'), - 'render_marks': AttributeBinding('je_auto_control.utils.set_of_marks', 'render_marks'), - 'resolve_mark': AttributeBinding('je_auto_control.utils.set_of_marks', 'resolve_mark'), - 'describe_screen': AttributeBinding('je_auto_control.utils.screen_state', 'describe_screen'), - 'diff_snapshots': AttributeBinding('je_auto_control.utils.screen_state', 'diff_snapshots'), - 'screen_changed': AttributeBinding('je_auto_control.utils.screen_state', 'screen_changed'), - 'snapshot': AttributeBinding('je_auto_control.utils.screen_state', 'snapshot'), - 'snapshot_screen': AttributeBinding('je_auto_control.utils.screen_state', 'snapshot_screen'), - 'replay_timeline': AttributeBinding('je_auto_control.utils.input_macro', 'replay_timeline'), - 'run_sequence': AttributeBinding('je_auto_control.utils.input_macro', 'run_sequence'), - 'CircuitBreaker': AttributeBinding('je_auto_control.utils.resilience', 'CircuitBreaker'), - 'CircuitOpenError': AttributeBinding('je_auto_control.utils.resilience', 'CircuitOpenError'), - 'RetryPolicy': AttributeBinding('je_auto_control.utils.resilience', 'RetryPolicy'), - 'retry_call': AttributeBinding('je_auto_control.utils.resilience', 'retry_call'), - 'IdempotencyConflict': AttributeBinding('je_auto_control.utils.idempotency', 'IdempotencyConflict'), - 'IdempotencyStore': AttributeBinding('je_auto_control.utils.idempotency', 'IdempotencyStore'), - 'request_fingerprint': AttributeBinding('je_auto_control.utils.idempotency', 'request_fingerprint'), + 'FlowDebugger': AttributeBinding(_UTILS_FLOW_DEBUGGER, 'FlowDebugger'), + 'trace_actions': AttributeBinding(_UTILS_FLOW_DEBUGGER, 'trace_actions'), + 'Skill': AttributeBinding(_UTILS_SKILL_LIBRARY, 'Skill'), + 'SkillLibrary': AttributeBinding(_UTILS_SKILL_LIBRARY, 'SkillLibrary'), + 'assess_text': AttributeBinding(_UTILS_GUARDRAIL, 'assess_text'), + 'redact_text': AttributeBinding(_UTILS_GUARDRAIL, 'redact_text'), + 'scan_text': AttributeBinding(_UTILS_GUARDRAIL, 'scan_text'), + 'build_agent_card': AttributeBinding(_UTILS_A2A, 'build_agent_card'), + 'write_agent_card': AttributeBinding(_UTILS_A2A, 'write_agent_card'), + 'read_workbook': AttributeBinding(_UTILS_OFFICE, 'read_workbook'), + 'write_workbook': AttributeBinding(_UTILS_OFFICE, 'write_workbook'), + 'read_document': AttributeBinding(_UTILS_OFFICE, 'read_document'), + 'write_document': AttributeBinding(_UTILS_OFFICE, 'write_document'), + 'read_presentation': AttributeBinding(_UTILS_OFFICE, 'read_presentation'), + 'write_presentation': AttributeBinding(_UTILS_OFFICE, 'write_presentation'), + 'AgentMemory': AttributeBinding(_UTILS_AGENT_MEMORY, 'AgentMemory'), + 'Episode': AttributeBinding(_UTILS_AGENT_MEMORY, 'Episode'), + 'DeterministicRun': AttributeBinding(_UTILS_DETERMINISTIC, 'DeterministicRun'), + 'seed_everything': AttributeBinding(_UTILS_DETERMINISTIC, 'seed_everything'), + 'ScreenObserver': AttributeBinding(_UTILS_OBSERVER, 'ScreenObserver'), + 'WatchRule': AttributeBinding(_UTILS_OBSERVER, 'WatchRule'), + 'default_observer': AttributeBinding(_UTILS_OBSERVER, 'default_observer'), + 'image_predicate': AttributeBinding(_UTILS_OBSERVER, 'image_predicate'), + 'pixel_predicate': AttributeBinding(_UTILS_OBSERVER, 'pixel_predicate'), + 'text_predicate': AttributeBinding(_UTILS_OBSERVER, 'text_predicate'), + 'build_sbom': AttributeBinding(_UTILS_SBOM, 'build_sbom'), + 'write_sbom': AttributeBinding(_UTILS_SBOM, 'write_sbom'), + 'merge_results': AttributeBinding(_UTILS_TEST_SHARD, 'merge_results'), + 'shard_flows': AttributeBinding(_UTILS_TEST_SHARD, 'shard_flows'), + 'extract_fields': AttributeBinding(_UTILS_DATA_QUALITY, 'extract_fields'), + 'mask_rows': AttributeBinding(_UTILS_DATA_QUALITY, 'mask_rows'), + 'validate_rows': AttributeBinding(_UTILS_DATA_QUALITY, 'validate_rows'), + 'infer_schema': AttributeBinding(_UTILS_DATA_PROFILE, 'infer_schema'), + 'profile_rows': AttributeBinding(_UTILS_DATA_PROFILE, 'profile_rows'), + 'categorical_drift': AttributeBinding(_UTILS_DATA_DRIFT, 'categorical_drift'), + 'detect_drift': AttributeBinding(_UTILS_DATA_DRIFT, 'detect_drift'), + 'ks_two_sample': AttributeBinding(_UTILS_DATA_DRIFT, 'ks_two_sample'), + 'psi': AttributeBinding(_UTILS_DATA_DRIFT, 'psi'), + 'SchemaChange': AttributeBinding(_UTILS_SCHEMA_COMPAT, 'SchemaChange'), + 'check_compatibility': AttributeBinding(_UTILS_SCHEMA_COMPAT, 'check_compatibility'), + 'diff_schemas': AttributeBinding(_UTILS_SCHEMA_COMPAT, 'diff_schemas'), + 'is_backward_compatible': AttributeBinding(_UTILS_SCHEMA_COMPAT, 'is_backward_compatible'), + 'is_forward_compatible': AttributeBinding(_UTILS_SCHEMA_COMPAT, 'is_forward_compatible'), + 'is_full_compatible': AttributeBinding(_UTILS_SCHEMA_COMPAT, 'is_full_compatible'), + 'cell_changes': AttributeBinding(_UTILS_DATASET_DIFF, 'cell_changes'), + 'diff_rows': AttributeBinding(_UTILS_DATASET_DIFF, 'diff_rows'), + 'summarize_diff': AttributeBinding(_UTILS_DATASET_DIFF, 'summarize_diff'), + 'check_accepted_values': AttributeBinding(_UTILS_REFERENTIAL, 'check_accepted_values'), + 'check_foreign_key': AttributeBinding(_UTILS_REFERENTIAL, 'check_foreign_key'), + 'check_row_count': AttributeBinding(_UTILS_REFERENTIAL, 'check_row_count'), + 'check_unique_key': AttributeBinding(_UTILS_REFERENTIAL, 'check_unique_key'), + 'check_catalog': AttributeBinding(_UTILS_I18N_TEST, 'check_catalog'), + 'check_overflow': AttributeBinding(_UTILS_I18N_TEST, 'check_overflow'), + 'pseudo_localize': AttributeBinding(_UTILS_I18N_TEST, 'pseudo_localize'), + 'pseudo_localize_catalog': AttributeBinding(_UTILS_I18N_TEST, 'pseudo_localize_catalog'), + 'Checkpoint': AttributeBinding(_UTILS_CHECKPOINT, 'Checkpoint'), + 'CheckpointStore': AttributeBinding(_UTILS_CHECKPOINT, 'CheckpointStore'), + 'CheckpointStoreError': AttributeBinding(_UTILS_CHECKPOINT, 'CheckpointStoreError'), + 'run_resumable': AttributeBinding(_UTILS_CHECKPOINT, 'run_resumable'), + 'mark_click': AttributeBinding(_UTILS_SET_OF_MARKS, 'mark_click'), + 'mark_elements': AttributeBinding(_UTILS_SET_OF_MARKS, 'mark_elements'), + 'mark_screen': AttributeBinding(_UTILS_SET_OF_MARKS, 'mark_screen'), + 'render_marks': AttributeBinding(_UTILS_SET_OF_MARKS, 'render_marks'), + 'resolve_mark': AttributeBinding(_UTILS_SET_OF_MARKS, 'resolve_mark'), + 'describe_screen': AttributeBinding(_UTILS_SCREEN_STATE, 'describe_screen'), + 'diff_snapshots': AttributeBinding(_UTILS_SCREEN_STATE, 'diff_snapshots'), + 'screen_changed': AttributeBinding(_UTILS_SCREEN_STATE, 'screen_changed'), + 'snapshot': AttributeBinding(_UTILS_SCREEN_STATE, 'snapshot'), + 'snapshot_screen': AttributeBinding(_UTILS_SCREEN_STATE, 'snapshot_screen'), + 'replay_timeline': AttributeBinding(_UTILS_INPUT_MACRO, 'replay_timeline'), + 'run_sequence': AttributeBinding(_UTILS_INPUT_MACRO, 'run_sequence'), + 'CircuitBreaker': AttributeBinding(_UTILS_RESILIENCE, 'CircuitBreaker'), + 'CircuitOpenError': AttributeBinding(_UTILS_RESILIENCE, 'CircuitOpenError'), + 'RetryPolicy': AttributeBinding(_UTILS_RESILIENCE, 'RetryPolicy'), + 'retry_call': AttributeBinding(_UTILS_RESILIENCE, 'retry_call'), + 'IdempotencyConflict': AttributeBinding(_UTILS_IDEMPOTENCY, 'IdempotencyConflict'), + 'IdempotencyStore': AttributeBinding(_UTILS_IDEMPOTENCY, 'IdempotencyStore'), + 'request_fingerprint': AttributeBinding(_UTILS_IDEMPOTENCY, 'request_fingerprint'), 'DedupWindow': AttributeBinding('je_auto_control.utils.dedup_window', 'DedupWindow'), 'SequenceTracker': AttributeBinding('je_auto_control.utils.sequence_gap', 'SequenceTracker'), - 'VersionConflict': AttributeBinding('je_auto_control.utils.optimistic', 'VersionConflict'), - 'VersionedStore': AttributeBinding('je_auto_control.utils.optimistic', 'VersionedStore'), - 'check_if_match': AttributeBinding('je_auto_control.utils.optimistic', 'check_if_match'), - 'if_match_header': AttributeBinding('je_auto_control.utils.optimistic', 'if_match_header'), + 'VersionConflict': AttributeBinding(_UTILS_OPTIMISTIC, 'VersionConflict'), + 'VersionedStore': AttributeBinding(_UTILS_OPTIMISTIC, 'VersionedStore'), + 'check_if_match': AttributeBinding(_UTILS_OPTIMISTIC, 'check_if_match'), + 'if_match_header': AttributeBinding(_UTILS_OPTIMISTIC, 'if_match_header'), 'Outbox': AttributeBinding('je_auto_control.utils.outbox', 'Outbox'), - 'collation_key': AttributeBinding('je_auto_control.utils.locale_collation', 'collation_key'), - 'collation_compare': AttributeBinding('je_auto_control.utils.locale_collation', 'compare'), - 'sort_strings': AttributeBinding('je_auto_control.utils.locale_collation', 'sort_strings'), - 'confusable_skeleton': AttributeBinding('je_auto_control.utils.confusables', 'skeleton'), - 'detect_homoglyphs': AttributeBinding('je_auto_control.utils.confusables', 'detect_homoglyphs'), - 'is_confusable': AttributeBinding('je_auto_control.utils.confusables', 'is_confusable'), - 'is_mixed_script': AttributeBinding('je_auto_control.utils.confusables', 'is_mixed_script'), - 'scripts_of': AttributeBinding('je_auto_control.utils.confusables', 'scripts_of'), - 'automated_readability_index': AttributeBinding('je_auto_control.utils.readability', 'automated_readability_index'), - 'count_syllables': AttributeBinding('je_auto_control.utils.readability', 'count_syllables'), - 'flesch_kincaid_grade': AttributeBinding('je_auto_control.utils.readability', 'flesch_kincaid_grade'), - 'flesch_reading_ease': AttributeBinding('je_auto_control.utils.readability', 'flesch_reading_ease'), - 'gunning_fog': AttributeBinding('je_auto_control.utils.readability', 'gunning_fog'), - 'readability_report': AttributeBinding('je_auto_control.utils.readability', 'readability_report'), - 'readability_stats': AttributeBinding('je_auto_control.utils.readability', 'readability_stats'), - 'smog_index': AttributeBinding('je_auto_control.utils.readability', 'smog_index'), - 'base_direction': AttributeBinding('je_auto_control.utils.bidi_check', 'base_direction'), - 'bidi_controls': AttributeBinding('je_auto_control.utils.bidi_check', 'bidi_controls'), - 'detect_bidi_issues': AttributeBinding('je_auto_control.utils.bidi_check', 'detect_bidi_issues'), - 'has_bidi_controls': AttributeBinding('je_auto_control.utils.bidi_check', 'has_bidi_controls'), - 'is_bidi_balanced': AttributeBinding('je_auto_control.utils.bidi_check', 'is_balanced'), - 'is_trojan_source': AttributeBinding('je_auto_control.utils.bidi_check', 'is_trojan_source'), - 'strip_bidi_controls': AttributeBinding('je_auto_control.utils.bidi_check', 'strip_bidi_controls'), + 'collation_key': AttributeBinding(_UTILS_LOCALE_COLLATION, 'collation_key'), + 'collation_compare': AttributeBinding(_UTILS_LOCALE_COLLATION, 'compare'), + 'sort_strings': AttributeBinding(_UTILS_LOCALE_COLLATION, 'sort_strings'), + 'confusable_skeleton': AttributeBinding(_UTILS_CONFUSABLES, 'skeleton'), + 'detect_homoglyphs': AttributeBinding(_UTILS_CONFUSABLES, 'detect_homoglyphs'), + 'is_confusable': AttributeBinding(_UTILS_CONFUSABLES, 'is_confusable'), + 'is_mixed_script': AttributeBinding(_UTILS_CONFUSABLES, 'is_mixed_script'), + 'scripts_of': AttributeBinding(_UTILS_CONFUSABLES, 'scripts_of'), + 'automated_readability_index': AttributeBinding(_UTILS_READABILITY, 'automated_readability_index'), + 'count_syllables': AttributeBinding(_UTILS_READABILITY, 'count_syllables'), + 'flesch_kincaid_grade': AttributeBinding(_UTILS_READABILITY, 'flesch_kincaid_grade'), + 'flesch_reading_ease': AttributeBinding(_UTILS_READABILITY, 'flesch_reading_ease'), + 'gunning_fog': AttributeBinding(_UTILS_READABILITY, 'gunning_fog'), + 'readability_report': AttributeBinding(_UTILS_READABILITY, 'readability_report'), + 'readability_stats': AttributeBinding(_UTILS_READABILITY, 'readability_stats'), + 'smog_index': AttributeBinding(_UTILS_READABILITY, 'smog_index'), + 'base_direction': AttributeBinding(_UTILS_BIDI_CHECK, 'base_direction'), + 'bidi_controls': AttributeBinding(_UTILS_BIDI_CHECK, 'bidi_controls'), + 'detect_bidi_issues': AttributeBinding(_UTILS_BIDI_CHECK, 'detect_bidi_issues'), + 'has_bidi_controls': AttributeBinding(_UTILS_BIDI_CHECK, 'has_bidi_controls'), + 'is_bidi_balanced': AttributeBinding(_UTILS_BIDI_CHECK, 'is_balanced'), + 'is_trojan_source': AttributeBinding(_UTILS_BIDI_CHECK, 'is_trojan_source'), + 'strip_bidi_controls': AttributeBinding(_UTILS_BIDI_CHECK, 'strip_bidi_controls'), 'format_list': AttributeBinding('je_auto_control.utils.list_format', 'format_list'), - 'format_message': AttributeBinding('je_auto_control.utils.message_format', 'format_message'), - 'ordinal_category': AttributeBinding('je_auto_control.utils.message_format', 'ordinal_category'), - 'plural_category': AttributeBinding('je_auto_control.utils.message_format', 'plural_category'), - 'GettextCatalog': AttributeBinding('je_auto_control.utils.gettext_catalog', 'GettextCatalog'), - 'parse_po': AttributeBinding('je_auto_control.utils.gettext_catalog', 'parse_po'), - 'parse_po_file': AttributeBinding('je_auto_control.utils.gettext_catalog', 'parse_po_file'), - 'read_mo': AttributeBinding('je_auto_control.utils.gettext_catalog', 'read_mo'), - 'read_mo_file': AttributeBinding('je_auto_control.utils.gettext_catalog', 'read_mo_file'), - 'luhn_validate': AttributeBinding('je_auto_control.utils.checksum', 'luhn_validate'), - 'luhn_check_digit': AttributeBinding('je_auto_control.utils.checksum', 'luhn_check_digit'), - 'verhoeff_validate': AttributeBinding('je_auto_control.utils.checksum', 'verhoeff_validate'), - 'verhoeff_check_digit': AttributeBinding('je_auto_control.utils.checksum', 'verhoeff_check_digit'), - 'damm_validate': AttributeBinding('je_auto_control.utils.checksum', 'damm_validate'), - 'damm_check_digit': AttributeBinding('je_auto_control.utils.checksum', 'damm_check_digit'), - 'mod97_10_validate': AttributeBinding('je_auto_control.utils.checksum', 'mod97_10_validate'), - 'mod97_10_check_digits': AttributeBinding('je_auto_control.utils.checksum', 'mod97_10_check_digits'), - 'plan_path': AttributeBinding('je_auto_control.utils.mouse_path', 'plan_path'), - 'move_along_path': AttributeBinding('je_auto_control.utils.mouse_path', 'move_along_path'), - 'drag_path': AttributeBinding('je_auto_control.utils.mouse_path', 'drag_path'), - 'path_easings': AttributeBinding('je_auto_control.utils.mouse_path', 'path_easings'), - 'plan_field_set': AttributeBinding('je_auto_control.utils.field_entry', 'plan_field_set'), - 'set_field_text': AttributeBinding('je_auto_control.utils.field_entry', 'set_field_text'), - 'plan_key_hold': AttributeBinding('je_auto_control.utils.key_hold', 'plan_key_hold'), - 'hold_key': AttributeBinding('je_auto_control.utils.key_hold', 'hold_key'), - 'move_mouse_relative': AttributeBinding('je_auto_control.utils.mouse_relative', 'move_mouse_relative'), - 'relative_target': AttributeBinding('je_auto_control.utils.mouse_relative', 'relative_target'), - 'type_unicode': AttributeBinding('je_auto_control.utils.text_unicode', 'type_unicode'), - 'type_unicode_keys': AttributeBinding('je_auto_control.utils.text_unicode', 'type_unicode_keys'), - 'type_unicode_text': AttributeBinding('je_auto_control.utils.text_unicode', 'type_unicode_text'), - 'plan_paste': AttributeBinding('je_auto_control.utils.text_unicode', 'plan_paste'), - 'plan_unicode_keys': AttributeBinding('je_auto_control.utils.text_unicode', 'plan_unicode_keys'), - 'unicode_code_units': AttributeBinding('je_auto_control.utils.text_unicode', 'unicode_code_units'), - 'unicode_keys_supported': AttributeBinding('je_auto_control.utils.text_unicode', 'unicode_keys_supported'), - 'hold_modifiers': AttributeBinding('je_auto_control.utils.modifier_state', 'hold_modifiers'), - 'plan_with_modifiers': AttributeBinding('je_auto_control.utils.modifier_state', 'plan_with_modifiers'), - 'cluster_grid': AttributeBinding('je_auto_control.utils.grid_locator', 'cluster_grid'), - 'locate_cell': AttributeBinding('je_auto_control.utils.grid_locator', 'locate_cell'), - 'TemplateMatch': AttributeBinding('je_auto_control.utils.visual_match', 'Match'), - 'match_template': AttributeBinding('je_auto_control.utils.visual_match', 'match_template'), - 'match_template_all': AttributeBinding('je_auto_control.utils.visual_match', 'match_template_all'), - 'match_masked': AttributeBinding('je_auto_control.utils.visual_match', 'match_masked'), - 'match_masked_all': AttributeBinding('je_auto_control.utils.visual_match', 'match_masked_all'), - 'best_matches': AttributeBinding('je_auto_control.utils.visual_match', 'best_matches'), - 'RotatedMatch': AttributeBinding('je_auto_control.utils.rotated_match', 'RotatedMatch'), - 'match_rotated': AttributeBinding('je_auto_control.utils.rotated_match', 'match_rotated'), - 'match_rotated_all': AttributeBinding('je_auto_control.utils.rotated_match', 'match_rotated_all'), - 'scale_space': AttributeBinding('je_auto_control.utils.rotated_match', 'scale_space'), - 'TrustedMatch': AttributeBinding('je_auto_control.utils.match_trust', 'TrustedMatch'), - 'match_with_trust': AttributeBinding('je_auto_control.utils.match_trust', 'match_with_trust'), - 'score_peaks': AttributeBinding('je_auto_control.utils.match_trust', 'score_peaks'), - 'auto_threshold': AttributeBinding('je_auto_control.utils.match_autothresh', 'auto_threshold'), - 'match_auto': AttributeBinding('je_auto_control.utils.match_autothresh', 'match_auto'), - 'edge_match': AttributeBinding('je_auto_control.utils.edge_match', 'edge_match'), - 'edge_match_all': AttributeBinding('je_auto_control.utils.edge_match', 'edge_match_all'), - 'chamfer_distance': AttributeBinding('je_auto_control.utils.edge_match', 'chamfer_distance'), - 'match_ensemble': AttributeBinding('je_auto_control.utils.match_ensemble', 'match_ensemble'), - 'vote_centers': AttributeBinding('je_auto_control.utils.match_ensemble', 'vote_centers'), - 'match_color': AttributeBinding('je_auto_control.utils.color_match', 'match_color'), - 'match_color_all': AttributeBinding('je_auto_control.utils.color_match', 'match_color_all'), - 'region_stability': AttributeBinding('je_auto_control.utils.match_stability', 'region_stability'), - 'match_persistence': AttributeBinding('je_auto_control.utils.match_stability', 'match_persistence'), - 'SubPixelMatch': AttributeBinding('je_auto_control.utils.subpixel_match', 'SubPixelMatch'), - 'match_subpixel': AttributeBinding('je_auto_control.utils.subpixel_match', 'match_subpixel'), - 'refine_peak': AttributeBinding('je_auto_control.utils.subpixel_match', 'refine_peak'), - 'GridCell': AttributeBinding('je_auto_control.utils.screen_grid', 'GridCell'), - 'grid_cells': AttributeBinding('je_auto_control.utils.screen_grid', 'grid_cells'), - 'cell_for_point': AttributeBinding('je_auto_control.utils.screen_grid', 'cell_for_point'), - 'point_for_cell': AttributeBinding('je_auto_control.utils.screen_grid', 'point_for_cell'), - 'assign_text_to_grid': AttributeBinding('je_auto_control.utils.table_grid_fill', 'assign_text_to_grid'), - 'populate_table': AttributeBinding('je_auto_control.utils.table_grid_fill', 'populate_table'), - 'table_to_records': AttributeBinding('je_auto_control.utils.table_grid_fill', 'table_to_records'), - 'table_to_csv': AttributeBinding('je_auto_control.utils.table_grid_fill', 'table_to_csv'), - 'delta_index': AttributeBinding('je_auto_control.utils.observation_delta', 'delta_index'), - 'delta_observation': AttributeBinding('je_auto_control.utils.observation_delta', 'delta_observation'), - 'summarize_delta': AttributeBinding('je_auto_control.utils.observation_delta', 'summarize_delta'), - 'vertical_projection': AttributeBinding('je_auto_control.utils.column_layout', 'vertical_projection'), - 'column_gutters': AttributeBinding('je_auto_control.utils.column_layout', 'column_gutters'), - 'assign_columns': AttributeBinding('je_auto_control.utils.column_layout', 'assign_columns'), - 'detect_borderless_table': AttributeBinding('je_auto_control.utils.column_layout', 'detect_borderless_table'), - 'xy_cut': AttributeBinding('je_auto_control.utils.reading_flow', 'xy_cut'), - 'flow_order': AttributeBinding('je_auto_control.utils.reading_flow', 'flow_order'), - 'to_blocks': AttributeBinding('je_auto_control.utils.reading_flow', 'to_blocks'), - 'group_paragraphs': AttributeBinding('je_auto_control.utils.text_blocks', 'group_paragraphs'), - 'detect_lists': AttributeBinding('je_auto_control.utils.text_blocks', 'detect_lists'), - 'classify_lines': AttributeBinding('je_auto_control.utils.heading_segment', 'classify_lines'), - 'outline': AttributeBinding('je_auto_control.utils.heading_segment', 'outline'), - 'associate_fields': AttributeBinding('je_auto_control.utils.form_fields', 'associate_fields'), - 'match_labels_to_widgets': AttributeBinding('je_auto_control.utils.form_fields', 'match_labels_to_widgets'), - 'checkbox_state': AttributeBinding('je_auto_control.utils.form_fields', 'checkbox_state'), - 'EffectVerdict': AttributeBinding('je_auto_control.utils.action_effect', 'EffectVerdict'), - 'classify_effect': AttributeBinding('je_auto_control.utils.action_effect', 'classify_effect'), - 'effect_near_point': AttributeBinding('je_auto_control.utils.action_effect', 'effect_near_point'), - 'is_no_op': AttributeBinding('je_auto_control.utils.action_effect', 'is_no_op'), - 'PostconditionReport': AttributeBinding('je_auto_control.utils.postcondition', 'PostconditionReport'), - 'check_postcondition': AttributeBinding('je_auto_control.utils.postcondition', 'check_postcondition'), - 'compile_postcondition': AttributeBinding('je_auto_control.utils.postcondition', 'compile_postcondition'), - 'RepairPolicy': AttributeBinding('je_auto_control.utils.step_repair', 'RepairPolicy'), - 'RepairOutcome': AttributeBinding('je_auto_control.utils.step_repair', 'RepairOutcome'), - 'plan_repair': AttributeBinding('je_auto_control.utils.step_repair', 'plan_repair'), - 'next_tactic': AttributeBinding('je_auto_control.utils.step_repair', 'next_tactic'), - 'run_with_repair': AttributeBinding('je_auto_control.utils.step_repair', 'run_with_repair'), - 'ConsensusResult': AttributeBinding('je_auto_control.utils.grounding_consensus', 'ConsensusResult'), - 'consensus_point': AttributeBinding('je_auto_control.utils.grounding_consensus', 'consensus_point'), - 'consensus_element': AttributeBinding('je_auto_control.utils.grounding_consensus', 'consensus_element'), - 'is_confident': AttributeBinding('je_auto_control.utils.grounding_consensus', 'is_confident'), - 'SettleState': AttributeBinding('je_auto_control.utils.settle_detector', 'SettleState'), - 'SettleTracker': AttributeBinding('je_auto_control.utils.settle_detector', 'SettleTracker'), - 'settle_point': AttributeBinding('je_auto_control.utils.settle_detector', 'settle_point'), - 'is_settled': AttributeBinding('je_auto_control.utils.settle_detector', 'is_settled'), - 'build_critic_record': AttributeBinding('je_auto_control.utils.critic_features', 'build_critic_record'), - 'score_step_rule_based': AttributeBinding('je_auto_control.utils.critic_features', 'score_step_rule_based'), - 'to_judge_prompt': AttributeBinding('je_auto_control.utils.critic_features', 'to_judge_prompt'), - 'find_color_region': AttributeBinding('je_auto_control.utils.color_region', 'find_color_region'), - 'find_color_regions': AttributeBinding('je_auto_control.utils.color_region', 'find_color_regions'), - 'ssim_compare': AttributeBinding('je_auto_control.utils.ssim', 'ssim_compare'), - 'ssim_changed_regions': AttributeBinding('je_auto_control.utils.ssim', 'ssim_changed_regions'), - 'feature_match': AttributeBinding('je_auto_control.utils.feature_match', 'feature_match'), - 'FeatureMatch': AttributeBinding('je_auto_control.utils.feature_match', 'FeatureMatch'), - 'find_shapes': AttributeBinding('je_auto_control.utils.shape_locator', 'find_shapes'), - 'find_rectangles': AttributeBinding('je_auto_control.utils.shape_locator', 'find_rectangles'), - 'WindowRect': AttributeBinding('je_auto_control.utils.window_layout', 'WindowRect'), - 'available_slots': AttributeBinding('je_auto_control.utils.window_layout', 'available_slots'), - 'tile_rect': AttributeBinding('je_auto_control.utils.window_layout', 'tile_rect'), - 'grid_rects': AttributeBinding('je_auto_control.utils.window_layout', 'grid_rects'), - 'cascade_rects': AttributeBinding('je_auto_control.utils.window_layout', 'cascade_rects'), - 'preprocess_image': AttributeBinding('je_auto_control.utils.preprocess', 'preprocess_image'), - 'to_grayscale': AttributeBinding('je_auto_control.utils.preprocess', 'to_grayscale'), - 'binarize': AttributeBinding('je_auto_control.utils.preprocess', 'binarize'), - 'upscale': AttributeBinding('je_auto_control.utils.preprocess', 'upscale'), - 'denoise': AttributeBinding('je_auto_control.utils.preprocess', 'denoise'), - 'deskew': AttributeBinding('je_auto_control.utils.preprocess', 'deskew'), - 'detect_skew_angle': AttributeBinding('je_auto_control.utils.preprocess', 'detect_skew_angle'), - 'enhance_contrast': AttributeBinding('je_auto_control.utils.preprocess', 'enhance_contrast'), - 'Monitor': AttributeBinding('je_auto_control.utils.monitor_layout', 'Monitor'), - 'enumerate_monitors': AttributeBinding('je_auto_control.utils.monitor_layout', 'enumerate_monitors'), - 'monitor_at_point': AttributeBinding('je_auto_control.utils.monitor_layout', 'monitor_at_point'), - 'monitor_for_window': AttributeBinding('je_auto_control.utils.monitor_layout', 'monitor_for_window'), - 'primary_monitor': AttributeBinding('je_auto_control.utils.monitor_layout', 'primary_monitor'), - 'remap_point': AttributeBinding('je_auto_control.utils.monitor_layout', 'remap_point'), - 'to_local': AttributeBinding('je_auto_control.utils.monitor_layout', 'to_local'), - 'to_virtual': AttributeBinding('je_auto_control.utils.monitor_layout', 'to_virtual'), - 'virtual_bounds': AttributeBinding('je_auto_control.utils.monitor_layout', 'virtual_bounds'), - 'grab_logical': AttributeBinding('je_auto_control.utils.monitor_layout', 'grab_logical'), - 'logical_virtual_rect': AttributeBinding('je_auto_control.utils.monitor_layout', 'logical_virtual_rect'), - 'logical_scale': AttributeBinding('je_auto_control.utils.monitor_layout', 'logical_scale'), - 'needs_rescale': AttributeBinding('je_auto_control.utils.monitor_layout', 'needs_rescale'), - 'wait_actionable': AttributeBinding('je_auto_control.utils.actionability', 'wait_actionable'), - 'act_when_ready': AttributeBinding('je_auto_control.utils.actionability', 'act_when_ready'), - 'ActionabilityReport': AttributeBinding('je_auto_control.utils.actionability', 'ActionabilityReport'), - 'GateConfig': AttributeBinding('je_auto_control.utils.actionability', 'GateConfig'), - 'iou': AttributeBinding('je_auto_control.utils.element_parse', 'iou'), - 'merge_boxes': AttributeBinding('je_auto_control.utils.element_parse', 'merge_boxes'), - 'fuse_elements': AttributeBinding('je_auto_control.utils.element_parse', 'fuse_elements'), - 'reading_order': AttributeBinding('je_auto_control.utils.element_parse', 'reading_order'), - 'segment_hsv': AttributeBinding('je_auto_control.utils.hsv_segment', 'segment_hsv'), - 'color_mask': AttributeBinding('je_auto_control.utils.hsv_segment', 'color_mask'), - 'dominant_hue_regions': AttributeBinding('je_auto_control.utils.hsv_segment', 'dominant_hue_regions'), - 'find_text_regions': AttributeBinding('je_auto_control.utils.text_regions', 'find_text_regions'), - 'find_text_lines': AttributeBinding('je_auto_control.utils.text_regions', 'find_text_lines'), - 'find_lines': AttributeBinding('je_auto_control.utils.edge_lines', 'find_lines'), - 'find_grid': AttributeBinding('je_auto_control.utils.edge_lines', 'find_grid'), - 'find_separators': AttributeBinding('je_auto_control.utils.edge_lines', 'find_separators'), - 'expect_poll': AttributeBinding('je_auto_control.utils.expect_poll', 'expect_poll'), - 'assert_poll': AttributeBinding('je_auto_control.utils.expect_poll', 'assert_poll'), - 'PollResult': AttributeBinding('je_auto_control.utils.expect_poll', 'PollResult'), - 'to_equal': AttributeBinding('je_auto_control.utils.expect_poll', 'to_equal'), - 'to_contain': AttributeBinding('je_auto_control.utils.expect_poll', 'to_contain'), - 'to_be_greater_than': AttributeBinding('je_auto_control.utils.expect_poll', 'to_be_greater_than'), - 'to_match_regex': AttributeBinding('je_auto_control.utils.expect_poll', 'to_match_regex'), - 'to_be_truthy': AttributeBinding('je_auto_control.utils.expect_poll', 'to_be_truthy'), - 'to_be_stable': AttributeBinding('je_auto_control.utils.expect_poll', 'to_be_stable'), - 'Candidates': AttributeBinding('je_auto_control.utils.locator_chain', 'Candidates'), - 'from_boxes': AttributeBinding('je_auto_control.utils.locator_chain', 'from_boxes'), - 'build_cf_html': AttributeBinding('je_auto_control.utils.rich_clipboard', 'build_cf_html'), - 'parse_cf_html': AttributeBinding('je_auto_control.utils.rich_clipboard', 'parse_cf_html'), - 'get_clipboard_html': AttributeBinding('je_auto_control.utils.rich_clipboard', 'get_clipboard_html'), - 'set_clipboard_html': AttributeBinding('je_auto_control.utils.rich_clipboard', 'set_clipboard_html'), - 'build_dropfiles': AttributeBinding('je_auto_control.utils.clipboard_files', 'build_dropfiles'), - 'parse_dropfiles': AttributeBinding('je_auto_control.utils.clipboard_files', 'parse_dropfiles'), - 'set_clipboard_files': AttributeBinding('je_auto_control.utils.clipboard_files', 'set_clipboard_files'), - 'get_clipboard_files': AttributeBinding('je_auto_control.utils.clipboard_files', 'get_clipboard_files'), - 'image_histogram': AttributeBinding('je_auto_control.utils.img_histogram', 'image_histogram'), - 'compare_histograms': AttributeBinding('je_auto_control.utils.img_histogram', 'compare_histograms'), - 'histogram_changed': AttributeBinding('je_auto_control.utils.img_histogram', 'histogram_changed'), - 'changed_regions': AttributeBinding('je_auto_control.utils.motion_regions', 'changed_regions'), - 'has_motion': AttributeBinding('je_auto_control.utils.motion_regions', 'has_motion'), - 'activity_score': AttributeBinding('je_auto_control.utils.motion_regions', 'activity_score'), - 'plan_zorder': AttributeBinding('je_auto_control.utils.window_zorder', 'plan_zorder'), - 'set_topmost': AttributeBinding('je_auto_control.utils.window_zorder', 'set_topmost'), - 'bring_to_front': AttributeBinding('je_auto_control.utils.window_zorder', 'bring_to_front'), - 'send_to_back': AttributeBinding('je_auto_control.utils.window_zorder', 'send_to_back'), - 'SoftAssertions': AttributeBinding('je_auto_control.utils.soft_assert', 'SoftAssertions'), - 'SoftAssertionsFailed': AttributeBinding('je_auto_control.utils.soft_assert', 'SoftAssertionsFailed'), - 'perceptual_diff': AttributeBinding('je_auto_control.utils.perceptual_diff', 'perceptual_diff'), - 'assert_perceptual': AttributeBinding('je_auto_control.utils.perceptual_diff', 'assert_perceptual'), - 'PerceptualDiffResult': AttributeBinding('je_auto_control.utils.perceptual_diff', 'PerceptualDiffResult'), - 'frame_insets': AttributeBinding('je_auto_control.utils.window_geometry', 'frame_insets'), - 'client_to_screen': AttributeBinding('je_auto_control.utils.window_geometry', 'client_to_screen'), - 'get_client_rect': AttributeBinding('je_auto_control.utils.window_geometry', 'get_client_rect'), - 'client_point': AttributeBinding('je_auto_control.utils.window_geometry', 'client_point'), - 'canonical_action': AttributeBinding('je_auto_control.utils.cua_action', 'canonical_action'), - 'from_anthropic': AttributeBinding('je_auto_control.utils.cua_action', 'from_anthropic'), - 'from_openai_cua': AttributeBinding('je_auto_control.utils.cua_action', 'from_openai_cua'), - 'to_ac_command': AttributeBinding('je_auto_control.utils.cua_action', 'to_ac_command'), - 'flatten_tree': AttributeBinding('je_auto_control.utils.observation', 'flatten_tree'), - 'observation_index': AttributeBinding('je_auto_control.utils.observation', 'observation_index'), - 'serialize_observation': AttributeBinding('je_auto_control.utils.observation', 'serialize_observation'), - 'in_bounds': AttributeBinding('je_auto_control.utils.action_grounding', 'in_bounds'), - 'snap_to_element': AttributeBinding('je_auto_control.utils.action_grounding', 'snap_to_element'), - 'validate_action': AttributeBinding('je_auto_control.utils.action_grounding', 'validate_action'), - 'record_step': AttributeBinding('je_auto_control.utils.agent_replay', 'record_step'), - 'to_jsonl': AttributeBinding('je_auto_control.utils.agent_replay', 'to_jsonl'), - 'from_jsonl': AttributeBinding('je_auto_control.utils.agent_replay', 'from_jsonl'), - 'replay_trace': AttributeBinding('je_auto_control.utils.agent_replay', 'replay_trace'), - 'match_elements': AttributeBinding('je_auto_control.utils.element_diff', 'match_elements'), - 'assign_stable_ids': AttributeBinding('je_auto_control.utils.element_diff', 'assign_stable_ids'), - 'score_candidates': AttributeBinding('je_auto_control.utils.element_scoring', 'score_candidates'), - 'best_candidate': AttributeBinding('je_auto_control.utils.element_scoring', 'best_candidate'), - 'ScoredCandidate': AttributeBinding('je_auto_control.utils.element_scoring', 'ScoredCandidate'), + 'format_message': AttributeBinding(_UTILS_MESSAGE_FORMAT, 'format_message'), + 'ordinal_category': AttributeBinding(_UTILS_MESSAGE_FORMAT, 'ordinal_category'), + 'plural_category': AttributeBinding(_UTILS_MESSAGE_FORMAT, 'plural_category'), + 'GettextCatalog': AttributeBinding(_UTILS_GETTEXT_CATALOG, 'GettextCatalog'), + 'parse_po': AttributeBinding(_UTILS_GETTEXT_CATALOG, 'parse_po'), + 'parse_po_file': AttributeBinding(_UTILS_GETTEXT_CATALOG, 'parse_po_file'), + 'read_mo': AttributeBinding(_UTILS_GETTEXT_CATALOG, 'read_mo'), + 'read_mo_file': AttributeBinding(_UTILS_GETTEXT_CATALOG, 'read_mo_file'), + 'luhn_validate': AttributeBinding(_UTILS_CHECKSUM, 'luhn_validate'), + 'luhn_check_digit': AttributeBinding(_UTILS_CHECKSUM, 'luhn_check_digit'), + 'verhoeff_validate': AttributeBinding(_UTILS_CHECKSUM, 'verhoeff_validate'), + 'verhoeff_check_digit': AttributeBinding(_UTILS_CHECKSUM, 'verhoeff_check_digit'), + 'damm_validate': AttributeBinding(_UTILS_CHECKSUM, 'damm_validate'), + 'damm_check_digit': AttributeBinding(_UTILS_CHECKSUM, 'damm_check_digit'), + 'mod97_10_validate': AttributeBinding(_UTILS_CHECKSUM, 'mod97_10_validate'), + 'mod97_10_check_digits': AttributeBinding(_UTILS_CHECKSUM, 'mod97_10_check_digits'), + 'plan_path': AttributeBinding(_UTILS_MOUSE_PATH, 'plan_path'), + 'move_along_path': AttributeBinding(_UTILS_MOUSE_PATH, 'move_along_path'), + 'drag_path': AttributeBinding(_UTILS_MOUSE_PATH, 'drag_path'), + 'path_easings': AttributeBinding(_UTILS_MOUSE_PATH, 'path_easings'), + 'plan_field_set': AttributeBinding(_UTILS_FIELD_ENTRY, 'plan_field_set'), + 'set_field_text': AttributeBinding(_UTILS_FIELD_ENTRY, 'set_field_text'), + 'plan_key_hold': AttributeBinding(_UTILS_KEY_HOLD, 'plan_key_hold'), + 'hold_key': AttributeBinding(_UTILS_KEY_HOLD, 'hold_key'), + 'move_mouse_relative': AttributeBinding(_UTILS_MOUSE_RELATIVE, 'move_mouse_relative'), + 'relative_target': AttributeBinding(_UTILS_MOUSE_RELATIVE, 'relative_target'), + 'type_unicode': AttributeBinding(_UTILS_TEXT_UNICODE, 'type_unicode'), + 'type_unicode_keys': AttributeBinding(_UTILS_TEXT_UNICODE, 'type_unicode_keys'), + 'type_unicode_text': AttributeBinding(_UTILS_TEXT_UNICODE, 'type_unicode_text'), + 'plan_paste': AttributeBinding(_UTILS_TEXT_UNICODE, 'plan_paste'), + 'plan_unicode_keys': AttributeBinding(_UTILS_TEXT_UNICODE, 'plan_unicode_keys'), + 'unicode_code_units': AttributeBinding(_UTILS_TEXT_UNICODE, 'unicode_code_units'), + 'unicode_keys_supported': AttributeBinding(_UTILS_TEXT_UNICODE, 'unicode_keys_supported'), + 'hold_modifiers': AttributeBinding(_UTILS_MODIFIER_STATE, 'hold_modifiers'), + 'plan_with_modifiers': AttributeBinding(_UTILS_MODIFIER_STATE, 'plan_with_modifiers'), + 'cluster_grid': AttributeBinding(_UTILS_GRID_LOCATOR, 'cluster_grid'), + 'locate_cell': AttributeBinding(_UTILS_GRID_LOCATOR, 'locate_cell'), + 'TemplateMatch': AttributeBinding(_UTILS_VISUAL_MATCH, 'Match'), + 'match_template': AttributeBinding(_UTILS_VISUAL_MATCH, 'match_template'), + 'match_template_all': AttributeBinding(_UTILS_VISUAL_MATCH, 'match_template_all'), + 'match_masked': AttributeBinding(_UTILS_VISUAL_MATCH, 'match_masked'), + 'match_masked_all': AttributeBinding(_UTILS_VISUAL_MATCH, 'match_masked_all'), + 'best_matches': AttributeBinding(_UTILS_VISUAL_MATCH, 'best_matches'), + 'RotatedMatch': AttributeBinding(_UTILS_ROTATED_MATCH, 'RotatedMatch'), + 'match_rotated': AttributeBinding(_UTILS_ROTATED_MATCH, 'match_rotated'), + 'match_rotated_all': AttributeBinding(_UTILS_ROTATED_MATCH, 'match_rotated_all'), + 'scale_space': AttributeBinding(_UTILS_ROTATED_MATCH, 'scale_space'), + 'TrustedMatch': AttributeBinding(_UTILS_MATCH_TRUST, 'TrustedMatch'), + 'match_with_trust': AttributeBinding(_UTILS_MATCH_TRUST, 'match_with_trust'), + 'score_peaks': AttributeBinding(_UTILS_MATCH_TRUST, 'score_peaks'), + 'auto_threshold': AttributeBinding(_UTILS_MATCH_AUTOTHRESH, 'auto_threshold'), + 'match_auto': AttributeBinding(_UTILS_MATCH_AUTOTHRESH, 'match_auto'), + 'edge_match': AttributeBinding(_UTILS_EDGE_MATCH, 'edge_match'), + 'edge_match_all': AttributeBinding(_UTILS_EDGE_MATCH, 'edge_match_all'), + 'chamfer_distance': AttributeBinding(_UTILS_EDGE_MATCH, 'chamfer_distance'), + 'match_ensemble': AttributeBinding(_UTILS_MATCH_ENSEMBLE, 'match_ensemble'), + 'vote_centers': AttributeBinding(_UTILS_MATCH_ENSEMBLE, 'vote_centers'), + 'match_color': AttributeBinding(_UTILS_COLOR_MATCH, 'match_color'), + 'match_color_all': AttributeBinding(_UTILS_COLOR_MATCH, 'match_color_all'), + 'region_stability': AttributeBinding(_UTILS_MATCH_STABILITY, 'region_stability'), + 'match_persistence': AttributeBinding(_UTILS_MATCH_STABILITY, 'match_persistence'), + 'SubPixelMatch': AttributeBinding(_UTILS_SUBPIXEL_MATCH, 'SubPixelMatch'), + 'match_subpixel': AttributeBinding(_UTILS_SUBPIXEL_MATCH, 'match_subpixel'), + 'refine_peak': AttributeBinding(_UTILS_SUBPIXEL_MATCH, 'refine_peak'), + 'GridCell': AttributeBinding(_UTILS_SCREEN_GRID, 'GridCell'), + 'grid_cells': AttributeBinding(_UTILS_SCREEN_GRID, 'grid_cells'), + 'cell_for_point': AttributeBinding(_UTILS_SCREEN_GRID, 'cell_for_point'), + 'point_for_cell': AttributeBinding(_UTILS_SCREEN_GRID, 'point_for_cell'), + 'assign_text_to_grid': AttributeBinding(_UTILS_TABLE_GRID_FILL, 'assign_text_to_grid'), + 'populate_table': AttributeBinding(_UTILS_TABLE_GRID_FILL, 'populate_table'), + 'table_to_records': AttributeBinding(_UTILS_TABLE_GRID_FILL, 'table_to_records'), + 'table_to_csv': AttributeBinding(_UTILS_TABLE_GRID_FILL, 'table_to_csv'), + 'delta_index': AttributeBinding(_UTILS_OBSERVATION_DELTA, 'delta_index'), + 'delta_observation': AttributeBinding(_UTILS_OBSERVATION_DELTA, 'delta_observation'), + 'summarize_delta': AttributeBinding(_UTILS_OBSERVATION_DELTA, 'summarize_delta'), + 'vertical_projection': AttributeBinding(_UTILS_COLUMN_LAYOUT, 'vertical_projection'), + 'column_gutters': AttributeBinding(_UTILS_COLUMN_LAYOUT, 'column_gutters'), + 'assign_columns': AttributeBinding(_UTILS_COLUMN_LAYOUT, 'assign_columns'), + 'detect_borderless_table': AttributeBinding(_UTILS_COLUMN_LAYOUT, 'detect_borderless_table'), + 'xy_cut': AttributeBinding(_UTILS_READING_FLOW, 'xy_cut'), + 'flow_order': AttributeBinding(_UTILS_READING_FLOW, 'flow_order'), + 'to_blocks': AttributeBinding(_UTILS_READING_FLOW, 'to_blocks'), + 'group_paragraphs': AttributeBinding(_UTILS_TEXT_BLOCKS, 'group_paragraphs'), + 'detect_lists': AttributeBinding(_UTILS_TEXT_BLOCKS, 'detect_lists'), + 'classify_lines': AttributeBinding(_UTILS_HEADING_SEGMENT, 'classify_lines'), + 'outline': AttributeBinding(_UTILS_HEADING_SEGMENT, 'outline'), + 'associate_fields': AttributeBinding(_UTILS_FORM_FIELDS, 'associate_fields'), + 'match_labels_to_widgets': AttributeBinding(_UTILS_FORM_FIELDS, 'match_labels_to_widgets'), + 'checkbox_state': AttributeBinding(_UTILS_FORM_FIELDS, 'checkbox_state'), + 'EffectVerdict': AttributeBinding(_UTILS_ACTION_EFFECT, 'EffectVerdict'), + 'classify_effect': AttributeBinding(_UTILS_ACTION_EFFECT, 'classify_effect'), + 'effect_near_point': AttributeBinding(_UTILS_ACTION_EFFECT, 'effect_near_point'), + 'is_no_op': AttributeBinding(_UTILS_ACTION_EFFECT, 'is_no_op'), + 'PostconditionReport': AttributeBinding(_UTILS_POSTCONDITION, 'PostconditionReport'), + 'check_postcondition': AttributeBinding(_UTILS_POSTCONDITION, 'check_postcondition'), + 'compile_postcondition': AttributeBinding(_UTILS_POSTCONDITION, 'compile_postcondition'), + 'RepairPolicy': AttributeBinding(_UTILS_STEP_REPAIR, 'RepairPolicy'), + 'RepairOutcome': AttributeBinding(_UTILS_STEP_REPAIR, 'RepairOutcome'), + 'plan_repair': AttributeBinding(_UTILS_STEP_REPAIR, 'plan_repair'), + 'next_tactic': AttributeBinding(_UTILS_STEP_REPAIR, 'next_tactic'), + 'run_with_repair': AttributeBinding(_UTILS_STEP_REPAIR, 'run_with_repair'), + 'ConsensusResult': AttributeBinding(_UTILS_GROUNDING_CONSENSUS, 'ConsensusResult'), + 'consensus_point': AttributeBinding(_UTILS_GROUNDING_CONSENSUS, 'consensus_point'), + 'consensus_element': AttributeBinding(_UTILS_GROUNDING_CONSENSUS, 'consensus_element'), + 'is_confident': AttributeBinding(_UTILS_GROUNDING_CONSENSUS, 'is_confident'), + 'SettleState': AttributeBinding(_UTILS_SETTLE_DETECTOR, 'SettleState'), + 'SettleTracker': AttributeBinding(_UTILS_SETTLE_DETECTOR, 'SettleTracker'), + 'settle_point': AttributeBinding(_UTILS_SETTLE_DETECTOR, 'settle_point'), + 'is_settled': AttributeBinding(_UTILS_SETTLE_DETECTOR, 'is_settled'), + 'build_critic_record': AttributeBinding(_UTILS_CRITIC_FEATURES, 'build_critic_record'), + 'score_step_rule_based': AttributeBinding(_UTILS_CRITIC_FEATURES, 'score_step_rule_based'), + 'to_judge_prompt': AttributeBinding(_UTILS_CRITIC_FEATURES, 'to_judge_prompt'), + 'find_color_region': AttributeBinding(_UTILS_COLOR_REGION, 'find_color_region'), + 'find_color_regions': AttributeBinding(_UTILS_COLOR_REGION, 'find_color_regions'), + 'ssim_compare': AttributeBinding(_UTILS_SSIM, 'ssim_compare'), + 'ssim_changed_regions': AttributeBinding(_UTILS_SSIM, 'ssim_changed_regions'), + 'feature_match': AttributeBinding(_UTILS_FEATURE_MATCH, 'feature_match'), + 'FeatureMatch': AttributeBinding(_UTILS_FEATURE_MATCH, 'FeatureMatch'), + 'find_shapes': AttributeBinding(_UTILS_SHAPE_LOCATOR, 'find_shapes'), + 'find_rectangles': AttributeBinding(_UTILS_SHAPE_LOCATOR, 'find_rectangles'), + 'WindowRect': AttributeBinding(_UTILS_WINDOW_LAYOUT, 'WindowRect'), + 'available_slots': AttributeBinding(_UTILS_WINDOW_LAYOUT, 'available_slots'), + 'tile_rect': AttributeBinding(_UTILS_WINDOW_LAYOUT, 'tile_rect'), + 'grid_rects': AttributeBinding(_UTILS_WINDOW_LAYOUT, 'grid_rects'), + 'cascade_rects': AttributeBinding(_UTILS_WINDOW_LAYOUT, 'cascade_rects'), + 'preprocess_image': AttributeBinding(_UTILS_PREPROCESS, 'preprocess_image'), + 'to_grayscale': AttributeBinding(_UTILS_PREPROCESS, 'to_grayscale'), + 'binarize': AttributeBinding(_UTILS_PREPROCESS, 'binarize'), + 'upscale': AttributeBinding(_UTILS_PREPROCESS, 'upscale'), + 'denoise': AttributeBinding(_UTILS_PREPROCESS, 'denoise'), + 'deskew': AttributeBinding(_UTILS_PREPROCESS, 'deskew'), + 'detect_skew_angle': AttributeBinding(_UTILS_PREPROCESS, 'detect_skew_angle'), + 'enhance_contrast': AttributeBinding(_UTILS_PREPROCESS, 'enhance_contrast'), + 'Monitor': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'Monitor'), + 'enumerate_monitors': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'enumerate_monitors'), + 'monitor_at_point': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'monitor_at_point'), + 'monitor_for_window': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'monitor_for_window'), + 'primary_monitor': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'primary_monitor'), + 'remap_point': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'remap_point'), + 'to_local': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'to_local'), + 'to_virtual': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'to_virtual'), + 'virtual_bounds': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'virtual_bounds'), + 'grab_logical': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'grab_logical'), + 'logical_virtual_rect': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'logical_virtual_rect'), + 'logical_scale': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'logical_scale'), + 'needs_rescale': AttributeBinding(_UTILS_MONITOR_LAYOUT, 'needs_rescale'), + 'wait_actionable': AttributeBinding(_UTILS_ACTIONABILITY, 'wait_actionable'), + 'act_when_ready': AttributeBinding(_UTILS_ACTIONABILITY, 'act_when_ready'), + 'ActionabilityReport': AttributeBinding(_UTILS_ACTIONABILITY, 'ActionabilityReport'), + 'GateConfig': AttributeBinding(_UTILS_ACTIONABILITY, 'GateConfig'), + 'iou': AttributeBinding(_UTILS_ELEMENT_PARSE, 'iou'), + 'merge_boxes': AttributeBinding(_UTILS_ELEMENT_PARSE, 'merge_boxes'), + 'fuse_elements': AttributeBinding(_UTILS_ELEMENT_PARSE, 'fuse_elements'), + 'reading_order': AttributeBinding(_UTILS_ELEMENT_PARSE, 'reading_order'), + 'segment_hsv': AttributeBinding(_UTILS_HSV_SEGMENT, 'segment_hsv'), + 'color_mask': AttributeBinding(_UTILS_HSV_SEGMENT, 'color_mask'), + 'dominant_hue_regions': AttributeBinding(_UTILS_HSV_SEGMENT, 'dominant_hue_regions'), + 'find_text_regions': AttributeBinding(_UTILS_TEXT_REGIONS, 'find_text_regions'), + 'find_text_lines': AttributeBinding(_UTILS_TEXT_REGIONS, 'find_text_lines'), + 'find_lines': AttributeBinding(_UTILS_EDGE_LINES, 'find_lines'), + 'find_grid': AttributeBinding(_UTILS_EDGE_LINES, 'find_grid'), + 'find_separators': AttributeBinding(_UTILS_EDGE_LINES, 'find_separators'), + 'expect_poll': AttributeBinding(_UTILS_EXPECT_POLL, 'expect_poll'), + 'assert_poll': AttributeBinding(_UTILS_EXPECT_POLL, 'assert_poll'), + 'PollResult': AttributeBinding(_UTILS_EXPECT_POLL, 'PollResult'), + 'to_equal': AttributeBinding(_UTILS_EXPECT_POLL, 'to_equal'), + 'to_contain': AttributeBinding(_UTILS_EXPECT_POLL, 'to_contain'), + 'to_be_greater_than': AttributeBinding(_UTILS_EXPECT_POLL, 'to_be_greater_than'), + 'to_match_regex': AttributeBinding(_UTILS_EXPECT_POLL, 'to_match_regex'), + 'to_be_truthy': AttributeBinding(_UTILS_EXPECT_POLL, 'to_be_truthy'), + 'to_be_stable': AttributeBinding(_UTILS_EXPECT_POLL, 'to_be_stable'), + 'Candidates': AttributeBinding(_UTILS_LOCATOR_CHAIN, 'Candidates'), + 'from_boxes': AttributeBinding(_UTILS_LOCATOR_CHAIN, 'from_boxes'), + 'build_cf_html': AttributeBinding(_UTILS_RICH_CLIPBOARD, 'build_cf_html'), + 'parse_cf_html': AttributeBinding(_UTILS_RICH_CLIPBOARD, 'parse_cf_html'), + 'get_clipboard_html': AttributeBinding(_UTILS_RICH_CLIPBOARD, 'get_clipboard_html'), + 'set_clipboard_html': AttributeBinding(_UTILS_RICH_CLIPBOARD, 'set_clipboard_html'), + 'build_dropfiles': AttributeBinding(_UTILS_CLIPBOARD_FILES, 'build_dropfiles'), + 'parse_dropfiles': AttributeBinding(_UTILS_CLIPBOARD_FILES, 'parse_dropfiles'), + 'set_clipboard_files': AttributeBinding(_UTILS_CLIPBOARD_FILES, 'set_clipboard_files'), + 'get_clipboard_files': AttributeBinding(_UTILS_CLIPBOARD_FILES, 'get_clipboard_files'), + 'image_histogram': AttributeBinding(_UTILS_IMG_HISTOGRAM, 'image_histogram'), + 'compare_histograms': AttributeBinding(_UTILS_IMG_HISTOGRAM, 'compare_histograms'), + 'histogram_changed': AttributeBinding(_UTILS_IMG_HISTOGRAM, 'histogram_changed'), + 'changed_regions': AttributeBinding(_UTILS_MOTION_REGIONS, 'changed_regions'), + 'has_motion': AttributeBinding(_UTILS_MOTION_REGIONS, 'has_motion'), + 'activity_score': AttributeBinding(_UTILS_MOTION_REGIONS, 'activity_score'), + 'plan_zorder': AttributeBinding(_UTILS_WINDOW_ZORDER, 'plan_zorder'), + 'set_topmost': AttributeBinding(_UTILS_WINDOW_ZORDER, 'set_topmost'), + 'bring_to_front': AttributeBinding(_UTILS_WINDOW_ZORDER, 'bring_to_front'), + 'send_to_back': AttributeBinding(_UTILS_WINDOW_ZORDER, 'send_to_back'), + 'SoftAssertions': AttributeBinding(_UTILS_SOFT_ASSERT, 'SoftAssertions'), + 'SoftAssertionsFailed': AttributeBinding(_UTILS_SOFT_ASSERT, 'SoftAssertionsFailed'), + 'perceptual_diff': AttributeBinding(_UTILS_PERCEPTUAL_DIFF, 'perceptual_diff'), + 'assert_perceptual': AttributeBinding(_UTILS_PERCEPTUAL_DIFF, 'assert_perceptual'), + 'PerceptualDiffResult': AttributeBinding(_UTILS_PERCEPTUAL_DIFF, 'PerceptualDiffResult'), + 'frame_insets': AttributeBinding(_UTILS_WINDOW_GEOMETRY, 'frame_insets'), + 'client_to_screen': AttributeBinding(_UTILS_WINDOW_GEOMETRY, 'client_to_screen'), + 'get_client_rect': AttributeBinding(_UTILS_WINDOW_GEOMETRY, 'get_client_rect'), + 'client_point': AttributeBinding(_UTILS_WINDOW_GEOMETRY, 'client_point'), + 'canonical_action': AttributeBinding(_UTILS_CUA_ACTION, 'canonical_action'), + 'from_anthropic': AttributeBinding(_UTILS_CUA_ACTION, 'from_anthropic'), + 'from_openai_cua': AttributeBinding(_UTILS_CUA_ACTION, 'from_openai_cua'), + 'to_ac_command': AttributeBinding(_UTILS_CUA_ACTION, 'to_ac_command'), + 'flatten_tree': AttributeBinding(_UTILS_OBSERVATION, 'flatten_tree'), + 'observation_index': AttributeBinding(_UTILS_OBSERVATION, 'observation_index'), + 'serialize_observation': AttributeBinding(_UTILS_OBSERVATION, 'serialize_observation'), + 'in_bounds': AttributeBinding(_UTILS_ACTION_GROUNDING, 'in_bounds'), + 'snap_to_element': AttributeBinding(_UTILS_ACTION_GROUNDING, 'snap_to_element'), + 'validate_action': AttributeBinding(_UTILS_ACTION_GROUNDING, 'validate_action'), + 'record_step': AttributeBinding(_UTILS_AGENT_REPLAY, 'record_step'), + 'to_jsonl': AttributeBinding(_UTILS_AGENT_REPLAY, 'to_jsonl'), + 'from_jsonl': AttributeBinding(_UTILS_AGENT_REPLAY, 'from_jsonl'), + 'replay_trace': AttributeBinding(_UTILS_AGENT_REPLAY, 'replay_trace'), + 'match_elements': AttributeBinding(_UTILS_ELEMENT_DIFF, 'match_elements'), + 'assign_stable_ids': AttributeBinding(_UTILS_ELEMENT_DIFF, 'assign_stable_ids'), + 'score_candidates': AttributeBinding(_UTILS_ELEMENT_SCORING, 'score_candidates'), + 'best_candidate': AttributeBinding(_UTILS_ELEMENT_SCORING, 'best_candidate'), + 'ScoredCandidate': AttributeBinding(_UTILS_ELEMENT_SCORING, 'ScoredCandidate'), 'read_barcodes': AttributeBinding('je_auto_control.utils.barcode', 'read_barcodes'), - 'emit_annotations': AttributeBinding('je_auto_control.utils.ci_annotations', 'emit_annotations'), - 'format_annotation': AttributeBinding('je_auto_control.utils.ci_annotations', 'format_annotation'), - 'ClipboardHistory': AttributeBinding('je_auto_control.utils.clipboard_history', 'ClipboardHistory'), - 'default_clipboard_history': - AttributeBinding('je_auto_control.utils.clipboard_history', 'default_clipboard_history'), - 'analyze_heal_log': AttributeBinding('je_auto_control.utils.heal_analytics', 'analyze_heal_log'), - 'heal_stats': AttributeBinding('je_auto_control.utils.heal_analytics', 'heal_stats'), + 'emit_annotations': AttributeBinding(_UTILS_CI_ANNOTATIONS, 'emit_annotations'), + 'format_annotation': AttributeBinding(_UTILS_CI_ANNOTATIONS, 'format_annotation'), + 'ClipboardHistory': AttributeBinding(_UTILS_CLIPBOARD_HISTORY, 'ClipboardHistory'), + 'default_clipboard_history': AttributeBinding(_UTILS_CLIPBOARD_HISTORY, 'default_clipboard_history'), + 'analyze_heal_log': AttributeBinding(_UTILS_HEAL_ANALYTICS, 'analyze_heal_log'), + 'heal_stats': AttributeBinding(_UTILS_HEAL_ANALYTICS, 'heal_stats'), 'scan_secrets': AttributeBinding('je_auto_control.utils.secrets_scan', 'scan_secrets'), - 'describe_step': AttributeBinding('je_auto_control.utils.process_doc', 'describe_step'), - 'generate_sop': AttributeBinding('je_auto_control.utils.process_doc', 'generate_sop'), - 'write_sop': AttributeBinding('je_auto_control.utils.process_doc', 'write_sop'), - 'easing_names': AttributeBinding('je_auto_control.utils.tween_drag', 'easing_names'), - 'tween_drag': AttributeBinding('je_auto_control.utils.tween_drag', 'tween_drag'), - 'tween_points': AttributeBinding('je_auto_control.utils.tween_drag', 'tween_points'), - 'COMMANDS_GROUP': AttributeBinding('je_auto_control.utils.plugin_sdk', 'COMMANDS_GROUP'), - 'discover_plugins': AttributeBinding('je_auto_control.utils.plugin_sdk', 'discover_plugins'), - 'load_plugins': AttributeBinding('je_auto_control.utils.plugin_sdk', 'load_plugins'), - 'ApprovalGate': AttributeBinding('je_auto_control.utils.governance', 'ApprovalGate'), - 'CredentialBroker': AttributeBinding('je_auto_control.utils.governance', 'CredentialBroker'), - 'CredentialBrokerError': AttributeBinding('je_auto_control.utils.governance', 'CredentialBrokerError'), - 'approval_gate': AttributeBinding('je_auto_control.utils.governance', 'approval_gate'), - 'default_broker': AttributeBinding('je_auto_control.utils.governance', 'default_broker'), - 'set_secret_resolver': AttributeBinding('je_auto_control.utils.governance', 'set_secret_resolver'), - 'EgressBlocked': AttributeBinding('je_auto_control.utils.egress', 'EgressBlocked'), - 'EgressPolicy': AttributeBinding('je_auto_control.utils.egress', 'EgressPolicy'), - 'get_egress_policy': AttributeBinding('je_auto_control.utils.egress', 'get_egress_policy'), - 'set_egress_policy': AttributeBinding('je_auto_control.utils.egress', 'set_egress_policy'), - 'ApprovalResult': AttributeBinding('je_auto_control.utils.approval', 'ApprovalResult'), - 'approve_artifact': AttributeBinding('je_auto_control.utils.approval', 'approve_artifact'), - 'pending_artifacts': AttributeBinding('je_auto_control.utils.approval', 'pending_artifacts'), - 'verify_artifact': AttributeBinding('je_auto_control.utils.approval', 'verify_artifact'), + 'describe_step': AttributeBinding(_UTILS_PROCESS_DOC, 'describe_step'), + 'generate_sop': AttributeBinding(_UTILS_PROCESS_DOC, 'generate_sop'), + 'write_sop': AttributeBinding(_UTILS_PROCESS_DOC, 'write_sop'), + 'easing_names': AttributeBinding(_UTILS_TWEEN_DRAG, 'easing_names'), + 'tween_drag': AttributeBinding(_UTILS_TWEEN_DRAG, 'tween_drag'), + 'tween_points': AttributeBinding(_UTILS_TWEEN_DRAG, 'tween_points'), + 'COMMANDS_GROUP': AttributeBinding(_UTILS_PLUGIN_SDK, 'COMMANDS_GROUP'), + 'discover_plugins': AttributeBinding(_UTILS_PLUGIN_SDK, 'discover_plugins'), + 'load_plugins': AttributeBinding(_UTILS_PLUGIN_SDK, 'load_plugins'), + 'ApprovalGate': AttributeBinding(_UTILS_GOVERNANCE, 'ApprovalGate'), + 'CredentialBroker': AttributeBinding(_UTILS_GOVERNANCE, 'CredentialBroker'), + 'CredentialBrokerError': AttributeBinding(_UTILS_GOVERNANCE, 'CredentialBrokerError'), + 'approval_gate': AttributeBinding(_UTILS_GOVERNANCE, 'approval_gate'), + 'default_broker': AttributeBinding(_UTILS_GOVERNANCE, 'default_broker'), + 'set_secret_resolver': AttributeBinding(_UTILS_GOVERNANCE, 'set_secret_resolver'), + 'EgressBlocked': AttributeBinding(_UTILS_EGRESS, 'EgressBlocked'), + 'EgressPolicy': AttributeBinding(_UTILS_EGRESS, 'EgressPolicy'), + 'get_egress_policy': AttributeBinding(_UTILS_EGRESS, 'get_egress_policy'), + 'set_egress_policy': AttributeBinding(_UTILS_EGRESS, 'set_egress_policy'), + 'ApprovalResult': AttributeBinding(_UTILS_APPROVAL, 'ApprovalResult'), + 'approve_artifact': AttributeBinding(_UTILS_APPROVAL, 'approve_artifact'), + 'pending_artifacts': AttributeBinding(_UTILS_APPROVAL, 'pending_artifacts'), + 'verify_artifact': AttributeBinding(_UTILS_APPROVAL, 'verify_artifact'), 'evaluate_trajectory': AttributeBinding('je_auto_control.utils.trajectory_eval', 'evaluate_trajectory'), - 'build_compliance_report': AttributeBinding('je_auto_control.utils.compliance', 'build_compliance_report'), - 'render_compliance_html': AttributeBinding('je_auto_control.utils.compliance', 'render_compliance_html'), - 'write_compliance_report': AttributeBinding('je_auto_control.utils.compliance', 'write_compliance_report'), - 'AgentTrace': AttributeBinding('je_auto_control.utils.agent_trace', 'AgentTrace'), - 'default_trace': AttributeBinding('je_auto_control.utils.agent_trace', 'default_trace'), - 'reset_trace': AttributeBinding('je_auto_control.utils.agent_trace', 'reset_trace'), - 'VideoStep': AttributeBinding('je_auto_control.utils.video_report', 'VideoStep'), - 'build_overlay_plan': AttributeBinding('je_auto_control.utils.video_report', 'build_overlay_plan'), - 'render_overlay_frame': AttributeBinding('je_auto_control.utils.video_report', 'render_overlay_frame'), - 'write_step_video': AttributeBinding('je_auto_control.utils.video_report', 'write_step_video'), - 'fuzzy_best_match': AttributeBinding('je_auto_control.utils.fuzzy', 'fuzzy_best_match'), - 'fuzzy_dedupe': AttributeBinding('je_auto_control.utils.fuzzy', 'fuzzy_dedupe'), - 'fuzzy_matches': AttributeBinding('je_auto_control.utils.fuzzy', 'fuzzy_matches'), - 'fuzzy_ratio': AttributeBinding('je_auto_control.utils.fuzzy', 'fuzzy_ratio'), - 'deaccent': AttributeBinding('je_auto_control.utils.text_normalize', 'deaccent'), - 'fold_whitespace': AttributeBinding('je_auto_control.utils.text_normalize', 'fold_whitespace'), - 'normalize_quotes': AttributeBinding('je_auto_control.utils.text_normalize', 'normalize_quotes'), - 'normalize_text': AttributeBinding('je_auto_control.utils.text_normalize', 'normalize_text'), - 'slugify': AttributeBinding('je_auto_control.utils.text_normalize', 'slugify'), - 'damerau_levenshtein': AttributeBinding('je_auto_control.utils.text_similarity', 'damerau_levenshtein'), - 'dice': AttributeBinding('je_auto_control.utils.text_similarity', 'dice'), - 'jaccard': AttributeBinding('je_auto_control.utils.text_similarity', 'jaccard'), - 'jaro': AttributeBinding('je_auto_control.utils.text_similarity', 'jaro'), - 'jaro_winkler': AttributeBinding('je_auto_control.utils.text_similarity', 'jaro_winkler'), - 'levenshtein': AttributeBinding('je_auto_control.utils.text_similarity', 'levenshtein'), - 'similarity': AttributeBinding('je_auto_control.utils.text_similarity', 'similarity'), - 'minhash_signature': AttributeBinding('je_auto_control.utils.near_dup', 'minhash_signature'), - 'minhash_similarity': AttributeBinding('je_auto_control.utils.near_dup', 'minhash_similarity'), - 'near_duplicates': AttributeBinding('je_auto_control.utils.near_dup', 'near_duplicates'), - 'simhash': AttributeBinding('je_auto_control.utils.near_dup', 'simhash'), - 'S3ArtifactStore': AttributeBinding('je_auto_control.utils.artifact_store', 'S3ArtifactStore'), - 'configure_default_store': AttributeBinding('je_auto_control.utils.artifact_store', 'configure_default_store'), - 'get_default_store': AttributeBinding('je_auto_control.utils.artifact_store', 'get_default_store'), - 'set_default_store': AttributeBinding('je_auto_control.utils.artifact_store', 'set_default_store'), - 'average_hash': AttributeBinding('je_auto_control.utils.image_dedup', 'average_hash'), - 'dedupe_images': AttributeBinding('je_auto_control.utils.image_dedup', 'dedupe_images'), - 'dhash': AttributeBinding('je_auto_control.utils.image_dedup', 'dhash'), - 'hamming_distance': AttributeBinding('je_auto_control.utils.image_dedup', 'hamming_distance'), - 'images_similar': AttributeBinding('je_auto_control.utils.image_dedup', 'images_similar'), - 'build_query': AttributeBinding('je_auto_control.utils.url_canon', 'build_query'), - 'canonicalize_url': AttributeBinding('je_auto_control.utils.url_canon', 'canonicalize_url'), - 'normalize_url': AttributeBinding('je_auto_control.utils.url_canon', 'normalize_url'), - 'parse_query': AttributeBinding('je_auto_control.utils.url_canon', 'parse_query'), - 'urls_equal': AttributeBinding('je_auto_control.utils.url_canon', 'urls_equal'), - 'format_currency': AttributeBinding('je_auto_control.utils.locale_parse', 'format_currency'), - 'format_date': AttributeBinding('je_auto_control.utils.locale_parse', 'format_date'), - 'format_decimal': AttributeBinding('je_auto_control.utils.locale_parse', 'format_decimal'), - 'parse_decimal': AttributeBinding('je_auto_control.utils.locale_parse', 'parse_decimal'), - 'parse_number': AttributeBinding('je_auto_control.utils.locale_parse', 'parse_number'), - 'VoiceCommand': AttributeBinding('je_auto_control.utils.voice', 'VoiceCommand'), - 'VoiceRouter': AttributeBinding('je_auto_control.utils.voice', 'VoiceRouter'), - 'default_voice_router': AttributeBinding('je_auto_control.utils.voice', 'default_voice_router'), - 'CoordinateSpace': AttributeBinding('je_auto_control.utils.coordinate_space', 'CoordinateSpace'), - 'downscale_png': AttributeBinding('je_auto_control.utils.coordinate_space', 'downscale_png'), - 'normalized_space': AttributeBinding('je_auto_control.utils.coordinate_space', 'normalized_space'), - 'xga_space': AttributeBinding('je_auto_control.utils.coordinate_space', 'xga_space'), - 'LoopGuard': AttributeBinding('je_auto_control.utils.loop_guard', 'LoopGuard'), - 'LoopVerdict': AttributeBinding('je_auto_control.utils.loop_guard', 'LoopVerdict'), - 'default_loop_guard': AttributeBinding('je_auto_control.utils.loop_guard', 'default_loop_guard'), - 'digest_result': AttributeBinding('je_auto_control.utils.loop_guard', 'digest_result'), - 'Candidate': AttributeBinding('je_auto_control.utils.process_mining', 'Candidate'), - 'MiningReport': AttributeBinding('je_auto_control.utils.process_mining', 'MiningReport'), - 'SequencePattern': AttributeBinding('je_auto_control.utils.process_mining', 'SequencePattern'), - 'directly_follows': AttributeBinding('je_auto_control.utils.process_mining', 'directly_follows'), - 'find_repeated_sequences': AttributeBinding('je_auto_control.utils.process_mining', 'find_repeated_sequences'), - 'mine_action_log': AttributeBinding('je_auto_control.utils.process_mining', 'mine_action_log'), - 'rank_automation_candidates': - AttributeBinding('je_auto_control.utils.process_mining', 'rank_automation_candidates'), - 'Asset': AttributeBinding('je_auto_control.utils.assets', 'Asset'), - 'AssetStore': AttributeBinding('je_auto_control.utils.assets', 'AssetStore'), - 'AssetValue': AttributeBinding('je_auto_control.utils.assets', 'AssetValue'), - 'active_environment': AttributeBinding('je_auto_control.utils.assets', 'active_environment'), - 'dotenv_values': AttributeBinding('je_auto_control.utils.dotenv', 'dotenv_values'), - 'dump_dotenv': AttributeBinding('je_auto_control.utils.dotenv', 'dump_dotenv'), - 'load_dotenv': AttributeBinding('je_auto_control.utils.dotenv', 'load_dotenv'), - 'parse_dotenv': AttributeBinding('je_auto_control.utils.dotenv', 'parse_dotenv'), - 'LayeredConfig': AttributeBinding('je_auto_control.utils.layered_config', 'LayeredConfig'), - 'SourceTrace': AttributeBinding('je_auto_control.utils.layered_config', 'SourceTrace'), - 'deep_merge': AttributeBinding('je_auto_control.utils.layered_config', 'deep_merge'), - 'ConfigField': AttributeBinding('je_auto_control.utils.config_schema', 'ConfigField'), - 'ConfigSchema': AttributeBinding('je_auto_control.utils.config_schema', 'ConfigSchema'), - 'coerce': AttributeBinding('je_auto_control.utils.config_schema', 'coerce'), - 'validate_config': AttributeBinding('je_auto_control.utils.config_schema', 'validate_config'), - 'PathNotAllowedError': AttributeBinding('je_auto_control.utils.path_guard', 'PathNotAllowedError'), - 'PathPolicy': AttributeBinding('je_auto_control.utils.path_guard', 'PathPolicy'), - 'RefResolver': AttributeBinding('je_auto_control.utils.secret_ref', 'RefResolver'), - 'SecretRefError': AttributeBinding('je_auto_control.utils.secret_ref', 'SecretRefError'), - 'is_ref': AttributeBinding('je_auto_control.utils.secret_ref', 'is_ref'), - 'resolve_ref': AttributeBinding('je_auto_control.utils.secret_ref', 'resolve_ref'), - 'resolve_refs_in': AttributeBinding('je_auto_control.utils.secret_ref', 'resolve_refs_in'), - 'redact_config': AttributeBinding('je_auto_control.utils.config_redaction', 'redact_config'), - 'redact_secret_text': AttributeBinding('je_auto_control.utils.config_redaction', 'redact_secret_text'), - 'EventEmitter': AttributeBinding('je_auto_control.utils.events', 'EventEmitter'), - 'post_cloudevent': AttributeBinding('je_auto_control.utils.events', 'post_cloudevent'), - 'to_cloudevent': AttributeBinding('je_auto_control.utils.events', 'to_cloudevent'), - 'WebhookChannel': AttributeBinding('je_auto_control.utils.notify_channels', 'WebhookChannel'), - 'WebhookResult': AttributeBinding('je_auto_control.utils.notify_channels', 'WebhookResult'), - 'notify_webhook': AttributeBinding('je_auto_control.utils.notify_channels', 'notify_webhook'), - 'set_default_poster': AttributeBinding('je_auto_control.utils.notify_channels', 'set_default_poster'), - 'json_extract': AttributeBinding('je_auto_control.utils.jsonpath', 'json_extract'), - 'json_query': AttributeBinding('je_auto_control.utils.jsonpath', 'json_query'), - 'json_query_one': AttributeBinding('je_auto_control.utils.jsonpath', 'json_query_one'), - 'Saga': AttributeBinding('je_auto_control.utils.saga', 'Saga'), - 'SagaResult': AttributeBinding('je_auto_control.utils.saga', 'SagaResult'), - 'run_saga': AttributeBinding('je_auto_control.utils.saga', 'run_saga'), - 'DecisionTable': AttributeBinding('je_auto_control.utils.decision_table', 'DecisionTable'), - 'Rule': AttributeBinding('je_auto_control.utils.decision_table', 'Rule'), - 'evaluate_table': AttributeBinding('je_auto_control.utils.decision_table', 'evaluate_table'), - 'RepairStore': AttributeBinding('je_auto_control.utils.locator_repair', 'RepairStore'), - 'RepairSuggestion': AttributeBinding('je_auto_control.utils.locator_repair', 'RepairSuggestion'), - 'repair_from_heal': AttributeBinding('je_auto_control.utils.locator_repair', 'repair_from_heal'), - 'PIIFinding': AttributeBinding('je_auto_control.utils.pii_text', 'PIIFinding'), - 'detect_pii': AttributeBinding('je_auto_control.utils.pii_text', 'detect_pii'), - 'redact_pii_text': AttributeBinding('je_auto_control.utils.pii_text', 'redact_pii_text'), - 'from_audit_findings': AttributeBinding('je_auto_control.utils.sarif', 'from_audit_findings'), - 'from_lint_issues': AttributeBinding('je_auto_control.utils.sarif', 'from_lint_issues'), - 'make_finding': AttributeBinding('je_auto_control.utils.sarif', 'make_finding'), - 'to_sarif': AttributeBinding('je_auto_control.utils.sarif', 'to_sarif'), - 'write_sarif': AttributeBinding('je_auto_control.utils.sarif', 'write_sarif'), - 'SchemaValidationResult': AttributeBinding('je_auto_control.utils.json_schema', 'SchemaValidationResult'), - 'assert_schema': AttributeBinding('je_auto_control.utils.json_schema', 'assert_schema'), - 'is_valid': AttributeBinding('je_auto_control.utils.json_schema', 'is_valid'), - 'validate_json': AttributeBinding('je_auto_control.utils.json_schema', 'validate_json'), - 'findings_to_sarif': AttributeBinding('je_auto_control.utils.vuln_scan', 'findings_to_sarif'), - 'is_affected': AttributeBinding('je_auto_control.utils.vuln_scan', 'is_affected'), - 'match_package': AttributeBinding('je_auto_control.utils.vuln_scan', 'match_package'), - 'scan_components': AttributeBinding('je_auto_control.utils.vuln_scan', 'scan_components'), - 'version_key': AttributeBinding('je_auto_control.utils.vuln_scan', 'version_key'), - 'VEX_JUSTIFICATIONS': AttributeBinding('je_auto_control.utils.vex', 'VEX_JUSTIFICATIONS'), - 'VEX_STATUSES': AttributeBinding('je_auto_control.utils.vex', 'VEX_STATUSES'), - 'apply_vex': AttributeBinding('je_auto_control.utils.vex', 'apply_vex'), - 'build_vex': AttributeBinding('je_auto_control.utils.vex', 'build_vex'), - 'vex_statement': AttributeBinding('je_auto_control.utils.vex', 'vex_statement'), - 'DEFAULT_COPYLEFT': AttributeBinding('je_auto_control.utils.license_policy', 'DEFAULT_COPYLEFT'), - 'evaluate_license': AttributeBinding('je_auto_control.utils.license_policy', 'evaluate_license'), - 'evaluate_sbom': AttributeBinding('je_auto_control.utils.license_policy', 'evaluate_sbom'), - 'license_findings_to_sarif': AttributeBinding('je_auto_control.utils.license_policy', 'license_findings_to_sarif'), - 'normalize_spdx': AttributeBinding('je_auto_control.utils.license_policy', 'normalize_spdx'), - 'ClaimsPolicy': AttributeBinding('je_auto_control.utils.jwt', 'ClaimsPolicy'), - 'ExpiredTokenError': AttributeBinding('je_auto_control.utils.jwt', 'ExpiredTokenError'), - 'InvalidSignatureError': AttributeBinding('je_auto_control.utils.jwt', 'InvalidSignatureError'), - 'JwtError': AttributeBinding('je_auto_control.utils.jwt', 'JwtError'), - 'decode_jwt': AttributeBinding('je_auto_control.utils.jwt', 'decode_jwt'), - 'encode_jwt': AttributeBinding('je_auto_control.utils.jwt', 'encode_jwt'), - 'SlidingWindowLimiter': AttributeBinding('je_auto_control.utils.rate_limit', 'SlidingWindowLimiter'), - 'TokenBucket': AttributeBinding('je_auto_control.utils.rate_limit', 'TokenBucket'), - 'throttle': AttributeBinding('je_auto_control.utils.rate_limit', 'throttle'), - 'PatchError': AttributeBinding('je_auto_control.utils.json_patch', 'PatchError'), - 'PatchTestFailed': AttributeBinding('je_auto_control.utils.json_patch', 'PatchTestFailed'), - 'apply_patch': AttributeBinding('je_auto_control.utils.json_patch', 'apply_patch'), - 'make_merge_patch': AttributeBinding('je_auto_control.utils.json_patch', 'make_merge_patch'), - 'make_patch': AttributeBinding('je_auto_control.utils.json_patch', 'make_patch'), - 'merge_patch': AttributeBinding('je_auto_control.utils.json_patch', 'merge_patch'), - 'remove_pointer': AttributeBinding('je_auto_control.utils.json_patch', 'remove_pointer'), - 'resolve_pointer': AttributeBinding('je_auto_control.utils.json_patch', 'resolve_pointer'), - 'set_pointer': AttributeBinding('je_auto_control.utils.json_patch', 'set_pointer'), - 'SearchHit': AttributeBinding('je_auto_control.utils.search_index', 'SearchHit'), - 'SearchIndex': AttributeBinding('je_auto_control.utils.search_index', 'SearchIndex'), - 'search_documents': AttributeBinding('je_auto_control.utils.search_index', 'search_documents'), - 'tokenize': AttributeBinding('je_auto_control.utils.search_index', 'tokenize'), - 'chi_square_2x2': AttributeBinding('je_auto_control.utils.stats', 'chi_square_2x2'), - 'cohens_d': AttributeBinding('je_auto_control.utils.stats', 'cohens_d'), - 'describe': AttributeBinding('je_auto_control.utils.stats', 'describe'), - 'normal_cdf': AttributeBinding('je_auto_control.utils.stats', 'normal_cdf'), - 'percentile': AttributeBinding('je_auto_control.utils.stats', 'percentile'), - 'two_proportion_z_test': AttributeBinding('je_auto_control.utils.stats', 'two_proportion_z_test'), - 'welch_t_test': AttributeBinding('je_auto_control.utils.stats', 'welch_t_test'), - 'Recurrence': AttributeBinding('je_auto_control.utils.recurrence', 'Recurrence'), - 'next_occurrence': AttributeBinding('je_auto_control.utils.recurrence', 'next_occurrence'), - 'occurrences': AttributeBinding('je_auto_control.utils.recurrence', 'occurrences'), - 'parse_rrule': AttributeBinding('je_auto_control.utils.recurrence', 'parse_rrule'), - 'MergeResult': AttributeBinding('je_auto_control.utils.text_diff', 'MergeResult'), - 'PatchApplyError': AttributeBinding('je_auto_control.utils.text_diff', 'PatchApplyError'), - 'apply_unified': AttributeBinding('je_auto_control.utils.text_diff', 'apply_unified'), - 'three_way_merge': AttributeBinding('je_auto_control.utils.text_diff', 'three_way_merge'), - 'unified_diff': AttributeBinding('je_auto_control.utils.text_diff', 'unified_diff'), - 'Flag': AttributeBinding('je_auto_control.utils.feature_flags', 'Flag'), - 'FlagStore': AttributeBinding('je_auto_control.utils.feature_flags', 'FlagStore'), - 'assign_variant': AttributeBinding('je_auto_control.utils.feature_flags', 'assign_variant'), - 'evaluate_flag': AttributeBinding('je_auto_control.utils.feature_flags', 'evaluate_flag'), - 'is_enabled': AttributeBinding('je_auto_control.utils.feature_flags', 'is_enabled'), - 'percentage_bucket': AttributeBinding('je_auto_control.utils.feature_flags', 'percentage_bucket'), - 'build_provenance': AttributeBinding('je_auto_control.utils.provenance', 'build_provenance'), - 'subject_for': AttributeBinding('je_auto_control.utils.provenance', 'subject_for'), - 'subject_for_bytes': AttributeBinding('je_auto_control.utils.provenance', 'subject_for_bytes'), - 'verify_provenance': AttributeBinding('je_auto_control.utils.provenance', 'verify_provenance'), - 'write_provenance': AttributeBinding('je_auto_control.utils.provenance', 'write_provenance'), - 'MatchReport': AttributeBinding('je_auto_control.utils.json_contract', 'MatchReport'), - 'diff_json': AttributeBinding('je_auto_control.utils.json_contract', 'diff_json'), - 'match_json': AttributeBinding('je_auto_control.utils.json_contract', 'match_json'), - 'normalize_json': AttributeBinding('je_auto_control.utils.json_contract', 'normalize_json'), - 'snapshot_json': AttributeBinding('je_auto_control.utils.json_contract', 'snapshot_json'), - 'ChaosExperiment': AttributeBinding('je_auto_control.utils.chaos', 'ChaosExperiment'), - 'Fault': AttributeBinding('je_auto_control.utils.chaos', 'Fault'), - 'Probe': AttributeBinding('je_auto_control.utils.chaos', 'Probe'), - 'exception_fault': AttributeBinding('je_auto_control.utils.chaos', 'exception_fault'), - 'latency_fault': AttributeBinding('je_auto_control.utils.chaos', 'latency_fault'), - 'run_experiment': AttributeBinding('je_auto_control.utils.chaos', 'run_experiment'), - 'BurnRule': AttributeBinding('je_auto_control.utils.slo', 'BurnRule'), - 'burn_alerts': AttributeBinding('je_auto_control.utils.slo', 'burn_alerts'), - 'burn_rate': AttributeBinding('je_auto_control.utils.slo', 'burn_rate'), - 'default_burn_rules': AttributeBinding('je_auto_control.utils.slo', 'default_burn_rules'), - 'evaluate_slo': AttributeBinding('je_auto_control.utils.slo', 'evaluate_slo'), - 'LatencyDigest': AttributeBinding('je_auto_control.utils.percentiles', 'LatencyDigest'), - 'exact_percentiles': AttributeBinding('je_auto_control.utils.percentiles', 'exact_percentiles'), - 'ts_delta': AttributeBinding('je_auto_control.utils.timeseries', 'ts_delta'), - 'ts_downsample': AttributeBinding('je_auto_control.utils.timeseries', 'ts_downsample'), - 'ts_idelta': AttributeBinding('je_auto_control.utils.timeseries', 'ts_idelta'), - 'ts_increase': AttributeBinding('je_auto_control.utils.timeseries', 'ts_increase'), - 'ts_irate': AttributeBinding('je_auto_control.utils.timeseries', 'ts_irate'), - 'ts_rate': AttributeBinding('je_auto_control.utils.timeseries', 'ts_rate'), - 'ts_resample': AttributeBinding('je_auto_control.utils.timeseries', 'ts_resample'), - 'detect_anomalies': AttributeBinding('je_auto_control.utils.anomaly', 'detect_anomalies'), - 'ewma_control': AttributeBinding('je_auto_control.utils.anomaly', 'ewma_control'), - 'mad_anomalies': AttributeBinding('je_auto_control.utils.anomaly', 'mad_anomalies'), - 'mad_scores': AttributeBinding('je_auto_control.utils.anomaly', 'mad_scores'), - 'zscore_anomalies': AttributeBinding('je_auto_control.utils.anomaly', 'zscore_anomalies'), - 'zscore_scores': AttributeBinding('je_auto_control.utils.anomaly', 'zscore_scores'), - 'ewma': AttributeBinding('je_auto_control.utils.smoothing', 'ewma'), - 'rolling': AttributeBinding('je_auto_control.utils.smoothing', 'rolling'), - 'sma': AttributeBinding('je_auto_control.utils.smoothing', 'sma'), - 'wma': AttributeBinding('je_auto_control.utils.smoothing', 'wma'), - 'Bulkhead': AttributeBinding('je_auto_control.utils.bulkhead', 'Bulkhead'), - 'BulkheadFullError': AttributeBinding('je_auto_control.utils.bulkhead', 'BulkheadFullError'), - 'next_delay': AttributeBinding('je_auto_control.utils.bulkhead', 'next_delay'), - 'parse_ratelimit': AttributeBinding('je_auto_control.utils.bulkhead', 'parse_ratelimit'), - 'parse_retry_after': AttributeBinding('je_auto_control.utils.bulkhead', 'parse_retry_after'), - 'Cassette': AttributeBinding('je_auto_control.utils.http_cassette', 'Cassette'), - 'CassetteMissError': AttributeBinding('je_auto_control.utils.http_cassette', 'CassetteMissError'), - 'HttpProblemError': AttributeBinding('je_auto_control.utils.http_problem', 'HttpProblemError'), - 'ProblemDetails': AttributeBinding('je_auto_control.utils.http_problem', 'ProblemDetails'), - 'is_problem': AttributeBinding('je_auto_control.utils.http_problem', 'is_problem'), - 'parse_problem': AttributeBinding('je_auto_control.utils.http_problem', 'parse_problem'), - 'raise_for_problem': AttributeBinding('je_auto_control.utils.http_problem', 'raise_for_problem'), - 'SSEEvent': AttributeBinding('je_auto_control.utils.sse_client', 'SSEEvent'), - 'SSEParser': AttributeBinding('je_auto_control.utils.sse_client', 'SSEParser'), - 'parse_event_stream': AttributeBinding('je_auto_control.utils.sse_client', 'parse_event_stream'), - 'Link': AttributeBinding('je_auto_control.utils.link_header', 'Link'), - 'links_by_rel': AttributeBinding('je_auto_control.utils.link_header', 'links_by_rel'), - 'next_url': AttributeBinding('je_auto_control.utils.link_header', 'next_url'), - 'paginate': AttributeBinding('je_auto_control.utils.link_header', 'paginate'), - 'parse_link_header': AttributeBinding('je_auto_control.utils.link_header', 'parse_link_header'), - 'MultipartFile': AttributeBinding('je_auto_control.utils.multipart', 'MultipartFile'), - 'build_multipart': AttributeBinding('je_auto_control.utils.multipart', 'build_multipart'), - 'new_boundary': AttributeBinding('je_auto_control.utils.multipart', 'new_boundary'), - 'parse_multipart': AttributeBinding('je_auto_control.utils.multipart', 'parse_multipart'), - 'build_accept': AttributeBinding('je_auto_control.utils.http_content', 'build_accept'), - 'build_accept_encoding': AttributeBinding('je_auto_control.utils.http_content', 'build_accept_encoding'), - 'decode_body': AttributeBinding('je_auto_control.utils.http_content', 'decode_body'), - 'negotiated_call': AttributeBinding('je_auto_control.utils.http_content', 'negotiated_call'), - 'parse_quality_values': AttributeBinding('je_auto_control.utils.http_content', 'parse_quality_values'), - 'CookieJar': AttributeBinding('je_auto_control.utils.cookie_jar', 'CookieJar'), - 'parse_set_cookie': AttributeBinding('je_auto_control.utils.cookie_jar', 'parse_set_cookie'), - 'conditioned_call': AttributeBinding('je_auto_control.utils.http_conditional', 'conditioned_call'), - 'is_fresh': AttributeBinding('je_auto_control.utils.http_conditional', 'is_fresh'), - 'is_not_modified': AttributeBinding('je_auto_control.utils.http_conditional', 'is_not_modified'), - 'parse_cache_control': AttributeBinding('je_auto_control.utils.http_conditional', 'parse_cache_control'), - 'store_validators': AttributeBinding('je_auto_control.utils.http_conditional', 'store_validators'), - 'SpanContext': AttributeBinding('je_auto_control.utils.trace_context', 'SpanContext'), - 'TraceContextError': AttributeBinding('je_auto_control.utils.trace_context', 'TraceContextError'), - 'child_context': AttributeBinding('je_auto_control.utils.trace_context', 'child_context'), - 'extract_context': AttributeBinding('je_auto_control.utils.trace_context', 'extract_context'), - 'format_traceparent': AttributeBinding('je_auto_control.utils.trace_context', 'format_traceparent'), - 'format_tracestate': AttributeBinding('je_auto_control.utils.trace_context', 'format_tracestate'), - 'inject_context': AttributeBinding('je_auto_control.utils.trace_context', 'inject_context'), - 'new_root_context': AttributeBinding('je_auto_control.utils.trace_context', 'new_root_context'), - 'new_span_id': AttributeBinding('je_auto_control.utils.trace_context', 'new_span_id'), - 'new_trace_id': AttributeBinding('je_auto_control.utils.trace_context', 'new_trace_id'), - 'parse_traceparent': AttributeBinding('je_auto_control.utils.trace_context', 'parse_traceparent'), - 'parse_tracestate': AttributeBinding('je_auto_control.utils.trace_context', 'parse_tracestate'), - 'Baggage': AttributeBinding('je_auto_control.utils.baggage', 'Baggage'), - 'extract_baggage': AttributeBinding('je_auto_control.utils.baggage', 'extract_baggage'), - 'format_baggage': AttributeBinding('je_auto_control.utils.baggage', 'format_baggage'), - 'inject_baggage': AttributeBinding('je_auto_control.utils.baggage', 'inject_baggage'), - 'parse_baggage': AttributeBinding('je_auto_control.utils.baggage', 'parse_baggage'), - 'CanonicalLogLine': AttributeBinding('je_auto_control.utils.canonical_log', 'CanonicalLogLine'), - 'JSONLogFormatter': AttributeBinding('je_auto_control.utils.canonical_log', 'JSONLogFormatter'), - 'bind_trace_context': AttributeBinding('je_auto_control.utils.canonical_log', 'bind_trace_context'), - 'attributes_to_otlp': AttributeBinding('je_auto_control.utils.otlp_export', 'attributes_to_otlp'), - 'spans_to_otlp': AttributeBinding('je_auto_control.utils.otlp_export', 'spans_to_otlp'), - 'write_otlp': AttributeBinding('je_auto_control.utils.otlp_export', 'write_otlp'), - 'ToolView': AttributeBinding('je_auto_control.utils.mcp_server.disclosure', 'ToolView'), - 'ToolPage': AttributeBinding('je_auto_control.utils.mcp_server.disclosure', 'ToolPage'), - 'plan_mcp_tool_profile': AttributeBinding('je_auto_control.utils.mcp_server.disclosure', 'plan_mcp_tool_profile'), - 'ToolDiscoveryError': AttributeBinding('je_auto_control.utils.mcp_server.discovery', 'ToolDiscoveryError'), - 'ToolIndex': AttributeBinding('je_auto_control.utils.mcp_server.discovery', 'ToolIndex'), - 'ToolSummary': AttributeBinding('je_auto_control.utils.mcp_server.discovery', 'ToolSummary'), - 'search_mcp_tools': AttributeBinding('je_auto_control.utils.mcp_server.discovery', 'search_mcp_tools'), - 'get_mcp_tool_schema': AttributeBinding('je_auto_control.utils.mcp_server.discovery', 'get_mcp_tool_schema'), - 'AuditLogger': AttributeBinding('je_auto_control.utils.mcp_server', 'AuditLogger'), - 'HttpMCPServer': AttributeBinding('je_auto_control.utils.mcp_server', 'HttpMCPServer'), - 'MCPContent': AttributeBinding('je_auto_control.utils.mcp_server', 'MCPContent'), - 'MCPPrompt': AttributeBinding('je_auto_control.utils.mcp_server', 'MCPPrompt'), - 'MCPPromptArgument': AttributeBinding('je_auto_control.utils.mcp_server', 'MCPPromptArgument'), - 'MCPResource': AttributeBinding('je_auto_control.utils.mcp_server', 'MCPResource'), - 'MCPServer': AttributeBinding('je_auto_control.utils.mcp_server', 'MCPServer'), - 'MCPTool': AttributeBinding('je_auto_control.utils.mcp_server', 'MCPTool'), - 'MCPToolAnnotations': AttributeBinding('je_auto_control.utils.mcp_server', 'MCPToolAnnotations'), - 'MCPToolResult': AttributeBinding('je_auto_control.utils.mcp_server', 'MCPToolResult'), - 'OperationCancelledError': AttributeBinding('je_auto_control.utils.mcp_server', 'OperationCancelledError'), - 'PromptProvider': AttributeBinding('je_auto_control.utils.mcp_server', 'PromptProvider'), - 'RateLimiter': AttributeBinding('je_auto_control.utils.mcp_server', 'RateLimiter'), - 'ResourceProvider': AttributeBinding('je_auto_control.utils.mcp_server', 'ResourceProvider'), - 'ToolCallContext': AttributeBinding('je_auto_control.utils.mcp_server', 'ToolCallContext'), - 'build_default_tool_registry': AttributeBinding('je_auto_control.utils.mcp_server', 'build_default_tool_registry'), - 'default_prompt_provider': AttributeBinding('je_auto_control.utils.mcp_server', 'default_prompt_provider'), - 'default_resource_provider': AttributeBinding('je_auto_control.utils.mcp_server', 'default_resource_provider'), - 'make_plugin_tool': AttributeBinding('je_auto_control.utils.mcp_server', 'make_plugin_tool'), - 'register_plugin_tools': AttributeBinding('je_auto_control.utils.mcp_server', 'register_plugin_tools'), - 'start_mcp_http_server': AttributeBinding('je_auto_control.utils.mcp_server', 'start_mcp_http_server'), - 'start_mcp_stdio_server': AttributeBinding('je_auto_control.utils.mcp_server', 'start_mcp_stdio_server'), - 'load_plugin_file': AttributeBinding('je_auto_control.utils.plugin_loader.plugin_loader', 'load_plugin_file'), - 'load_plugin_directory': - AttributeBinding('je_auto_control.utils.plugin_loader.plugin_loader', 'load_plugin_directory'), - 'discover_plugin_commands': - AttributeBinding('je_auto_control.utils.plugin_loader.plugin_loader', 'discover_plugin_commands'), - 'register_plugin_commands': - AttributeBinding('je_auto_control.utils.plugin_loader.plugin_loader', 'register_plugin_commands'), - 'RestApiServer': AttributeBinding('je_auto_control.utils.rest_api.rest_server', 'RestApiServer'), - 'start_rest_api_server': AttributeBinding('je_auto_control.utils.rest_api.rest_server', 'start_rest_api_server'), - 'AuthorizationContext': AttributeBinding('je_auto_control.utils.rbac', 'AuthorizationContext'), - 'AuthorizationError': AttributeBinding('je_auto_control.utils.rbac', 'AuthorizationError'), - 'UserAuthError': AttributeBinding('je_auto_control.utils.rbac', 'UserAuthError'), - 'UserRecord': AttributeBinding('je_auto_control.utils.rbac', 'UserRecord'), - 'UserStore': AttributeBinding('je_auto_control.utils.rbac', 'UserStore'), - 'authorization_scope': AttributeBinding('je_auto_control.utils.rbac', 'authorization_scope'), - 'AdminConsoleClient': AttributeBinding('je_auto_control.utils.admin', 'AdminConsoleClient'), - 'AdminHost': AttributeBinding('je_auto_control.utils.admin', 'AdminHost'), - 'default_admin_console': AttributeBinding('je_auto_control.utils.admin', 'default_admin_console'), - 'WebRTCInspector': AttributeBinding('je_auto_control.utils.remote_desktop.webrtc_inspector', 'WebRTCInspector'), - 'default_webrtc_inspector': - AttributeBinding('je_auto_control.utils.remote_desktop.webrtc_inspector', 'default_webrtc_inspector'), - 'UsbDevice': AttributeBinding('je_auto_control.utils.usb', 'UsbDevice'), - 'UsbEnumerationResult': AttributeBinding('je_auto_control.utils.usb', 'UsbEnumerationResult'), - 'list_usb_devices': AttributeBinding('je_auto_control.utils.usb', 'list_usb_devices'), - 'UsbEvent': AttributeBinding('je_auto_control.utils.usb', 'UsbEvent'), - 'UsbHotplugWatcher': AttributeBinding('je_auto_control.utils.usb', 'UsbHotplugWatcher'), - 'default_usb_watcher': AttributeBinding('je_auto_control.utils.usb', 'default_usb_watcher'), - 'UsbPassthroughSession': AttributeBinding('je_auto_control.utils.usb', 'UsbPassthroughSession'), - 'UsbPassthroughClient': AttributeBinding('je_auto_control.utils.usb', 'UsbPassthroughClient'), - 'UsbAcl': AttributeBinding('je_auto_control.utils.usb', 'UsbAcl'), - 'enable_usb_passthrough': AttributeBinding('je_auto_control.utils.usb', 'enable_usb_passthrough'), - 'is_usb_passthrough_enabled': AttributeBinding('je_auto_control.utils.usb', 'is_usb_passthrough_enabled'), - 'BackendContext': AttributeBinding('je_auto_control.wrapper.capabilities', 'BackendContext'), - 'CapabilitySnapshot': AttributeBinding('je_auto_control.wrapper.capabilities', 'CapabilitySnapshot'), - 'CapabilityStatus': AttributeBinding('je_auto_control.wrapper.capabilities', 'CapabilityStatus'), - 'probe_capabilities': AttributeBinding('je_auto_control.wrapper.capabilities', 'probe_capabilities'), - 'CancellationToken': AttributeBinding('je_auto_control.utils.exception.cancellation', 'CancellationToken'), - 'TaskProgress': AttributeBinding('je_auto_control.utils.exception.cancellation', 'TaskProgress'), - 'cancellation_scope': AttributeBinding('je_auto_control.utils.exception.cancellation', 'cancellation_scope'), - 'Check': AttributeBinding('je_auto_control.utils.diagnostics', 'Check'), - 'DiagnosticsReport': AttributeBinding('je_auto_control.utils.diagnostics', 'DiagnosticsReport'), - 'run_diagnostics': AttributeBinding('je_auto_control.utils.diagnostics', 'run_diagnostics'), - 'ConfigBundleExporter': AttributeBinding('je_auto_control.utils.config_bundle', 'ConfigBundleExporter'), - 'ConfigBundleImporter': AttributeBinding('je_auto_control.utils.config_bundle', 'ConfigBundleImporter'), - 'ImportReport': AttributeBinding('je_auto_control.utils.config_bundle', 'ImportReport'), - 'export_config_bundle': AttributeBinding('je_auto_control.utils.config_bundle', 'export_config_bundle'), - 'import_config_bundle': AttributeBinding('je_auto_control.utils.config_bundle', 'import_config_bundle'), - 'TriggerEngine': AttributeBinding('je_auto_control.utils.triggers.trigger_engine', 'TriggerEngine'), - 'default_trigger_engine': - AttributeBinding('je_auto_control.utils.triggers.trigger_engine', 'default_trigger_engine'), - 'ImageAppearsTrigger': AttributeBinding('je_auto_control.utils.triggers.trigger_engine', 'ImageAppearsTrigger'), - 'WindowAppearsTrigger': AttributeBinding('je_auto_control.utils.triggers.trigger_engine', 'WindowAppearsTrigger'), - 'PixelColorTrigger': AttributeBinding('je_auto_control.utils.triggers.trigger_engine', 'PixelColorTrigger'), - 'FilePathTrigger': AttributeBinding('je_auto_control.utils.triggers.trigger_engine', 'FilePathTrigger'), - 'AllOfTrigger': AttributeBinding('je_auto_control.utils.triggers.trigger_engine', 'AllOfTrigger'), - 'AnyOfTrigger': AttributeBinding('je_auto_control.utils.triggers.trigger_engine', 'AnyOfTrigger'), - 'SequenceTrigger': AttributeBinding('je_auto_control.utils.triggers.trigger_engine', 'SequenceTrigger'), - 'CronTrigger': AttributeBinding('je_auto_control.utils.triggers.trigger_engine', 'CronTrigger'), - 'WebhookTrigger': AttributeBinding('je_auto_control.utils.triggers.webhook_server', 'WebhookTrigger'), - 'WebhookTriggerServer': AttributeBinding('je_auto_control.utils.triggers.webhook_server', 'WebhookTriggerServer'), - 'default_webhook_server': - AttributeBinding('je_auto_control.utils.triggers.webhook_server', 'default_webhook_server'), - 'EmailTrigger': AttributeBinding('je_auto_control.utils.triggers.email_trigger', 'EmailTrigger'), - 'EmailTriggerWatcher': AttributeBinding('je_auto_control.utils.triggers.email_trigger', 'EmailTriggerWatcher'), - 'default_email_trigger_watcher': - AttributeBinding('je_auto_control.utils.triggers.email_trigger', 'default_email_trigger_watcher'), - 'ActionProfiler': AttributeBinding('je_auto_control.utils.profiler', 'ActionProfiler'), - 'ActionStats': AttributeBinding('je_auto_control.utils.profiler', 'ActionStats'), - 'default_profiler': AttributeBinding('je_auto_control.utils.profiler', 'default_profiler'), - 'SecretManager': AttributeBinding('je_auto_control.utils.secrets', 'SecretManager'), - 'SecretStoreError': AttributeBinding('je_auto_control.utils.secrets', 'SecretStoreError'), - 'SecretStoreLocked': AttributeBinding('je_auto_control.utils.secrets', 'SecretStoreLocked'), - 'default_secret_manager': AttributeBinding('je_auto_control.utils.secrets', 'default_secret_manager'), - 'default_secret_store_path': AttributeBinding('je_auto_control.utils.secrets', 'default_secret_store_path'), - 'VerifyResult': AttributeBinding('je_auto_control.utils.action_signing', 'VerifyResult'), - 'sign_action_file': AttributeBinding('je_auto_control.utils.action_signing', 'sign_action_file'), - 'verify_action_file': AttributeBinding('je_auto_control.utils.action_signing', 'verify_action_file'), - 'SigningConfig': AttributeBinding('je_auto_control.utils.action_signing', 'SigningConfig'), - 'action_signing_config': AttributeBinding('je_auto_control.utils.action_signing', 'action_signing_config'), - 'create_signing_keypair': AttributeBinding('je_auto_control.utils.action_signing', 'create_signing_keypair'), - 'CryptographyUnavailableError': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'CryptographyUnavailableError'), - 'require_signed_actions': AttributeBinding('je_auto_control.utils.action_signing', 'require_signed_actions'), - 'encrypt_action_file': AttributeBinding('je_auto_control.utils.action_signing', 'encrypt_action_file'), - 'decrypt_action_file': AttributeBinding('je_auto_control.utils.action_signing', 'decrypt_action_file'), - 'MetricCounter': AttributeBinding('je_auto_control.utils.observability', 'Counter'), - 'MetricGauge': AttributeBinding('je_auto_control.utils.observability', 'Gauge'), - 'MetricHistogram': AttributeBinding('je_auto_control.utils.observability', 'Histogram'), - 'MetricRegistry': AttributeBinding('je_auto_control.utils.observability', 'MetricRegistry'), - 'default_metric_registry': AttributeBinding('je_auto_control.utils.observability', 'default_registry'), - 'PrometheusExporter': AttributeBinding('je_auto_control.utils.observability', 'PrometheusExporter'), - 'default_metrics_exporter': AttributeBinding('je_auto_control.utils.observability', 'default_exporter'), - 'render_metrics_text': AttributeBinding('je_auto_control.utils.observability', 'render_metrics_text'), - 'Tracer': AttributeBinding('je_auto_control.utils.observability', 'Tracer'), - 'default_tracer': AttributeBinding('je_auto_control.utils.observability', 'default_tracer'), - 'traced': AttributeBinding('je_auto_control.utils.observability', 'traced'), - 'HistoryStore': AttributeBinding('je_auto_control.utils.run_history.history_store', 'HistoryStore'), - 'JournalLink': AttributeBinding('je_auto_control.utils.run_history.history_store', 'JournalLink'), - 'RunRecord': AttributeBinding('je_auto_control.utils.run_history.history_store', 'RunRecord'), - 'default_history_store': - AttributeBinding('je_auto_control.utils.run_history.history_store', 'default_history_store'), - 'HistoryBinding': AttributeBinding('je_auto_control.utils.run_history', 'HistoryBinding'), - 'manual_history_scope': AttributeBinding('je_auto_control.utils.run_history', 'manual_history_scope'), - 'ActionEvent': AttributeBinding('je_auto_control.utils.action_journal', 'ActionEvent'), - 'ActionJournal': AttributeBinding('je_auto_control.utils.action_journal', 'ActionJournal'), - 'ActionJournalError': AttributeBinding('je_auto_control.utils.action_journal', 'ActionJournalError'), - 'JournalBinding': AttributeBinding('je_auto_control.utils.action_journal', 'JournalBinding'), - 'JournalRunOptions': AttributeBinding('je_auto_control.utils.action_journal', 'JournalRunOptions'), - 'JournalRunResult': AttributeBinding('je_auto_control.utils.action_journal', 'JournalRunResult'), - 'execute_action_with_journal': - AttributeBinding('je_auto_control.utils.action_journal', 'execute_action_with_journal'), - 'execute_journal_run': AttributeBinding('je_auto_control.utils.action_journal', 'execute_journal_run'), - 'inspect_journal': AttributeBinding('je_auto_control.utils.action_journal', 'inspect_journal'), - 'journal_scope': AttributeBinding('je_auto_control.utils.action_journal', 'journal_scope'), - 'read_events': AttributeBinding('je_auto_control.utils.action_journal', 'read_events'), - 'AccessibilityElement': AttributeBinding('je_auto_control.utils.accessibility', 'AccessibilityElement'), - 'AccessibilityNotAvailableError': - AttributeBinding('je_auto_control.utils.accessibility', 'AccessibilityNotAvailableError'), - 'accessibility_status': AttributeBinding('je_auto_control.utils.accessibility', 'accessibility_status'), - 'AccessibilityRecorder': AttributeBinding('je_auto_control.utils.accessibility', 'AccessibilityRecorder'), - 'AXRecorderEvent': AttributeBinding('je_auto_control.utils.accessibility', 'AXRecorderEvent'), - 'AXTreeNode': AttributeBinding('je_auto_control.utils.accessibility', 'AXTreeNode'), - 'click_accessibility_element': - AttributeBinding('je_auto_control.utils.accessibility', 'click_accessibility_element'), - 'dump_accessibility_tree': AttributeBinding('je_auto_control.utils.accessibility', 'dump_accessibility_tree'), - 'find_accessibility_element': AttributeBinding('je_auto_control.utils.accessibility', 'find_accessibility_element'), - 'find_accessibility_elements': - AttributeBinding('je_auto_control.utils.accessibility', 'find_accessibility_elements'), - 'focused_accessibility_element': - AttributeBinding('je_auto_control.utils.accessibility', 'focused_accessibility_element'), - 'list_accessibility_elements': - AttributeBinding('je_auto_control.utils.accessibility', 'list_accessibility_elements'), - 'control_get_state': AttributeBinding('je_auto_control.utils.accessibility', 'control_get_state'), - 'control_get_value': AttributeBinding('je_auto_control.utils.accessibility', 'control_get_value'), - 'control_set_value': AttributeBinding('je_auto_control.utils.accessibility', 'control_set_value'), - 'control_invoke': AttributeBinding('je_auto_control.utils.accessibility', 'control_invoke'), - 'control_toggle': AttributeBinding('je_auto_control.utils.accessibility', 'control_toggle'), - 'read_control_table': AttributeBinding('je_auto_control.utils.accessibility', 'read_control_table'), - 'expand_control': AttributeBinding('je_auto_control.utils.control_patterns', 'expand_control'), - 'collapse_control': AttributeBinding('je_auto_control.utils.control_patterns', 'collapse_control'), - 'control_expand_state': AttributeBinding('je_auto_control.utils.control_patterns', 'control_expand_state'), - 'select_control_item': AttributeBinding('je_auto_control.utils.control_patterns', 'select_control_item'), - 'control_range': AttributeBinding('je_auto_control.utils.control_patterns', 'control_range'), - 'set_control_range': AttributeBinding('je_auto_control.utils.control_patterns', 'set_control_range'), - 'scroll_control_into_view': AttributeBinding('je_auto_control.utils.control_patterns', 'scroll_control_into_view'), - 'get_control_text': AttributeBinding('je_auto_control.utils.ax_text', 'get_control_text'), - 'get_selected_text': AttributeBinding('je_auto_control.utils.ax_text', 'get_selected_text'), - 'get_visible_text': AttributeBinding('je_auto_control.utils.ax_text', 'get_visible_text'), - 'find_control_text': AttributeBinding('je_auto_control.utils.ax_text', 'find_control_text'), - 'select_control_text': AttributeBinding('je_auto_control.utils.ax_text', 'select_control_text'), - 'control_text_attributes': AttributeBinding('je_auto_control.utils.ax_text', 'control_text_attributes'), - 'control_type_name': AttributeBinding('je_auto_control.utils.ax_tree_walk', 'control_type_name'), - 'humanize_role': AttributeBinding('je_auto_control.utils.ax_tree_walk', 'humanize_role'), - 'humanize_tree': AttributeBinding('je_auto_control.utils.ax_tree_walk', 'humanize_tree'), - 'assign_node_paths': AttributeBinding('je_auto_control.utils.ax_tree_walk', 'assign_node_paths'), - 'find_by_path': AttributeBinding('je_auto_control.utils.ax_tree_walk', 'find_by_path'), - 'is_interactive_role': AttributeBinding('je_auto_control.utils.focus_order', 'is_interactive_role'), - 'tab_order': AttributeBinding('je_auto_control.utils.focus_order', 'tab_order'), - 'audit_focus_order': AttributeBinding('je_auto_control.utils.focus_order', 'audit_focus_order'), - 'focus_control': AttributeBinding('je_auto_control.utils.focus_order', 'focus_control'), + 'build_compliance_report': AttributeBinding(_UTILS_COMPLIANCE, 'build_compliance_report'), + 'render_compliance_html': AttributeBinding(_UTILS_COMPLIANCE, 'render_compliance_html'), + 'write_compliance_report': AttributeBinding(_UTILS_COMPLIANCE, 'write_compliance_report'), + 'AgentTrace': AttributeBinding(_UTILS_AGENT_TRACE, 'AgentTrace'), + 'default_trace': AttributeBinding(_UTILS_AGENT_TRACE, 'default_trace'), + 'reset_trace': AttributeBinding(_UTILS_AGENT_TRACE, 'reset_trace'), + 'VideoStep': AttributeBinding(_UTILS_VIDEO_REPORT, 'VideoStep'), + 'build_overlay_plan': AttributeBinding(_UTILS_VIDEO_REPORT, 'build_overlay_plan'), + 'render_overlay_frame': AttributeBinding(_UTILS_VIDEO_REPORT, 'render_overlay_frame'), + 'write_step_video': AttributeBinding(_UTILS_VIDEO_REPORT, 'write_step_video'), + 'fuzzy_best_match': AttributeBinding(_UTILS_FUZZY, 'fuzzy_best_match'), + 'fuzzy_dedupe': AttributeBinding(_UTILS_FUZZY, 'fuzzy_dedupe'), + 'fuzzy_matches': AttributeBinding(_UTILS_FUZZY, 'fuzzy_matches'), + 'fuzzy_ratio': AttributeBinding(_UTILS_FUZZY, 'fuzzy_ratio'), + 'deaccent': AttributeBinding(_UTILS_TEXT_NORMALIZE, 'deaccent'), + 'fold_whitespace': AttributeBinding(_UTILS_TEXT_NORMALIZE, 'fold_whitespace'), + 'normalize_quotes': AttributeBinding(_UTILS_TEXT_NORMALIZE, 'normalize_quotes'), + 'normalize_text': AttributeBinding(_UTILS_TEXT_NORMALIZE, 'normalize_text'), + 'slugify': AttributeBinding(_UTILS_TEXT_NORMALIZE, 'slugify'), + 'damerau_levenshtein': AttributeBinding(_UTILS_TEXT_SIMILARITY, 'damerau_levenshtein'), + 'dice': AttributeBinding(_UTILS_TEXT_SIMILARITY, 'dice'), + 'jaccard': AttributeBinding(_UTILS_TEXT_SIMILARITY, 'jaccard'), + 'jaro': AttributeBinding(_UTILS_TEXT_SIMILARITY, 'jaro'), + 'jaro_winkler': AttributeBinding(_UTILS_TEXT_SIMILARITY, 'jaro_winkler'), + 'levenshtein': AttributeBinding(_UTILS_TEXT_SIMILARITY, 'levenshtein'), + 'similarity': AttributeBinding(_UTILS_TEXT_SIMILARITY, 'similarity'), + 'minhash_signature': AttributeBinding(_UTILS_NEAR_DUP, 'minhash_signature'), + 'minhash_similarity': AttributeBinding(_UTILS_NEAR_DUP, 'minhash_similarity'), + 'near_duplicates': AttributeBinding(_UTILS_NEAR_DUP, 'near_duplicates'), + 'simhash': AttributeBinding(_UTILS_NEAR_DUP, 'simhash'), + 'S3ArtifactStore': AttributeBinding(_UTILS_ARTIFACT_STORE, 'S3ArtifactStore'), + 'configure_default_store': AttributeBinding(_UTILS_ARTIFACT_STORE, 'configure_default_store'), + 'get_default_store': AttributeBinding(_UTILS_ARTIFACT_STORE, 'get_default_store'), + 'set_default_store': AttributeBinding(_UTILS_ARTIFACT_STORE, 'set_default_store'), + 'average_hash': AttributeBinding(_UTILS_IMAGE_DEDUP, 'average_hash'), + 'dedupe_images': AttributeBinding(_UTILS_IMAGE_DEDUP, 'dedupe_images'), + 'dhash': AttributeBinding(_UTILS_IMAGE_DEDUP, 'dhash'), + 'hamming_distance': AttributeBinding(_UTILS_IMAGE_DEDUP, 'hamming_distance'), + 'images_similar': AttributeBinding(_UTILS_IMAGE_DEDUP, 'images_similar'), + 'build_query': AttributeBinding(_UTILS_URL_CANON, 'build_query'), + 'canonicalize_url': AttributeBinding(_UTILS_URL_CANON, 'canonicalize_url'), + 'normalize_url': AttributeBinding(_UTILS_URL_CANON, 'normalize_url'), + 'parse_query': AttributeBinding(_UTILS_URL_CANON, 'parse_query'), + 'urls_equal': AttributeBinding(_UTILS_URL_CANON, 'urls_equal'), + 'format_currency': AttributeBinding(_UTILS_LOCALE_PARSE, 'format_currency'), + 'format_date': AttributeBinding(_UTILS_LOCALE_PARSE, 'format_date'), + 'format_decimal': AttributeBinding(_UTILS_LOCALE_PARSE, 'format_decimal'), + 'parse_decimal': AttributeBinding(_UTILS_LOCALE_PARSE, 'parse_decimal'), + 'parse_number': AttributeBinding(_UTILS_LOCALE_PARSE, 'parse_number'), + 'VoiceCommand': AttributeBinding(_UTILS_VOICE, 'VoiceCommand'), + 'VoiceRouter': AttributeBinding(_UTILS_VOICE, 'VoiceRouter'), + 'default_voice_router': AttributeBinding(_UTILS_VOICE, 'default_voice_router'), + 'CoordinateSpace': AttributeBinding(_UTILS_COORDINATE_SPACE, 'CoordinateSpace'), + 'downscale_png': AttributeBinding(_UTILS_COORDINATE_SPACE, 'downscale_png'), + 'normalized_space': AttributeBinding(_UTILS_COORDINATE_SPACE, 'normalized_space'), + 'xga_space': AttributeBinding(_UTILS_COORDINATE_SPACE, 'xga_space'), + 'LoopGuard': AttributeBinding(_UTILS_LOOP_GUARD, 'LoopGuard'), + 'LoopVerdict': AttributeBinding(_UTILS_LOOP_GUARD, 'LoopVerdict'), + 'default_loop_guard': AttributeBinding(_UTILS_LOOP_GUARD, 'default_loop_guard'), + 'digest_result': AttributeBinding(_UTILS_LOOP_GUARD, 'digest_result'), + 'Candidate': AttributeBinding(_UTILS_PROCESS_MINING, 'Candidate'), + 'MiningReport': AttributeBinding(_UTILS_PROCESS_MINING, 'MiningReport'), + 'SequencePattern': AttributeBinding(_UTILS_PROCESS_MINING, 'SequencePattern'), + 'directly_follows': AttributeBinding(_UTILS_PROCESS_MINING, 'directly_follows'), + 'find_repeated_sequences': AttributeBinding(_UTILS_PROCESS_MINING, 'find_repeated_sequences'), + 'mine_action_log': AttributeBinding(_UTILS_PROCESS_MINING, 'mine_action_log'), + 'rank_automation_candidates': AttributeBinding(_UTILS_PROCESS_MINING, 'rank_automation_candidates'), + 'Asset': AttributeBinding(_UTILS_ASSETS, 'Asset'), + 'AssetStore': AttributeBinding(_UTILS_ASSETS, 'AssetStore'), + 'AssetValue': AttributeBinding(_UTILS_ASSETS, 'AssetValue'), + 'active_environment': AttributeBinding(_UTILS_ASSETS, 'active_environment'), + 'dotenv_values': AttributeBinding(_UTILS_DOTENV, 'dotenv_values'), + 'dump_dotenv': AttributeBinding(_UTILS_DOTENV, 'dump_dotenv'), + 'load_dotenv': AttributeBinding(_UTILS_DOTENV, 'load_dotenv'), + 'parse_dotenv': AttributeBinding(_UTILS_DOTENV, 'parse_dotenv'), + 'LayeredConfig': AttributeBinding(_UTILS_LAYERED_CONFIG, 'LayeredConfig'), + 'SourceTrace': AttributeBinding(_UTILS_LAYERED_CONFIG, 'SourceTrace'), + 'deep_merge': AttributeBinding(_UTILS_LAYERED_CONFIG, 'deep_merge'), + 'ConfigField': AttributeBinding(_UTILS_CONFIG_SCHEMA, 'ConfigField'), + 'ConfigSchema': AttributeBinding(_UTILS_CONFIG_SCHEMA, 'ConfigSchema'), + 'coerce': AttributeBinding(_UTILS_CONFIG_SCHEMA, 'coerce'), + 'validate_config': AttributeBinding(_UTILS_CONFIG_SCHEMA, 'validate_config'), + 'PathNotAllowedError': AttributeBinding(_UTILS_PATH_GUARD, 'PathNotAllowedError'), + 'PathPolicy': AttributeBinding(_UTILS_PATH_GUARD, 'PathPolicy'), + 'RefResolver': AttributeBinding(_UTILS_SECRET_REF, 'RefResolver'), + 'SecretRefError': AttributeBinding(_UTILS_SECRET_REF, 'SecretRefError'), + 'is_ref': AttributeBinding(_UTILS_SECRET_REF, 'is_ref'), + 'resolve_ref': AttributeBinding(_UTILS_SECRET_REF, 'resolve_ref'), + 'resolve_refs_in': AttributeBinding(_UTILS_SECRET_REF, 'resolve_refs_in'), + 'redact_config': AttributeBinding(_UTILS_CONFIG_REDACTION, 'redact_config'), + 'redact_secret_text': AttributeBinding(_UTILS_CONFIG_REDACTION, 'redact_secret_text'), + 'EventEmitter': AttributeBinding(_UTILS_EVENTS, 'EventEmitter'), + 'post_cloudevent': AttributeBinding(_UTILS_EVENTS, 'post_cloudevent'), + 'to_cloudevent': AttributeBinding(_UTILS_EVENTS, 'to_cloudevent'), + 'WebhookChannel': AttributeBinding(_UTILS_NOTIFY_CHANNELS, 'WebhookChannel'), + 'WebhookResult': AttributeBinding(_UTILS_NOTIFY_CHANNELS, 'WebhookResult'), + 'notify_webhook': AttributeBinding(_UTILS_NOTIFY_CHANNELS, 'notify_webhook'), + 'set_default_poster': AttributeBinding(_UTILS_NOTIFY_CHANNELS, 'set_default_poster'), + 'json_extract': AttributeBinding(_UTILS_JSONPATH, 'json_extract'), + 'json_query': AttributeBinding(_UTILS_JSONPATH, 'json_query'), + 'json_query_one': AttributeBinding(_UTILS_JSONPATH, 'json_query_one'), + 'Saga': AttributeBinding(_UTILS_SAGA, 'Saga'), + 'SagaResult': AttributeBinding(_UTILS_SAGA, 'SagaResult'), + 'run_saga': AttributeBinding(_UTILS_SAGA, 'run_saga'), + 'DecisionTable': AttributeBinding(_UTILS_DECISION_TABLE, 'DecisionTable'), + 'Rule': AttributeBinding(_UTILS_DECISION_TABLE, 'Rule'), + 'evaluate_table': AttributeBinding(_UTILS_DECISION_TABLE, 'evaluate_table'), + 'RepairStore': AttributeBinding(_UTILS_LOCATOR_REPAIR, 'RepairStore'), + 'RepairSuggestion': AttributeBinding(_UTILS_LOCATOR_REPAIR, 'RepairSuggestion'), + 'repair_from_heal': AttributeBinding(_UTILS_LOCATOR_REPAIR, 'repair_from_heal'), + 'PIIFinding': AttributeBinding(_UTILS_PII_TEXT, 'PIIFinding'), + 'detect_pii': AttributeBinding(_UTILS_PII_TEXT, 'detect_pii'), + 'redact_pii_text': AttributeBinding(_UTILS_PII_TEXT, 'redact_pii_text'), + 'from_audit_findings': AttributeBinding(_UTILS_SARIF, 'from_audit_findings'), + 'from_lint_issues': AttributeBinding(_UTILS_SARIF, 'from_lint_issues'), + 'make_finding': AttributeBinding(_UTILS_SARIF, 'make_finding'), + 'to_sarif': AttributeBinding(_UTILS_SARIF, 'to_sarif'), + 'write_sarif': AttributeBinding(_UTILS_SARIF, 'write_sarif'), + 'SchemaValidationResult': AttributeBinding(_UTILS_JSON_SCHEMA, 'SchemaValidationResult'), + 'assert_schema': AttributeBinding(_UTILS_JSON_SCHEMA, 'assert_schema'), + 'is_valid': AttributeBinding(_UTILS_JSON_SCHEMA, 'is_valid'), + 'validate_json': AttributeBinding(_UTILS_JSON_SCHEMA, 'validate_json'), + 'findings_to_sarif': AttributeBinding(_UTILS_VULN_SCAN, 'findings_to_sarif'), + 'is_affected': AttributeBinding(_UTILS_VULN_SCAN, 'is_affected'), + 'match_package': AttributeBinding(_UTILS_VULN_SCAN, 'match_package'), + 'scan_components': AttributeBinding(_UTILS_VULN_SCAN, 'scan_components'), + 'version_key': AttributeBinding(_UTILS_VULN_SCAN, 'version_key'), + 'VEX_JUSTIFICATIONS': AttributeBinding(_UTILS_VEX, 'VEX_JUSTIFICATIONS'), + 'VEX_STATUSES': AttributeBinding(_UTILS_VEX, 'VEX_STATUSES'), + 'apply_vex': AttributeBinding(_UTILS_VEX, 'apply_vex'), + 'build_vex': AttributeBinding(_UTILS_VEX, 'build_vex'), + 'vex_statement': AttributeBinding(_UTILS_VEX, 'vex_statement'), + 'DEFAULT_COPYLEFT': AttributeBinding(_UTILS_LICENSE_POLICY, 'DEFAULT_COPYLEFT'), + 'evaluate_license': AttributeBinding(_UTILS_LICENSE_POLICY, 'evaluate_license'), + 'evaluate_sbom': AttributeBinding(_UTILS_LICENSE_POLICY, 'evaluate_sbom'), + 'license_findings_to_sarif': AttributeBinding(_UTILS_LICENSE_POLICY, 'license_findings_to_sarif'), + 'normalize_spdx': AttributeBinding(_UTILS_LICENSE_POLICY, 'normalize_spdx'), + 'ClaimsPolicy': AttributeBinding(_UTILS_JWT, 'ClaimsPolicy'), + 'ExpiredTokenError': AttributeBinding(_UTILS_JWT, 'ExpiredTokenError'), + 'InvalidSignatureError': AttributeBinding(_UTILS_JWT, 'InvalidSignatureError'), + 'JwtError': AttributeBinding(_UTILS_JWT, 'JwtError'), + 'decode_jwt': AttributeBinding(_UTILS_JWT, 'decode_jwt'), + 'encode_jwt': AttributeBinding(_UTILS_JWT, 'encode_jwt'), + 'SlidingWindowLimiter': AttributeBinding(_UTILS_RATE_LIMIT, 'SlidingWindowLimiter'), + 'TokenBucket': AttributeBinding(_UTILS_RATE_LIMIT, 'TokenBucket'), + 'throttle': AttributeBinding(_UTILS_RATE_LIMIT, 'throttle'), + 'PatchError': AttributeBinding(_UTILS_JSON_PATCH, 'PatchError'), + 'PatchTestFailed': AttributeBinding(_UTILS_JSON_PATCH, 'PatchTestFailed'), + 'apply_patch': AttributeBinding(_UTILS_JSON_PATCH, 'apply_patch'), + 'make_merge_patch': AttributeBinding(_UTILS_JSON_PATCH, 'make_merge_patch'), + 'make_patch': AttributeBinding(_UTILS_JSON_PATCH, 'make_patch'), + 'merge_patch': AttributeBinding(_UTILS_JSON_PATCH, 'merge_patch'), + 'remove_pointer': AttributeBinding(_UTILS_JSON_PATCH, 'remove_pointer'), + 'resolve_pointer': AttributeBinding(_UTILS_JSON_PATCH, 'resolve_pointer'), + 'set_pointer': AttributeBinding(_UTILS_JSON_PATCH, 'set_pointer'), + 'SearchHit': AttributeBinding(_UTILS_SEARCH_INDEX, 'SearchHit'), + 'SearchIndex': AttributeBinding(_UTILS_SEARCH_INDEX, 'SearchIndex'), + 'search_documents': AttributeBinding(_UTILS_SEARCH_INDEX, 'search_documents'), + 'tokenize': AttributeBinding(_UTILS_SEARCH_INDEX, 'tokenize'), + 'chi_square_2x2': AttributeBinding(_UTILS_STATS, 'chi_square_2x2'), + 'cohens_d': AttributeBinding(_UTILS_STATS, 'cohens_d'), + 'describe': AttributeBinding(_UTILS_STATS, 'describe'), + 'normal_cdf': AttributeBinding(_UTILS_STATS, 'normal_cdf'), + 'percentile': AttributeBinding(_UTILS_STATS, 'percentile'), + 'two_proportion_z_test': AttributeBinding(_UTILS_STATS, 'two_proportion_z_test'), + 'welch_t_test': AttributeBinding(_UTILS_STATS, 'welch_t_test'), + 'Recurrence': AttributeBinding(_UTILS_RECURRENCE, 'Recurrence'), + 'next_occurrence': AttributeBinding(_UTILS_RECURRENCE, 'next_occurrence'), + 'occurrences': AttributeBinding(_UTILS_RECURRENCE, 'occurrences'), + 'parse_rrule': AttributeBinding(_UTILS_RECURRENCE, 'parse_rrule'), + 'MergeResult': AttributeBinding(_UTILS_TEXT_DIFF, 'MergeResult'), + 'PatchApplyError': AttributeBinding(_UTILS_TEXT_DIFF, 'PatchApplyError'), + 'apply_unified': AttributeBinding(_UTILS_TEXT_DIFF, 'apply_unified'), + 'three_way_merge': AttributeBinding(_UTILS_TEXT_DIFF, 'three_way_merge'), + 'unified_diff': AttributeBinding(_UTILS_TEXT_DIFF, 'unified_diff'), + 'Flag': AttributeBinding(_UTILS_FEATURE_FLAGS, 'Flag'), + 'FlagStore': AttributeBinding(_UTILS_FEATURE_FLAGS, 'FlagStore'), + 'assign_variant': AttributeBinding(_UTILS_FEATURE_FLAGS, 'assign_variant'), + 'evaluate_flag': AttributeBinding(_UTILS_FEATURE_FLAGS, 'evaluate_flag'), + 'is_enabled': AttributeBinding(_UTILS_FEATURE_FLAGS, 'is_enabled'), + 'percentage_bucket': AttributeBinding(_UTILS_FEATURE_FLAGS, 'percentage_bucket'), + 'build_provenance': AttributeBinding(_UTILS_PROVENANCE, 'build_provenance'), + 'subject_for': AttributeBinding(_UTILS_PROVENANCE, 'subject_for'), + 'subject_for_bytes': AttributeBinding(_UTILS_PROVENANCE, 'subject_for_bytes'), + 'verify_provenance': AttributeBinding(_UTILS_PROVENANCE, 'verify_provenance'), + 'write_provenance': AttributeBinding(_UTILS_PROVENANCE, 'write_provenance'), + 'MatchReport': AttributeBinding(_UTILS_JSON_CONTRACT, 'MatchReport'), + 'diff_json': AttributeBinding(_UTILS_JSON_CONTRACT, 'diff_json'), + 'match_json': AttributeBinding(_UTILS_JSON_CONTRACT, 'match_json'), + 'normalize_json': AttributeBinding(_UTILS_JSON_CONTRACT, 'normalize_json'), + 'snapshot_json': AttributeBinding(_UTILS_JSON_CONTRACT, 'snapshot_json'), + 'ChaosExperiment': AttributeBinding(_UTILS_CHAOS, 'ChaosExperiment'), + 'Fault': AttributeBinding(_UTILS_CHAOS, 'Fault'), + 'Probe': AttributeBinding(_UTILS_CHAOS, 'Probe'), + 'exception_fault': AttributeBinding(_UTILS_CHAOS, 'exception_fault'), + 'latency_fault': AttributeBinding(_UTILS_CHAOS, 'latency_fault'), + 'run_experiment': AttributeBinding(_UTILS_CHAOS, 'run_experiment'), + 'BurnRule': AttributeBinding(_UTILS_SLO, 'BurnRule'), + 'burn_alerts': AttributeBinding(_UTILS_SLO, 'burn_alerts'), + 'burn_rate': AttributeBinding(_UTILS_SLO, 'burn_rate'), + 'default_burn_rules': AttributeBinding(_UTILS_SLO, 'default_burn_rules'), + 'evaluate_slo': AttributeBinding(_UTILS_SLO, 'evaluate_slo'), + 'LatencyDigest': AttributeBinding(_UTILS_PERCENTILES, 'LatencyDigest'), + 'exact_percentiles': AttributeBinding(_UTILS_PERCENTILES, 'exact_percentiles'), + 'ts_delta': AttributeBinding(_UTILS_TIMESERIES, 'ts_delta'), + 'ts_downsample': AttributeBinding(_UTILS_TIMESERIES, 'ts_downsample'), + 'ts_idelta': AttributeBinding(_UTILS_TIMESERIES, 'ts_idelta'), + 'ts_increase': AttributeBinding(_UTILS_TIMESERIES, 'ts_increase'), + 'ts_irate': AttributeBinding(_UTILS_TIMESERIES, 'ts_irate'), + 'ts_rate': AttributeBinding(_UTILS_TIMESERIES, 'ts_rate'), + 'ts_resample': AttributeBinding(_UTILS_TIMESERIES, 'ts_resample'), + 'detect_anomalies': AttributeBinding(_UTILS_ANOMALY, 'detect_anomalies'), + 'ewma_control': AttributeBinding(_UTILS_ANOMALY, 'ewma_control'), + 'mad_anomalies': AttributeBinding(_UTILS_ANOMALY, 'mad_anomalies'), + 'mad_scores': AttributeBinding(_UTILS_ANOMALY, 'mad_scores'), + 'zscore_anomalies': AttributeBinding(_UTILS_ANOMALY, 'zscore_anomalies'), + 'zscore_scores': AttributeBinding(_UTILS_ANOMALY, 'zscore_scores'), + 'ewma': AttributeBinding(_UTILS_SMOOTHING, 'ewma'), + 'rolling': AttributeBinding(_UTILS_SMOOTHING, 'rolling'), + 'sma': AttributeBinding(_UTILS_SMOOTHING, 'sma'), + 'wma': AttributeBinding(_UTILS_SMOOTHING, 'wma'), + 'Bulkhead': AttributeBinding(_UTILS_BULKHEAD, 'Bulkhead'), + 'BulkheadFullError': AttributeBinding(_UTILS_BULKHEAD, 'BulkheadFullError'), + 'next_delay': AttributeBinding(_UTILS_BULKHEAD, 'next_delay'), + 'parse_ratelimit': AttributeBinding(_UTILS_BULKHEAD, 'parse_ratelimit'), + 'parse_retry_after': AttributeBinding(_UTILS_BULKHEAD, 'parse_retry_after'), + 'Cassette': AttributeBinding(_UTILS_HTTP_CASSETTE, 'Cassette'), + 'CassetteMissError': AttributeBinding(_UTILS_HTTP_CASSETTE, 'CassetteMissError'), + 'HttpProblemError': AttributeBinding(_UTILS_HTTP_PROBLEM, 'HttpProblemError'), + 'ProblemDetails': AttributeBinding(_UTILS_HTTP_PROBLEM, 'ProblemDetails'), + 'is_problem': AttributeBinding(_UTILS_HTTP_PROBLEM, 'is_problem'), + 'parse_problem': AttributeBinding(_UTILS_HTTP_PROBLEM, 'parse_problem'), + 'raise_for_problem': AttributeBinding(_UTILS_HTTP_PROBLEM, 'raise_for_problem'), + 'SSEEvent': AttributeBinding(_UTILS_SSE_CLIENT, 'SSEEvent'), + 'SSEParser': AttributeBinding(_UTILS_SSE_CLIENT, 'SSEParser'), + 'parse_event_stream': AttributeBinding(_UTILS_SSE_CLIENT, 'parse_event_stream'), + 'Link': AttributeBinding(_UTILS_LINK_HEADER, 'Link'), + 'links_by_rel': AttributeBinding(_UTILS_LINK_HEADER, 'links_by_rel'), + 'next_url': AttributeBinding(_UTILS_LINK_HEADER, 'next_url'), + 'paginate': AttributeBinding(_UTILS_LINK_HEADER, 'paginate'), + 'parse_link_header': AttributeBinding(_UTILS_LINK_HEADER, 'parse_link_header'), + 'MultipartFile': AttributeBinding(_UTILS_MULTIPART, 'MultipartFile'), + 'build_multipart': AttributeBinding(_UTILS_MULTIPART, 'build_multipart'), + 'new_boundary': AttributeBinding(_UTILS_MULTIPART, 'new_boundary'), + 'parse_multipart': AttributeBinding(_UTILS_MULTIPART, 'parse_multipart'), + 'build_accept': AttributeBinding(_UTILS_HTTP_CONTENT, 'build_accept'), + 'build_accept_encoding': AttributeBinding(_UTILS_HTTP_CONTENT, 'build_accept_encoding'), + 'decode_body': AttributeBinding(_UTILS_HTTP_CONTENT, 'decode_body'), + 'negotiated_call': AttributeBinding(_UTILS_HTTP_CONTENT, 'negotiated_call'), + 'parse_quality_values': AttributeBinding(_UTILS_HTTP_CONTENT, 'parse_quality_values'), + 'CookieJar': AttributeBinding(_UTILS_COOKIE_JAR, 'CookieJar'), + 'parse_set_cookie': AttributeBinding(_UTILS_COOKIE_JAR, 'parse_set_cookie'), + 'conditioned_call': AttributeBinding(_UTILS_HTTP_CONDITIONAL, 'conditioned_call'), + 'is_fresh': AttributeBinding(_UTILS_HTTP_CONDITIONAL, 'is_fresh'), + 'is_not_modified': AttributeBinding(_UTILS_HTTP_CONDITIONAL, 'is_not_modified'), + 'parse_cache_control': AttributeBinding(_UTILS_HTTP_CONDITIONAL, 'parse_cache_control'), + 'store_validators': AttributeBinding(_UTILS_HTTP_CONDITIONAL, 'store_validators'), + 'SpanContext': AttributeBinding(_UTILS_TRACE_CONTEXT, 'SpanContext'), + 'TraceContextError': AttributeBinding(_UTILS_TRACE_CONTEXT, 'TraceContextError'), + 'child_context': AttributeBinding(_UTILS_TRACE_CONTEXT, 'child_context'), + 'extract_context': AttributeBinding(_UTILS_TRACE_CONTEXT, 'extract_context'), + 'format_traceparent': AttributeBinding(_UTILS_TRACE_CONTEXT, 'format_traceparent'), + 'format_tracestate': AttributeBinding(_UTILS_TRACE_CONTEXT, 'format_tracestate'), + 'inject_context': AttributeBinding(_UTILS_TRACE_CONTEXT, 'inject_context'), + 'new_root_context': AttributeBinding(_UTILS_TRACE_CONTEXT, 'new_root_context'), + 'new_span_id': AttributeBinding(_UTILS_TRACE_CONTEXT, 'new_span_id'), + 'new_trace_id': AttributeBinding(_UTILS_TRACE_CONTEXT, 'new_trace_id'), + 'parse_traceparent': AttributeBinding(_UTILS_TRACE_CONTEXT, 'parse_traceparent'), + 'parse_tracestate': AttributeBinding(_UTILS_TRACE_CONTEXT, 'parse_tracestate'), + 'Baggage': AttributeBinding(_UTILS_BAGGAGE, 'Baggage'), + 'extract_baggage': AttributeBinding(_UTILS_BAGGAGE, 'extract_baggage'), + 'format_baggage': AttributeBinding(_UTILS_BAGGAGE, 'format_baggage'), + 'inject_baggage': AttributeBinding(_UTILS_BAGGAGE, 'inject_baggage'), + 'parse_baggage': AttributeBinding(_UTILS_BAGGAGE, 'parse_baggage'), + 'CanonicalLogLine': AttributeBinding(_UTILS_CANONICAL_LOG, 'CanonicalLogLine'), + 'JSONLogFormatter': AttributeBinding(_UTILS_CANONICAL_LOG, 'JSONLogFormatter'), + 'bind_trace_context': AttributeBinding(_UTILS_CANONICAL_LOG, 'bind_trace_context'), + 'attributes_to_otlp': AttributeBinding(_UTILS_OTLP_EXPORT, 'attributes_to_otlp'), + 'spans_to_otlp': AttributeBinding(_UTILS_OTLP_EXPORT, 'spans_to_otlp'), + 'write_otlp': AttributeBinding(_UTILS_OTLP_EXPORT, 'write_otlp'), + 'ToolView': AttributeBinding(_UTILS_MCP_SERVER_DISCLOSURE, 'ToolView'), + 'ToolPage': AttributeBinding(_UTILS_MCP_SERVER_DISCLOSURE, 'ToolPage'), + 'plan_mcp_tool_profile': AttributeBinding(_UTILS_MCP_SERVER_DISCLOSURE, 'plan_mcp_tool_profile'), + 'ToolDiscoveryError': AttributeBinding(_UTILS_MCP_SERVER_DISCOVERY, 'ToolDiscoveryError'), + 'ToolIndex': AttributeBinding(_UTILS_MCP_SERVER_DISCOVERY, 'ToolIndex'), + 'ToolSummary': AttributeBinding(_UTILS_MCP_SERVER_DISCOVERY, 'ToolSummary'), + 'search_mcp_tools': AttributeBinding(_UTILS_MCP_SERVER_DISCOVERY, 'search_mcp_tools'), + 'get_mcp_tool_schema': AttributeBinding(_UTILS_MCP_SERVER_DISCOVERY, 'get_mcp_tool_schema'), + 'AuditLogger': AttributeBinding(_UTILS_MCP_SERVER, 'AuditLogger'), + 'HttpMCPServer': AttributeBinding(_UTILS_MCP_SERVER, 'HttpMCPServer'), + 'MCPContent': AttributeBinding(_UTILS_MCP_SERVER, 'MCPContent'), + 'MCPPrompt': AttributeBinding(_UTILS_MCP_SERVER, 'MCPPrompt'), + 'MCPPromptArgument': AttributeBinding(_UTILS_MCP_SERVER, 'MCPPromptArgument'), + 'MCPResource': AttributeBinding(_UTILS_MCP_SERVER, 'MCPResource'), + 'MCPServer': AttributeBinding(_UTILS_MCP_SERVER, 'MCPServer'), + 'MCPTool': AttributeBinding(_UTILS_MCP_SERVER, 'MCPTool'), + 'MCPToolAnnotations': AttributeBinding(_UTILS_MCP_SERVER, 'MCPToolAnnotations'), + 'MCPToolResult': AttributeBinding(_UTILS_MCP_SERVER, 'MCPToolResult'), + 'OperationCancelledError': AttributeBinding(_UTILS_MCP_SERVER, 'OperationCancelledError'), + 'PromptProvider': AttributeBinding(_UTILS_MCP_SERVER, 'PromptProvider'), + 'RateLimiter': AttributeBinding(_UTILS_MCP_SERVER, 'RateLimiter'), + 'ResourceProvider': AttributeBinding(_UTILS_MCP_SERVER, 'ResourceProvider'), + 'ToolCallContext': AttributeBinding(_UTILS_MCP_SERVER, 'ToolCallContext'), + 'build_default_tool_registry': AttributeBinding(_UTILS_MCP_SERVER, 'build_default_tool_registry'), + 'default_prompt_provider': AttributeBinding(_UTILS_MCP_SERVER, 'default_prompt_provider'), + 'default_resource_provider': AttributeBinding(_UTILS_MCP_SERVER, 'default_resource_provider'), + 'make_plugin_tool': AttributeBinding(_UTILS_MCP_SERVER, 'make_plugin_tool'), + 'register_plugin_tools': AttributeBinding(_UTILS_MCP_SERVER, 'register_plugin_tools'), + 'start_mcp_http_server': AttributeBinding(_UTILS_MCP_SERVER, 'start_mcp_http_server'), + 'start_mcp_stdio_server': AttributeBinding(_UTILS_MCP_SERVER, 'start_mcp_stdio_server'), + 'load_plugin_file': AttributeBinding(_UTILS_PLUGIN_LOADER_PLUGIN_LOADER, 'load_plugin_file'), + 'load_plugin_directory': AttributeBinding(_UTILS_PLUGIN_LOADER_PLUGIN_LOADER, 'load_plugin_directory'), + 'discover_plugin_commands': AttributeBinding(_UTILS_PLUGIN_LOADER_PLUGIN_LOADER, 'discover_plugin_commands'), + 'register_plugin_commands': AttributeBinding(_UTILS_PLUGIN_LOADER_PLUGIN_LOADER, 'register_plugin_commands'), + 'RestApiServer': AttributeBinding(_UTILS_REST_API_REST_SERVER, 'RestApiServer'), + 'start_rest_api_server': AttributeBinding(_UTILS_REST_API_REST_SERVER, 'start_rest_api_server'), + 'AuthorizationContext': AttributeBinding(_UTILS_RBAC, 'AuthorizationContext'), + 'AuthorizationError': AttributeBinding(_UTILS_RBAC, 'AuthorizationError'), + 'UserAuthError': AttributeBinding(_UTILS_RBAC, 'UserAuthError'), + 'UserRecord': AttributeBinding(_UTILS_RBAC, 'UserRecord'), + 'UserStore': AttributeBinding(_UTILS_RBAC, 'UserStore'), + 'authorization_scope': AttributeBinding(_UTILS_RBAC, 'authorization_scope'), + 'AdminConsoleClient': AttributeBinding(_UTILS_ADMIN, 'AdminConsoleClient'), + 'AdminHost': AttributeBinding(_UTILS_ADMIN, 'AdminHost'), + 'default_admin_console': AttributeBinding(_UTILS_ADMIN, 'default_admin_console'), + 'WebRTCInspector': AttributeBinding(_UTILS_REMOTE_DESKTOP_WEBRTC_INSPECTOR, 'WebRTCInspector'), + 'default_webrtc_inspector': AttributeBinding(_UTILS_REMOTE_DESKTOP_WEBRTC_INSPECTOR, 'default_webrtc_inspector'), + 'UsbDevice': AttributeBinding(_UTILS_USB, 'UsbDevice'), + 'UsbEnumerationResult': AttributeBinding(_UTILS_USB, 'UsbEnumerationResult'), + 'list_usb_devices': AttributeBinding(_UTILS_USB, 'list_usb_devices'), + 'UsbEvent': AttributeBinding(_UTILS_USB, 'UsbEvent'), + 'UsbHotplugWatcher': AttributeBinding(_UTILS_USB, 'UsbHotplugWatcher'), + 'default_usb_watcher': AttributeBinding(_UTILS_USB, 'default_usb_watcher'), + 'UsbPassthroughSession': AttributeBinding(_UTILS_USB, 'UsbPassthroughSession'), + 'UsbPassthroughClient': AttributeBinding(_UTILS_USB, 'UsbPassthroughClient'), + 'UsbAcl': AttributeBinding(_UTILS_USB, 'UsbAcl'), + 'enable_usb_passthrough': AttributeBinding(_UTILS_USB, 'enable_usb_passthrough'), + 'is_usb_passthrough_enabled': AttributeBinding(_UTILS_USB, 'is_usb_passthrough_enabled'), + 'BackendContext': AttributeBinding(_WRAPPER_CAPABILITIES, 'BackendContext'), + 'CapabilitySnapshot': AttributeBinding(_WRAPPER_CAPABILITIES, 'CapabilitySnapshot'), + 'CapabilityStatus': AttributeBinding(_WRAPPER_CAPABILITIES, 'CapabilityStatus'), + 'probe_capabilities': AttributeBinding(_WRAPPER_CAPABILITIES, 'probe_capabilities'), + 'CancellationToken': AttributeBinding(_UTILS_EXCEPTION_CANCELLATION, 'CancellationToken'), + 'TaskProgress': AttributeBinding(_UTILS_EXCEPTION_CANCELLATION, 'TaskProgress'), + 'cancellation_scope': AttributeBinding(_UTILS_EXCEPTION_CANCELLATION, 'cancellation_scope'), + 'Check': AttributeBinding(_UTILS_DIAGNOSTICS, 'Check'), + 'DiagnosticsReport': AttributeBinding(_UTILS_DIAGNOSTICS, 'DiagnosticsReport'), + 'run_diagnostics': AttributeBinding(_UTILS_DIAGNOSTICS, 'run_diagnostics'), + 'ConfigBundleExporter': AttributeBinding(_UTILS_CONFIG_BUNDLE, 'ConfigBundleExporter'), + 'ConfigBundleImporter': AttributeBinding(_UTILS_CONFIG_BUNDLE, 'ConfigBundleImporter'), + 'ImportReport': AttributeBinding(_UTILS_CONFIG_BUNDLE, 'ImportReport'), + 'export_config_bundle': AttributeBinding(_UTILS_CONFIG_BUNDLE, 'export_config_bundle'), + 'import_config_bundle': AttributeBinding(_UTILS_CONFIG_BUNDLE, 'import_config_bundle'), + 'TriggerEngine': AttributeBinding(_UTILS_TRIGGERS_TRIGGER_ENGINE, 'TriggerEngine'), + 'default_trigger_engine': AttributeBinding(_UTILS_TRIGGERS_TRIGGER_ENGINE, 'default_trigger_engine'), + 'ImageAppearsTrigger': AttributeBinding(_UTILS_TRIGGERS_TRIGGER_ENGINE, 'ImageAppearsTrigger'), + 'WindowAppearsTrigger': AttributeBinding(_UTILS_TRIGGERS_TRIGGER_ENGINE, 'WindowAppearsTrigger'), + 'PixelColorTrigger': AttributeBinding(_UTILS_TRIGGERS_TRIGGER_ENGINE, 'PixelColorTrigger'), + 'FilePathTrigger': AttributeBinding(_UTILS_TRIGGERS_TRIGGER_ENGINE, 'FilePathTrigger'), + 'AllOfTrigger': AttributeBinding(_UTILS_TRIGGERS_TRIGGER_ENGINE, 'AllOfTrigger'), + 'AnyOfTrigger': AttributeBinding(_UTILS_TRIGGERS_TRIGGER_ENGINE, 'AnyOfTrigger'), + 'SequenceTrigger': AttributeBinding(_UTILS_TRIGGERS_TRIGGER_ENGINE, 'SequenceTrigger'), + 'CronTrigger': AttributeBinding(_UTILS_TRIGGERS_TRIGGER_ENGINE, 'CronTrigger'), + 'WebhookTrigger': AttributeBinding(_UTILS_TRIGGERS_WEBHOOK_SERVER, 'WebhookTrigger'), + 'WebhookTriggerServer': AttributeBinding(_UTILS_TRIGGERS_WEBHOOK_SERVER, 'WebhookTriggerServer'), + 'default_webhook_server': AttributeBinding(_UTILS_TRIGGERS_WEBHOOK_SERVER, 'default_webhook_server'), + 'EmailTrigger': AttributeBinding(_UTILS_TRIGGERS_EMAIL_TRIGGER, 'EmailTrigger'), + 'EmailTriggerWatcher': AttributeBinding(_UTILS_TRIGGERS_EMAIL_TRIGGER, 'EmailTriggerWatcher'), + 'default_email_trigger_watcher': AttributeBinding(_UTILS_TRIGGERS_EMAIL_TRIGGER, 'default_email_trigger_watcher'), + 'ActionProfiler': AttributeBinding(_UTILS_PROFILER, 'ActionProfiler'), + 'ActionStats': AttributeBinding(_UTILS_PROFILER, 'ActionStats'), + 'default_profiler': AttributeBinding(_UTILS_PROFILER, 'default_profiler'), + 'SecretManager': AttributeBinding(_UTILS_SECRETS, 'SecretManager'), + 'SecretStoreError': AttributeBinding(_UTILS_SECRETS, 'SecretStoreError'), + 'SecretStoreLocked': AttributeBinding(_UTILS_SECRETS, 'SecretStoreLocked'), + 'default_secret_manager': AttributeBinding(_UTILS_SECRETS, 'default_secret_manager'), + 'default_secret_store_path': AttributeBinding(_UTILS_SECRETS, 'default_secret_store_path'), + 'VerifyResult': AttributeBinding(_UTILS_ACTION_SIGNING, 'VerifyResult'), + 'sign_action_file': AttributeBinding(_UTILS_ACTION_SIGNING, 'sign_action_file'), + 'verify_action_file': AttributeBinding(_UTILS_ACTION_SIGNING, 'verify_action_file'), + 'SigningConfig': AttributeBinding(_UTILS_ACTION_SIGNING, 'SigningConfig'), + 'action_signing_config': AttributeBinding(_UTILS_ACTION_SIGNING, 'action_signing_config'), + 'create_signing_keypair': AttributeBinding(_UTILS_ACTION_SIGNING, 'create_signing_keypair'), + 'CryptographyUnavailableError': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'CryptographyUnavailableError'), + 'require_signed_actions': AttributeBinding(_UTILS_ACTION_SIGNING, 'require_signed_actions'), + 'encrypt_action_file': AttributeBinding(_UTILS_ACTION_SIGNING, 'encrypt_action_file'), + 'decrypt_action_file': AttributeBinding(_UTILS_ACTION_SIGNING, 'decrypt_action_file'), + 'MetricCounter': AttributeBinding(_UTILS_OBSERVABILITY, 'Counter'), + 'MetricGauge': AttributeBinding(_UTILS_OBSERVABILITY, 'Gauge'), + 'MetricHistogram': AttributeBinding(_UTILS_OBSERVABILITY, 'Histogram'), + 'MetricRegistry': AttributeBinding(_UTILS_OBSERVABILITY, 'MetricRegistry'), + 'default_metric_registry': AttributeBinding(_UTILS_OBSERVABILITY, 'default_registry'), + 'PrometheusExporter': AttributeBinding(_UTILS_OBSERVABILITY, 'PrometheusExporter'), + 'default_metrics_exporter': AttributeBinding(_UTILS_OBSERVABILITY, 'default_exporter'), + 'render_metrics_text': AttributeBinding(_UTILS_OBSERVABILITY, 'render_metrics_text'), + 'Tracer': AttributeBinding(_UTILS_OBSERVABILITY, 'Tracer'), + 'default_tracer': AttributeBinding(_UTILS_OBSERVABILITY, 'default_tracer'), + 'traced': AttributeBinding(_UTILS_OBSERVABILITY, 'traced'), + 'HistoryStore': AttributeBinding(_UTILS_RUN_HISTORY_HISTORY_STORE, 'HistoryStore'), + 'JournalLink': AttributeBinding(_UTILS_RUN_HISTORY_HISTORY_STORE, 'JournalLink'), + 'RunRecord': AttributeBinding(_UTILS_RUN_HISTORY_HISTORY_STORE, 'RunRecord'), + 'default_history_store': AttributeBinding(_UTILS_RUN_HISTORY_HISTORY_STORE, 'default_history_store'), + 'HistoryBinding': AttributeBinding(_UTILS_RUN_HISTORY, 'HistoryBinding'), + 'manual_history_scope': AttributeBinding(_UTILS_RUN_HISTORY, 'manual_history_scope'), + 'ActionEvent': AttributeBinding(_UTILS_ACTION_JOURNAL, 'ActionEvent'), + 'ActionJournal': AttributeBinding(_UTILS_ACTION_JOURNAL, 'ActionJournal'), + 'ActionJournalError': AttributeBinding(_UTILS_ACTION_JOURNAL, 'ActionJournalError'), + 'JournalBinding': AttributeBinding(_UTILS_ACTION_JOURNAL, 'JournalBinding'), + 'JournalRunOptions': AttributeBinding(_UTILS_ACTION_JOURNAL, 'JournalRunOptions'), + 'JournalRunResult': AttributeBinding(_UTILS_ACTION_JOURNAL, 'JournalRunResult'), + 'execute_action_with_journal': AttributeBinding(_UTILS_ACTION_JOURNAL, 'execute_action_with_journal'), + 'execute_journal_run': AttributeBinding(_UTILS_ACTION_JOURNAL, 'execute_journal_run'), + 'inspect_journal': AttributeBinding(_UTILS_ACTION_JOURNAL, 'inspect_journal'), + 'journal_scope': AttributeBinding(_UTILS_ACTION_JOURNAL, 'journal_scope'), + 'read_events': AttributeBinding(_UTILS_ACTION_JOURNAL, 'read_events'), + 'AccessibilityElement': AttributeBinding(_UTILS_ACCESSIBILITY, 'AccessibilityElement'), + 'AccessibilityNotAvailableError': AttributeBinding(_UTILS_ACCESSIBILITY, 'AccessibilityNotAvailableError'), + 'accessibility_status': AttributeBinding(_UTILS_ACCESSIBILITY, 'accessibility_status'), + 'AccessibilityRecorder': AttributeBinding(_UTILS_ACCESSIBILITY, 'AccessibilityRecorder'), + 'AXRecorderEvent': AttributeBinding(_UTILS_ACCESSIBILITY, 'AXRecorderEvent'), + 'AXTreeNode': AttributeBinding(_UTILS_ACCESSIBILITY, 'AXTreeNode'), + 'click_accessibility_element': AttributeBinding(_UTILS_ACCESSIBILITY, 'click_accessibility_element'), + 'dump_accessibility_tree': AttributeBinding(_UTILS_ACCESSIBILITY, 'dump_accessibility_tree'), + 'find_accessibility_element': AttributeBinding(_UTILS_ACCESSIBILITY, 'find_accessibility_element'), + 'find_accessibility_elements': AttributeBinding(_UTILS_ACCESSIBILITY, 'find_accessibility_elements'), + 'focused_accessibility_element': AttributeBinding(_UTILS_ACCESSIBILITY, 'focused_accessibility_element'), + 'list_accessibility_elements': AttributeBinding(_UTILS_ACCESSIBILITY, 'list_accessibility_elements'), + 'control_get_state': AttributeBinding(_UTILS_ACCESSIBILITY, 'control_get_state'), + 'control_get_value': AttributeBinding(_UTILS_ACCESSIBILITY, 'control_get_value'), + 'control_set_value': AttributeBinding(_UTILS_ACCESSIBILITY, 'control_set_value'), + 'control_invoke': AttributeBinding(_UTILS_ACCESSIBILITY, 'control_invoke'), + 'control_toggle': AttributeBinding(_UTILS_ACCESSIBILITY, 'control_toggle'), + 'read_control_table': AttributeBinding(_UTILS_ACCESSIBILITY, 'read_control_table'), + 'expand_control': AttributeBinding(_UTILS_CONTROL_PATTERNS, 'expand_control'), + 'collapse_control': AttributeBinding(_UTILS_CONTROL_PATTERNS, 'collapse_control'), + 'control_expand_state': AttributeBinding(_UTILS_CONTROL_PATTERNS, 'control_expand_state'), + 'select_control_item': AttributeBinding(_UTILS_CONTROL_PATTERNS, 'select_control_item'), + 'control_range': AttributeBinding(_UTILS_CONTROL_PATTERNS, 'control_range'), + 'set_control_range': AttributeBinding(_UTILS_CONTROL_PATTERNS, 'set_control_range'), + 'scroll_control_into_view': AttributeBinding(_UTILS_CONTROL_PATTERNS, 'scroll_control_into_view'), + 'get_control_text': AttributeBinding(_UTILS_AX_TEXT, 'get_control_text'), + 'get_selected_text': AttributeBinding(_UTILS_AX_TEXT, 'get_selected_text'), + 'get_visible_text': AttributeBinding(_UTILS_AX_TEXT, 'get_visible_text'), + 'find_control_text': AttributeBinding(_UTILS_AX_TEXT, 'find_control_text'), + 'select_control_text': AttributeBinding(_UTILS_AX_TEXT, 'select_control_text'), + 'control_text_attributes': AttributeBinding(_UTILS_AX_TEXT, 'control_text_attributes'), + 'control_type_name': AttributeBinding(_UTILS_AX_TREE_WALK, 'control_type_name'), + 'humanize_role': AttributeBinding(_UTILS_AX_TREE_WALK, 'humanize_role'), + 'humanize_tree': AttributeBinding(_UTILS_AX_TREE_WALK, 'humanize_tree'), + 'assign_node_paths': AttributeBinding(_UTILS_AX_TREE_WALK, 'assign_node_paths'), + 'find_by_path': AttributeBinding(_UTILS_AX_TREE_WALK, 'find_by_path'), + 'is_interactive_role': AttributeBinding(_UTILS_FOCUS_ORDER, 'is_interactive_role'), + 'tab_order': AttributeBinding(_UTILS_FOCUS_ORDER, 'tab_order'), + 'audit_focus_order': AttributeBinding(_UTILS_FOCUS_ORDER, 'audit_focus_order'), + 'focus_control': AttributeBinding(_UTILS_FOCUS_ORDER, 'focus_control'), 'realize_item': AttributeBinding('je_auto_control.utils.virtualized', 'realize_item'), - 'get_element_properties': AttributeBinding('je_auto_control.utils.ax_props', 'get_element_properties'), - 'is_element_enabled': AttributeBinding('je_auto_control.utils.ax_props', 'is_element_enabled'), - 'table_headers': AttributeBinding('je_auto_control.utils.table_pattern', 'table_headers'), - 'table_cell': AttributeBinding('je_auto_control.utils.table_pattern', 'table_cell'), - 'cell_by_header': AttributeBinding('je_auto_control.utils.table_pattern', 'cell_by_header'), - 'move_element': AttributeBinding('je_auto_control.utils.transform_window', 'move_element'), - 'resize_element': AttributeBinding('je_auto_control.utils.transform_window', 'resize_element'), - 'set_window_state': AttributeBinding('je_auto_control.utils.transform_window', 'set_window_state'), - 'window_interaction_state': AttributeBinding('je_auto_control.utils.transform_window', 'window_interaction_state'), - 'legacy_info': AttributeBinding('je_auto_control.utils.legacy_accessible', 'legacy_info'), - 'legacy_default_action': AttributeBinding('je_auto_control.utils.legacy_accessible', 'legacy_default_action'), - 'get_selection': AttributeBinding('je_auto_control.utils.selection_view', 'get_selection'), - 'list_views': AttributeBinding('je_auto_control.utils.selection_view', 'list_views'), - 'set_view': AttributeBinding('je_auto_control.utils.selection_view', 'set_view'), + 'get_element_properties': AttributeBinding(_UTILS_AX_PROPS, 'get_element_properties'), + 'is_element_enabled': AttributeBinding(_UTILS_AX_PROPS, 'is_element_enabled'), + 'table_headers': AttributeBinding(_UTILS_TABLE_PATTERN, 'table_headers'), + 'table_cell': AttributeBinding(_UTILS_TABLE_PATTERN, 'table_cell'), + 'cell_by_header': AttributeBinding(_UTILS_TABLE_PATTERN, 'cell_by_header'), + 'move_element': AttributeBinding(_UTILS_TRANSFORM_WINDOW, 'move_element'), + 'resize_element': AttributeBinding(_UTILS_TRANSFORM_WINDOW, 'resize_element'), + 'set_window_state': AttributeBinding(_UTILS_TRANSFORM_WINDOW, 'set_window_state'), + 'window_interaction_state': AttributeBinding(_UTILS_TRANSFORM_WINDOW, 'window_interaction_state'), + 'legacy_info': AttributeBinding(_UTILS_LEGACY_ACCESSIBLE, 'legacy_info'), + 'legacy_default_action': AttributeBinding(_UTILS_LEGACY_ACCESSIBLE, 'legacy_default_action'), + 'get_selection': AttributeBinding(_UTILS_SELECTION_VIEW, 'get_selection'), + 'list_views': AttributeBinding(_UTILS_SELECTION_VIEW, 'list_views'), + 'set_view': AttributeBinding(_UTILS_SELECTION_VIEW, 'set_view'), 'wait_for_focus_change': AttributeBinding('je_auto_control.utils.ax_events', 'wait_for_focus_change'), - 'plan_open': AttributeBinding('je_auto_control.utils.shell_open', 'plan_open'), - 'open_path': AttributeBinding('je_auto_control.utils.shell_open', 'open_path'), - 'idle_seconds': AttributeBinding('je_auto_control.utils.idle_keepawake', 'idle_seconds'), - 'is_idle': AttributeBinding('je_auto_control.utils.idle_keepawake', 'is_idle'), - 'plan_keep_awake': AttributeBinding('je_auto_control.utils.idle_keepawake', 'plan_keep_awake'), - 'keep_awake': AttributeBinding('je_auto_control.utils.idle_keepawake', 'keep_awake'), - 'keep_awake_on': AttributeBinding('je_auto_control.utils.idle_keepawake', 'keep_awake_on'), - 'allow_sleep': AttributeBinding('je_auto_control.utils.idle_keepawake', 'allow_sleep'), - 'normalize_ext': AttributeBinding('je_auto_control.utils.file_assoc', 'normalize_ext'), - 'file_association': AttributeBinding('je_auto_control.utils.file_assoc', 'file_association'), - 'get_volume': AttributeBinding('je_auto_control.utils.system_volume', 'get_volume'), - 'set_volume': AttributeBinding('je_auto_control.utils.system_volume', 'set_volume'), - 'change_volume': AttributeBinding('je_auto_control.utils.system_volume', 'change_volume'), - 'is_muted': AttributeBinding('je_auto_control.utils.system_volume', 'is_muted'), - 'set_mute': AttributeBinding('je_auto_control.utils.system_volume', 'set_mute'), - 'mute': AttributeBinding('je_auto_control.utils.system_volume', 'mute'), - 'unmute': AttributeBinding('je_auto_control.utils.system_volume', 'unmute'), - 'toggle_mute': AttributeBinding('je_auto_control.utils.system_volume', 'toggle_mute'), - 'lock_session': AttributeBinding('je_auto_control.utils.lock_session', 'lock_session'), - 'plan_lock_session': AttributeBinding('je_auto_control.utils.lock_session', 'plan_lock_session'), - 'wait_for_unlock': AttributeBinding('je_auto_control.utils.lock_session', 'wait_for_unlock'), - 'wait_for_lock': AttributeBinding('je_auto_control.utils.lock_session', 'wait_for_lock'), - 'classify_lock_transitions': AttributeBinding('je_auto_control.utils.lock_session', 'classify_lock_transitions'), - 'ime_state': AttributeBinding('je_auto_control.utils.ime_state', 'ime_state'), - 'is_composing': AttributeBinding('je_auto_control.utils.ime_state', 'is_composing'), - 'wait_for_composition_commit': AttributeBinding('je_auto_control.utils.ime_state', 'wait_for_composition_commit'), - 'decode_conversion_mode': AttributeBinding('je_auto_control.utils.ime_state', 'decode_conversion_mode'), - 'RetryBudget': AttributeBinding('je_auto_control.utils.retry_budget', 'RetryBudget'), - 'run_with_budget': AttributeBinding('je_auto_control.utils.retry_budget', 'run_with_budget'), - 'backoff_delay': AttributeBinding('je_auto_control.utils.retry_budget', 'backoff_delay'), - 'jittered_delay': AttributeBinding('je_auto_control.utils.retry_budget', 'jittered_delay'), - 'compare_field_value': AttributeBinding('je_auto_control.utils.verify_field', 'compare_field_value'), - 'verify_field_value': AttributeBinding('je_auto_control.utils.verify_field', 'verify_field_value'), - 'fill_and_verify': AttributeBinding('je_auto_control.utils.verify_field', 'fill_and_verify'), - 'recommend_timeout': AttributeBinding('je_auto_control.utils.adaptive_timeout', 'recommend_timeout'), - 'timeout_stats': AttributeBinding('je_auto_control.utils.adaptive_timeout', 'timeout_stats'), - 'ensure_state': AttributeBinding('je_auto_control.utils.ensure_state', 'ensure_state'), - 'ensure_toggle': AttributeBinding('je_auto_control.utils.ensure_state', 'ensure_toggle'), - 'wait_until_app_idle': AttributeBinding('je_auto_control.utils.app_idle', 'wait_until_app_idle'), - 'idle_point': AttributeBinding('je_auto_control.utils.app_idle', 'idle_point'), - 'simulate_cvd': AttributeBinding('je_auto_control.utils.cvd_simulate', 'simulate_cvd'), - 'colors_collide': AttributeBinding('je_auto_control.utils.cvd_simulate', 'colors_collide'), - 'color_distance': AttributeBinding('je_auto_control.utils.cvd_simulate', 'color_distance'), - 'place_labels': AttributeBinding('je_auto_control.utils.marks_layout', 'place_labels'), - 'label_color': AttributeBinding('je_auto_control.utils.marks_layout', 'label_color'), - 'grade_contrast': AttributeBinding('je_auto_control.utils.contrast_map', 'grade_contrast'), - 'dominant_pair': AttributeBinding('je_auto_control.utils.contrast_map', 'dominant_pair'), - 'region_contrast': AttributeBinding('je_auto_control.utils.contrast_map', 'region_contrast'), - 'normalize_theme': AttributeBinding('je_auto_control.utils.theme_normalize', 'normalize_theme'), - 'match_theme': AttributeBinding('je_auto_control.utils.theme_normalize', 'match_theme'), - 'localize_changes': AttributeBinding('je_auto_control.utils.change_localize', 'localize_changes'), - 'rank_changes': AttributeBinding('je_auto_control.utils.change_localize', 'rank_changes'), - 'classify_widget': AttributeBinding('je_auto_control.utils.icon_classify', 'classify_widget'), - 'box_features': AttributeBinding('je_auto_control.utils.icon_classify', 'box_features'), - 'classify_icon': AttributeBinding('je_auto_control.utils.icon_classify', 'classify_icon'), - 'propose_elements': AttributeBinding('je_auto_control.utils.element_proposal', 'propose_elements'), - 'tag_kinds': AttributeBinding('je_auto_control.utils.element_proposal', 'tag_kinds'), - 'act_in_view': AttributeBinding('je_auto_control.utils.act_in_view', 'act_in_view'), - 'ScrollPlan': AttributeBinding('je_auto_control.utils.act_in_view', 'ScrollPlan'), + 'plan_open': AttributeBinding(_UTILS_SHELL_OPEN, 'plan_open'), + 'open_path': AttributeBinding(_UTILS_SHELL_OPEN, 'open_path'), + 'idle_seconds': AttributeBinding(_UTILS_IDLE_KEEPAWAKE, 'idle_seconds'), + 'is_idle': AttributeBinding(_UTILS_IDLE_KEEPAWAKE, 'is_idle'), + 'plan_keep_awake': AttributeBinding(_UTILS_IDLE_KEEPAWAKE, 'plan_keep_awake'), + 'keep_awake': AttributeBinding(_UTILS_IDLE_KEEPAWAKE, 'keep_awake'), + 'keep_awake_on': AttributeBinding(_UTILS_IDLE_KEEPAWAKE, 'keep_awake_on'), + 'allow_sleep': AttributeBinding(_UTILS_IDLE_KEEPAWAKE, 'allow_sleep'), + 'normalize_ext': AttributeBinding(_UTILS_FILE_ASSOC, 'normalize_ext'), + 'file_association': AttributeBinding(_UTILS_FILE_ASSOC, 'file_association'), + 'get_volume': AttributeBinding(_UTILS_SYSTEM_VOLUME, 'get_volume'), + 'set_volume': AttributeBinding(_UTILS_SYSTEM_VOLUME, 'set_volume'), + 'change_volume': AttributeBinding(_UTILS_SYSTEM_VOLUME, 'change_volume'), + 'is_muted': AttributeBinding(_UTILS_SYSTEM_VOLUME, 'is_muted'), + 'set_mute': AttributeBinding(_UTILS_SYSTEM_VOLUME, 'set_mute'), + 'mute': AttributeBinding(_UTILS_SYSTEM_VOLUME, 'mute'), + 'unmute': AttributeBinding(_UTILS_SYSTEM_VOLUME, 'unmute'), + 'toggle_mute': AttributeBinding(_UTILS_SYSTEM_VOLUME, 'toggle_mute'), + 'lock_session': AttributeBinding(_UTILS_LOCK_SESSION, 'lock_session'), + 'plan_lock_session': AttributeBinding(_UTILS_LOCK_SESSION, 'plan_lock_session'), + 'wait_for_unlock': AttributeBinding(_UTILS_LOCK_SESSION, 'wait_for_unlock'), + 'wait_for_lock': AttributeBinding(_UTILS_LOCK_SESSION, 'wait_for_lock'), + 'classify_lock_transitions': AttributeBinding(_UTILS_LOCK_SESSION, 'classify_lock_transitions'), + 'ime_state': AttributeBinding(_UTILS_IME_STATE, 'ime_state'), + 'is_composing': AttributeBinding(_UTILS_IME_STATE, 'is_composing'), + 'wait_for_composition_commit': AttributeBinding(_UTILS_IME_STATE, 'wait_for_composition_commit'), + 'decode_conversion_mode': AttributeBinding(_UTILS_IME_STATE, 'decode_conversion_mode'), + 'RetryBudget': AttributeBinding(_UTILS_RETRY_BUDGET, 'RetryBudget'), + 'run_with_budget': AttributeBinding(_UTILS_RETRY_BUDGET, 'run_with_budget'), + 'backoff_delay': AttributeBinding(_UTILS_RETRY_BUDGET, 'backoff_delay'), + 'jittered_delay': AttributeBinding(_UTILS_RETRY_BUDGET, 'jittered_delay'), + 'compare_field_value': AttributeBinding(_UTILS_VERIFY_FIELD, 'compare_field_value'), + 'verify_field_value': AttributeBinding(_UTILS_VERIFY_FIELD, 'verify_field_value'), + 'fill_and_verify': AttributeBinding(_UTILS_VERIFY_FIELD, 'fill_and_verify'), + 'recommend_timeout': AttributeBinding(_UTILS_ADAPTIVE_TIMEOUT, 'recommend_timeout'), + 'timeout_stats': AttributeBinding(_UTILS_ADAPTIVE_TIMEOUT, 'timeout_stats'), + 'ensure_state': AttributeBinding(_UTILS_ENSURE_STATE, 'ensure_state'), + 'ensure_toggle': AttributeBinding(_UTILS_ENSURE_STATE, 'ensure_toggle'), + 'wait_until_app_idle': AttributeBinding(_UTILS_APP_IDLE, 'wait_until_app_idle'), + 'idle_point': AttributeBinding(_UTILS_APP_IDLE, 'idle_point'), + 'simulate_cvd': AttributeBinding(_UTILS_CVD_SIMULATE, 'simulate_cvd'), + 'colors_collide': AttributeBinding(_UTILS_CVD_SIMULATE, 'colors_collide'), + 'color_distance': AttributeBinding(_UTILS_CVD_SIMULATE, 'color_distance'), + 'place_labels': AttributeBinding(_UTILS_MARKS_LAYOUT, 'place_labels'), + 'label_color': AttributeBinding(_UTILS_MARKS_LAYOUT, 'label_color'), + 'grade_contrast': AttributeBinding(_UTILS_CONTRAST_MAP, 'grade_contrast'), + 'dominant_pair': AttributeBinding(_UTILS_CONTRAST_MAP, 'dominant_pair'), + 'region_contrast': AttributeBinding(_UTILS_CONTRAST_MAP, 'region_contrast'), + 'normalize_theme': AttributeBinding(_UTILS_THEME_NORMALIZE, 'normalize_theme'), + 'match_theme': AttributeBinding(_UTILS_THEME_NORMALIZE, 'match_theme'), + 'localize_changes': AttributeBinding(_UTILS_CHANGE_LOCALIZE, 'localize_changes'), + 'rank_changes': AttributeBinding(_UTILS_CHANGE_LOCALIZE, 'rank_changes'), + 'classify_widget': AttributeBinding(_UTILS_ICON_CLASSIFY, 'classify_widget'), + 'box_features': AttributeBinding(_UTILS_ICON_CLASSIFY, 'box_features'), + 'classify_icon': AttributeBinding(_UTILS_ICON_CLASSIFY, 'classify_icon'), + 'propose_elements': AttributeBinding(_UTILS_ELEMENT_PROPOSAL, 'propose_elements'), + 'tag_kinds': AttributeBinding(_UTILS_ELEMENT_PROPOSAL, 'tag_kinds'), + 'act_in_view': AttributeBinding(_UTILS_ACT_IN_VIEW, 'act_in_view'), + 'ScrollPlan': AttributeBinding(_UTILS_ACT_IN_VIEW, 'ScrollPlan'), 'act_with_mode': AttributeBinding('je_auto_control.utils.act_modes', 'act_with_mode'), - 'build_rtf': AttributeBinding('je_auto_control.utils.clipboard_rich_formats', 'build_rtf'), - 'rtf_to_text': AttributeBinding('je_auto_control.utils.clipboard_rich_formats', 'rtf_to_text'), - 'rows_to_csv': AttributeBinding('je_auto_control.utils.clipboard_rich_formats', 'rows_to_csv'), - 'csv_to_rows': AttributeBinding('je_auto_control.utils.clipboard_rich_formats', 'csv_to_rows'), - 'set_clipboard_rtf': AttributeBinding('je_auto_control.utils.clipboard_rich_formats', 'set_clipboard_rtf'), - 'get_clipboard_rtf': AttributeBinding('je_auto_control.utils.clipboard_rich_formats', 'get_clipboard_rtf'), - 'set_clipboard_csv': AttributeBinding('je_auto_control.utils.clipboard_rich_formats', 'set_clipboard_csv'), - 'get_clipboard_csv': AttributeBinding('je_auto_control.utils.clipboard_rich_formats', 'get_clipboard_csv'), - 'classify_format': AttributeBinding('je_auto_control.utils.clipboard_formats', 'classify_format'), - 'classify_formats': AttributeBinding('je_auto_control.utils.clipboard_formats', 'classify_formats'), - 'diff_formats': AttributeBinding('je_auto_control.utils.clipboard_formats', 'diff_formats'), - 'list_clipboard_formats': AttributeBinding('je_auto_control.utils.clipboard_formats', 'list_clipboard_formats'), - 'clipboard_formats': AttributeBinding('je_auto_control.utils.clipboard_formats', 'clipboard_formats'), - 'plan_file_drop': AttributeBinding('je_auto_control.utils.file_drop', 'plan_file_drop'), - 'drop_files': AttributeBinding('je_auto_control.utils.file_drop', 'drop_files'), - 'image_quality': AttributeBinding('je_auto_control.utils.image_quality', 'image_quality'), - 'is_blurry': AttributeBinding('je_auto_control.utils.image_quality', 'is_blurry'), - 'quality_gate': AttributeBinding('je_auto_control.utils.image_quality', 'quality_gate'), - 'detect_scale': AttributeBinding('je_auto_control.utils.scale_detect', 'detect_scale'), - 'scale_sweep': AttributeBinding('je_auto_control.utils.scale_detect', 'scale_sweep'), - 'saliency_map': AttributeBinding('je_auto_control.utils.saliency', 'saliency_map'), - 'salient_regions': AttributeBinding('je_auto_control.utils.saliency', 'salient_regions'), - 'most_salient': AttributeBinding('je_auto_control.utils.saliency', 'most_salient'), - 'normalize_error': AttributeBinding('je_auto_control.utils.failure_signature', 'normalize_error'), - 'failure_signature': AttributeBinding('je_auto_control.utils.failure_signature', 'failure_signature'), - 'group_failures': AttributeBinding('je_auto_control.utils.failure_signature', 'group_failures'), - 'diff_runs': AttributeBinding('je_auto_control.utils.run_diff', 'diff_runs'), - 'summarize_run_diff': AttributeBinding('je_auto_control.utils.run_diff', 'summarize_run_diff'), - 'cofailure_pairs': AttributeBinding('je_auto_control.utils.flake_cluster', 'cofailure_pairs'), - 'failure_clusters': AttributeBinding('je_auto_control.utils.flake_cluster', 'failure_clusters'), - 'build_timeline': AttributeBinding('je_auto_control.utils.step_timeline', 'build_timeline'), - 'critical_steps': AttributeBinding('je_auto_control.utils.step_timeline', 'critical_steps'), - 'VLMNotAvailableError': AttributeBinding('je_auto_control.utils.vision', 'VLMNotAvailableError'), - 'locate_by_description': AttributeBinding('je_auto_control.utils.vision', 'locate_by_description'), - 'click_by_description': AttributeBinding('je_auto_control.utils.vision', 'click_by_description'), - 'verify_description': AttributeBinding('je_auto_control.utils.vision', 'verify_description'), - 'LLMBackend': AttributeBinding('je_auto_control.utils.llm', 'LLMBackend'), - 'LLMNotAvailableError': AttributeBinding('je_auto_control.utils.llm', 'LLMNotAvailableError'), - 'LLMPlanError': AttributeBinding('je_auto_control.utils.llm', 'LLMPlanError'), - 'plan_actions': AttributeBinding('je_auto_control.utils.llm', 'plan_actions'), - 'run_from_description': AttributeBinding('je_auto_control.utils.llm', 'run_from_description'), - 'AgentBackend': AttributeBinding('je_auto_control.utils.agent.agent_loop', 'AgentBackend'), - 'AgentBudget': AttributeBinding('je_auto_control.utils.agent.agent_loop', 'AgentBudget'), - 'AgentLoop': AttributeBinding('je_auto_control.utils.agent.agent_loop', 'AgentLoop'), - 'AgentResult': AttributeBinding('je_auto_control.utils.agent.agent_loop', 'AgentResult'), - 'AgentStep': AttributeBinding('je_auto_control.utils.agent.agent_loop', 'AgentStep'), - 'FakeAgentBackend': AttributeBinding('je_auto_control.utils.agent.agent_loop', 'FakeAgentBackend'), - 'run_agent': AttributeBinding('je_auto_control.utils.agent.agent_loop', 'run_agent'), - 'AgentBackendError': AttributeBinding('je_auto_control.utils.agent.backends', 'AgentBackendError'), - 'AnthropicAgentBackend': AttributeBinding('je_auto_control.utils.agent.backends', 'AnthropicAgentBackend'), - 'ComputerUseAgentBackend': AttributeBinding('je_auto_control.utils.agent.backends', 'ComputerUseAgentBackend'), - 'OpenAIAgentBackend': AttributeBinding('je_auto_control.utils.agent.backends', 'OpenAIAgentBackend'), - 'run_computer_use': AttributeBinding('je_auto_control.utils.agent.computer_use', 'run_computer_use'), - 'computer_use_result_to_dict': AttributeBinding('je_auto_control.utils.agent.computer_use', 'result_to_dict'), - 'DagDefinition': AttributeBinding('je_auto_control.utils.dag', 'DagDefinition'), - 'DagDefinitionError': AttributeBinding('je_auto_control.utils.dag', 'DagDefinitionError'), - 'DagNode': AttributeBinding('je_auto_control.utils.dag', 'DagNode'), - 'DagRunResult': AttributeBinding('je_auto_control.utils.dag', 'DagRunResult'), - 'NodeResult': AttributeBinding('je_auto_control.utils.dag', 'NodeResult'), - 'parse_dag': AttributeBinding('je_auto_control.utils.dag', 'parse_dag'), - 'run_dag': AttributeBinding('je_auto_control.utils.dag', 'run_dag'), - 'PresenceError': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'PresenceError'), - 'PresenceRegistry': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'PresenceRegistry'), - 'ROLE_CONTROLLER': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'ROLE_CONTROLLER'), - 'ROLE_OBSERVER': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'ROLE_OBSERVER'), - 'ViewerPresence': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'ViewerPresence'), - 'default_presence_registry': - AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'default_presence_registry'), - 'ChatOpsError': AttributeBinding('je_auto_control.utils.chatops', 'ChatOpsError'), - 'CommandResult': AttributeBinding('je_auto_control.utils.chatops', 'CommandResult'), - 'CommandRouter': AttributeBinding('je_auto_control.utils.chatops', 'CommandRouter'), - 'SlackBot': AttributeBinding('je_auto_control.utils.chatops', 'SlackBot'), - 'SlackError': AttributeBinding('je_auto_control.utils.chatops', 'SlackError'), - 'make_default_slack_bot': AttributeBinding('je_auto_control.utils.chatops', 'make_default_slack_bot'), - 'register_chatops_default_commands': - AttributeBinding('je_auto_control.utils.chatops', 'register_chatops_default_commands'), - 'AnchorLocator': AttributeBinding('je_auto_control.utils.anchor_locator', 'Locator'), - 'AnchorLocatorError': AttributeBinding('je_auto_control.utils.anchor_locator', 'AnchorLocatorError'), - 'AnchorOutcome': AttributeBinding('je_auto_control.utils.anchor_locator', 'AnchorOutcome'), - 'a11y_locator': AttributeBinding('je_auto_control.utils.anchor_locator', 'a11y_locator'), - 'anchor_locate': AttributeBinding('je_auto_control.utils.anchor_locator', 'anchor_locate'), - 'anchor_locate_all': AttributeBinding('je_auto_control.utils.anchor_locator', 'anchor_locate_all'), - 'image_locator': AttributeBinding('je_auto_control.utils.anchor_locator', 'image_locator'), - 'ocr_locator': AttributeBinding('je_auto_control.utils.anchor_locator', 'ocr_locator'), - 'vlm_locator': AttributeBinding('je_auto_control.utils.anchor_locator', 'vlm_locator'), - 'OCRField': AttributeBinding('je_auto_control.utils.ocr.structure', 'OCRField'), - 'OCRRow': AttributeBinding('je_auto_control.utils.ocr.structure', 'OCRRow'), - 'OCRTable': AttributeBinding('je_auto_control.utils.ocr.structure', 'OCRTable'), - 'StructuredOCR': AttributeBinding('je_auto_control.utils.ocr.structure', 'StructuredOCR'), - 'ocr_cluster_matches': AttributeBinding('je_auto_control.utils.ocr.structure', 'cluster_matches'), - 'ocr_read_structure': AttributeBinding('je_auto_control.utils.ocr.structure', 'read_structure'), - 'WaitOutcome': AttributeBinding('je_auto_control.utils.smart_waits', 'WaitOutcome'), - 'wait_until_pixel_changes': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_pixel_changes'), - 'wait_until_region_idle': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_region_idle'), - 'wait_until_screen_stable': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_screen_stable'), - 'wait_until_clipboard_changes': - AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_clipboard_changes'), - 'wait_until_window_closed': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_window_closed'), - 'wait_until_file': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_file'), - 'wait_until_port': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_port'), - 'wait_until_process': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_process'), - 'wait_until_gone': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_gone'), - 'wait_until_image_gone': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_image_gone'), - 'wait_until_text_gone': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_text_gone'), - 'wait_until_color': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_color'), - 'wait_until_window_title': AttributeBinding('je_auto_control.utils.smart_waits', 'wait_until_window_title'), - 'take_golden': AttributeBinding('je_auto_control.utils.visual_regression', 'take_golden'), - 'compare_to_golden': AttributeBinding('je_auto_control.utils.visual_regression', 'compare_to_golden'), - 'image_difference': AttributeBinding('je_auto_control.utils.visual_regression', 'image_difference'), - 'DiffResult': AttributeBinding('je_auto_control.utils.visual_regression', 'DiffResult'), - 'MaskRegion': AttributeBinding('je_auto_control.utils.visual_regression', 'MaskRegion'), - 'run_state_machine': AttributeBinding('je_auto_control.utils.state_machine', 'run_state_machine'), - 'StateMachine': AttributeBinding('je_auto_control.utils.state_machine', 'StateMachine'), - 'StateMachineError': AttributeBinding('je_auto_control.utils.state_machine', 'StateMachineError'), - 'AssertionResult': AttributeBinding('je_auto_control.utils.assertion', 'AssertionResult'), - 'assert_image': AttributeBinding('je_auto_control.utils.assertion', 'assert_image'), - 'assert_pixel': AttributeBinding('je_auto_control.utils.assertion', 'assert_pixel'), - 'assert_text': AttributeBinding('je_auto_control.utils.assertion', 'assert_text'), - 'assert_window': AttributeBinding('je_auto_control.utils.assertion', 'assert_window'), - 'assert_clipboard': AttributeBinding('je_auto_control.utils.assertion', 'assert_clipboard'), - 'assert_process': AttributeBinding('je_auto_control.utils.assertion', 'assert_process'), - 'assert_file': AttributeBinding('je_auto_control.utils.assertion', 'assert_file'), - 'assert_http': AttributeBinding('je_auto_control.utils.assertion', 'assert_http'), - 'assert_by_description': AttributeBinding('je_auto_control.utils.assertion', 'assert_by_description'), - 'assert_duration': AttributeBinding('je_auto_control.utils.assertion', 'assert_duration'), - 'assert_variable': AttributeBinding('je_auto_control.utils.assertion', 'assert_variable'), - 'GroupAssertionResult': AttributeBinding('je_auto_control.utils.assertion', 'GroupAssertionResult'), - 'assert_all': AttributeBinding('je_auto_control.utils.assertion', 'assert_all'), - 'assert_any': AttributeBinding('je_auto_control.utils.assertion', 'assert_any'), - 'assert_eventually': AttributeBinding('je_auto_control.utils.assertion', 'assert_eventually'), - 'run_assertion_spec': AttributeBinding('je_auto_control.utils.assertion', 'run_assertion_spec'), - 'data_source_kinds': AttributeBinding('je_auto_control.utils.data_source', 'data_source_kinds'), - 'load_rows': AttributeBinding('je_auto_control.utils.data_source', 'load_rows'), - 'FlakinessReport': AttributeBinding('je_auto_control.utils.flakiness', 'FlakinessReport'), - 'FlakyEntry': AttributeBinding('je_auto_control.utils.flakiness', 'FlakyEntry'), - 'analyze_flakiness': AttributeBinding('je_auto_control.utils.flakiness', 'analyze_flakiness'), - 'TestCaseResult': AttributeBinding('je_auto_control.utils.test_suite', 'TestCaseResult'), - 'TestSuiteResult': AttributeBinding('je_auto_control.utils.test_suite', 'TestSuiteResult'), - 'run_suite': AttributeBinding('je_auto_control.utils.test_suite', 'run_suite'), - 'to_allure_results': AttributeBinding('je_auto_control.utils.test_suite', 'to_allure_results'), - 'to_junit_xml': AttributeBinding('je_auto_control.utils.test_suite', 'to_junit_xml'), - 'write_allure_results': AttributeBinding('je_auto_control.utils.test_suite', 'write_allure_results'), - 'write_junit_xml': AttributeBinding('je_auto_control.utils.test_suite', 'write_junit_xml'), - 'QuarantineEntry': AttributeBinding('je_auto_control.utils.quarantine', 'QuarantineEntry'), - 'QuarantineStore': AttributeBinding('je_auto_control.utils.quarantine', 'QuarantineStore'), - 'auto_quarantine_from_flakiness': - AttributeBinding('je_auto_control.utils.quarantine', 'auto_quarantine_from_flakiness'), - 'default_quarantine_store': AttributeBinding('je_auto_control.utils.quarantine', 'default_quarantine_store'), - 'AuditIssue': AttributeBinding('je_auto_control.utils.a11y_audit', 'AuditIssue'), - 'AuditReport': AttributeBinding('je_auto_control.utils.a11y_audit', 'AuditReport'), - 'audit_contrast': AttributeBinding('je_auto_control.utils.a11y_audit', 'audit_contrast'), - 'audit_missing_labels': AttributeBinding('je_auto_control.utils.a11y_audit', 'audit_missing_labels'), - 'audit_target_size': AttributeBinding('je_auto_control.utils.a11y_audit', 'audit_target_size'), - 'contrast_ratio': AttributeBinding('je_auto_control.utils.a11y_audit', 'contrast_ratio'), - 'detect_truncation': AttributeBinding('je_auto_control.utils.a11y_audit', 'detect_truncation'), - 'run_audit': AttributeBinding('je_auto_control.utils.a11y_audit', 'run_audit'), - 'wcag_audit': AttributeBinding('je_auto_control.utils.a11y_audit', 'wcag_audit'), - 'DeviceResult': AttributeBinding('je_auto_control.utils.device_matrix', 'DeviceResult'), - 'MatrixReport': AttributeBinding('je_auto_control.utils.device_matrix', 'MatrixReport'), - 'run_on_devices': AttributeBinding('je_auto_control.utils.device_matrix', 'run_on_devices'), - 'MediaAssertionResult': AttributeBinding('je_auto_control.utils.media_assert', 'MediaAssertionResult'), - 'assert_audio_activity': AttributeBinding('je_auto_control.utils.media_assert', 'assert_audio_activity'), - 'assert_video_changes': AttributeBinding('je_auto_control.utils.media_assert', 'assert_video_changes'), - 'measure_audio_rms': AttributeBinding('je_auto_control.utils.media_assert', 'measure_audio_rms'), - 'video_segment_motion': AttributeBinding('je_auto_control.utils.media_assert', 'video_segment_motion'), - 'CostEvent': AttributeBinding('je_auto_control.utils.cost_telemetry', 'CostEvent'), - 'CostSummary': AttributeBinding('je_auto_control.utils.cost_telemetry', 'CostSummary'), - 'default_cost_store': AttributeBinding('je_auto_control.utils.cost_telemetry', 'default_cost_store'), - 'estimate_llm_usd': AttributeBinding('je_auto_control.utils.cost_telemetry', 'estimate_llm_usd'), - 'record_llm_call': AttributeBinding('je_auto_control.utils.cost_telemetry', 'record_llm_call'), - 'summarise_llm_costs': AttributeBinding('je_auto_control.utils.cost_telemetry', 'summarise_llm_costs'), - 'FailureHookManager': AttributeBinding('je_auto_control.utils.failure_hooks', 'FailureHookManager'), - 'FailureReport': AttributeBinding('je_auto_control.utils.failure_hooks', 'FailureReport'), - 'GitHubBackend': AttributeBinding('je_auto_control.utils.failure_hooks', 'GitHubBackend'), - 'JiraBackend': AttributeBinding('je_auto_control.utils.failure_hooks', 'JiraBackend'), - 'LinearBackend': AttributeBinding('je_auto_control.utils.failure_hooks', 'LinearBackend'), - 'TicketResult': AttributeBinding('je_auto_control.utils.failure_hooks', 'TicketResult'), - 'default_failure_hook_manager': - AttributeBinding('je_auto_control.utils.failure_hooks', 'default_failure_hook_manager'), - 'ABRunOutcome': AttributeBinding('je_auto_control.utils.ab_locator', 'ABRunOutcome'), - 'ab_best_strategy': AttributeBinding('je_auto_control.utils.ab_locator', 'ab_best_strategy'), - 'ab_locate': AttributeBinding('je_auto_control.utils.ab_locator', 'ab_locate'), - 'ab_report_for': AttributeBinding('je_auto_control.utils.ab_locator', 'ab_report_for'), - 'default_ab_store': AttributeBinding('je_auto_control.utils.ab_locator', 'default_ab_store'), - 'HealEvent': AttributeBinding('je_auto_control.utils.self_healing', 'HealEvent'), - 'HealEventLog': AttributeBinding('je_auto_control.utils.self_healing', 'HealEventLog'), - 'HealOutcome': AttributeBinding('je_auto_control.utils.self_healing', 'HealOutcome'), - 'SelfHealError': AttributeBinding('je_auto_control.utils.self_healing', 'SelfHealError'), - 'default_heal_log': AttributeBinding('je_auto_control.utils.self_healing', 'default_heal_log'), - 'self_heal_click': AttributeBinding('je_auto_control.utils.self_healing', 'self_heal_click'), - 'self_heal_locate': AttributeBinding('je_auto_control.utils.self_healing', 'self_heal_locate'), - 'SelfHealRequest': AttributeBinding('je_auto_control.utils.self_healing', 'SelfHealRequest'), - 'SelfHealRequestError': AttributeBinding('je_auto_control.utils.self_healing', 'SelfHealRequestError'), - 'SelfHealRunResult': AttributeBinding('je_auto_control.utils.self_healing', 'SelfHealRunResult'), - 'SelfHealVerificationError': AttributeBinding('je_auto_control.utils.self_healing', 'SelfHealVerificationError'), - 'VerificationRequest': AttributeBinding('je_auto_control.utils.self_healing', 'VerificationRequest'), - 'self_heal_run': AttributeBinding('je_auto_control.utils.self_healing', 'self_heal_run'), - 'inspect_heal_source': AttributeBinding('je_auto_control.utils.self_healing', 'inspect_heal_source'), - 'create_template_candidate': AttributeBinding('je_auto_control.utils.self_healing', 'create_template_candidate'), - 'preview_template_candidate': AttributeBinding('je_auto_control.utils.self_healing', 'preview_template_candidate'), - 'verify_template_candidate': AttributeBinding('je_auto_control.utils.self_healing', 'verify_template_candidate'), - 'accept_template_candidate': AttributeBinding('je_auto_control.utils.self_healing', 'accept_template_candidate'), - 'revert_template_candidate': AttributeBinding('je_auto_control.utils.self_healing', 'revert_template_candidate'), - 'TemplateCandidateState': AttributeBinding('je_auto_control.utils.self_healing', 'TemplateCandidateState'), - 'TemplateCandidatePreview': AttributeBinding('je_auto_control.utils.self_healing', 'TemplateCandidatePreview'), - 'TemplateCandidateVerification': - AttributeBinding('je_auto_control.utils.self_healing', 'TemplateCandidateVerification'), - 'TemplateCandidateTransition': - AttributeBinding('je_auto_control.utils.self_healing', 'TemplateCandidateTransition'), - 'TemplateRevisionError': AttributeBinding('je_auto_control.utils.self_healing', 'TemplateRevisionError'), - 'TemplateRevisionConflict': AttributeBinding('je_auto_control.utils.self_healing', 'TemplateRevisionConflict'), - 'EvaluationRatio': AttributeBinding('je_auto_control.utils.self_healing', 'EvaluationRatio'), - 'EvaluationSample': AttributeBinding('je_auto_control.utils.self_healing', 'EvaluationSample'), - 'HealingAttempt': AttributeBinding('je_auto_control.utils.self_healing', 'HealingAttempt'), - 'HealingComparison': AttributeBinding('je_auto_control.utils.self_healing', 'HealingComparison'), - 'HealingEvaluationError': AttributeBinding('je_auto_control.utils.self_healing', 'HealingEvaluationError'), - 'HealingVersionReport': AttributeBinding('je_auto_control.utils.self_healing', 'HealingVersionReport'), - 'LocatorCandidate': AttributeBinding('je_auto_control.utils.self_healing', 'LocatorCandidate'), - 'LocatorStrategy': AttributeBinding('je_auto_control.utils.self_healing', 'LocatorStrategy'), - 'TemplateFrameLocator': AttributeBinding('je_auto_control.utils.self_healing', 'TemplateFrameLocator'), - 'VLMFrameLocator': AttributeBinding('je_auto_control.utils.self_healing', 'VLMFrameLocator'), - 'evaluate_healing_manifest': AttributeBinding('je_auto_control.utils.self_healing', 'evaluate_healing_manifest'), - 'evaluate_locators': AttributeBinding('je_auto_control.utils.self_healing', 'evaluate_locators'), - 'write_healing_report': AttributeBinding('je_auto_control.utils.self_healing', 'write_healing_report'), - 'POLICY_MODERATE': AttributeBinding('je_auto_control.utils.redaction', 'POLICY_MODERATE'), - 'POLICY_OFF': AttributeBinding('je_auto_control.utils.redaction', 'POLICY_OFF'), - 'POLICY_STRICT': AttributeBinding('je_auto_control.utils.redaction', 'POLICY_STRICT'), - 'RedactionEngine': AttributeBinding('je_auto_control.utils.redaction', 'RedactionEngine'), - 'RedactionPolicy': AttributeBinding('je_auto_control.utils.redaction', 'RedactionPolicy'), - 'RedactionResult': AttributeBinding('je_auto_control.utils.redaction', 'RedactionResult'), - 'default_redaction_policy': AttributeBinding('je_auto_control.utils.redaction', 'default_policy'), - 'redaction_policy_from_name': AttributeBinding('je_auto_control.utils.redaction', 'policy_from_name'), - 'redact_png_bytes': AttributeBinding('je_auto_control.utils.redaction', 'redact_png_bytes'), + 'build_rtf': AttributeBinding(_UTILS_CLIPBOARD_RICH_FORMATS, 'build_rtf'), + 'rtf_to_text': AttributeBinding(_UTILS_CLIPBOARD_RICH_FORMATS, 'rtf_to_text'), + 'rows_to_csv': AttributeBinding(_UTILS_CLIPBOARD_RICH_FORMATS, 'rows_to_csv'), + 'csv_to_rows': AttributeBinding(_UTILS_CLIPBOARD_RICH_FORMATS, 'csv_to_rows'), + 'set_clipboard_rtf': AttributeBinding(_UTILS_CLIPBOARD_RICH_FORMATS, 'set_clipboard_rtf'), + 'get_clipboard_rtf': AttributeBinding(_UTILS_CLIPBOARD_RICH_FORMATS, 'get_clipboard_rtf'), + 'set_clipboard_csv': AttributeBinding(_UTILS_CLIPBOARD_RICH_FORMATS, 'set_clipboard_csv'), + 'get_clipboard_csv': AttributeBinding(_UTILS_CLIPBOARD_RICH_FORMATS, 'get_clipboard_csv'), + 'classify_format': AttributeBinding(_UTILS_CLIPBOARD_FORMATS, 'classify_format'), + 'classify_formats': AttributeBinding(_UTILS_CLIPBOARD_FORMATS, 'classify_formats'), + 'diff_formats': AttributeBinding(_UTILS_CLIPBOARD_FORMATS, 'diff_formats'), + 'list_clipboard_formats': AttributeBinding(_UTILS_CLIPBOARD_FORMATS, 'list_clipboard_formats'), + 'clipboard_formats': AttributeBinding(_UTILS_CLIPBOARD_FORMATS, 'clipboard_formats'), + 'plan_file_drop': AttributeBinding(_UTILS_FILE_DROP, 'plan_file_drop'), + 'drop_files': AttributeBinding(_UTILS_FILE_DROP, 'drop_files'), + 'image_quality': AttributeBinding(_UTILS_IMAGE_QUALITY, 'image_quality'), + 'is_blurry': AttributeBinding(_UTILS_IMAGE_QUALITY, 'is_blurry'), + 'quality_gate': AttributeBinding(_UTILS_IMAGE_QUALITY, 'quality_gate'), + 'detect_scale': AttributeBinding(_UTILS_SCALE_DETECT, 'detect_scale'), + 'scale_sweep': AttributeBinding(_UTILS_SCALE_DETECT, 'scale_sweep'), + 'saliency_map': AttributeBinding(_UTILS_SALIENCY, 'saliency_map'), + 'salient_regions': AttributeBinding(_UTILS_SALIENCY, 'salient_regions'), + 'most_salient': AttributeBinding(_UTILS_SALIENCY, 'most_salient'), + 'normalize_error': AttributeBinding(_UTILS_FAILURE_SIGNATURE, 'normalize_error'), + 'failure_signature': AttributeBinding(_UTILS_FAILURE_SIGNATURE, 'failure_signature'), + 'group_failures': AttributeBinding(_UTILS_FAILURE_SIGNATURE, 'group_failures'), + 'diff_runs': AttributeBinding(_UTILS_RUN_DIFF, 'diff_runs'), + 'summarize_run_diff': AttributeBinding(_UTILS_RUN_DIFF, 'summarize_run_diff'), + 'cofailure_pairs': AttributeBinding(_UTILS_FLAKE_CLUSTER, 'cofailure_pairs'), + 'failure_clusters': AttributeBinding(_UTILS_FLAKE_CLUSTER, 'failure_clusters'), + 'build_timeline': AttributeBinding(_UTILS_STEP_TIMELINE, 'build_timeline'), + 'critical_steps': AttributeBinding(_UTILS_STEP_TIMELINE, 'critical_steps'), + 'VLMNotAvailableError': AttributeBinding(_UTILS_VISION, 'VLMNotAvailableError'), + 'locate_by_description': AttributeBinding(_UTILS_VISION, 'locate_by_description'), + 'click_by_description': AttributeBinding(_UTILS_VISION, 'click_by_description'), + 'verify_description': AttributeBinding(_UTILS_VISION, 'verify_description'), + 'LLMBackend': AttributeBinding(_UTILS_LLM, 'LLMBackend'), + 'LLMNotAvailableError': AttributeBinding(_UTILS_LLM, 'LLMNotAvailableError'), + 'LLMPlanError': AttributeBinding(_UTILS_LLM, 'LLMPlanError'), + 'plan_actions': AttributeBinding(_UTILS_LLM, 'plan_actions'), + 'run_from_description': AttributeBinding(_UTILS_LLM, 'run_from_description'), + 'AgentBackend': AttributeBinding(_UTILS_AGENT_AGENT_LOOP, 'AgentBackend'), + 'AgentBudget': AttributeBinding(_UTILS_AGENT_AGENT_LOOP, 'AgentBudget'), + 'AgentLoop': AttributeBinding(_UTILS_AGENT_AGENT_LOOP, 'AgentLoop'), + 'AgentResult': AttributeBinding(_UTILS_AGENT_AGENT_LOOP, 'AgentResult'), + 'AgentStep': AttributeBinding(_UTILS_AGENT_AGENT_LOOP, 'AgentStep'), + 'FakeAgentBackend': AttributeBinding(_UTILS_AGENT_AGENT_LOOP, 'FakeAgentBackend'), + 'run_agent': AttributeBinding(_UTILS_AGENT_AGENT_LOOP, 'run_agent'), + 'AgentBackendError': AttributeBinding(_UTILS_AGENT_BACKENDS, 'AgentBackendError'), + 'AnthropicAgentBackend': AttributeBinding(_UTILS_AGENT_BACKENDS, 'AnthropicAgentBackend'), + 'ComputerUseAgentBackend': AttributeBinding(_UTILS_AGENT_BACKENDS, 'ComputerUseAgentBackend'), + 'OpenAIAgentBackend': AttributeBinding(_UTILS_AGENT_BACKENDS, 'OpenAIAgentBackend'), + 'run_computer_use': AttributeBinding(_UTILS_AGENT_COMPUTER_USE, 'run_computer_use'), + 'computer_use_result_to_dict': AttributeBinding(_UTILS_AGENT_COMPUTER_USE, 'result_to_dict'), + 'DagDefinition': AttributeBinding(_UTILS_DAG, 'DagDefinition'), + 'DagDefinitionError': AttributeBinding(_UTILS_DAG, 'DagDefinitionError'), + 'DagNode': AttributeBinding(_UTILS_DAG, 'DagNode'), + 'DagRunResult': AttributeBinding(_UTILS_DAG, 'DagRunResult'), + 'NodeResult': AttributeBinding(_UTILS_DAG, 'NodeResult'), + 'parse_dag': AttributeBinding(_UTILS_DAG, 'parse_dag'), + 'run_dag': AttributeBinding(_UTILS_DAG, 'run_dag'), + 'PresenceError': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'PresenceError'), + 'PresenceRegistry': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'PresenceRegistry'), + 'ROLE_CONTROLLER': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'ROLE_CONTROLLER'), + 'ROLE_OBSERVER': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'ROLE_OBSERVER'), + 'ViewerPresence': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'ViewerPresence'), + 'default_presence_registry': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'default_presence_registry'), + 'ChatOpsError': AttributeBinding(_UTILS_CHATOPS, 'ChatOpsError'), + 'CommandResult': AttributeBinding(_UTILS_CHATOPS, 'CommandResult'), + 'CommandRouter': AttributeBinding(_UTILS_CHATOPS, 'CommandRouter'), + 'SlackBot': AttributeBinding(_UTILS_CHATOPS, 'SlackBot'), + 'SlackError': AttributeBinding(_UTILS_CHATOPS, 'SlackError'), + 'make_default_slack_bot': AttributeBinding(_UTILS_CHATOPS, 'make_default_slack_bot'), + 'register_chatops_default_commands': AttributeBinding(_UTILS_CHATOPS, 'register_chatops_default_commands'), + 'AnchorLocator': AttributeBinding(_UTILS_ANCHOR_LOCATOR, 'Locator'), + 'AnchorLocatorError': AttributeBinding(_UTILS_ANCHOR_LOCATOR, 'AnchorLocatorError'), + 'AnchorOutcome': AttributeBinding(_UTILS_ANCHOR_LOCATOR, 'AnchorOutcome'), + 'a11y_locator': AttributeBinding(_UTILS_ANCHOR_LOCATOR, 'a11y_locator'), + 'anchor_locate': AttributeBinding(_UTILS_ANCHOR_LOCATOR, 'anchor_locate'), + 'anchor_locate_all': AttributeBinding(_UTILS_ANCHOR_LOCATOR, 'anchor_locate_all'), + 'image_locator': AttributeBinding(_UTILS_ANCHOR_LOCATOR, 'image_locator'), + 'ocr_locator': AttributeBinding(_UTILS_ANCHOR_LOCATOR, 'ocr_locator'), + 'vlm_locator': AttributeBinding(_UTILS_ANCHOR_LOCATOR, 'vlm_locator'), + 'OCRField': AttributeBinding(_UTILS_OCR_STRUCTURE, 'OCRField'), + 'OCRRow': AttributeBinding(_UTILS_OCR_STRUCTURE, 'OCRRow'), + 'OCRTable': AttributeBinding(_UTILS_OCR_STRUCTURE, 'OCRTable'), + 'StructuredOCR': AttributeBinding(_UTILS_OCR_STRUCTURE, 'StructuredOCR'), + 'ocr_cluster_matches': AttributeBinding(_UTILS_OCR_STRUCTURE, 'cluster_matches'), + 'ocr_read_structure': AttributeBinding(_UTILS_OCR_STRUCTURE, 'read_structure'), + 'WaitOutcome': AttributeBinding(_UTILS_SMART_WAITS, 'WaitOutcome'), + 'wait_until_pixel_changes': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_pixel_changes'), + 'wait_until_region_idle': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_region_idle'), + 'wait_until_screen_stable': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_screen_stable'), + 'wait_until_clipboard_changes': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_clipboard_changes'), + 'wait_until_window_closed': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_window_closed'), + 'wait_until_file': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_file'), + 'wait_until_port': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_port'), + 'wait_until_process': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_process'), + 'wait_until_gone': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_gone'), + 'wait_until_image_gone': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_image_gone'), + 'wait_until_text_gone': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_text_gone'), + 'wait_until_color': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_color'), + 'wait_until_window_title': AttributeBinding(_UTILS_SMART_WAITS, 'wait_until_window_title'), + 'take_golden': AttributeBinding(_UTILS_VISUAL_REGRESSION, 'take_golden'), + 'compare_to_golden': AttributeBinding(_UTILS_VISUAL_REGRESSION, 'compare_to_golden'), + 'image_difference': AttributeBinding(_UTILS_VISUAL_REGRESSION, 'image_difference'), + 'DiffResult': AttributeBinding(_UTILS_VISUAL_REGRESSION, 'DiffResult'), + 'MaskRegion': AttributeBinding(_UTILS_VISUAL_REGRESSION, 'MaskRegion'), + 'run_state_machine': AttributeBinding(_UTILS_STATE_MACHINE, 'run_state_machine'), + 'StateMachine': AttributeBinding(_UTILS_STATE_MACHINE, 'StateMachine'), + 'StateMachineError': AttributeBinding(_UTILS_STATE_MACHINE, 'StateMachineError'), + 'AssertionResult': AttributeBinding(_UTILS_ASSERTION, 'AssertionResult'), + 'assert_image': AttributeBinding(_UTILS_ASSERTION, 'assert_image'), + 'assert_pixel': AttributeBinding(_UTILS_ASSERTION, 'assert_pixel'), + 'assert_text': AttributeBinding(_UTILS_ASSERTION, 'assert_text'), + 'assert_window': AttributeBinding(_UTILS_ASSERTION, 'assert_window'), + 'assert_clipboard': AttributeBinding(_UTILS_ASSERTION, 'assert_clipboard'), + 'assert_process': AttributeBinding(_UTILS_ASSERTION, 'assert_process'), + 'assert_file': AttributeBinding(_UTILS_ASSERTION, 'assert_file'), + 'assert_http': AttributeBinding(_UTILS_ASSERTION, 'assert_http'), + 'assert_by_description': AttributeBinding(_UTILS_ASSERTION, 'assert_by_description'), + 'assert_duration': AttributeBinding(_UTILS_ASSERTION, 'assert_duration'), + 'assert_variable': AttributeBinding(_UTILS_ASSERTION, 'assert_variable'), + 'GroupAssertionResult': AttributeBinding(_UTILS_ASSERTION, 'GroupAssertionResult'), + 'assert_all': AttributeBinding(_UTILS_ASSERTION, 'assert_all'), + 'assert_any': AttributeBinding(_UTILS_ASSERTION, 'assert_any'), + 'assert_eventually': AttributeBinding(_UTILS_ASSERTION, 'assert_eventually'), + 'run_assertion_spec': AttributeBinding(_UTILS_ASSERTION, 'run_assertion_spec'), + 'data_source_kinds': AttributeBinding(_UTILS_DATA_SOURCE, 'data_source_kinds'), + 'load_rows': AttributeBinding(_UTILS_DATA_SOURCE, 'load_rows'), + 'FlakinessReport': AttributeBinding(_UTILS_FLAKINESS, 'FlakinessReport'), + 'FlakyEntry': AttributeBinding(_UTILS_FLAKINESS, 'FlakyEntry'), + 'analyze_flakiness': AttributeBinding(_UTILS_FLAKINESS, 'analyze_flakiness'), + 'TestCaseResult': AttributeBinding(_UTILS_TEST_SUITE, 'TestCaseResult'), + 'TestSuiteResult': AttributeBinding(_UTILS_TEST_SUITE, 'TestSuiteResult'), + 'run_suite': AttributeBinding(_UTILS_TEST_SUITE, 'run_suite'), + 'to_allure_results': AttributeBinding(_UTILS_TEST_SUITE, 'to_allure_results'), + 'to_junit_xml': AttributeBinding(_UTILS_TEST_SUITE, 'to_junit_xml'), + 'write_allure_results': AttributeBinding(_UTILS_TEST_SUITE, 'write_allure_results'), + 'write_junit_xml': AttributeBinding(_UTILS_TEST_SUITE, 'write_junit_xml'), + 'QuarantineEntry': AttributeBinding(_UTILS_QUARANTINE, 'QuarantineEntry'), + 'QuarantineStore': AttributeBinding(_UTILS_QUARANTINE, 'QuarantineStore'), + 'auto_quarantine_from_flakiness': AttributeBinding(_UTILS_QUARANTINE, 'auto_quarantine_from_flakiness'), + 'default_quarantine_store': AttributeBinding(_UTILS_QUARANTINE, 'default_quarantine_store'), + 'AuditIssue': AttributeBinding(_UTILS_A11Y_AUDIT, 'AuditIssue'), + 'AuditReport': AttributeBinding(_UTILS_A11Y_AUDIT, 'AuditReport'), + 'audit_contrast': AttributeBinding(_UTILS_A11Y_AUDIT, 'audit_contrast'), + 'audit_missing_labels': AttributeBinding(_UTILS_A11Y_AUDIT, 'audit_missing_labels'), + 'audit_target_size': AttributeBinding(_UTILS_A11Y_AUDIT, 'audit_target_size'), + 'contrast_ratio': AttributeBinding(_UTILS_A11Y_AUDIT, 'contrast_ratio'), + 'detect_truncation': AttributeBinding(_UTILS_A11Y_AUDIT, 'detect_truncation'), + 'run_audit': AttributeBinding(_UTILS_A11Y_AUDIT, 'run_audit'), + 'wcag_audit': AttributeBinding(_UTILS_A11Y_AUDIT, 'wcag_audit'), + 'DeviceResult': AttributeBinding(_UTILS_DEVICE_MATRIX, 'DeviceResult'), + 'MatrixReport': AttributeBinding(_UTILS_DEVICE_MATRIX, 'MatrixReport'), + 'run_on_devices': AttributeBinding(_UTILS_DEVICE_MATRIX, 'run_on_devices'), + 'MediaAssertionResult': AttributeBinding(_UTILS_MEDIA_ASSERT, 'MediaAssertionResult'), + 'assert_audio_activity': AttributeBinding(_UTILS_MEDIA_ASSERT, 'assert_audio_activity'), + 'assert_video_changes': AttributeBinding(_UTILS_MEDIA_ASSERT, 'assert_video_changes'), + 'measure_audio_rms': AttributeBinding(_UTILS_MEDIA_ASSERT, 'measure_audio_rms'), + 'video_segment_motion': AttributeBinding(_UTILS_MEDIA_ASSERT, 'video_segment_motion'), + 'CostEvent': AttributeBinding(_UTILS_COST_TELEMETRY, 'CostEvent'), + 'CostSummary': AttributeBinding(_UTILS_COST_TELEMETRY, 'CostSummary'), + 'default_cost_store': AttributeBinding(_UTILS_COST_TELEMETRY, 'default_cost_store'), + 'estimate_llm_usd': AttributeBinding(_UTILS_COST_TELEMETRY, 'estimate_llm_usd'), + 'record_llm_call': AttributeBinding(_UTILS_COST_TELEMETRY, 'record_llm_call'), + 'summarise_llm_costs': AttributeBinding(_UTILS_COST_TELEMETRY, 'summarise_llm_costs'), + 'FailureHookManager': AttributeBinding(_UTILS_FAILURE_HOOKS, 'FailureHookManager'), + 'FailureReport': AttributeBinding(_UTILS_FAILURE_HOOKS, 'FailureReport'), + 'GitHubBackend': AttributeBinding(_UTILS_FAILURE_HOOKS, 'GitHubBackend'), + 'JiraBackend': AttributeBinding(_UTILS_FAILURE_HOOKS, 'JiraBackend'), + 'LinearBackend': AttributeBinding(_UTILS_FAILURE_HOOKS, 'LinearBackend'), + 'TicketResult': AttributeBinding(_UTILS_FAILURE_HOOKS, 'TicketResult'), + 'default_failure_hook_manager': AttributeBinding(_UTILS_FAILURE_HOOKS, 'default_failure_hook_manager'), + 'ABRunOutcome': AttributeBinding(_UTILS_AB_LOCATOR, 'ABRunOutcome'), + 'ab_best_strategy': AttributeBinding(_UTILS_AB_LOCATOR, 'ab_best_strategy'), + 'ab_locate': AttributeBinding(_UTILS_AB_LOCATOR, 'ab_locate'), + 'ab_report_for': AttributeBinding(_UTILS_AB_LOCATOR, 'ab_report_for'), + 'default_ab_store': AttributeBinding(_UTILS_AB_LOCATOR, 'default_ab_store'), + 'HealEvent': AttributeBinding(_UTILS_SELF_HEALING, 'HealEvent'), + 'HealEventLog': AttributeBinding(_UTILS_SELF_HEALING, 'HealEventLog'), + 'HealOutcome': AttributeBinding(_UTILS_SELF_HEALING, 'HealOutcome'), + 'SelfHealError': AttributeBinding(_UTILS_SELF_HEALING, 'SelfHealError'), + 'default_heal_log': AttributeBinding(_UTILS_SELF_HEALING, 'default_heal_log'), + 'self_heal_click': AttributeBinding(_UTILS_SELF_HEALING, 'self_heal_click'), + 'self_heal_locate': AttributeBinding(_UTILS_SELF_HEALING, 'self_heal_locate'), + 'SelfHealRequest': AttributeBinding(_UTILS_SELF_HEALING, 'SelfHealRequest'), + 'SelfHealRequestError': AttributeBinding(_UTILS_SELF_HEALING, 'SelfHealRequestError'), + 'SelfHealRunResult': AttributeBinding(_UTILS_SELF_HEALING, 'SelfHealRunResult'), + 'SelfHealVerificationError': AttributeBinding(_UTILS_SELF_HEALING, 'SelfHealVerificationError'), + 'VerificationRequest': AttributeBinding(_UTILS_SELF_HEALING, 'VerificationRequest'), + 'self_heal_run': AttributeBinding(_UTILS_SELF_HEALING, 'self_heal_run'), + 'inspect_heal_source': AttributeBinding(_UTILS_SELF_HEALING, 'inspect_heal_source'), + 'create_template_candidate': AttributeBinding(_UTILS_SELF_HEALING, 'create_template_candidate'), + 'preview_template_candidate': AttributeBinding(_UTILS_SELF_HEALING, 'preview_template_candidate'), + 'verify_template_candidate': AttributeBinding(_UTILS_SELF_HEALING, 'verify_template_candidate'), + 'accept_template_candidate': AttributeBinding(_UTILS_SELF_HEALING, 'accept_template_candidate'), + 'revert_template_candidate': AttributeBinding(_UTILS_SELF_HEALING, 'revert_template_candidate'), + 'TemplateCandidateState': AttributeBinding(_UTILS_SELF_HEALING, 'TemplateCandidateState'), + 'TemplateCandidatePreview': AttributeBinding(_UTILS_SELF_HEALING, 'TemplateCandidatePreview'), + 'TemplateCandidateVerification': AttributeBinding(_UTILS_SELF_HEALING, 'TemplateCandidateVerification'), + 'TemplateCandidateTransition': AttributeBinding(_UTILS_SELF_HEALING, 'TemplateCandidateTransition'), + 'TemplateRevisionError': AttributeBinding(_UTILS_SELF_HEALING, 'TemplateRevisionError'), + 'TemplateRevisionConflict': AttributeBinding(_UTILS_SELF_HEALING, 'TemplateRevisionConflict'), + 'EvaluationRatio': AttributeBinding(_UTILS_SELF_HEALING, 'EvaluationRatio'), + 'EvaluationSample': AttributeBinding(_UTILS_SELF_HEALING, 'EvaluationSample'), + 'HealingAttempt': AttributeBinding(_UTILS_SELF_HEALING, 'HealingAttempt'), + 'HealingComparison': AttributeBinding(_UTILS_SELF_HEALING, 'HealingComparison'), + 'HealingEvaluationError': AttributeBinding(_UTILS_SELF_HEALING, 'HealingEvaluationError'), + 'HealingVersionReport': AttributeBinding(_UTILS_SELF_HEALING, 'HealingVersionReport'), + 'LocatorCandidate': AttributeBinding(_UTILS_SELF_HEALING, 'LocatorCandidate'), + 'LocatorStrategy': AttributeBinding(_UTILS_SELF_HEALING, 'LocatorStrategy'), + 'TemplateFrameLocator': AttributeBinding(_UTILS_SELF_HEALING, 'TemplateFrameLocator'), + 'VLMFrameLocator': AttributeBinding(_UTILS_SELF_HEALING, 'VLMFrameLocator'), + 'evaluate_healing_manifest': AttributeBinding(_UTILS_SELF_HEALING, 'evaluate_healing_manifest'), + 'evaluate_locators': AttributeBinding(_UTILS_SELF_HEALING, 'evaluate_locators'), + 'write_healing_report': AttributeBinding(_UTILS_SELF_HEALING, 'write_healing_report'), + 'POLICY_MODERATE': AttributeBinding(_UTILS_REDACTION, 'POLICY_MODERATE'), + 'POLICY_OFF': AttributeBinding(_UTILS_REDACTION, 'POLICY_OFF'), + 'POLICY_STRICT': AttributeBinding(_UTILS_REDACTION, 'POLICY_STRICT'), + 'RedactionEngine': AttributeBinding(_UTILS_REDACTION, 'RedactionEngine'), + 'RedactionPolicy': AttributeBinding(_UTILS_REDACTION, 'RedactionPolicy'), + 'RedactionResult': AttributeBinding(_UTILS_REDACTION, 'RedactionResult'), + 'default_redaction_policy': AttributeBinding(_UTILS_REDACTION, 'default_policy'), + 'redaction_policy_from_name': AttributeBinding(_UTILS_REDACTION, 'policy_from_name'), + 'redact_png_bytes': AttributeBinding(_UTILS_REDACTION, 'redact_png_bytes'), 'annotate_screenshot': AttributeBinding('je_auto_control.utils.annotate', 'annotate_screenshot'), - 'NotifyResult': AttributeBinding('je_auto_control.utils.notify', 'NotifyResult'), - 'notify': AttributeBinding('je_auto_control.utils.notify', 'notify'), - 'ColorStats': AttributeBinding('je_auto_control.utils.color_stats', 'ColorStats'), - 'region_color_stats': AttributeBinding('je_auto_control.utils.color_stats', 'region_color_stats'), - 'capture_window': AttributeBinding('je_auto_control.utils.window_capture', 'capture_window'), - 'get_window_geometry': AttributeBinding('je_auto_control.utils.window_capture', 'get_window_geometry'), - 'save_window_layout': AttributeBinding('je_auto_control.utils.window_capture', 'save_window_layout'), - 'restore_window_layout': AttributeBinding('je_auto_control.utils.window_capture', 'restore_window_layout'), - 'snap_window': AttributeBinding('je_auto_control.utils.window_capture', 'snap_window'), - 'arrange_grid': AttributeBinding('je_auto_control.utils.window_capture', 'arrange_grid'), - 'arrange_cascade': AttributeBinding('je_auto_control.utils.window_capture', 'arrange_cascade'), + 'NotifyResult': AttributeBinding(_UTILS_NOTIFY, 'NotifyResult'), + 'notify': AttributeBinding(_UTILS_NOTIFY, 'notify'), + 'ColorStats': AttributeBinding(_UTILS_COLOR_STATS, 'ColorStats'), + 'region_color_stats': AttributeBinding(_UTILS_COLOR_STATS, 'region_color_stats'), + 'capture_window': AttributeBinding(_UTILS_WINDOW_CAPTURE, 'capture_window'), + 'get_window_geometry': AttributeBinding(_UTILS_WINDOW_CAPTURE, 'get_window_geometry'), + 'save_window_layout': AttributeBinding(_UTILS_WINDOW_CAPTURE, 'save_window_layout'), + 'restore_window_layout': AttributeBinding(_UTILS_WINDOW_CAPTURE, 'restore_window_layout'), + 'snap_window': AttributeBinding(_UTILS_WINDOW_CAPTURE, 'snap_window'), + 'arrange_grid': AttributeBinding(_UTILS_WINDOW_CAPTURE, 'arrange_grid'), + 'arrange_cascade': AttributeBinding(_UTILS_WINDOW_CAPTURE, 'arrange_cascade'), 'scroll_until_visible': AttributeBinding('je_auto_control.utils.scroll_find', 'scroll_until_visible'), 'move_to_trash': AttributeBinding('je_auto_control.utils.trash', 'move_to_trash'), 'read_qr_codes': AttributeBinding('je_auto_control.utils.qr', 'read_qr_codes'), - 'WebRunnerBridgeError': AttributeBinding('je_auto_control.utils.webrunner_bridge', 'WebRunnerBridgeError'), - 'is_webrunner_available': AttributeBinding('je_auto_control.utils.webrunner_bridge', 'is_webrunner_available'), - 'list_webrunner_commands': AttributeBinding('je_auto_control.utils.webrunner_bridge', 'list_webrunner_commands'), - 'run_webrunner_action': AttributeBinding('je_auto_control.utils.webrunner_bridge', 'run_webrunner_action'), - 'run_webrunner_actions': AttributeBinding('je_auto_control.utils.webrunner_bridge', 'run_webrunner_actions'), - 'web_current_url': AttributeBinding('je_auto_control.utils.webrunner_bridge', 'web_current_url'), - 'web_open': AttributeBinding('je_auto_control.utils.webrunner_bridge', 'web_open'), - 'web_quit': AttributeBinding('je_auto_control.utils.webrunner_bridge', 'web_quit'), - 'web_screenshot': AttributeBinding('je_auto_control.utils.webrunner_bridge', 'web_screenshot'), - 'RemoteDesktopHost': AttributeBinding('je_auto_control.utils.remote_desktop', 'RemoteDesktopHost'), - 'RemoteDesktopViewer': AttributeBinding('je_auto_control.utils.remote_desktop', 'RemoteDesktopViewer'), - 'RemoteDesktopAuthError': AttributeBinding('je_auto_control.utils.remote_desktop', 'AuthenticationError'), - 'RemoteDesktopInputError': AttributeBinding('je_auto_control.utils.remote_desktop', 'InputDispatchError'), - 'RemoteDesktopProtocolError': AttributeBinding('je_auto_control.utils.remote_desktop', 'ProtocolError'), + 'WebRunnerBridgeError': AttributeBinding(_UTILS_WEBRUNNER_BRIDGE, 'WebRunnerBridgeError'), + 'is_webrunner_available': AttributeBinding(_UTILS_WEBRUNNER_BRIDGE, 'is_webrunner_available'), + 'list_webrunner_commands': AttributeBinding(_UTILS_WEBRUNNER_BRIDGE, 'list_webrunner_commands'), + 'run_webrunner_action': AttributeBinding(_UTILS_WEBRUNNER_BRIDGE, 'run_webrunner_action'), + 'run_webrunner_actions': AttributeBinding(_UTILS_WEBRUNNER_BRIDGE, 'run_webrunner_actions'), + 'web_current_url': AttributeBinding(_UTILS_WEBRUNNER_BRIDGE, 'web_current_url'), + 'web_open': AttributeBinding(_UTILS_WEBRUNNER_BRIDGE, 'web_open'), + 'web_quit': AttributeBinding(_UTILS_WEBRUNNER_BRIDGE, 'web_quit'), + 'web_screenshot': AttributeBinding(_UTILS_WEBRUNNER_BRIDGE, 'web_screenshot'), + 'RemoteDesktopHost': AttributeBinding(_UTILS_REMOTE_DESKTOP, 'RemoteDesktopHost'), + 'RemoteDesktopViewer': AttributeBinding(_UTILS_REMOTE_DESKTOP, 'RemoteDesktopViewer'), + 'RemoteDesktopAuthError': AttributeBinding(_UTILS_REMOTE_DESKTOP, 'AuthenticationError'), + 'RemoteDesktopInputError': AttributeBinding(_UTILS_REMOTE_DESKTOP, 'InputDispatchError'), + 'RemoteDesktopProtocolError': AttributeBinding(_UTILS_REMOTE_DESKTOP, 'ProtocolError'), 'remote_desktop_registry': AttributeBinding('je_auto_control.utils.remote_desktop.registry', 'registry'), - 'RemoteDesktopConnectTarget': - AttributeBinding('je_auto_control.utils.remote_desktop.connect_coordinator', 'ConnectTarget'), + 'RemoteDesktopConnectTarget': AttributeBinding(_UTILS_REMOTE_DESKTOP_CONNECT_COORDINATOR, 'ConnectTarget'), 'RemoteDesktopUnresolvableTargetError': - AttributeBinding('je_auto_control.utils.remote_desktop.connect_coordinator', 'UnresolvableTargetError'), - 'parse_remote_desktop_target': - AttributeBinding('je_auto_control.utils.remote_desktop.connect_coordinator', 'parse_target'), - 'VirtualGamepad': AttributeBinding('je_auto_control.utils.gamepad', 'VirtualGamepad'), - 'GamepadUnavailable': AttributeBinding('je_auto_control.utils.gamepad', 'GamepadUnavailable'), - 'default_virtual_gamepad': AttributeBinding('je_auto_control.utils.gamepad', 'default_gamepad'), - 'is_virtual_gamepad_available': AttributeBinding('je_auto_control.utils.gamepad', 'is_available'), - 'generate_html': AttributeBinding('je_auto_control.utils.generate_report.generate_html_report', 'generate_html'), - 'generate_html_report': - AttributeBinding('je_auto_control.utils.generate_report.generate_html_report', 'generate_html_report'), - 'generate_json': AttributeBinding('je_auto_control.utils.generate_report.generate_json_report', 'generate_json'), - 'generate_json_report': - AttributeBinding('je_auto_control.utils.generate_report.generate_json_report', 'generate_json_report'), - 'generate_xml': AttributeBinding('je_auto_control.utils.generate_report.generate_xml_report', 'generate_xml'), - 'generate_xml_report': - AttributeBinding('je_auto_control.utils.generate_report.generate_xml_report', 'generate_xml_report'), + AttributeBinding(_UTILS_REMOTE_DESKTOP_CONNECT_COORDINATOR, 'UnresolvableTargetError'), + 'parse_remote_desktop_target': AttributeBinding(_UTILS_REMOTE_DESKTOP_CONNECT_COORDINATOR, 'parse_target'), + 'VirtualGamepad': AttributeBinding(_UTILS_GAMEPAD, 'VirtualGamepad'), + 'GamepadUnavailable': AttributeBinding(_UTILS_GAMEPAD, 'GamepadUnavailable'), + 'default_virtual_gamepad': AttributeBinding(_UTILS_GAMEPAD, 'default_gamepad'), + 'is_virtual_gamepad_available': AttributeBinding(_UTILS_GAMEPAD, 'is_available'), + 'generate_html': AttributeBinding(_UTILS_GENERATE_REPORT_GENERATE_HTML_REPORT, 'generate_html'), + 'generate_html_report': AttributeBinding(_UTILS_GENERATE_REPORT_GENERATE_HTML_REPORT, 'generate_html_report'), + 'generate_json': AttributeBinding(_UTILS_GENERATE_REPORT_GENERATE_JSON_REPORT, 'generate_json'), + 'generate_json_report': AttributeBinding(_UTILS_GENERATE_REPORT_GENERATE_JSON_REPORT, 'generate_json_report'), + 'generate_xml': AttributeBinding(_UTILS_GENERATE_REPORT_GENERATE_XML_REPORT, 'generate_xml'), + 'generate_xml_report': AttributeBinding(_UTILS_GENERATE_REPORT_GENERATE_XML_REPORT, 'generate_xml_report'), 'get_dir_files_as_list': AttributeBinding('je_auto_control.utils.file_process.get_dir_file_list', 'get_dir_files_as_list'), 'create_project_dir': @@ -1505,76 +1770,70 @@ AttributeBinding('je_auto_control.utils.callback.callback_function_executor', 'callback_executor'), 'package_manager': AttributeBinding('je_auto_control.utils.package_manager.package_manager_class', 'package_manager'), - 'ShellManager': AttributeBinding('je_auto_control.utils.shell_process.shell_exec', 'ShellManager'), - 'default_shell_manager': - AttributeBinding('je_auto_control.utils.shell_process.shell_exec', 'default_shell_manager'), + 'ShellManager': AttributeBinding(_UTILS_SHELL_PROCESS_SHELL_EXEC, 'ShellManager'), + 'default_shell_manager': AttributeBinding(_UTILS_SHELL_PROCESS_SHELL_EXEC, 'default_shell_manager'), 'RecordingThread': AttributeBinding('je_auto_control.utils.cv2_utils.video_recording', 'RecordingThread'), - 'send_key_event_to_window': - AttributeBinding('je_auto_control.wrapper.auto_control_keyboard', 'send_key_event_to_window'), - 'send_mouse_event_to_window': - AttributeBinding('je_auto_control.wrapper.auto_control_mouse', 'send_mouse_event_to_window'), + 'send_key_event_to_window': AttributeBinding(_WRAPPER_AUTO_CONTROL_KEYBOARD, 'send_key_event_to_window'), + 'send_mouse_event_to_window': AttributeBinding(_WRAPPER_AUTO_CONTROL_MOUSE, 'send_mouse_event_to_window'), 'windows_window_manage': ModuleBinding('je_auto_control.windows.window.windows_window_manage'), 'ScreenRecorder': AttributeBinding('je_auto_control.utils.cv2_utils.screen_record', 'ScreenRecorder'), - 'get_pixel': AttributeBinding('je_auto_control.wrapper.auto_control_screen', 'get_pixel'), - 'AlertNotPresentError': AttributeBinding('je_auto_control.wrapper.device_context', 'AlertNotPresentError'), - 'AppState': AttributeBinding('je_auto_control.wrapper.device_context', 'AppState'), - 'AutoControlSignatureException': - AttributeBinding('je_auto_control.utils.exception.exceptions', 'AutoControlSignatureException'), - 'DeviceCancelledError': AttributeBinding('je_auto_control.wrapper.device_context', 'DeviceCancelledError'), - 'DeviceCapability': AttributeBinding('je_auto_control.wrapper.device_context', 'DeviceCapability'), - 'DeviceClosedError': AttributeBinding('je_auto_control.wrapper.device_context', 'DeviceClosedError'), - 'DeviceContext': AttributeBinding('je_auto_control.wrapper.device_context', 'DeviceContext'), - 'DeviceError': AttributeBinding('je_auto_control.wrapper.device_context', 'DeviceError'), + 'get_pixel': AttributeBinding(_WRAPPER_AUTO_CONTROL_SCREEN, 'get_pixel'), + 'AlertNotPresentError': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'AlertNotPresentError'), + 'AppState': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'AppState'), + 'AutoControlSignatureException': AttributeBinding(_UTILS_EXCEPTION_EXCEPTIONS, 'AutoControlSignatureException'), + 'DeviceCancelledError': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DeviceCancelledError'), + 'DeviceCapability': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DeviceCapability'), + 'DeviceClosedError': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DeviceClosedError'), + 'DeviceContext': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DeviceContext'), + 'DeviceError': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DeviceError'), 'DeviceFrame': AttributeBinding('je_auto_control.wrapper.device_frame', 'DeviceFrame'), - 'DevicePermissionError': AttributeBinding('je_auto_control.wrapper.device_context', 'DevicePermissionError'), - 'DeviceSession': AttributeBinding('je_auto_control.wrapper.device_context', 'DeviceSession'), - 'DeviceSetupReport': AttributeBinding('je_auto_control.wrapper.device_context', 'DeviceSetupReport'), - 'DeviceTimeoutError': AttributeBinding('je_auto_control.wrapper.device_context', 'DeviceTimeoutError'), - 'DeviceUnavailableError': AttributeBinding('je_auto_control.wrapper.device_context', 'DeviceUnavailableError'), - 'DeviceUnsupportedError': AttributeBinding('je_auto_control.wrapper.device_context', 'DeviceUnsupportedError'), - 'Drag': AttributeBinding('je_auto_control.wrapper.device_context', 'Drag'), - 'ExecutionStopped': AttributeBinding('je_auto_control.utils.executor.run_control', 'ExecutionStopped'), - 'InputDevice': AttributeBinding('je_auto_control.linux_wayland.input_events', 'InputDevice'), - 'InputEvent': AttributeBinding('je_auto_control.linux_wayland.input_events', 'InputEvent'), - 'InputPermissionError': AttributeBinding('je_auto_control.linux_wayland.input_events', 'InputPermissionError'), - 'InputRecordingError': AttributeBinding('je_auto_control.linux_wayland.input_events', 'InputRecordingError'), - 'InputStepLog': AttributeBinding('je_auto_control.linux_wayland.input_events', 'InputStepLog'), - 'LongPress': AttributeBinding('je_auto_control.wrapper.device_context', 'LongPress'), - 'MOBILE_COMMANDS': AttributeBinding('je_auto_control.wrapper.mobile_commands', 'MOBILE_COMMANDS'), - 'MobileCommand': AttributeBinding('je_auto_control.wrapper.mobile_commands', 'MobileCommand'), - 'MobileExtension': AttributeBinding('je_auto_control.wrapper.mobile_extensions', 'MobileExtension'), - 'PhysicalRecorder': AttributeBinding('je_auto_control.linux_wayland.input_events', 'PhysicalRecorder'), - 'Pinch': AttributeBinding('je_auto_control.wrapper.device_context', 'Pinch'), - 'RobotStructureError': AttributeBinding('je_auto_control.utils.codegen.robot_check', 'RobotStructureError'), - 'ShortcutPermissionError': - AttributeBinding('je_auto_control.linux_wayland.global_shortcuts', 'ShortcutPermissionError'), - 'ShortcutUnavailable': AttributeBinding('je_auto_control.linux_wayland.global_shortcuts', 'ShortcutUnavailable'), - 'StopShortcutSession': AttributeBinding('je_auto_control.linux_wayland.global_shortcuts', 'StopShortcutSession'), - 'StopToken': AttributeBinding('je_auto_control.utils.executor.run_control', 'StopToken'), - 'Swipe': AttributeBinding('je_auto_control.wrapper.device_context', 'Swipe'), - 'Tap': AttributeBinding('je_auto_control.wrapper.device_context', 'Tap'), - 'accept_alert': AttributeBinding('je_auto_control.wrapper.mobile_extensions', 'accept_alert'), - 'active_executions': AttributeBinding('je_auto_control.utils.executor.run_control', 'active_executions'), - 'app_state': AttributeBinding('je_auto_control.wrapper.mobile_extensions', 'app_state'), - 'check_robot_structure': AttributeBinding('je_auto_control.utils.codegen.robot_check', 'check_robot_structure'), - 'device_setup_report': AttributeBinding('je_auto_control.wrapper.mobile_commands', 'device_setup_report'), - 'dismiss_alert': AttributeBinding('je_auto_control.wrapper.mobile_extensions', 'dismiss_alert'), - 'launch_app': AttributeBinding('je_auto_control.wrapper.mobile_extensions', 'launch_app'), - 'list_input_devices': AttributeBinding('je_auto_control.linux_wayland.input_events', 'list_input_devices'), - 'mobile_capability_matrix': AttributeBinding('je_auto_control.wrapper.mobile_commands', 'mobile_capability_matrix'), - 'mobile_extension': AttributeBinding('je_auto_control.wrapper.mobile_extensions', 'mobile_extension'), - 'open_device': AttributeBinding('je_auto_control.wrapper.device_context', 'open_device'), - 'rbac_add_user': AttributeBinding('je_auto_control.utils.rbac.admin', 'add_user'), - 'rbac_list_users': AttributeBinding('je_auto_control.utils.rbac.admin', 'list_users'), - 'rbac_remove_user': AttributeBinding('je_auto_control.utils.rbac.admin', 'remove_user'), - 'rbac_rotate_user_token': AttributeBinding('je_auto_control.utils.rbac.admin', 'rotate_user_token'), - 'rbac_set_user_role': AttributeBinding('je_auto_control.utils.rbac.admin', 'set_user_role'), - 'register_mobile_extension': - AttributeBinding('je_auto_control.wrapper.mobile_extensions', 'register_mobile_extension'), - 'run_mobile_command': AttributeBinding('je_auto_control.wrapper.mobile_commands', 'run_mobile_command'), - 'stop_app': AttributeBinding('je_auto_control.wrapper.mobile_extensions', 'stop_app'), - 'stop_execution': AttributeBinding('je_auto_control.utils.executor.run_control', 'stop_execution'), - 'stoppable_run': AttributeBinding('je_auto_control.utils.executor.run_control', 'stoppable_run'), - 'use_device': AttributeBinding('je_auto_control.wrapper.device_context', 'use_device'), - 'wait_for_app': AttributeBinding('je_auto_control.wrapper.mobile_extensions', 'wait_for_app'), + 'DevicePermissionError': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DevicePermissionError'), + 'DeviceSession': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DeviceSession'), + 'DeviceSetupReport': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DeviceSetupReport'), + 'DeviceTimeoutError': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DeviceTimeoutError'), + 'DeviceUnavailableError': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DeviceUnavailableError'), + 'DeviceUnsupportedError': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'DeviceUnsupportedError'), + 'Drag': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'Drag'), + 'ExecutionStopped': AttributeBinding(_UTILS_EXECUTOR_RUN_CONTROL, 'ExecutionStopped'), + 'InputDevice': AttributeBinding(_LINUX_WAYLAND_INPUT_EVENTS, 'InputDevice'), + 'InputEvent': AttributeBinding(_LINUX_WAYLAND_INPUT_EVENTS, 'InputEvent'), + 'InputPermissionError': AttributeBinding(_LINUX_WAYLAND_INPUT_EVENTS, 'InputPermissionError'), + 'InputRecordingError': AttributeBinding(_LINUX_WAYLAND_INPUT_EVENTS, 'InputRecordingError'), + 'InputStepLog': AttributeBinding(_LINUX_WAYLAND_INPUT_EVENTS, 'InputStepLog'), + 'LongPress': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'LongPress'), + 'MOBILE_COMMANDS': AttributeBinding(_WRAPPER_MOBILE_COMMANDS, 'MOBILE_COMMANDS'), + 'MobileCommand': AttributeBinding(_WRAPPER_MOBILE_COMMANDS, 'MobileCommand'), + 'MobileExtension': AttributeBinding(_WRAPPER_MOBILE_EXTENSIONS, 'MobileExtension'), + 'PhysicalRecorder': AttributeBinding(_LINUX_WAYLAND_INPUT_EVENTS, 'PhysicalRecorder'), + 'Pinch': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'Pinch'), + 'RobotStructureError': AttributeBinding(_UTILS_CODEGEN_ROBOT_CHECK, 'RobotStructureError'), + 'ShortcutPermissionError': AttributeBinding(_LINUX_WAYLAND_GLOBAL_SHORTCUTS, 'ShortcutPermissionError'), + 'ShortcutUnavailable': AttributeBinding(_LINUX_WAYLAND_GLOBAL_SHORTCUTS, 'ShortcutUnavailable'), + 'StopShortcutSession': AttributeBinding(_LINUX_WAYLAND_GLOBAL_SHORTCUTS, 'StopShortcutSession'), + 'StopToken': AttributeBinding(_UTILS_EXECUTOR_RUN_CONTROL, 'StopToken'), + 'Swipe': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'Swipe'), + 'Tap': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'Tap'), + 'accept_alert': AttributeBinding(_WRAPPER_MOBILE_EXTENSIONS, 'accept_alert'), + 'active_executions': AttributeBinding(_UTILS_EXECUTOR_RUN_CONTROL, 'active_executions'), + 'app_state': AttributeBinding(_WRAPPER_MOBILE_EXTENSIONS, 'app_state'), + 'check_robot_structure': AttributeBinding(_UTILS_CODEGEN_ROBOT_CHECK, 'check_robot_structure'), + 'device_setup_report': AttributeBinding(_WRAPPER_MOBILE_COMMANDS, 'device_setup_report'), + 'dismiss_alert': AttributeBinding(_WRAPPER_MOBILE_EXTENSIONS, 'dismiss_alert'), + 'launch_app': AttributeBinding(_WRAPPER_MOBILE_EXTENSIONS, 'launch_app'), + 'list_input_devices': AttributeBinding(_LINUX_WAYLAND_INPUT_EVENTS, 'list_input_devices'), + 'mobile_capability_matrix': AttributeBinding(_WRAPPER_MOBILE_COMMANDS, 'mobile_capability_matrix'), + 'mobile_extension': AttributeBinding(_WRAPPER_MOBILE_EXTENSIONS, 'mobile_extension'), + 'open_device': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'open_device'), + 'rbac_add_user': AttributeBinding(_UTILS_RBAC_ADMIN, 'add_user'), + 'rbac_list_users': AttributeBinding(_UTILS_RBAC_ADMIN, 'list_users'), + 'rbac_remove_user': AttributeBinding(_UTILS_RBAC_ADMIN, 'remove_user'), + 'rbac_rotate_user_token': AttributeBinding(_UTILS_RBAC_ADMIN, 'rotate_user_token'), + 'rbac_set_user_role': AttributeBinding(_UTILS_RBAC_ADMIN, 'set_user_role'), + 'register_mobile_extension': AttributeBinding(_WRAPPER_MOBILE_EXTENSIONS, 'register_mobile_extension'), + 'run_mobile_command': AttributeBinding(_WRAPPER_MOBILE_COMMANDS, 'run_mobile_command'), + 'stop_app': AttributeBinding(_WRAPPER_MOBILE_EXTENSIONS, 'stop_app'), + 'stop_execution': AttributeBinding(_UTILS_EXECUTOR_RUN_CONTROL, 'stop_execution'), + 'stoppable_run': AttributeBinding(_UTILS_EXECUTOR_RUN_CONTROL, 'stoppable_run'), + 'use_device': AttributeBinding(_WRAPPER_DEVICE_CONTEXT, 'use_device'), + 'wait_for_app': AttributeBinding(_WRAPPER_MOBILE_EXTENSIONS, 'wait_for_app'), } diff --git a/je_auto_control/gui/agent_tasks_tab.py b/je_auto_control/gui/agent_tasks_tab.py index 468c96805..00be069e2 100644 --- a/je_auto_control/gui/agent_tasks_tab.py +++ b/je_auto_control/gui/agent_tasks_tab.py @@ -34,6 +34,9 @@ ) from je_auto_control.utils.exception.cancellation import CancellationToken +_EXAMPLE_ORIGIN = "https://example.invalid" +_TRIGGER_VIEW_PREFIX = "trigger:" + def _translate(key: str) -> str: return language_wrapper.translate(key, key) @@ -297,14 +300,14 @@ def _preview(self) -> None: def _browser_example(self) -> None: profile = AgentTargetProfile(name="reviewed-browser", kind="browser", allowed_tools=tuple(sorted(BROWSER_CORE_TOOLS)), - settings={"allowed_origins": ["https://example.invalid"]}) + settings={"allowed_origins": [_EXAMPLE_ORIGIN]}) self._target_request.setPlainText(json.dumps(profile.to_dict(), ensure_ascii=False, indent=2)) self._pages.setCurrentIndex(self._page_keys.index("agent_page_targets")) def _browser_completion_example(self) -> None: spec = AgentTaskSpec(goal="Review browser completion", target="reviewed-browser", allowed_tools=("AC_browser_navigate", "AC_browser_read"), - options={"allowed_origins": ["https://example.invalid"]}, + options={"allowed_origins": [_EXAMPLE_ORIGIN]}, success_criteria=({"kind": "browser", "operation": "text", "selector": "#confirmation", "value": "expected"},)).to_dict() self._target.setText(spec.pop("target")) @@ -317,12 +320,12 @@ def _browser_files_example(self) -> None: root = str(Path.cwd() / "agent-files") profile = AgentTargetProfile(name="reviewed-browser", kind="browser", roots=(root,), allowed_tools=tuple(sorted(BROWSER_ADAPTER_TOOLS)), - settings={"allowed_origins": ["https://example.invalid"]}) + settings={"allowed_origins": [_EXAMPLE_ORIGIN]}) self._target_request.setPlainText(json.dumps(profile.to_dict(), ensure_ascii=False, indent=2)) self._workflow.setCurrentText("download_archive") self._workflow_config.setPlainText(json.dumps({"backend": "browser", "roots": [root], - "directory": root, "url": "https://example.invalid/reports", "target": "reviewed-browser", - "allowed_origins": ["https://example.invalid"], + "directory": root, "url": _EXAMPLE_ORIGIN + "/reports", "target": "reviewed-browser", + "allowed_origins": [_EXAMPLE_ORIGIN], "files": [{"name": "report.bin", "selector": "#download"}]}, ensure_ascii=False, indent=2)) self._action.setPlainText(json.dumps({"tool": "AC_browser_upload", "arguments": { "selector": "input[type=file]", "path": str(Path(root) / "upload.bin"), "sha256": ""}}, indent=2)) @@ -511,7 +514,7 @@ def _select_request_view(self, operation: str, data: dict) -> None: self._view_task = '' self._handoff_view = (self._handoff_operation.currentData(), self._handoff_request.toPlainText()) if operation == "trigger" and "trigger_id" in data.get("request", {}): - self._view_task = "trigger:" + str(data["request"]["trigger_id"]) + self._view_task = _TRIGGER_VIEW_PREFIX + str(data["request"]["trigger_id"]) def _completed(self, outcome: TaskOutcome) -> None: if self._handle is not None and outcome.task_id != self._handle.task_id: @@ -532,8 +535,8 @@ def _completed(self, outcome: TaskOutcome) -> None: def _view_current(self) -> bool: if self._handoff_view is not None: return self._handoff_view == (self._handoff_operation.currentData(), self._handoff_request.toPlainText()) - if self._view_task.startswith("trigger:"): - return self._view_task == "trigger:" + self._trigger_id.text() + if self._view_task.startswith(_TRIGGER_VIEW_PREFIX): + return self._view_task == _TRIGGER_VIEW_PREFIX + self._trigger_id.text() return not self._view_task or self._view_task == self._task_id.text() def _present_result(self, result: dict) -> None: diff --git a/je_auto_control/gui/dag_tab.py b/je_auto_control/gui/dag_tab.py index e666652f9..7b514cce5 100644 --- a/je_auto_control/gui/dag_tab.py +++ b/je_auto_control/gui/dag_tab.py @@ -3,7 +3,7 @@ import threading from functools import partial from contextvars import copy_context -from typing import Optional +from typing import Any, Callable, Optional from PySide6.QtCore import QObject, Qt, Signal from PySide6.QtWidgets import ( @@ -264,8 +264,13 @@ def _spawn_worker(self, definition: dict, *, inspect_only: bool = False, batch_o self._inspection_source = self._editor.toPlainText() self.setProperty("dag_batch_source", self._batch_id.text()) self._inspection_output.clear() - on_done = (self._on_batch_finished if batch_operation and batch_operation != "run_continuation" - else self._on_inspection_finished if inspect_only else self._on_worker_finished) + on_done: Callable[[Any], None] + if batch_operation and batch_operation != "run_continuation": + on_done = self._on_batch_finished + elif inspect_only: + on_done = self._on_inspection_finished + else: + on_done = self._on_worker_finished self._thread = start_worker( self, worker, on_done=on_done, on_fail=self._on_worker_failed, on_thread_done=self._on_thread_done) diff --git a/je_auto_control/gui/healing_evaluation_panel.py b/je_auto_control/gui/healing_evaluation_panel.py index 76469af6d..876ac70ac 100644 --- a/je_auto_control/gui/healing_evaluation_panel.py +++ b/je_auto_control/gui/healing_evaluation_panel.py @@ -56,8 +56,14 @@ def _version_row(name: str, version: HealingVersionReport) -> list[str]: return [name, *ratios, str(version.unknown), *latency] +def _correctness(correct: bool | None) -> str: + if correct is None: + return _t("unknown") + return _t("correct_true" if correct else "correct_false") + + def _case_row(row: HealingAttempt) -> list[str]: - correct = _t("unknown") if row.correct is None else _t("correct_true" if row.correct else "correct_false") + correct = _correctness(row.correct) return [row.version, row.sample_id, correct, row.run_id or "", row.step_id or ""] diff --git a/je_auto_control/gui/language_wrapper/japanese.py b/je_auto_control/gui/language_wrapper/japanese.py index a649c0ea8..a22bf1e94 100644 --- a/je_auto_control/gui/language_wrapper/japanese.py +++ b/je_auto_control/gui/language_wrapper/japanese.py @@ -12,6 +12,7 @@ _HOTKEYS = "ホットキー" _ACTION = "アクション" _RUNNING = "実行中..." +_VERSION = "バージョン" japanese_word_dict = { "application_name": "AutoControlGUI", @@ -1076,7 +1077,7 @@ "self_heal_verify_label": "クリック後の検証", "self_heal_verify_placeholder": "任意の JSON チェック。例: {\"type\": \"image_gone\"}", "self_heal_verify_invalid": "チェックは JSON オブジェクトで指定してください", - "self_heal_cmp_version": "バージョン", + "self_heal_cmp_version": _VERSION, "self_heal_cmp_located": "検出", "self_heal_cmp_accuracy": "正解率", "self_heal_cmp_false_positive": "誤検出", @@ -1103,7 +1104,7 @@ "self_heal_col_template_path": "テンプレート", "self_heal_col_description": "説明", "self_heal_col_duration_ms": "ミリ秒", - "self_heal_col_locator_version": "バージョン", + "self_heal_col_locator_version": _VERSION, "self_heal_col_action_verified": "検証", "self_heal_verified_yes": "検証済み", "self_heal_verified_no": "検証失敗", @@ -1573,7 +1574,7 @@ "healing_eval_cancel": "比較をキャンセル", "healing_eval_export_json": "比較 JSON を保存", "healing_eval_export_html": "比較 HTML を保存", - "healing_eval_version": "バージョン", + "healing_eval_version": _VERSION, "healing_eval_accuracy": "正解/ラベル済み", "healing_eval_false_positive": "誤検出/検出済み", "healing_eval_recovery": "検証済み復旧", diff --git a/je_auto_control/gui/main_widget.py b/je_auto_control/gui/main_widget.py index cdfdc9f67..8c44acc35 100644 --- a/je_auto_control/gui/main_widget.py +++ b/je_auto_control/gui/main_widget.py @@ -1,6 +1,6 @@ """Workspace widget: owns the tab bar and the registry of every tab it can open.""" import json -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from PySide6.QtCore import QTimer, Signal, QObject from PySide6.QtGui import QKeyEvent, Qt @@ -172,7 +172,7 @@ def _build_remote_desktop_tab() -> QWidget: # --- tab registry API ---------------------------------------------------- def _add_tab( - self, key: str, title_key: str, widget: Union[QWidget, WidgetFactory], + self, key: str, title_key: str, widget: QWidget | WidgetFactory, category: str = "core", default_visible: bool = False, actions: MenuActions = (), ) -> None: diff --git a/je_auto_control/gui/remote_desktop/folder_sync_io.py b/je_auto_control/gui/remote_desktop/folder_sync_io.py index 7a03247fb..6125057f4 100644 --- a/je_auto_control/gui/remote_desktop/folder_sync_io.py +++ b/je_auto_control/gui/remote_desktop/folder_sync_io.py @@ -58,6 +58,7 @@ def make_folder_engine(viewer: _Viewer, directory: str, current_viewer: Callable """Create inert polling work; the caller attaches the returned engine before starting it.""" authority = DeferredAuthorization.capture() with authority.scope(): + # reason: entering the scope re-checks the owner's current rights; nothing runs inside it pass engine: FolderSyncEngine connection = viewer.connection_identity @@ -71,6 +72,7 @@ def cancelled() -> bool: return True try: with authority.scope(): + # reason: entering the scope re-checks the owner's current rights; nothing runs inside it pass except (AutoControlException, OSError, ValueError) as error: raise_caused_run_interruption(error) diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index 48e921703..dd70ccdac 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -60,6 +60,13 @@ class CommandSpec: _POINT_PLACEHOLDER = "[10, 20]" _RECT_PLACEHOLDER = "[x, y, width, height]" _RECT4_PLACEHOLDER = "[x, y, w, h]" +_OUTBOX_PLACEHOLDER = "config-outbox.sqlite3" +_MODIFIER_PLACEHOLDER = "ctrl | command" +_UNICODE_TEXT_PLACEHOLDER = "café 🚀 値" +_SIZE_PLACEHOLDER = "[width, height]" +_TIMEOUT_PLACEHOLDER = "timeout seconds" +_POLL_INTERVAL_PLACEHOLDER = "poll interval seconds" +_RGB_PLACEHOLDER = "[r, g, b]" _APPROVALS_DIR = ".approvals" _DOTTED_KEY_PLACEHOLDER = "db.host" _POINTS_JSON_PLACEHOLDER = '[{"x":..,"y":..,"width":..,"height":..}]' @@ -122,17 +129,22 @@ def _add_template_candidate_specs(specs: List[CommandSpec]) -> None: def _add_self_healing_specs(specs: List[CommandSpec]) -> None: _add_template_candidate_specs(specs) - specs.append(CommandSpec( - "AC_self_heal_source", "Image", "Inspect Original Healing Step", - fields=(FieldSpec("path", FieldType.FILE_PATH), FieldSpec("run_id", FieldType.STRING), - FieldSpec("step_id", FieldType.STRING), FieldSpec("limit", FieldType.INT, default=100, optional=True)), - description="Read sanitized snapshots using a healing record's journal_path, run_id and step_id.", - )) - specs.append(CommandSpec( - "AC_self_heal_run", "Image", "Run Self-Healing with Verification", - fields=(FieldSpec("request", FieldType.JSON, placeholder='{"operation":"locate","description":"submit"}'),), - description="Explicit click plus verification.description opts into a fresh post-click configured-VLM check.", - )) + specs.extend([ + CommandSpec( + "AC_self_heal_source", "Image", "Inspect Original Healing Step", + fields=(FieldSpec("path", FieldType.FILE_PATH), FieldSpec("run_id", FieldType.STRING), + FieldSpec("step_id", FieldType.STRING), + FieldSpec("limit", FieldType.INT, default=100, optional=True)), + description="Read sanitized snapshots using a healing record's journal_path, run_id and step_id.", + ), + CommandSpec( + "AC_self_heal_run", "Image", "Run Self-Healing with Verification", + fields=(FieldSpec("request", FieldType.JSON, + placeholder='{"operation":"locate","description":"submit"}'),), + description=("Explicit click plus verification.description opts into a fresh post-click " + "configured-VLM check."), + ), + ]) fields = ( FieldSpec("template_path", FieldType.FILE_PATH, optional=True), FieldSpec("description", FieldType.STRING, optional=True), @@ -200,7 +212,7 @@ def _add_config_sync_specs(specs: List[CommandSpec]) -> None: description="Publish a complete hash-verified generation in a local cache. No settings activation, " "script execution or trigger enabling."), CommandSpec("AC_sync_peer_control", "Data", "Control Durable Sync Peer", - fields=(FieldSpec("path", FieldType.FILE_PATH, placeholder="config-outbox.sqlite3"), + fields=(FieldSpec("path", FieldType.FILE_PATH, placeholder=_OUTBOX_PLACEHOLDER), endpoint, user, FieldSpec("operation", FieldType.ENUM, default="status", choices=("status", "register", "ack", "collect", "retire", "full_resync")), options), description="Durable peer control or local status. Retirement uses options.peer_id; full resync " @@ -213,18 +225,18 @@ def _add_config_sync_specs(specs: List[CommandSpec]) -> None: FieldSpec("operation_id", FieldType.STRING), options), description="Explicit causal actor commit. Reuse exact request/ID after uncertain results."), CommandSpec("AC_sync_outbox_flush", "Data", "Deliver Local Sync Queue", - fields=(FieldSpec("path", FieldType.FILE_PATH, placeholder="config-outbox.sqlite3"), + fields=(FieldSpec("path", FieldType.FILE_PATH, placeholder=_OUTBOX_PLACEHOLDER), endpoint, user, options), description="Bounded v3 HTTP delivery. options may include policy/max_rebases; no script runs."), CommandSpec("AC_sync_entry_merge", "Data", "Review Causal Entry Merge", fields=(FieldSpec("left", FieldType.JSON), FieldSpec("right", FieldType.JSON)), description="Merge supplied causal JSON only; concurrent values remain explicit alternatives."), CommandSpec("AC_sync_outbox_enqueue", "Data", "Queue Local Sync Operation", - fields=(FieldSpec("path", FieldType.FILE_PATH, placeholder="config-outbox.sqlite3"), + fields=(FieldSpec("path", FieldType.FILE_PATH, placeholder=_OUTBOX_PLACEHOLDER), FieldSpec("operation", FieldType.JSON)), description="Persist a local queued edit without HTTP delivery or applying configuration."), CommandSpec("AC_sync_outbox_status", "Data", "Inspect Local Sync Queue", - fields=(FieldSpec("path", FieldType.FILE_PATH, placeholder="config-outbox.sqlite3"), + fields=(FieldSpec("path", FieldType.FILE_PATH, placeholder=_OUTBOX_PLACEHOLDER), FieldSpec("endpoint", FieldType.STRING), user, FieldSpec("include_pending", FieldType.BOOL, optional=True, default=False), FieldSpec("limit", FieldType.INT, optional=True, default=100, min_value=1, max_value=1000)), @@ -248,792 +260,804 @@ def _add_config_sync_specs(specs: List[CommandSpec]) -> None: def _add_journal_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_generate_candidate_from_log", "Data", "Generate Journal Candidate", - fields=(FieldSpec("path", FieldType.FILE_PATH), FieldSpec("run_id", FieldType.STRING), - FieldSpec("target", FieldType.ENUM, choices=("pytest", "python", "robot"), default="pytest"), - FieldSpec("style", FieldType.ENUM, choices=("actions", "calls"), default="actions"), - FieldSpec("output", FieldType.FILE_PATH, optional=True, placeholder="review.zip")), - description="Review-only journal source/provenance; optional ZIP export. " - "No input dispatch or automatic replay.", - )) - specs.append(CommandSpec( - "AC_history_capture", "Flow", "Record Manual Run History", - fields=(FieldSpec("source_id", FieldType.STRING, optional=True, default="script"), - FieldSpec("script_path", FieldType.STRING, optional=True, default="")), - body_keys=("body",), - description="Record the nested body without replacing its executor or variables; labels do not open files.", - )) + specs.extend([ + CommandSpec( + "AC_generate_candidate_from_log", "Data", "Generate Journal Candidate", + fields=(FieldSpec("path", FieldType.FILE_PATH), FieldSpec("run_id", FieldType.STRING), + FieldSpec("target", FieldType.ENUM, choices=("pytest", "python", "robot"), default="pytest"), + FieldSpec("style", FieldType.ENUM, choices=("actions", "calls"), default="actions"), + FieldSpec("output", FieldType.FILE_PATH, optional=True, placeholder="review.zip")), + description="Review-only journal source/provenance; optional ZIP export. " + "No input dispatch or automatic replay.", + ), + CommandSpec( + "AC_history_capture", "Flow", "Record Manual Run History", + fields=(FieldSpec("source_id", FieldType.STRING, optional=True, default="script"), + FieldSpec("script_path", FieldType.STRING, optional=True, default="")), + body_keys=("body",), + description="Record the nested body without replacing its executor or variables; labels do not open files.", + ), + ]) path = FieldSpec("path", FieldType.FILE_PATH) run_id = FieldSpec("run_id", FieldType.STRING, optional=True) - specs.append(CommandSpec( - "AC_journal_capture", "Flow", "Capture Action Journal", - fields=(path, run_id, FieldSpec("device_id", FieldType.STRING, optional=True), - FieldSpec("session_id", FieldType.STRING, optional=True)), - body_keys=("body",), - description="Capture the nested body on its owning executor; preserve variables and strictness.", - )) - specs.append(CommandSpec( - "AC_journal_read", "Data", "Read Action Journal", - fields=(path, run_id, FieldSpec("limit", FieldType.INT, optional=True, default=100, - min_value=1, max_value=10000)), - description="Return sanitized lifecycle snapshots in append order; validate the whole file.", - )) + specs.extend([ + CommandSpec( + "AC_journal_capture", "Flow", "Capture Action Journal", + fields=(path, run_id, FieldSpec("device_id", FieldType.STRING, optional=True), + FieldSpec("session_id", FieldType.STRING, optional=True)), + body_keys=("body",), + description="Capture the nested body on its owning executor; preserve variables and strictness.", + ), + CommandSpec( + "AC_journal_read", "Data", "Read Action Journal", + fields=(path, run_id, FieldSpec("limit", FieldType.INT, optional=True, default=100, + min_value=1, max_value=10000)), + description="Return sanitized lifecycle snapshots in append order; validate the whole file.", + ), + ]) def _add_mouse_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_click_mouse", "Mouse", "Click Mouse", - fields=( - FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, - default="mouse_left"), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), - FieldSpec("clicks", FieldType.INT, optional=True, default=1, - min_value=1), - FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.0, - min_value=0.0), + specs.extend([ + CommandSpec( + "AC_click_mouse", "Mouse", "Click Mouse", + fields=( + FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, + default="mouse_left"), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + FieldSpec("clicks", FieldType.INT, optional=True, default=1, + min_value=1), + FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.0, + min_value=0.0), + ), ), - )) - specs.append(CommandSpec( - "AC_set_mouse_position", "Mouse", "Move Mouse To", - fields=( - FieldSpec("x", FieldType.INT, default=0), - FieldSpec("y", FieldType.INT, default=0), + CommandSpec( + "AC_set_mouse_position", "Mouse", "Move Mouse To", + fields=( + FieldSpec("x", FieldType.INT, default=0), + FieldSpec("y", FieldType.INT, default=0), + ), ), - )) - specs.append(CommandSpec( - "AC_human_move", "Mouse", "Human-like Move To", - fields=( - FieldSpec("x", FieldType.INT, default=0), - FieldSpec("y", FieldType.INT, default=0), - FieldSpec("duration_s", FieldType.FLOAT, optional=True, - default=0.4, min_value=0.0), - FieldSpec("curve", FieldType.FLOAT, optional=True, default=0.2), - FieldSpec("overshoot", FieldType.FLOAT, optional=True, - default=0.0), - FieldSpec("jitter", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("seed", FieldType.INT, optional=True), - ), - description="Move the cursor along a curved, human-like path.", - )) - specs.append(CommandSpec( - "AC_press_mouse", "Mouse", "Press Mouse Button", - fields=( - FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, - default="mouse_left"), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), + CommandSpec( + "AC_human_move", "Mouse", "Human-like Move To", + fields=( + FieldSpec("x", FieldType.INT, default=0), + FieldSpec("y", FieldType.INT, default=0), + FieldSpec("duration_s", FieldType.FLOAT, optional=True, + default=0.4, min_value=0.0), + FieldSpec("curve", FieldType.FLOAT, optional=True, default=0.2), + FieldSpec("overshoot", FieldType.FLOAT, optional=True, + default=0.0), + FieldSpec("jitter", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("seed", FieldType.INT, optional=True), + ), + description="Move the cursor along a curved, human-like path.", ), - )) - specs.append(CommandSpec( - "AC_release_mouse", "Mouse", "Release Mouse Button", - fields=( - FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, - default="mouse_left"), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), + CommandSpec( + "AC_press_mouse", "Mouse", "Press Mouse Button", + fields=( + FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, + default="mouse_left"), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + ), ), - )) - specs.append(CommandSpec( - "AC_mouse_scroll", "Mouse", "Scroll Wheel", - fields=( - FieldSpec("scroll_value", FieldType.INT, default=1), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), + CommandSpec( + "AC_release_mouse", "Mouse", "Release Mouse Button", + fields=( + FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, + default="mouse_left"), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + ), ), - )) - specs.append(CommandSpec( - "AC_get_mouse_position", "Mouse", "Get Mouse Position" - )) + CommandSpec( + "AC_mouse_scroll", "Mouse", "Scroll Wheel", + fields=( + FieldSpec("scroll_value", FieldType.INT, default=1), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + ), + ), + CommandSpec( + "AC_get_mouse_position", "Mouse", "Get Mouse Position" + ), + ]) def _add_keyboard_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_type_keyboard", "Keyboard", "Type Key", - fields=( - FieldSpec("keycode", FieldType.STRING, placeholder="e.g. a, enter, 65"), + specs.extend([ + CommandSpec( + "AC_type_keyboard", "Keyboard", "Type Key", + fields=( + FieldSpec("keycode", FieldType.STRING, placeholder="e.g. a, enter, 65"), + ), ), - )) - specs.append(CommandSpec( - "AC_press_keyboard_key", "Keyboard", "Press Key", - fields=( - FieldSpec("keycode", FieldType.STRING, placeholder="e.g. shift"), + CommandSpec( + "AC_press_keyboard_key", "Keyboard", "Press Key", + fields=( + FieldSpec("keycode", FieldType.STRING, placeholder="e.g. shift"), + ), ), - )) - specs.append(CommandSpec( - "AC_release_keyboard_key", "Keyboard", "Release Key", - fields=( - FieldSpec("keycode", FieldType.STRING, placeholder="e.g. shift"), + CommandSpec( + "AC_release_keyboard_key", "Keyboard", "Release Key", + fields=( + FieldSpec("keycode", FieldType.STRING, placeholder="e.g. shift"), + ), ), - )) - specs.append(CommandSpec( - "AC_write", "Keyboard", "Write Text", - fields=( - FieldSpec("write_string", FieldType.STRING, placeholder="Hello, world"), + CommandSpec( + "AC_write", "Keyboard", "Write Text", + fields=( + FieldSpec("write_string", FieldType.STRING, placeholder="Hello, world"), + ), ), - )) - specs.append(CommandSpec( - "AC_write_secret", "Keyboard", "Write Secret Text", - fields=(FieldSpec("secret", FieldType.STRING),), - description="Type a password or token exactly; never logged, recorded or returned.", - )) - specs.append(CommandSpec( - "AC_human_type", "Keyboard", "Human-like Type", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="Hello, world"), - FieldSpec("base_delay", FieldType.FLOAT, optional=True, - default=0.05, min_value=0.0), - FieldSpec("jitter", FieldType.FLOAT, optional=True, default=0.04, - min_value=0.0), - FieldSpec("pause_chance", FieldType.FLOAT, optional=True, - default=0.0, min_value=0.0, max_value=1.0), - FieldSpec("seed", FieldType.INT, optional=True), - ), - description="Type text with randomized per-key delays.", - )) - specs.append(CommandSpec( - "AC_set_field_text", "Keyboard", "Set Field Text", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="new value"), - FieldSpec("clear", FieldType.ENUM, choices=("select_all", "none"), - optional=True, default="select_all"), - FieldSpec("paste", FieldType.BOOL, optional=True, default=False), - FieldSpec("modifier", FieldType.STRING, optional=True, - default="ctrl", placeholder="ctrl | command"), - ), - description="Clear the focused field then enter text (paste for Unicode).", - )) - specs.append(CommandSpec( - "AC_hold_key", "Keyboard", "Hold Key", - fields=( - FieldSpec("key", FieldType.STRING, placeholder="e.g. key_d, space"), - FieldSpec("duration_s", FieldType.FLOAT, default=1.0, - min_value=0.01), - FieldSpec("rate_hz", FieldType.FLOAT, optional=True, - placeholder="auto-repeat presses/sec (blank = hold)"), + CommandSpec( + "AC_write_secret", "Keyboard", "Write Secret Text", + fields=(FieldSpec("secret", FieldType.STRING),), + description="Type a password or token exactly; never logged, recorded or returned.", ), - description="Hold a key for a duration, or auto-repeat it at rate_hz.", - )) - specs.append(CommandSpec( - "AC_type_unicode", "Keyboard", "Type Unicode (emoji / CJK)", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="café 🚀 値"), - FieldSpec("modifier", FieldType.STRING, optional=True, - default="ctrl", placeholder="ctrl | command"), + CommandSpec( + "AC_human_type", "Keyboard", "Human-like Type", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="Hello, world"), + FieldSpec("base_delay", FieldType.FLOAT, optional=True, + default=0.05, min_value=0.0), + FieldSpec("jitter", FieldType.FLOAT, optional=True, default=0.04, + min_value=0.0), + FieldSpec("pause_chance", FieldType.FLOAT, optional=True, + default=0.0, min_value=0.0, max_value=1.0), + FieldSpec("seed", FieldType.INT, optional=True), + ), + description="Type text with randomized per-key delays.", ), - description="Enter any Unicode text via clipboard paste (write can't).", - )) - specs.append(CommandSpec( - "AC_type_unicode_keys", "Keyboard", "Type Unicode (key events)", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="café 🚀 値"), + CommandSpec( + "AC_set_field_text", "Keyboard", "Set Field Text", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="new value"), + FieldSpec("clear", FieldType.ENUM, choices=("select_all", "none"), + optional=True, default="select_all"), + FieldSpec("paste", FieldType.BOOL, optional=True, default=False), + FieldSpec("modifier", FieldType.STRING, optional=True, + default="ctrl", placeholder=_MODIFIER_PLACEHOLDER), + ), + description="Clear the focused field then enter text (paste for Unicode).", ), - description="Enter any Unicode text as key events; leaves the clipboard " - "untouched. Needs a backend that supports it (Windows).", - )) - specs.append(CommandSpec( - "AC_type_unicode_text", "Keyboard", "Type Unicode (best route)", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="café 🚀 値"), - FieldSpec("modifier", FieldType.STRING, optional=True, - default="ctrl", placeholder="ctrl | command"), + CommandSpec( + "AC_hold_key", "Keyboard", "Hold Key", + fields=( + FieldSpec("key", FieldType.STRING, placeholder="e.g. key_d, space"), + FieldSpec("duration_s", FieldType.FLOAT, default=1.0, + min_value=0.01), + FieldSpec("rate_hz", FieldType.FLOAT, optional=True, + placeholder="auto-repeat presses/sec (blank = hold)"), + ), + description="Hold a key for a duration, or auto-repeat it at rate_hz.", ), - description="Enter any Unicode text: key events where available, " - "clipboard paste otherwise.", - )) - specs.append(CommandSpec( - "AC_with_modifiers", "Keyboard", "With Modifiers Held", - fields=( - FieldSpec("modifiers", FieldType.STRING, placeholder="ctrl+shift"), - FieldSpec("actions", FieldType.STRING, - placeholder='[["AC_click_mouse", {...}], ...]'), + CommandSpec( + "AC_type_unicode", "Keyboard", "Type Unicode (emoji / CJK)", + fields=( + FieldSpec("text", FieldType.STRING, placeholder=_UNICODE_TEXT_PLACEHOLDER), + FieldSpec("modifier", FieldType.STRING, optional=True, + default="ctrl", placeholder=_MODIFIER_PLACEHOLDER), + ), + description="Enter any Unicode text via clipboard paste (write can't).", ), - description="Run nested actions while modifiers are held (release-safe).", - )) - specs.append(CommandSpec( - "AC_hotkey", "Keyboard", "Hotkey", - fields=( - FieldSpec("key_code_list", FieldType.STRING, - placeholder="ctrl, shift, s"), + CommandSpec( + "AC_type_unicode_keys", "Keyboard", "Type Unicode (key events)", + fields=( + FieldSpec("text", FieldType.STRING, placeholder=_UNICODE_TEXT_PLACEHOLDER), + ), + description="Enter any Unicode text as key events; leaves the clipboard " + "untouched. Needs a backend that supports it (Windows).", ), - )) + CommandSpec( + "AC_type_unicode_text", "Keyboard", "Type Unicode (best route)", + fields=( + FieldSpec("text", FieldType.STRING, placeholder=_UNICODE_TEXT_PLACEHOLDER), + FieldSpec("modifier", FieldType.STRING, optional=True, + default="ctrl", placeholder=_MODIFIER_PLACEHOLDER), + ), + description="Enter any Unicode text: key events where available, " + "clipboard paste otherwise.", + ), + CommandSpec( + "AC_with_modifiers", "Keyboard", "With Modifiers Held", + fields=( + FieldSpec("modifiers", FieldType.STRING, placeholder="ctrl+shift"), + FieldSpec("actions", FieldType.STRING, + placeholder='[["AC_click_mouse", {...}], ...]'), + ), + description="Run nested actions while modifiers are held (release-safe).", + ), + CommandSpec( + "AC_hotkey", "Keyboard", "Hotkey", + fields=( + FieldSpec("key_code_list", FieldType.STRING, + placeholder="ctrl, shift, s"), + ), + ), + ]) def _add_screen_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_screenshot", "Screen", "Screenshot", - fields=( - FieldSpec("file_path", FieldType.FILE_PATH, optional=True), - FieldSpec("screen_region", FieldType.STRING, optional=True, - placeholder="0,0,800,600"), + specs.extend([ + CommandSpec( + "AC_screenshot", "Screen", "Screenshot", + fields=( + FieldSpec("file_path", FieldType.FILE_PATH, optional=True), + FieldSpec("screen_region", FieldType.STRING, optional=True, + placeholder="0,0,800,600"), + ), ), - )) - specs.append(CommandSpec("AC_screen_size", "Screen", "Get Screen Size")) + CommandSpec("AC_screen_size", "Screen", "Get Screen Size"), + ]) def _add_image_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_locate_image_center", "Image", "Locate Image", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, - default=1.0, min_value=0.0, max_value=1.0), - FieldSpec("draw_image", FieldType.BOOL, optional=True, default=False), + specs.extend([ + CommandSpec( + "AC_locate_image_center", "Image", "Locate Image", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, + default=1.0, min_value=0.0, max_value=1.0), + FieldSpec("draw_image", FieldType.BOOL, optional=True, default=False), + ), ), - )) - specs.append(CommandSpec( - "AC_locate_and_click", "Image", "Locate & Click", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, - default="mouse_left"), - FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, - default=1.0, min_value=0.0, max_value=1.0), - FieldSpec("draw_image", FieldType.BOOL, optional=True, default=False), + CommandSpec( + "AC_locate_and_click", "Image", "Locate & Click", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, + default="mouse_left"), + FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, + default=1.0, min_value=0.0, max_value=1.0), + FieldSpec("draw_image", FieldType.BOOL, optional=True, default=False), + ), ), - )) - specs.append(CommandSpec( - "AC_locate_all_image", "Image", "Locate All", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, - default=1.0, min_value=0.0, max_value=1.0), + CommandSpec( + "AC_locate_all_image", "Image", "Locate All", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, + default=1.0, min_value=0.0, max_value=1.0), + ), ), - )) - specs.append(CommandSpec( - "AC_match_template", "Image", "Match Template (scored)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder=_SCALES_PLACEHOLDER), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Locate a template and return its confidence score + scale.", - )) - specs.append(CommandSpec( - "AC_match_template_all", "Image", "Match Template All (scored)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, - min_value=0.0, max_value=1.0), - ), - description="Find every occurrence of a template (scored, NMS-deduped).", - )) - specs.append(CommandSpec( - "AC_match_masked", "Image", "Match Masked Template", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("mask", FieldType.FILE_PATH, optional=True), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.9, - min_value=0.0, max_value=1.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Match counting only opaque/masked pixels (alpha or mask).", - )) - specs.append(CommandSpec( - "AC_match_masked_all", "Image", "Match Masked Template All", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("mask", FieldType.FILE_PATH, optional=True), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.9, - min_value=0.0, max_value=1.0), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, - min_value=0.0, max_value=1.0), - ), - description="Find every masked match of a template (NMS-deduped).", - )) - specs.append(CommandSpec( - "AC_match_rotated", "Image", "Match Template (rotated)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("angles", FieldType.STRING, optional=True, - placeholder="[-10, 0, 10]"), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder=_SCALES_PLACEHOLDER), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Locate a template tolerating rotation + scale; reports angle.", - )) - specs.append(CommandSpec( - "AC_match_rotated_all", "Image", "Match Template All (rotated)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("angles", FieldType.STRING, optional=True, - placeholder="[-10, 0, 10]"), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder=_SCALES_PLACEHOLDER), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, - min_value=0.0, max_value=1.0), - ), - description="Find every rotation/scale-tolerant match (NMS-deduped).", - )) - specs.append(CommandSpec( - "AC_match_with_trust", "Image", "Match Template (trust-scored)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.0, - min_value=0.0, max_value=1.0), - FieldSpec("ambiguous_ratio", FieldType.FLOAT, optional=True, - default=0.9, min_value=0.0, max_value=1.0), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder=_SCALES_PLACEHOLDER), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Match a template and flag if it is ambiguous (duplicate peak).", - )) - specs.append(CommandSpec( - "AC_match_auto", "Image", "Match Template (auto-threshold)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("floor", FieldType.FLOAT, optional=True, default=0.5, - min_value=0.0, max_value=1.0), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Find a template with an Otsu auto-threshold (no min_score).", - )) - specs.append(CommandSpec( - "AC_auto_threshold", "Image", "Auto Threshold (Otsu on scores)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_match_template", "Image", "Match Template (scored)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder=_SCALES_PLACEHOLDER), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Locate a template and return its confidence score + scale.", ), - description="Derive a match threshold + separability from the score map.", - )) - specs.append(CommandSpec( - "AC_edge_match", "Image", "Match Template (edge shape)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, - min_value=0.0, max_value=1.0), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder=_SCALES_PLACEHOLDER), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Locate by edge shape (Chamfer) — robust to fill / theme / AA.", - )) - specs.append(CommandSpec( - "AC_edge_match_all", "Image", "Match Template All (edge shape)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, - min_value=0.0, max_value=1.0), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, - min_value=0.0, max_value=1.0), - ), - description="Find every edge-shape (Chamfer) match (NMS-deduped).", - )) - specs.append(CommandSpec( - "AC_match_subpixel", "Image", "Match Template (sub-pixel)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.0, - min_value=0.0, max_value=1.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_match_template_all", "Image", "Match Template All (scored)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, + min_value=0.0, max_value=1.0), + ), + description="Find every occurrence of a template (scored, NMS-deduped).", ), - description="Match with a sub-pixel-refined centre (drag / slider precision).", - )) - specs.append(CommandSpec( - "AC_match_ensemble", "Image", "Match Ensemble (vote references)", - fields=( - FieldSpec("templates", FieldType.STRING, - placeholder='["default.png", "hover.png", "pressed.png"]'), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("agree_px", FieldType.INT, optional=True, default=10), - FieldSpec("min_votes", FieldType.INT, optional=True, default=2), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Vote several reference crops onto one consensus location.", - )) - specs.append(CommandSpec( - "AC_vote_centers", "Image", "Vote Centers (consensus)", - fields=( - FieldSpec("centers", FieldType.STRING, - placeholder="[[100, 100], [102, 98]]"), - FieldSpec("agree_px", FieldType.INT, optional=True, default=10), - FieldSpec("min_votes", FieldType.INT, optional=True, default=2), + CommandSpec( + "AC_match_masked", "Image", "Match Masked Template", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("mask", FieldType.FILE_PATH, optional=True), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.9, + min_value=0.0, max_value=1.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Match counting only opaque/masked pixels (alpha or mask).", ), - description="Vote candidate hit centres into one consensus target.", - )) - specs.append(CommandSpec( - "AC_match_color", "Image", "Match Template (colour/HSV)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("channels", FieldType.STRING, optional=True, - placeholder='["h", "s"]'), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, - min_value=0.0, max_value=1.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Match by colour on HSV channels (red vs green, not grayscale).", - )) - specs.append(CommandSpec( - "AC_match_color_all", "Image", "Match Template All (colour/HSV)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("channels", FieldType.STRING, optional=True, - placeholder='["h", "s"]'), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, - min_value=0.0, max_value=1.0), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, - min_value=0.0, max_value=1.0), - ), - description="Find every colour (HSV) match of a template (NMS-deduped).", - )) - specs.append(CommandSpec( - "AC_region_stability", "Image", "Region Stability (frames)", - fields=( - FieldSpec("frames", FieldType.STRING, - placeholder='["f1.png", "f2.png", "f3.png"]'), - FieldSpec("settle_threshold", FieldType.FLOAT, optional=True, - default=0.99, min_value=0.0, max_value=1.0), + CommandSpec( + "AC_match_masked_all", "Image", "Match Masked Template All", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("mask", FieldType.FILE_PATH, optional=True), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.9, + min_value=0.0, max_value=1.0), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, + min_value=0.0, max_value=1.0), + ), + description="Find every masked match of a template (NMS-deduped).", ), - description="Is a frame sequence settled? (consecutive-frame SSIM).", - )) - specs.append(CommandSpec( - "AC_match_persistence", "Image", "Match Persistence (frames)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("frames", FieldType.STRING, - placeholder='["f1.png", "f2.png", "f3.png"]'), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("agree_px", FieldType.INT, optional=True, default=8), - ), - description="Does a template match hold steady across frames? (jitter).", - )) - specs.append(CommandSpec( - "AC_grid_cells", "Image", "Grid Cells (coarse grounding)", - fields=( - FieldSpec("rows", FieldType.INT, default=3), - FieldSpec("cols", FieldType.INT, default=3), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + CommandSpec( + "AC_match_rotated", "Image", "Match Template (rotated)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("angles", FieldType.STRING, optional=True, + placeholder="[-10, 0, 10]"), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder=_SCALES_PLACEHOLDER), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Locate a template tolerating rotation + scale; reports angle.", ), - description="Label an rows x cols grid over the screen for VLM grounding.", - )) - specs.append(CommandSpec( - "AC_cell_for_point", "Image", "Cell For Point", - fields=( - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), - FieldSpec("rows", FieldType.INT, default=3), - FieldSpec("cols", FieldType.INT, default=3), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Return the grid cell label containing a screen point.", - )) - specs.append(CommandSpec( - "AC_point_for_cell", "Image", "Point For Cell", - fields=( - FieldSpec("label", FieldType.STRING, placeholder="C3"), - FieldSpec("rows", FieldType.INT, default=3), - FieldSpec("cols", FieldType.INT, default=3), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + CommandSpec( + "AC_match_rotated_all", "Image", "Match Template All (rotated)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("angles", FieldType.STRING, optional=True, + placeholder="[-10, 0, 10]"), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder=_SCALES_PLACEHOLDER), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, + min_value=0.0, max_value=1.0), + ), + description="Find every rotation/scale-tolerant match (NMS-deduped).", ), - description="Return the centre point of a named grid cell (click target).", - )) - specs.append(CommandSpec( - "AC_find_color_region", "Image", "Find Colour Region", - fields=( - FieldSpec("rgb", FieldType.STRING, placeholder="[0, 200, 0]"), - FieldSpec("tolerance", FieldType.INT, optional=True, default=20), - FieldSpec("min_area", FieldType.INT, optional=True, default=50), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + CommandSpec( + "AC_match_with_trust", "Image", "Match Template (trust-scored)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.0, + min_value=0.0, max_value=1.0), + FieldSpec("ambiguous_ratio", FieldType.FLOAT, optional=True, + default=0.9, min_value=0.0, max_value=1.0), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder=_SCALES_PLACEHOLDER), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Match a template and flag if it is ambiguous (duplicate peak).", ), - description="Locate regions by colour (status light / banner / fill).", - )) - specs.append(CommandSpec( - "AC_ssim_compare", "Image", "SSIM Compare", - fields=( - FieldSpec("reference", FieldType.FILE_PATH), - FieldSpec("current", FieldType.FILE_PATH, optional=True), - FieldSpec("ignore", FieldType.STRING, optional=True, - placeholder="[[x, y, w, h], ...]"), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Structural-similarity score (0..1) vs reference / screen.", - )) - specs.append(CommandSpec( - "AC_ssim_changed_regions", "Image", "SSIM Changed Regions", - fields=( - FieldSpec("reference", FieldType.FILE_PATH), - FieldSpec("current", FieldType.FILE_PATH, optional=True), - FieldSpec("ignore", FieldType.STRING, optional=True, - placeholder="[[x, y, w, h], ...]"), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.35, - min_value=0.0, max_value=1.0), - FieldSpec("min_area", FieldType.INT, optional=True, default=50), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Boxes of the regions that structurally changed.", - )) - specs.append(CommandSpec( - "AC_feature_match", "Image", "Feature Match (ORB)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("max_features", FieldType.INT, optional=True, default=500), - FieldSpec("ratio", FieldType.FLOAT, optional=True, default=0.75, - min_value=0.0, max_value=1.0), - FieldSpec("min_inliers", FieldType.INT, optional=True, default=10), - ), - description="Locate a template under rotation / scale / theme change.", - )) - specs.append(CommandSpec( - "AC_find_shapes", "Image", "Find Shapes", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("min_area", FieldType.INT, optional=True, default=400), - FieldSpec("max_area", FieldType.INT, optional=True), + CommandSpec( + "AC_match_auto", "Image", "Match Template (auto-threshold)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("floor", FieldType.FLOAT, optional=True, default=0.5, + min_value=0.0, max_value=1.0), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Find a template with an Otsu auto-threshold (no min_score).", ), - description="Locate distinct shapes by edge/contour detection (no template).", - )) - specs.append(CommandSpec( - "AC_find_rectangles", "Image", "Find Rectangles", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("min_area", FieldType.INT, optional=True, default=400), - FieldSpec("max_area", FieldType.INT, optional=True), - FieldSpec("aspect_range", FieldType.STRING, optional=True, - placeholder="[1.5, 8.0]"), - FieldSpec("epsilon", FieldType.FLOAT, optional=True, default=0.04, - min_value=0.0, max_value=1.0), - ), - description="Locate rectangular regions (buttons / cards / input fields).", - )) - specs.append(CommandSpec( - "AC_preprocess_image", "Image", "Preprocess Image (OCR/match)", - fields=( - FieldSpec("output_path", FieldType.FILE_PATH), - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("steps", FieldType.STRING, optional=True, - placeholder="grayscale,upscale,binarize"), - FieldSpec("scale", FieldType.FLOAT, optional=True, default=2.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Clean up an image for OCR / matching (grayscale/binarize/…).", - )) - specs.append(CommandSpec( - "AC_segment_hsv", "Image", "Segment by HSV", - fields=( - FieldSpec("lower_hsv", FieldType.STRING, placeholder="[40, 80, 80]"), - FieldSpec("upper_hsv", FieldType.STRING, placeholder="[80, 255, 255]"), - FieldSpec("min_area", FieldType.INT, optional=True, default=50), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + CommandSpec( + "AC_auto_threshold", "Image", "Auto Threshold (Otsu on scores)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Derive a match threshold + separability from the score map.", ), - description="Locate regions inside an HSV band (lighting-robust).", - )) - specs.append(CommandSpec( - "AC_dominant_hue_regions", "Image", "Find Hue Regions", - fields=( - FieldSpec("hue", FieldType.INT, placeholder="0=red, 60=green, 120=blue"), - FieldSpec("hue_tol", FieldType.INT, optional=True, default=10), - FieldSpec("sat_min", FieldType.INT, optional=True, default=80), - FieldSpec("val_min", FieldType.INT, optional=True, default=80), - FieldSpec("min_area", FieldType.INT, optional=True, default=50), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Locate any shade of a hue, any brightness (handles red wrap).", - )) - specs.append(CommandSpec( - "AC_find_text_regions", "Image", "Find Text Regions (MSER)", - fields=( - FieldSpec("min_area", FieldType.INT, optional=True, default=60), - FieldSpec("max_area", FieldType.INT, optional=True), - FieldSpec("merge", FieldType.BOOL, optional=True, default=True), - FieldSpec("max_aspect", FieldType.FLOAT, optional=True, default=12.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Locate text regions without OCR (crop to feed an OCR engine).", - )) - specs.append(CommandSpec( - "AC_find_text_lines", "Image", "Find Text Lines (MSER)", - fields=( - FieldSpec("y_tolerance", FieldType.INT, optional=True, default=8), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_edge_match", "Image", "Match Template (edge shape)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, + min_value=0.0, max_value=1.0), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder=_SCALES_PLACEHOLDER), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Locate by edge shape (Chamfer) — robust to fill / theme / AA.", ), - description="Locate horizontal text lines without OCR.", - )) - specs.append(CommandSpec( - "AC_find_lines", "Image", "Find Lines (Hough)", - fields=( - FieldSpec("min_length", FieldType.INT, optional=True, default=80), - FieldSpec("max_gap", FieldType.INT, optional=True, default=10), - FieldSpec("orientation", FieldType.ENUM, optional=True, default="any", - choices=("any", "horizontal", "vertical", "diagonal")), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Detect straight line segments on raw pixels.", - )) - specs.append(CommandSpec( - "AC_find_grid", "Image", "Find Table Grid", - fields=( - FieldSpec("min_length", FieldType.INT, optional=True, default=120), - FieldSpec("tol", FieldType.INT, optional=True, default=10), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_edge_match_all", "Image", "Match Template All (edge shape)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, + min_value=0.0, max_value=1.0), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, + min_value=0.0, max_value=1.0), + ), + description="Find every edge-shape (Chamfer) match (NMS-deduped).", ), - description="Recover a table's rows / columns / cells from its lines.", - )) - specs.append(CommandSpec( - "AC_find_separators", "Image", "Find Separator Lines", - fields=( - FieldSpec("axis", FieldType.ENUM, optional=True, default="horizontal", - choices=("horizontal", "vertical")), - FieldSpec("min_length", FieldType.INT, optional=True, default=120), - FieldSpec("tol", FieldType.INT, optional=True, default=10), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Coordinates of long divider lines along an axis.", - )) - specs.append(CommandSpec( - "AC_fuse_elements", "Image", "Fuse Element Boxes", - fields=( - FieldSpec("ocr", FieldType.STRING, optional=True, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("icon", FieldType.STRING, optional=True), - FieldSpec("a11y", FieldType.STRING, optional=True), - FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.9, - min_value=0.0, max_value=1.0), - ), - description="Union OCR/icon/a11y boxes, dropping cross-source duplicates.", - )) - specs.append(CommandSpec( - "AC_reading_order", "Image", "Reading Order", - fields=( - FieldSpec("elements", FieldType.STRING, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("row_tol", FieldType.INT, optional=True, default=12), + CommandSpec( + "AC_match_subpixel", "Image", "Match Template (sub-pixel)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.0, + min_value=0.0, max_value=1.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Match with a sub-pixel-refined centre (drag / slider precision).", ), - description="Order element boxes top-to-bottom, left-to-right (+ index).", - )) - specs.append(CommandSpec( - "AC_locate_chain", "Image", "Locate Chain (refine boxes)", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("ops", FieldType.STRING, - placeholder='[{"op":"filter","has_text":"OK"},{"op":"first"}]'), + CommandSpec( + "AC_match_ensemble", "Image", "Match Ensemble (vote references)", + fields=( + FieldSpec("templates", FieldType.STRING, + placeholder='["default.png", "hover.png", "pressed.png"]'), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("agree_px", FieldType.INT, optional=True, default=10), + FieldSpec("min_votes", FieldType.INT, optional=True, default=2), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Vote several reference crops onto one consensus location.", ), - description="Refine candidate boxes (within / filter / reading / nth / …).", - )) - specs.append(CommandSpec( - "AC_image_histogram", "Image", "Image Histogram", - fields=( - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("bins", FieldType.INT, optional=True, default=32), - FieldSpec("space", FieldType.ENUM, optional=True, default="hsv", - choices=("hsv", "rgb", "gray")), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Colour-histogram fingerprint of an image / the screen.", - )) - specs.append(CommandSpec( - "AC_histogram_changed", "Image", "Histogram Changed?", - fields=( - FieldSpec("reference", FieldType.FILE_PATH), - FieldSpec("current", FieldType.FILE_PATH, optional=True), - FieldSpec("method", FieldType.ENUM, optional=True, - default="correlation", - choices=("correlation", "chisqr", "intersection", - "bhattacharyya")), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.9, - min_value=0.0, max_value=1.0), - FieldSpec("space", FieldType.ENUM, optional=True, default="hsv", - choices=("hsv", "rgb", "gray")), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Detect a palette/view change vs a reference (illumination-robust).", - )) - specs.append(CommandSpec( - "AC_image_quality", "Image", "Image Quality", - fields=( - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_vote_centers", "Image", "Vote Centers (consensus)", + fields=( + FieldSpec("centers", FieldType.STRING, + placeholder="[[100, 100], [102, 98]]"), + FieldSpec("agree_px", FieldType.INT, optional=True, default=10), + FieldSpec("min_votes", FieldType.INT, optional=True, default=2), + ), + description="Vote candidate hit centres into one consensus target.", ), - description="Sharpness / contrast / brightness of an image or the screen.", - )) - specs.append(CommandSpec( - "AC_quality_gate", "Image", "Quality Gate (OCR-ready?)", - fields=( - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("min_sharpness", FieldType.FLOAT, optional=True, - default=100.0), - FieldSpec("min_contrast", FieldType.FLOAT, optional=True, - default=12.0), - ), - description="Pass/fail an image for OCR readability with named issues.", - )) - specs.append(CommandSpec( - "AC_detect_scale", "Image", "Detect Display Scale (DPI)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("haystack", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder="[1.0, 1.25, 1.5, 1.75, 2.0]"), - ), - description="Infer the display scale a template renders at (visual DPI).", - )) - specs.append(CommandSpec( - "AC_scale_sweep", "Image", "Scale Sweep (per-scale scores)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("haystack", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder="[1.0, 1.25, 1.5, 1.75, 2.0]"), - ), - description="Per-scale match-score profile of a template.", - )) + CommandSpec( + "AC_match_color", "Image", "Match Template (colour/HSV)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("channels", FieldType.STRING, optional=True, + placeholder='["h", "s"]'), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, + min_value=0.0, max_value=1.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Match by colour on HSV channels (red vs green, not grayscale).", + ), + CommandSpec( + "AC_match_color_all", "Image", "Match Template All (colour/HSV)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("channels", FieldType.STRING, optional=True, + placeholder='["h", "s"]'), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, + min_value=0.0, max_value=1.0), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, + min_value=0.0, max_value=1.0), + ), + description="Find every colour (HSV) match of a template (NMS-deduped).", + ), + CommandSpec( + "AC_region_stability", "Image", "Region Stability (frames)", + fields=( + FieldSpec("frames", FieldType.STRING, + placeholder='["f1.png", "f2.png", "f3.png"]'), + FieldSpec("settle_threshold", FieldType.FLOAT, optional=True, + default=0.99, min_value=0.0, max_value=1.0), + ), + description="Is a frame sequence settled? (consecutive-frame SSIM).", + ), + CommandSpec( + "AC_match_persistence", "Image", "Match Persistence (frames)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("frames", FieldType.STRING, + placeholder='["f1.png", "f2.png", "f3.png"]'), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("agree_px", FieldType.INT, optional=True, default=8), + ), + description="Does a template match hold steady across frames? (jitter).", + ), + CommandSpec( + "AC_grid_cells", "Image", "Grid Cells (coarse grounding)", + fields=( + FieldSpec("rows", FieldType.INT, default=3), + FieldSpec("cols", FieldType.INT, default=3), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Label an rows x cols grid over the screen for VLM grounding.", + ), + CommandSpec( + "AC_cell_for_point", "Image", "Cell For Point", + fields=( + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + FieldSpec("rows", FieldType.INT, default=3), + FieldSpec("cols", FieldType.INT, default=3), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Return the grid cell label containing a screen point.", + ), + CommandSpec( + "AC_point_for_cell", "Image", "Point For Cell", + fields=( + FieldSpec("label", FieldType.STRING, placeholder="C3"), + FieldSpec("rows", FieldType.INT, default=3), + FieldSpec("cols", FieldType.INT, default=3), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Return the centre point of a named grid cell (click target).", + ), + CommandSpec( + "AC_find_color_region", "Image", "Find Colour Region", + fields=( + FieldSpec("rgb", FieldType.STRING, placeholder="[0, 200, 0]"), + FieldSpec("tolerance", FieldType.INT, optional=True, default=20), + FieldSpec("min_area", FieldType.INT, optional=True, default=50), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Locate regions by colour (status light / banner / fill).", + ), + CommandSpec( + "AC_ssim_compare", "Image", "SSIM Compare", + fields=( + FieldSpec("reference", FieldType.FILE_PATH), + FieldSpec("current", FieldType.FILE_PATH, optional=True), + FieldSpec("ignore", FieldType.STRING, optional=True, + placeholder="[[x, y, w, h], ...]"), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Structural-similarity score (0..1) vs reference / screen.", + ), + CommandSpec( + "AC_ssim_changed_regions", "Image", "SSIM Changed Regions", + fields=( + FieldSpec("reference", FieldType.FILE_PATH), + FieldSpec("current", FieldType.FILE_PATH, optional=True), + FieldSpec("ignore", FieldType.STRING, optional=True, + placeholder="[[x, y, w, h], ...]"), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.35, + min_value=0.0, max_value=1.0), + FieldSpec("min_area", FieldType.INT, optional=True, default=50), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Boxes of the regions that structurally changed.", + ), + CommandSpec( + "AC_feature_match", "Image", "Feature Match (ORB)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("max_features", FieldType.INT, optional=True, default=500), + FieldSpec("ratio", FieldType.FLOAT, optional=True, default=0.75, + min_value=0.0, max_value=1.0), + FieldSpec("min_inliers", FieldType.INT, optional=True, default=10), + ), + description="Locate a template under rotation / scale / theme change.", + ), + CommandSpec( + "AC_find_shapes", "Image", "Find Shapes", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("min_area", FieldType.INT, optional=True, default=400), + FieldSpec("max_area", FieldType.INT, optional=True), + ), + description="Locate distinct shapes by edge/contour detection (no template).", + ), + CommandSpec( + "AC_find_rectangles", "Image", "Find Rectangles", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("min_area", FieldType.INT, optional=True, default=400), + FieldSpec("max_area", FieldType.INT, optional=True), + FieldSpec("aspect_range", FieldType.STRING, optional=True, + placeholder="[1.5, 8.0]"), + FieldSpec("epsilon", FieldType.FLOAT, optional=True, default=0.04, + min_value=0.0, max_value=1.0), + ), + description="Locate rectangular regions (buttons / cards / input fields).", + ), + CommandSpec( + "AC_preprocess_image", "Image", "Preprocess Image (OCR/match)", + fields=( + FieldSpec("output_path", FieldType.FILE_PATH), + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("steps", FieldType.STRING, optional=True, + placeholder="grayscale,upscale,binarize"), + FieldSpec("scale", FieldType.FLOAT, optional=True, default=2.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Clean up an image for OCR / matching (grayscale/binarize/…).", + ), + CommandSpec( + "AC_segment_hsv", "Image", "Segment by HSV", + fields=( + FieldSpec("lower_hsv", FieldType.STRING, placeholder="[40, 80, 80]"), + FieldSpec("upper_hsv", FieldType.STRING, placeholder="[80, 255, 255]"), + FieldSpec("min_area", FieldType.INT, optional=True, default=50), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Locate regions inside an HSV band (lighting-robust).", + ), + CommandSpec( + "AC_dominant_hue_regions", "Image", "Find Hue Regions", + fields=( + FieldSpec("hue", FieldType.INT, placeholder="0=red, 60=green, 120=blue"), + FieldSpec("hue_tol", FieldType.INT, optional=True, default=10), + FieldSpec("sat_min", FieldType.INT, optional=True, default=80), + FieldSpec("val_min", FieldType.INT, optional=True, default=80), + FieldSpec("min_area", FieldType.INT, optional=True, default=50), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Locate any shade of a hue, any brightness (handles red wrap).", + ), + CommandSpec( + "AC_find_text_regions", "Image", "Find Text Regions (MSER)", + fields=( + FieldSpec("min_area", FieldType.INT, optional=True, default=60), + FieldSpec("max_area", FieldType.INT, optional=True), + FieldSpec("merge", FieldType.BOOL, optional=True, default=True), + FieldSpec("max_aspect", FieldType.FLOAT, optional=True, default=12.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Locate text regions without OCR (crop to feed an OCR engine).", + ), + CommandSpec( + "AC_find_text_lines", "Image", "Find Text Lines (MSER)", + fields=( + FieldSpec("y_tolerance", FieldType.INT, optional=True, default=8), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Locate horizontal text lines without OCR.", + ), + CommandSpec( + "AC_find_lines", "Image", "Find Lines (Hough)", + fields=( + FieldSpec("min_length", FieldType.INT, optional=True, default=80), + FieldSpec("max_gap", FieldType.INT, optional=True, default=10), + FieldSpec("orientation", FieldType.ENUM, optional=True, default="any", + choices=("any", "horizontal", "vertical", "diagonal")), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Detect straight line segments on raw pixels.", + ), + CommandSpec( + "AC_find_grid", "Image", "Find Table Grid", + fields=( + FieldSpec("min_length", FieldType.INT, optional=True, default=120), + FieldSpec("tol", FieldType.INT, optional=True, default=10), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Recover a table's rows / columns / cells from its lines.", + ), + CommandSpec( + "AC_find_separators", "Image", "Find Separator Lines", + fields=( + FieldSpec("axis", FieldType.ENUM, optional=True, default="horizontal", + choices=("horizontal", "vertical")), + FieldSpec("min_length", FieldType.INT, optional=True, default=120), + FieldSpec("tol", FieldType.INT, optional=True, default=10), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Coordinates of long divider lines along an axis.", + ), + CommandSpec( + "AC_fuse_elements", "Image", "Fuse Element Boxes", + fields=( + FieldSpec("ocr", FieldType.STRING, optional=True, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("icon", FieldType.STRING, optional=True), + FieldSpec("a11y", FieldType.STRING, optional=True), + FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.9, + min_value=0.0, max_value=1.0), + ), + description="Union OCR/icon/a11y boxes, dropping cross-source duplicates.", + ), + CommandSpec( + "AC_reading_order", "Image", "Reading Order", + fields=( + FieldSpec("elements", FieldType.STRING, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("row_tol", FieldType.INT, optional=True, default=12), + ), + description="Order element boxes top-to-bottom, left-to-right (+ index).", + ), + CommandSpec( + "AC_locate_chain", "Image", "Locate Chain (refine boxes)", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("ops", FieldType.STRING, + placeholder='[{"op":"filter","has_text":"OK"},{"op":"first"}]'), + ), + description="Refine candidate boxes (within / filter / reading / nth / …).", + ), + CommandSpec( + "AC_image_histogram", "Image", "Image Histogram", + fields=( + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("bins", FieldType.INT, optional=True, default=32), + FieldSpec("space", FieldType.ENUM, optional=True, default="hsv", + choices=("hsv", "rgb", "gray")), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Colour-histogram fingerprint of an image / the screen.", + ), + CommandSpec( + "AC_histogram_changed", "Image", "Histogram Changed?", + fields=( + FieldSpec("reference", FieldType.FILE_PATH), + FieldSpec("current", FieldType.FILE_PATH, optional=True), + FieldSpec("method", FieldType.ENUM, optional=True, + default="correlation", + choices=("correlation", "chisqr", "intersection", + "bhattacharyya")), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.9, + min_value=0.0, max_value=1.0), + FieldSpec("space", FieldType.ENUM, optional=True, default="hsv", + choices=("hsv", "rgb", "gray")), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Detect a palette/view change vs a reference (illumination-robust).", + ), + CommandSpec( + "AC_image_quality", "Image", "Image Quality", + fields=( + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Sharpness / contrast / brightness of an image or the screen.", + ), + CommandSpec( + "AC_quality_gate", "Image", "Quality Gate (OCR-ready?)", + fields=( + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("min_sharpness", FieldType.FLOAT, optional=True, + default=100.0), + FieldSpec("min_contrast", FieldType.FLOAT, optional=True, + default=12.0), + ), + description="Pass/fail an image for OCR readability with named issues.", + ), + CommandSpec( + "AC_detect_scale", "Image", "Detect Display Scale (DPI)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("haystack", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder="[1.0, 1.25, 1.5, 1.75, 2.0]"), + ), + description="Infer the display scale a template renders at (visual DPI).", + ), + CommandSpec( + "AC_scale_sweep", "Image", "Scale Sweep (per-scale scores)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("haystack", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder="[1.0, 1.25, 1.5, 1.75, 2.0]"), + ), + description="Per-scale match-score profile of a template.", + ), + ]) saliency_fields = ( FieldSpec("source", FieldType.FILE_PATH, optional=True), FieldSpec("region", FieldType.STRING, optional=True, @@ -1042,50 +1066,52 @@ def _add_image_specs(specs: List[CommandSpec]) -> None: FieldSpec("threshold", FieldType.FLOAT, optional=True), FieldSpec("min_area", FieldType.INT, optional=True, default=4), ) - specs.append(CommandSpec( - "AC_salient_regions", "Image", "Salient Regions", - fields=saliency_fields, - description="Visually salient regions (spectral-residual; where to look).", - )) - specs.append(CommandSpec( - "AC_most_salient", "Image", "Most Salient Region", - fields=saliency_fields, - description="The single most visually salient region of an image/screen.", - )) - specs.append(CommandSpec( - "AC_changed_regions", "Image", "Changed Regions (motion)", - fields=( - FieldSpec("before", FieldType.FILE_PATH), - FieldSpec("after", FieldType.FILE_PATH, optional=True), - FieldSpec("threshold", FieldType.INT, optional=True, default=25), - FieldSpec("min_area", FieldType.INT, optional=True, default=80), - FieldSpec("blur", FieldType.INT, optional=True, default=5), + specs.extend([ + CommandSpec( + "AC_salient_regions", "Image", "Salient Regions", + fields=saliency_fields, + description="Visually salient regions (spectral-residual; where to look).", ), - description="Boxes of regions that moved between two frames (after=screen).", - )) - specs.append(CommandSpec( - "AC_has_motion", "Image", "Has Motion?", - fields=( - FieldSpec("before", FieldType.FILE_PATH), - FieldSpec("after", FieldType.FILE_PATH, optional=True), - FieldSpec("threshold", FieldType.INT, optional=True, default=25), - FieldSpec("min_area", FieldType.INT, optional=True, default=80), + CommandSpec( + "AC_most_salient", "Image", "Most Salient Region", + fields=saliency_fields, + description="The single most visually salient region of an image/screen.", ), - description="Whether anything moved between two frames (+ activity score).", - )) - specs.append(CommandSpec( - "AC_perceptual_diff", "Image", "Perceptual Diff (YIQ)", - fields=( - FieldSpec("actual", FieldType.FILE_PATH), - FieldSpec("expected", FieldType.FILE_PATH), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.1, - min_value=0.0, max_value=1.0), - FieldSpec("include_aa", FieldType.BOOL, optional=True, default=False), - FieldSpec("max_diff_ratio", FieldType.FLOAT, optional=True, - min_value=0.0, max_value=1.0), - ), - description="Perceptual image diff that ignores anti-aliased edges.", - )) + CommandSpec( + "AC_changed_regions", "Image", "Changed Regions (motion)", + fields=( + FieldSpec("before", FieldType.FILE_PATH), + FieldSpec("after", FieldType.FILE_PATH, optional=True), + FieldSpec("threshold", FieldType.INT, optional=True, default=25), + FieldSpec("min_area", FieldType.INT, optional=True, default=80), + FieldSpec("blur", FieldType.INT, optional=True, default=5), + ), + description="Boxes of regions that moved between two frames (after=screen).", + ), + CommandSpec( + "AC_has_motion", "Image", "Has Motion?", + fields=( + FieldSpec("before", FieldType.FILE_PATH), + FieldSpec("after", FieldType.FILE_PATH, optional=True), + FieldSpec("threshold", FieldType.INT, optional=True, default=25), + FieldSpec("min_area", FieldType.INT, optional=True, default=80), + ), + description="Whether anything moved between two frames (+ activity score).", + ), + CommandSpec( + "AC_perceptual_diff", "Image", "Perceptual Diff (YIQ)", + fields=( + FieldSpec("actual", FieldType.FILE_PATH), + FieldSpec("expected", FieldType.FILE_PATH), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.1, + min_value=0.0, max_value=1.0), + FieldSpec("include_aa", FieldType.BOOL, optional=True, default=False), + FieldSpec("max_diff_ratio", FieldType.FLOAT, optional=True, + min_value=0.0, max_value=1.0), + ), + description="Perceptual image diff that ignores anti-aliased edges.", + ), + ]) def _add_ocr_specs(specs: List[CommandSpec]) -> None: @@ -1110,759 +1136,765 @@ def _add_ocr_specs(specs: List[CommandSpec]) -> None: description="Installed Tesseract languages; null when the engine cannot be asked.", ), )) - specs.append(CommandSpec( - "AC_read_qr", "OCR", "Read QR Codes", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + specs.extend([ + CommandSpec( + "AC_read_qr", "OCR", "Read QR Codes", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Decode QR codes in a screen region (OpenCV).", ), - description="Decode QR codes in a screen region (OpenCV).", - )) - specs.append(CommandSpec( - "AC_read_barcodes", "OCR", "Read Barcodes (1-D)", - fields=( - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_read_barcodes", "OCR", "Read Barcodes (1-D)", + fields=( + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Decode 1-D barcodes (EAN / UPC) in an image / screen region.", ), - description="Decode 1-D barcodes (EAN / UPC) in an image / screen region.", - )) - specs.append(CommandSpec( - "AC_populate_table", "OCR", "Fill Table From Grid + OCR", - fields=( - FieldSpec("grid", FieldType.STRING, - placeholder='{"rows": [0, 30, 60], "cols": [0, 100, 200]}'), - FieldSpec("text_boxes", FieldType.STRING, - placeholder='[{"x": 10, "y": 5, "width": 60, "height": 20, ' - '"text": "Name"}]'), - FieldSpec("overlap", FieldType.FLOAT, optional=True, default=0.4, - min_value=0.0, max_value=1.0), - ), - description="Drop OCR text boxes into a ruling-line grid → addressable table.", - )) - specs.append(CommandSpec( - "AC_detect_borderless_table", "OCR", "Detect Borderless Table", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder='[{"x":10,"y":0,"width":60,"height":18,' - '"text":"Name"}]'), - FieldSpec("min_gap", FieldType.INT, optional=True, default=8), - FieldSpec("page_width", FieldType.INT, optional=True), + CommandSpec( + "AC_populate_table", "OCR", "Fill Table From Grid + OCR", + fields=( + FieldSpec("grid", FieldType.STRING, + placeholder='{"rows": [0, 30, 60], "cols": [0, 100, 200]}'), + FieldSpec("text_boxes", FieldType.STRING, + placeholder='[{"x": 10, "y": 5, "width": 60, "height": 20, ' + '"text": "Name"}]'), + FieldSpec("overlap", FieldType.FLOAT, optional=True, default=0.4, + min_value=0.0, max_value=1.0), + ), + description="Drop OCR text boxes into a ruling-line grid → addressable table.", ), - description="Infer a borderless table from OCR boxes via whitespace columns.", - )) - specs.append(CommandSpec( - "AC_column_gutters", "OCR", "Column Gutters (whitespace)", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder='[{"x":10,"y":0,"width":60,"height":18}]'), - FieldSpec("min_gap", FieldType.INT, optional=True, default=8), - FieldSpec("page_width", FieldType.INT, optional=True), + CommandSpec( + "AC_detect_borderless_table", "OCR", "Detect Borderless Table", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder='[{"x":10,"y":0,"width":60,"height":18,' + '"text":"Name"}]'), + FieldSpec("min_gap", FieldType.INT, optional=True, default=8), + FieldSpec("page_width", FieldType.INT, optional=True), + ), + description="Infer a borderless table from OCR boxes via whitespace columns.", ), - description="Find borderless-table column separators by whitespace projection.", - )) - specs.append(CommandSpec( - "AC_associate_fields", "OCR", "Associate Form Fields", - fields=( - FieldSpec("text_boxes", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":60,"height":20,' - '"text":"Name:"}]'), - FieldSpec("directions", FieldType.STRING, optional=True, - placeholder='["right", "below"]'), - FieldSpec("max_gap", FieldType.INT, optional=True, default=150), - ), - description="Pair 'label:' boxes with the nearest aligned value (right/below).", - )) - specs.append(CommandSpec( - "AC_match_labels_to_widgets", "OCR", "Match Labels To Widgets", - fields=( - FieldSpec("labels", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":60,"height":20,' - '"text":"Accept"}]'), - FieldSpec("widgets", FieldType.STRING, - placeholder='[{"x":120,"y":0,"width":16,"height":16}]'), + CommandSpec( + "AC_column_gutters", "OCR", "Column Gutters (whitespace)", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder='[{"x":10,"y":0,"width":60,"height":18}]'), + FieldSpec("min_gap", FieldType.INT, optional=True, default=8), + FieldSpec("page_width", FieldType.INT, optional=True), + ), + description="Find borderless-table column separators by whitespace projection.", ), - description="Match each checkbox/radio/input to its nearest label by centre.", - )) - specs.append(CommandSpec( - "AC_flow_order", "OCR", "Reading Order (column-aware)", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":40,"height":20,' - '"text":"A1"}]'), - FieldSpec("min_gap", FieldType.INT, optional=True, default=12), + CommandSpec( + "AC_associate_fields", "OCR", "Associate Form Fields", + fields=( + FieldSpec("text_boxes", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":60,"height":20,' + '"text":"Name:"}]'), + FieldSpec("directions", FieldType.STRING, optional=True, + placeholder='["right", "below"]'), + FieldSpec("max_gap", FieldType.INT, optional=True, default=150), + ), + description="Pair 'label:' boxes with the nearest aligned value (right/below).", ), - description="Column-aware reading order via XY-cut (reads down columns).", - )) - specs.append(CommandSpec( - "AC_xy_cut", "OCR", "XY-Cut Region Tree", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":40,"height":20}]'), - FieldSpec("min_gap", FieldType.INT, optional=True, default=12), + CommandSpec( + "AC_match_labels_to_widgets", "OCR", "Match Labels To Widgets", + fields=( + FieldSpec("labels", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":60,"height":20,' + '"text":"Accept"}]'), + FieldSpec("widgets", FieldType.STRING, + placeholder='[{"x":120,"y":0,"width":16,"height":16}]'), + ), + description="Match each checkbox/radio/input to its nearest label by centre.", ), - description="Recursive XY-cut region tree (split at widest whitespace).", - )) - specs.append(CommandSpec( - "AC_group_paragraphs", "OCR", "Group Paragraphs", - fields=( - FieldSpec("lines", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":200,"height":20,' - '"text":"line"}]'), - FieldSpec("line_gap_factor", FieldType.FLOAT, optional=True, - default=1.6), + CommandSpec( + "AC_flow_order", "OCR", "Reading Order (column-aware)", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":40,"height":20,' + '"text":"A1"}]'), + FieldSpec("min_gap", FieldType.INT, optional=True, default=12), + ), + description="Column-aware reading order via XY-cut (reads down columns).", ), - description="Group OCR lines into paragraphs by vertical spacing.", - )) - specs.append(CommandSpec( - "AC_detect_lists", "OCR", "Detect Lists", - fields=( - FieldSpec("lines", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":200,"height":20,' - '"text":"* item"}]'), + CommandSpec( + "AC_xy_cut", "OCR", "XY-Cut Region Tree", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":40,"height":20}]'), + FieldSpec("min_gap", FieldType.INT, optional=True, default=12), + ), + description="Recursive XY-cut region tree (split at widest whitespace).", ), - description="Detect bulleted / numbered list items among OCR lines.", - )) - specs.append(CommandSpec( - "AC_classify_lines", "OCR", "Classify Headings vs Body", - fields=( - FieldSpec("lines", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":200,"height":40,' - '"text":"Title"}]'), - FieldSpec("heading_ratio", FieldType.FLOAT, optional=True, default=1.2), + CommandSpec( + "AC_group_paragraphs", "OCR", "Group Paragraphs", + fields=( + FieldSpec("lines", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":200,"height":20,' + '"text":"line"}]'), + FieldSpec("line_gap_factor", FieldType.FLOAT, optional=True, + default=1.6), + ), + description="Group OCR lines into paragraphs by vertical spacing.", ), - description="Tag OCR lines as heading/body by height; assign heading levels.", - )) - specs.append(CommandSpec( - "AC_outline", "OCR", "Document Outline", - fields=( - FieldSpec("lines", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":200,"height":40,' - '"text":"Title"}]'), - FieldSpec("heading_ratio", FieldType.FLOAT, optional=True, default=1.2), + CommandSpec( + "AC_detect_lists", "OCR", "Detect Lists", + fields=( + FieldSpec("lines", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":200,"height":20,' + '"text":"* item"}]'), + ), + description="Detect bulleted / numbered list items among OCR lines.", ), - description="Headings in order with levels (document outline) from OCR lines.", - )) - specs.append(CommandSpec( - "AC_scroll_to_find", "OCR", "Scroll Until Visible", - fields=( - FieldSpec("target", FieldType.STRING), - FieldSpec("kind", FieldType.ENUM, choices=("image", "text"), - default="image"), - FieldSpec("direction", FieldType.ENUM, - choices=("down", "up", "left", "right"), default="down"), - FieldSpec("max_scrolls", FieldType.INT, optional=True, default=10, - min_value=1), - FieldSpec("scroll_amount", FieldType.INT, optional=True, default=3, - min_value=1), - ), - description="Scroll until a template image or OCR text appears.", - )) - specs.append(CommandSpec( - "AC_ocr_to_var", "OCR", "Read Text into Variable", - fields=( - FieldSpec("var", FieldType.STRING, default="ocr_text"), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), - FieldSpec("min_confidence", FieldType.FLOAT, optional=True, - default=60.0, min_value=0.0, max_value=100.0), + CommandSpec( + "AC_classify_lines", "OCR", "Classify Headings vs Body", + fields=( + FieldSpec("lines", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":200,"height":40,' + '"text":"Title"}]'), + FieldSpec("heading_ratio", FieldType.FLOAT, optional=True, default=1.2), + ), + description="Tag OCR lines as heading/body by height; assign heading levels.", ), - description="OCR a region and store the text in a flow variable.", - )) - specs.append(CommandSpec( - "AC_wait_text", "OCR", "Wait for Text", - fields=( - FieldSpec("target", FieldType.STRING), - FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.5), - FieldSpec("min_confidence", FieldType.FLOAT, optional=True, - default=60.0, min_value=0.0, max_value=100.0), + CommandSpec( + "AC_outline", "OCR", "Document Outline", + fields=( + FieldSpec("lines", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":200,"height":40,' + '"text":"Title"}]'), + FieldSpec("heading_ratio", FieldType.FLOAT, optional=True, default=1.2), + ), + description="Headings in order with levels (document outline) from OCR lines.", ), - )) - specs.append(CommandSpec( - "AC_click_text", "OCR", "Click Text", - fields=( - FieldSpec("target", FieldType.STRING), - FieldSpec("mouse_keycode", FieldType.ENUM, - choices=_MOUSE_BUTTONS, default="mouse_left"), - FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), - FieldSpec("min_confidence", FieldType.FLOAT, optional=True, - default=60.0, min_value=0.0, max_value=100.0), + CommandSpec( + "AC_scroll_to_find", "OCR", "Scroll Until Visible", + fields=( + FieldSpec("target", FieldType.STRING), + FieldSpec("kind", FieldType.ENUM, choices=("image", "text"), + default="image"), + FieldSpec("direction", FieldType.ENUM, + choices=("down", "up", "left", "right"), default="down"), + FieldSpec("max_scrolls", FieldType.INT, optional=True, default=10, + min_value=1), + FieldSpec("scroll_amount", FieldType.INT, optional=True, default=3, + min_value=1), + ), + description="Scroll until a template image or OCR text appears.", ), - )) + CommandSpec( + "AC_ocr_to_var", "OCR", "Read Text into Variable", + fields=( + FieldSpec("var", FieldType.STRING, default="ocr_text"), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), + FieldSpec("min_confidence", FieldType.FLOAT, optional=True, + default=60.0, min_value=0.0, max_value=100.0), + ), + description="OCR a region and store the text in a flow variable.", + ), + CommandSpec( + "AC_wait_text", "OCR", "Wait for Text", + fields=( + FieldSpec("target", FieldType.STRING), + FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.5), + FieldSpec("min_confidence", FieldType.FLOAT, optional=True, + default=60.0, min_value=0.0, max_value=100.0), + ), + ), + CommandSpec( + "AC_click_text", "OCR", "Click Text", + fields=( + FieldSpec("target", FieldType.STRING), + FieldSpec("mouse_keycode", FieldType.ENUM, + choices=_MOUSE_BUTTONS, default="mouse_left"), + FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), + FieldSpec("min_confidence", FieldType.FLOAT, optional=True, + default=60.0, min_value=0.0, max_value=100.0), + ), + ), + ]) def _add_window_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_focus_window", "Window", "Focus Window", - fields=( - FieldSpec("title_substring", FieldType.STRING), - FieldSpec("case_sensitive", FieldType.BOOL, optional=True, default=False), - ), - )) - specs.append(CommandSpec( - "AC_wait_window", "Window", "Wait for Window", - fields=( - FieldSpec("title_substring", FieldType.STRING), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.5), + specs.extend([ + CommandSpec( + "AC_focus_window", "Window", "Focus Window", + fields=( + FieldSpec("title_substring", FieldType.STRING), + FieldSpec("case_sensitive", FieldType.BOOL, optional=True, default=False), + ), ), - )) - specs.append(CommandSpec( - "AC_close_window", "Window", "Close Window", - fields=(FieldSpec("title_substring", FieldType.STRING),), - description="Ask the first matching window to close (posts WM_CLOSE).", - )) - specs.append(CommandSpec( - "AC_minimize_window", "Window", "Minimize Window", - fields=(FieldSpec("title_substring", FieldType.STRING),), - description="Minimise the first matching window.", - )) - specs.append(CommandSpec( - "AC_foreground_window", "Window", "Foreground Window", - description="The window the user is currently working in.", - )) - specs.append(CommandSpec( - "AC_window_rect", "Window", "Window Rectangle", - fields=(FieldSpec("title_substring", FieldType.STRING),), - description="Screen rectangle (left, top, right, bottom) of a window.", - )) - specs.append(CommandSpec( - "AC_foreground_window_pid", "Window", "Foreground Window PID", - description="PID owning the foreground window (0 when unavailable).", - )) - specs.append(CommandSpec( - "AC_windows_for_pid", "Window", "Windows by Process ID", - fields=(FieldSpec("pid", FieldType.INT),), - description="Visible top-level windows owned by a process.", - )) - specs.append(CommandSpec( - "AC_minimize_windows_for_pid", "Window", "Minimize Windows by PID", - fields=(FieldSpec("pid", FieldType.INT),), - description="Minimise every window a process owns.", - )) - specs.append(CommandSpec( - "AC_post_key_to_window", "Window", "Post Key to Window", - fields=( - FieldSpec("title_substring", FieldType.STRING), - FieldSpec("key", FieldType.STRING), + CommandSpec( + "AC_wait_window", "Window", "Wait for Window", + fields=( + FieldSpec("title_substring", FieldType.STRING), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.5), + ), ), - description="Type one key into a window without focusing it " - "(best effort: some applications ignore posted input).", - )) - specs.append(CommandSpec( - "AC_post_click_to_window", "Window", "Post Click to Window", - fields=( - FieldSpec("title_substring", FieldType.STRING), - FieldSpec("button", FieldType.STRING, optional=True), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), + CommandSpec( + "AC_close_window", "Window", "Close Window", + fields=(FieldSpec("title_substring", FieldType.STRING),), + description="Ask the first matching window to close (posts WM_CLOSE).", ), - description="Click inside a window without focusing it; x / y are " - "relative to the window's top-left corner.", - )) - specs.append(CommandSpec( - "AC_window_pid", "Window", "Window PID by Title", - fields=(FieldSpec("title_substring", FieldType.STRING),), - description="PID owning the first matching window (0 when none).", - )) - specs.append(CommandSpec( - "AC_move_window", "Window", "Move Window by Title", - fields=( - FieldSpec("title_substring", FieldType.STRING), - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), - FieldSpec("width", FieldType.INT, optional=True), - FieldSpec("height", FieldType.INT, optional=True), + CommandSpec( + "AC_minimize_window", "Window", "Minimize Window", + fields=(FieldSpec("title_substring", FieldType.STRING),), + description="Minimise the first matching window.", ), - description="Move, and optionally resize, the first matching window.", - )) - specs.append(CommandSpec( - "AC_drop_files", "Window", "Drop Files onto Window", - fields=( - FieldSpec("hwnd", FieldType.INT), - FieldSpec("paths", FieldType.STRING, - placeholder='["C:\\\\a\\\\one.txt"]'), - FieldSpec("point", FieldType.STRING, optional=True, - placeholder=_POINT_PLACEHOLDER), + CommandSpec( + "AC_foreground_window", "Window", "Foreground Window", + description="The window the user is currently working in.", ), - description="Drop files onto a window via WM_DROPFILES (Windows).", - )) - specs.append(CommandSpec( - "AC_plan_file_drop", "Window", "Plan File Drop", - fields=( - FieldSpec("paths", FieldType.STRING, - placeholder='["C:\\\\a\\\\one.txt"]'), - FieldSpec("point", FieldType.STRING, optional=True, - placeholder=_POINT_PLACEHOLDER), + CommandSpec( + "AC_window_rect", "Window", "Window Rectangle", + fields=(FieldSpec("title_substring", FieldType.STRING),), + description="Screen rectangle (left, top, right, bottom) of a window.", ), - description="Build the WM_DROPFILES payload without sending (pure).", - )) - specs.append(CommandSpec( - "AC_snap_window", "Window", "Snap / Tile Window", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("position", FieldType.ENUM, - choices=("left", "right", "top", "bottom", - "top-left", "top-right", "bottom-left", - "bottom-right", "max"), - default="left"), - ), - description="Move a window to a screen half / quarter / maximize.", - )) - specs.append(CommandSpec( - "AC_arrange_grid", "Window", "Arrange Windows in Grid", - fields=( - FieldSpec("titles", FieldType.STRING, - placeholder='["Editor", "Browser", "Terminal"]'), - FieldSpec("rows", FieldType.INT, optional=True), - FieldSpec("cols", FieldType.INT, optional=True), - FieldSpec("gap", FieldType.INT, optional=True, default=0), + CommandSpec( + "AC_foreground_window_pid", "Window", "Foreground Window PID", + description="PID owning the foreground window (0 when unavailable).", ), - description="Tile a list of windows into a grid (auto-shape if unset).", - )) - specs.append(CommandSpec( - "AC_arrange_cascade", "Window", "Arrange Windows in Cascade", - fields=( - FieldSpec("titles", FieldType.STRING, - placeholder='["Editor", "Browser", "Terminal"]'), - FieldSpec("offset", FieldType.INT, optional=True, default=30), + CommandSpec( + "AC_windows_for_pid", "Window", "Windows by Process ID", + fields=(FieldSpec("pid", FieldType.INT),), + description="Visible top-level windows owned by a process.", ), - description="Cascade a list of windows diagonally.", - )) - specs.append(CommandSpec( - "AC_set_topmost", "Window", "Set Always-On-Top", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("on", FieldType.BOOL, optional=True, default=True), + CommandSpec( + "AC_minimize_windows_for_pid", "Window", "Minimize Windows by PID", + fields=(FieldSpec("pid", FieldType.INT),), + description="Minimise every window a process owns.", ), - description="Pin a window always-on-top (or release it).", - )) - specs.append(CommandSpec( - "AC_bring_to_front", "Window", "Bring Window to Front", - fields=(FieldSpec("title", FieldType.STRING),), - description="Raise a window to the top of the z-order.", - )) - specs.append(CommandSpec( - "AC_send_to_back", "Window", "Send Window to Back", - fields=(FieldSpec("title", FieldType.STRING),), - description="Send a window to the bottom of the z-order.", - )) - specs.append(CommandSpec( - "AC_get_client_rect", "Window", "Get Client Rect", - fields=(FieldSpec("title", FieldType.STRING),), - description="A window's client-area rect (excludes title bar / borders).", - )) - specs.append(CommandSpec( - "AC_client_point", "Window", "Client-Relative Point", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), + CommandSpec( + "AC_post_key_to_window", "Window", "Post Key to Window", + fields=( + FieldSpec("title_substring", FieldType.STRING), + FieldSpec("key", FieldType.STRING), + ), + description="Type one key into a window without focusing it " + "(best effort: some applications ignore posted input).", ), - description="Screen point for an (x, y) inside a window's client area.", - )) - specs.append(CommandSpec( - "AC_wait_window_closed", "Window", "Wait for Window to Close", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), + CommandSpec( + "AC_post_click_to_window", "Window", "Post Click to Window", + fields=( + FieldSpec("title_substring", FieldType.STRING), + FieldSpec("button", FieldType.STRING, optional=True), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + ), + description="Click inside a window without focusing it; x / y are " + "relative to the window's top-left corner.", ), - description="Wait until a window matching the title disappears.", - )) - specs.append(CommandSpec( - "AC_wait_for_file", "Flow", "Wait for File", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=30.0), - FieldSpec("stable_for_s", FieldType.FLOAT, optional=True, - default=1.0, min_value=0.0), - FieldSpec("min_size", FieldType.INT, optional=True, default=1, - min_value=0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.25, min_value=0.01), - ), - description="Wait until a file appears and stops growing (download done).", - )) - specs.append(CommandSpec( - "AC_wait_for_port", "Flow", "Wait for TCP Port", - fields=( - FieldSpec("host", FieldType.STRING, default="127.0.0.1"), - FieldSpec("port", FieldType.INT, min_value=1, max_value=65535), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=30.0), - FieldSpec("connect_timeout_s", FieldType.FLOAT, optional=True, - default=1.0, min_value=0.01), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.25, min_value=0.01), - ), - description="Wait until a TCP host:port accepts connections.", - )) - specs.append(CommandSpec( - "AC_wait_for_process", "Flow", "Wait for Process", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("present", FieldType.BOOL, optional=True, default=True), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=30.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.25, min_value=0.01), - ), - description="Wait until a process appears (or exits). Requires psutil.", - )) - specs.append(CommandSpec("AC_list_windows", "Window", "List Windows")) - specs.append(CommandSpec( - "AC_capture_window", "Window", "Capture Window", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("output_path", FieldType.STRING), + CommandSpec( + "AC_window_pid", "Window", "Window PID by Title", + fields=(FieldSpec("title_substring", FieldType.STRING),), + description="PID owning the first matching window (0 when none).", ), - description="Screenshot the window matching title to a PNG.", - )) - specs.append(CommandSpec( - "AC_save_window_layout", "Window", "Save Window Layout", - fields=(FieldSpec("path", FieldType.STRING, optional=True),), - description="Snapshot every window's position (optionally to a file).", - )) - specs.append(CommandSpec( - "AC_restore_window_layout", "Window", "Restore Window Layout", - fields=(FieldSpec("layout", FieldType.FILE_PATH),), - description="Move windows back to a saved layout file.", - )) - specs.append(CommandSpec( - "AC_tile_rect", "Window", "Tile Rect (compute)", - fields=( - FieldSpec("slot", FieldType.ENUM, - choices=("full", "left", "right", "top", "bottom", - "top_left", "top_right", "bottom_left", - "bottom_right", "center", "left_third", - "center_third", "right_third"), default="left"), - FieldSpec("screen", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("gap", FieldType.INT, optional=True, default=0), - ), - description="Compute the rectangle for a tiling slot of the screen.", - )) - specs.append(CommandSpec( - "AC_grid_rects", "Window", "Grid Rects (compute)", - fields=( - FieldSpec("rows", FieldType.INT, default=2), - FieldSpec("cols", FieldType.INT, default=2), - FieldSpec("screen", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("gap", FieldType.INT, optional=True, default=0), + CommandSpec( + "AC_move_window", "Window", "Move Window by Title", + fields=( + FieldSpec("title_substring", FieldType.STRING), + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + FieldSpec("width", FieldType.INT, optional=True), + FieldSpec("height", FieldType.INT, optional=True), + ), + description="Move, and optionally resize, the first matching window.", ), - description="Compute the cell rectangles of an R×C screen grid.", - )) - specs.append(CommandSpec( - "AC_cascade_rects", "Window", "Cascade Rects (compute)", - fields=( - FieldSpec("count", FieldType.INT, default=3), - FieldSpec("screen", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("offset", FieldType.INT, optional=True, default=30), - FieldSpec("size", FieldType.STRING, optional=True, - placeholder="[width, height]"), - ), - description="Compute staggered, overlapping window rectangles.", - )) - specs.append(CommandSpec( - "AC_enumerate_monitors", "Window", "Enumerate Monitors", - description="List monitors + virtual bounds (multi-display geometry).", - )) - specs.append(CommandSpec( - "AC_monitor_at_point", "Window", "Monitor at Point", - fields=( - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), + CommandSpec( + "AC_drop_files", "Window", "Drop Files onto Window", + fields=( + FieldSpec("hwnd", FieldType.INT), + FieldSpec("paths", FieldType.STRING, + placeholder='["C:\\\\a\\\\one.txt"]'), + FieldSpec("point", FieldType.STRING, optional=True, + placeholder=_POINT_PLACEHOLDER), + ), + description="Drop files onto a window via WM_DROPFILES (Windows).", ), - description="Report which monitor contains a virtual point.", - )) - - -def _add_flow_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_sleep", "Flow", "Sleep", - fields=( - FieldSpec("seconds", FieldType.FLOAT, default=1.0, min_value=0.0), + CommandSpec( + "AC_plan_file_drop", "Window", "Plan File Drop", + fields=( + FieldSpec("paths", FieldType.STRING, + placeholder='["C:\\\\a\\\\one.txt"]'), + FieldSpec("point", FieldType.STRING, optional=True, + placeholder=_POINT_PLACEHOLDER), + ), + description="Build the WM_DROPFILES payload without sending (pure).", ), - )) - specs.append(CommandSpec( - "AC_wait_image", "Flow", "Wait for Image", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.2, - min_value=0.01), + CommandSpec( + "AC_snap_window", "Window", "Snap / Tile Window", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("position", FieldType.ENUM, + choices=("left", "right", "top", "bottom", + "top-left", "top-right", "bottom-left", + "bottom-right", "max"), + default="left"), + ), + description="Move a window to a screen half / quarter / maximize.", ), - )) - specs.append(CommandSpec( - "AC_wait_actionable", "Flow", "Wait Until Actionable", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=5.0), - FieldSpec("stable_for_s", FieldType.FLOAT, optional=True, default=0.3), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Wait until a target is visible + stable before acting.", - )) - specs.append(CommandSpec( - "AC_expect_poll", "Flow", "Expect (poll until match)", - fields=( - FieldSpec("action", FieldType.STRING, - placeholder='["AC_get_clipboard"]'), - FieldSpec("key", FieldType.STRING, optional=True, - placeholder="result dict key, e.g. text"), - FieldSpec("op", FieldType.ENUM, optional=True, default="truthy", - choices=("truthy", "equals", "contains", "gt", "regex")), - FieldSpec("expected", FieldType.STRING, optional=True), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=5.0), - FieldSpec("interval_s", FieldType.FLOAT, optional=True, default=0.25), - ), - description="Re-run an action until a key of its result matches.", - )) - specs.append(CommandSpec( - "AC_soft_assert", "Flow", "Soft Assert (aggregate)", - fields=( - FieldSpec("checks", FieldType.STRING, - placeholder='[{"value":5,"op":"gt","expected":3}]'), - FieldSpec("raise_on_fail", FieldType.BOOL, optional=True, - default=False), + CommandSpec( + "AC_arrange_grid", "Window", "Arrange Windows in Grid", + fields=( + FieldSpec("titles", FieldType.STRING, + placeholder='["Editor", "Browser", "Terminal"]'), + FieldSpec("rows", FieldType.INT, optional=True), + FieldSpec("cols", FieldType.INT, optional=True), + FieldSpec("gap", FieldType.INT, optional=True, default=0), + ), + description="Tile a list of windows into a grid (auto-shape if unset).", ), - description="Aggregate many checks and report all failures (not just first).", - )) - specs.append(CommandSpec( - "AC_replay_trace", "Flow", "Replay Agent Trace", - fields=( - FieldSpec("trace", FieldType.STRING, - placeholder='[{"action":["AC_click_mouse",{...}]}]'), + CommandSpec( + "AC_arrange_cascade", "Window", "Arrange Windows in Cascade", + fields=( + FieldSpec("titles", FieldType.STRING, + placeholder='["Editor", "Browser", "Terminal"]'), + FieldSpec("offset", FieldType.INT, optional=True, default=30), + ), + description="Cascade a list of windows diagonally.", ), - description="Replay a recorded trajectory's actions through the executor.", - )) - specs.append(CommandSpec( - "AC_wait_pixel", "Flow", "Wait for Pixel", - fields=( - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), - FieldSpec("rgb", FieldType.RGB, placeholder="255,255,255"), - FieldSpec("tolerance", FieldType.INT, optional=True, default=0, - min_value=0, max_value=255), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.2, - min_value=0.01), + CommandSpec( + "AC_set_topmost", "Window", "Set Always-On-Top", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("on", FieldType.BOOL, optional=True, default=True), + ), + description="Pin a window always-on-top (or release it).", ), - )) - specs.append(CommandSpec( - "AC_wait_clipboard_change", "Flow", "Wait for Clipboard Change", - fields=( - FieldSpec("target", FieldType.STRING, optional=True), - FieldSpec("contains", FieldType.BOOL, optional=True, default=False), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), - ), - description="Wait until the clipboard changes or matches target.", - )) - specs.append(CommandSpec( - "AC_wait_image_gone", "Flow", "Wait for Image to Vanish", - fields=( - FieldSpec("image", FieldType.STRING, placeholder="path/to/spinner.png"), - FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, - default=1.0), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), - FieldSpec("gone_for_s", FieldType.FLOAT, optional=True, default=0.0), - ), - description="Block until an image (spinner/toast) leaves the screen.", - )) - specs.append(CommandSpec( - "AC_wait_text_gone", "Flow", "Wait for Text to Vanish", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="Loading..."), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), - FieldSpec("gone_for_s", FieldType.FLOAT, optional=True, default=0.0), + CommandSpec( + "AC_bring_to_front", "Window", "Bring Window to Front", + fields=(FieldSpec("title", FieldType.STRING),), + description="Raise a window to the top of the z-order.", ), - description="Block until on-screen text (OCR) disappears.", - )) - specs.append(CommandSpec( - "AC_wait_color", "Flow", "Wait for Region Colour", - fields=( - FieldSpec("target_rgb", FieldType.STRING, placeholder="[0, 200, 0]"), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - FieldSpec("tolerance", FieldType.INT, optional=True, default=10), - FieldSpec("min_fraction", FieldType.FLOAT, optional=True, - default=0.5, min_value=0.0, max_value=1.0), - FieldSpec("present", FieldType.BOOL, optional=True, default=True), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), - ), - description="Block until a colour fills (or leaves) a screen region.", - )) - specs.append(CommandSpec( - "AC_wait_window_title", "Flow", "Wait for Window Title", - fields=( - FieldSpec("pattern", FieldType.STRING, placeholder="Checkout$"), - FieldSpec("present", FieldType.BOOL, optional=True, default=True), - FieldSpec("regex", FieldType.BOOL, optional=True, default=True), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), - ), - description="Block until a window title matches a regex (or vanishes).", - )) - specs.append(CommandSpec( - "AC_loop", "Flow", "Loop (N times)", - fields=( - FieldSpec("times", FieldType.INT, default=3, min_value=1), + CommandSpec( + "AC_send_to_back", "Window", "Send Window to Back", + fields=(FieldSpec("title", FieldType.STRING),), + description="Send a window to the bottom of the z-order.", ), - body_keys=("body",), - )) - specs.append(CommandSpec( - "AC_while_image", "Flow", "While Image Visible", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("max_iter", FieldType.INT, optional=True, default=100, - min_value=1), + CommandSpec( + "AC_get_client_rect", "Window", "Get Client Rect", + fields=(FieldSpec("title", FieldType.STRING),), + description="A window's client-area rect (excludes title bar / borders).", ), - body_keys=("body",), - )) - specs.append(CommandSpec( - "AC_if_image_found", "Flow", "If Image Found", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), + CommandSpec( + "AC_client_point", "Window", "Client-Relative Point", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + ), + description="Screen point for an (x, y) inside a window's client area.", ), - body_keys=("then", "else"), - )) - specs.append(CommandSpec( - "AC_if_pixel", "Flow", "If Pixel Matches", - fields=( - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), - FieldSpec("rgb", FieldType.RGB, placeholder="255,255,255"), - FieldSpec("tolerance", FieldType.INT, optional=True, default=0, - min_value=0, max_value=255), + CommandSpec( + "AC_wait_window_closed", "Window", "Wait for Window to Close", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + ), + description="Wait until a window matching the title disappears.", ), - body_keys=("then", "else"), - )) - specs.append(CommandSpec( - "AC_retry", "Flow", "Retry on Failure", - fields=( - FieldSpec("max_attempts", FieldType.INT, optional=True, default=3, - min_value=1), - FieldSpec("backoff", FieldType.FLOAT, optional=True, default=0.5, - min_value=0.0), + CommandSpec( + "AC_wait_for_file", "Flow", "Wait for File", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=30.0), + FieldSpec("stable_for_s", FieldType.FLOAT, optional=True, + default=1.0, min_value=0.0), + FieldSpec("min_size", FieldType.INT, optional=True, default=1, + min_value=0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.25, min_value=0.01), + ), + description="Wait until a file appears and stops growing (download done).", ), - body_keys=("body",), - )) - specs.append(CommandSpec("AC_break", "Flow", "Break Loop")) - specs.append(CommandSpec("AC_continue", "Flow", "Continue Loop")) - specs.append(CommandSpec( - "AC_transform_var", "Flow", "Transform Variable", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("op", FieldType.ENUM, - choices=("upper", "lower", "strip", "title", - "lstrip", "rstrip", "replace", "regex", - "slice"), - default="strip"), - FieldSpec("into", FieldType.STRING, optional=True), - FieldSpec("find", FieldType.STRING, optional=True), - FieldSpec("replace_with", FieldType.STRING, optional=True), - FieldSpec("pattern", FieldType.STRING, optional=True), - ), - description="String-transform a variable (upper/strip/replace/regex/...).", - )) - specs.append(CommandSpec( - "AC_now_to_var", "Flow", "Timestamp into Variable", - fields=( - FieldSpec("var", FieldType.STRING, default="now"), - FieldSpec("format", FieldType.STRING, optional=True, - default="%Y-%m-%d %H:%M:%S"), + CommandSpec( + "AC_wait_for_port", "Flow", "Wait for TCP Port", + fields=( + FieldSpec("host", FieldType.STRING, default="127.0.0.1"), + FieldSpec("port", FieldType.INT, min_value=1, max_value=65535), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=30.0), + FieldSpec("connect_timeout_s", FieldType.FLOAT, optional=True, + default=1.0, min_value=0.01), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.25, min_value=0.01), + ), + description="Wait until a TCP host:port accepts connections.", ), - description="Store the current time (strftime format) in a variable.", - )) - specs.append(CommandSpec( - "AC_random_to_var", "Flow", "Random into Variable", - fields=( - FieldSpec("var", FieldType.STRING, default="random"), - FieldSpec("kind", FieldType.ENUM, - choices=("int", "float", "choice"), default="int"), - FieldSpec("min", FieldType.FLOAT, optional=True, default=0.0), - FieldSpec("max", FieldType.FLOAT, optional=True, default=100.0), - FieldSpec("seed", FieldType.INT, optional=True), - ), - description="Store a random int / float / choice in a variable.", - )) - specs.append(CommandSpec( - "AC_assert_var", "Flow", "Assert Variable", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("op", FieldType.ENUM, - choices=("eq", "ne", "lt", "le", "gt", "ge", - "contains", "startswith", "endswith", - "regex"), default="eq"), - FieldSpec("value", FieldType.STRING, optional=True), - ), - description="Fail if a flow variable doesn't satisfy the condition.", - )) - specs.append(CommandSpec( - "AC_assert_duration", "Flow", "Assert Duration (perf budget)", - fields=( - FieldSpec("max_ms", FieldType.FLOAT, default=1000.0, min_value=0.0), - FieldSpec("min_ms", FieldType.FLOAT, optional=True, default=0.0, - min_value=0.0), + CommandSpec( + "AC_wait_for_process", "Flow", "Wait for Process", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("present", FieldType.BOOL, optional=True, default=True), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=30.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.25, min_value=0.01), + ), + description="Wait until a process appears (or exits). Requires psutil.", ), - body_keys=("body",), - description="Fail if the body takes longer than max_ms.", - )) - specs.append(CommandSpec( - "AC_parallel", "Flow", "Parallel Branches", - fields=( - FieldSpec("branches", FieldType.STRING, - placeholder='[[["AC_sleep",{"seconds":1}]]]'), + CommandSpec("AC_list_windows", "Window", "List Windows"), + CommandSpec( + "AC_capture_window", "Window", "Capture Window", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("output_path", FieldType.STRING), + ), + description="Screenshot the window matching title to a PNG.", ), - description="Run each branch action list concurrently (JSON list).", - )) - specs.append(CommandSpec( - "AC_define_macro", "Flow", "Define Macro", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("params", FieldType.STRING, optional=True, - placeholder="x, y"), + CommandSpec( + "AC_save_window_layout", "Window", "Save Window Layout", + fields=(FieldSpec("path", FieldType.STRING, optional=True),), + description="Snapshot every window's position (optionally to a file).", ), - body_keys=("body",), - description="Register a named, parameterised action sub-routine.", - )) - specs.append(CommandSpec( - "AC_call_macro", "Flow", "Call Macro", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("args", FieldType.STRING, optional=True, - placeholder='{"x": 10, "y": 20}'), + CommandSpec( + "AC_restore_window_layout", "Window", "Restore Window Layout", + fields=(FieldSpec("layout", FieldType.FILE_PATH),), + description="Move windows back to a saved layout file.", ), - description="Invoke a macro defined by AC_define_macro.", - )) + CommandSpec( + "AC_tile_rect", "Window", "Tile Rect (compute)", + fields=( + FieldSpec("slot", FieldType.ENUM, + choices=("full", "left", "right", "top", "bottom", + "top_left", "top_right", "bottom_left", + "bottom_right", "center", "left_third", + "center_third", "right_third"), default="left"), + FieldSpec("screen", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("gap", FieldType.INT, optional=True, default=0), + ), + description="Compute the rectangle for a tiling slot of the screen.", + ), + CommandSpec( + "AC_grid_rects", "Window", "Grid Rects (compute)", + fields=( + FieldSpec("rows", FieldType.INT, default=2), + FieldSpec("cols", FieldType.INT, default=2), + FieldSpec("screen", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("gap", FieldType.INT, optional=True, default=0), + ), + description="Compute the cell rectangles of an R×C screen grid.", + ), + CommandSpec( + "AC_cascade_rects", "Window", "Cascade Rects (compute)", + fields=( + FieldSpec("count", FieldType.INT, default=3), + FieldSpec("screen", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("offset", FieldType.INT, optional=True, default=30), + FieldSpec("size", FieldType.STRING, optional=True, + placeholder=_SIZE_PLACEHOLDER), + ), + description="Compute staggered, overlapping window rectangles.", + ), + CommandSpec( + "AC_enumerate_monitors", "Window", "Enumerate Monitors", + description="List monitors + virtual bounds (multi-display geometry).", + ), + CommandSpec( + "AC_monitor_at_point", "Window", "Monitor at Point", + fields=( + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + ), + description="Report which monitor contains a virtual point.", + ), + ]) + + +def _add_flow_specs(specs: List[CommandSpec]) -> None: + specs.extend([ + CommandSpec( + "AC_sleep", "Flow", "Sleep", + fields=( + FieldSpec("seconds", FieldType.FLOAT, default=1.0, min_value=0.0), + ), + ), + CommandSpec( + "AC_wait_image", "Flow", "Wait for Image", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.2, + min_value=0.01), + ), + ), + CommandSpec( + "AC_wait_actionable", "Flow", "Wait Until Actionable", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=5.0), + FieldSpec("stable_for_s", FieldType.FLOAT, optional=True, default=0.3), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Wait until a target is visible + stable before acting.", + ), + CommandSpec( + "AC_expect_poll", "Flow", "Expect (poll until match)", + fields=( + FieldSpec("action", FieldType.STRING, + placeholder='["AC_get_clipboard"]'), + FieldSpec("key", FieldType.STRING, optional=True, + placeholder="result dict key, e.g. text"), + FieldSpec("op", FieldType.ENUM, optional=True, default="truthy", + choices=("truthy", "equals", "contains", "gt", "regex")), + FieldSpec("expected", FieldType.STRING, optional=True), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=5.0), + FieldSpec("interval_s", FieldType.FLOAT, optional=True, default=0.25), + ), + description="Re-run an action until a key of its result matches.", + ), + CommandSpec( + "AC_soft_assert", "Flow", "Soft Assert (aggregate)", + fields=( + FieldSpec("checks", FieldType.STRING, + placeholder='[{"value":5,"op":"gt","expected":3}]'), + FieldSpec("raise_on_fail", FieldType.BOOL, optional=True, + default=False), + ), + description="Aggregate many checks and report all failures (not just first).", + ), + CommandSpec( + "AC_replay_trace", "Flow", "Replay Agent Trace", + fields=( + FieldSpec("trace", FieldType.STRING, + placeholder='[{"action":["AC_click_mouse",{...}]}]'), + ), + description="Replay a recorded trajectory's actions through the executor.", + ), + CommandSpec( + "AC_wait_pixel", "Flow", "Wait for Pixel", + fields=( + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + FieldSpec("rgb", FieldType.RGB, placeholder="255,255,255"), + FieldSpec("tolerance", FieldType.INT, optional=True, default=0, + min_value=0, max_value=255), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.2, + min_value=0.01), + ), + ), + CommandSpec( + "AC_wait_clipboard_change", "Flow", "Wait for Clipboard Change", + fields=( + FieldSpec("target", FieldType.STRING, optional=True), + FieldSpec("contains", FieldType.BOOL, optional=True, default=False), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + ), + description="Wait until the clipboard changes or matches target.", + ), + CommandSpec( + "AC_wait_image_gone", "Flow", "Wait for Image to Vanish", + fields=( + FieldSpec("image", FieldType.STRING, placeholder="path/to/spinner.png"), + FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, + default=1.0), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + FieldSpec("gone_for_s", FieldType.FLOAT, optional=True, default=0.0), + ), + description="Block until an image (spinner/toast) leaves the screen.", + ), + CommandSpec( + "AC_wait_text_gone", "Flow", "Wait for Text to Vanish", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="Loading..."), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + FieldSpec("gone_for_s", FieldType.FLOAT, optional=True, default=0.0), + ), + description="Block until on-screen text (OCR) disappears.", + ), + CommandSpec( + "AC_wait_color", "Flow", "Wait for Region Colour", + fields=( + FieldSpec("target_rgb", FieldType.STRING, placeholder="[0, 200, 0]"), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + FieldSpec("tolerance", FieldType.INT, optional=True, default=10), + FieldSpec("min_fraction", FieldType.FLOAT, optional=True, + default=0.5, min_value=0.0, max_value=1.0), + FieldSpec("present", FieldType.BOOL, optional=True, default=True), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + ), + description="Block until a colour fills (or leaves) a screen region.", + ), + CommandSpec( + "AC_wait_window_title", "Flow", "Wait for Window Title", + fields=( + FieldSpec("pattern", FieldType.STRING, placeholder="Checkout$"), + FieldSpec("present", FieldType.BOOL, optional=True, default=True), + FieldSpec("regex", FieldType.BOOL, optional=True, default=True), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + ), + description="Block until a window title matches a regex (or vanishes).", + ), + CommandSpec( + "AC_loop", "Flow", "Loop (N times)", + fields=( + FieldSpec("times", FieldType.INT, default=3, min_value=1), + ), + body_keys=("body",), + ), + CommandSpec( + "AC_while_image", "Flow", "While Image Visible", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("max_iter", FieldType.INT, optional=True, default=100, + min_value=1), + ), + body_keys=("body",), + ), + CommandSpec( + "AC_if_image_found", "Flow", "If Image Found", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + ), + body_keys=("then", "else"), + ), + CommandSpec( + "AC_if_pixel", "Flow", "If Pixel Matches", + fields=( + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + FieldSpec("rgb", FieldType.RGB, placeholder="255,255,255"), + FieldSpec("tolerance", FieldType.INT, optional=True, default=0, + min_value=0, max_value=255), + ), + body_keys=("then", "else"), + ), + CommandSpec( + "AC_retry", "Flow", "Retry on Failure", + fields=( + FieldSpec("max_attempts", FieldType.INT, optional=True, default=3, + min_value=1), + FieldSpec("backoff", FieldType.FLOAT, optional=True, default=0.5, + min_value=0.0), + ), + body_keys=("body",), + ), + CommandSpec("AC_break", "Flow", "Break Loop"), + CommandSpec("AC_continue", "Flow", "Continue Loop"), + CommandSpec( + "AC_transform_var", "Flow", "Transform Variable", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("op", FieldType.ENUM, + choices=("upper", "lower", "strip", "title", + "lstrip", "rstrip", "replace", "regex", + "slice"), + default="strip"), + FieldSpec("into", FieldType.STRING, optional=True), + FieldSpec("find", FieldType.STRING, optional=True), + FieldSpec("replace_with", FieldType.STRING, optional=True), + FieldSpec("pattern", FieldType.STRING, optional=True), + ), + description="String-transform a variable (upper/strip/replace/regex/...).", + ), + CommandSpec( + "AC_now_to_var", "Flow", "Timestamp into Variable", + fields=( + FieldSpec("var", FieldType.STRING, default="now"), + FieldSpec("format", FieldType.STRING, optional=True, + default="%Y-%m-%d %H:%M:%S"), + ), + description="Store the current time (strftime format) in a variable.", + ), + CommandSpec( + "AC_random_to_var", "Flow", "Random into Variable", + fields=( + FieldSpec("var", FieldType.STRING, default="random"), + FieldSpec("kind", FieldType.ENUM, + choices=("int", "float", "choice"), default="int"), + FieldSpec("min", FieldType.FLOAT, optional=True, default=0.0), + FieldSpec("max", FieldType.FLOAT, optional=True, default=100.0), + FieldSpec("seed", FieldType.INT, optional=True), + ), + description="Store a random int / float / choice in a variable.", + ), + CommandSpec( + "AC_assert_var", "Flow", "Assert Variable", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("op", FieldType.ENUM, + choices=("eq", "ne", "lt", "le", "gt", "ge", + "contains", "startswith", "endswith", + "regex"), default="eq"), + FieldSpec("value", FieldType.STRING, optional=True), + ), + description="Fail if a flow variable doesn't satisfy the condition.", + ), + CommandSpec( + "AC_assert_duration", "Flow", "Assert Duration (perf budget)", + fields=( + FieldSpec("max_ms", FieldType.FLOAT, default=1000.0, min_value=0.0), + FieldSpec("min_ms", FieldType.FLOAT, optional=True, default=0.0, + min_value=0.0), + ), + body_keys=("body",), + description="Fail if the body takes longer than max_ms.", + ), + CommandSpec( + "AC_parallel", "Flow", "Parallel Branches", + fields=( + FieldSpec("branches", FieldType.STRING, + placeholder='[[["AC_sleep",{"seconds":1}]]]'), + ), + description="Run each branch action list concurrently (JSON list).", + ), + CommandSpec( + "AC_define_macro", "Flow", "Define Macro", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("params", FieldType.STRING, optional=True, + placeholder="x, y"), + ), + body_keys=("body",), + description="Register a named, parameterised action sub-routine.", + ), + CommandSpec( + "AC_call_macro", "Flow", "Call Macro", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("args", FieldType.STRING, optional=True, + placeholder='{"x": 10, "y": 20}'), + ), + description="Invoke a macro defined by AC_define_macro.", + ), + ]) def _add_native_control_specs(specs: List[CommandSpec]) -> None: @@ -1872,358 +1904,365 @@ def _add_native_control_specs(specs: List[CommandSpec]) -> None: FieldSpec("app_name", FieldType.STRING, optional=True), FieldSpec("automation_id", FieldType.STRING, optional=True), ) - specs.append(CommandSpec( - "AC_control_get_value", _NATIVE_UI, "Get Control Value", - fields=fields, - description="Read a native control's value via the accessibility API.", - )) - specs.append(CommandSpec( - "AC_control_set_value", _NATIVE_UI, "Set Control Value", - fields=(FieldSpec("value", FieldType.STRING),) + fields, - description="Set a native control's value directly (no per-key typing).", - )) - specs.append(CommandSpec( - "AC_control_invoke", _NATIVE_UI, "Invoke Control", - fields=fields, - description="Invoke a native control (e.g. press a button).", - )) - specs.append(CommandSpec( - "AC_control_toggle", _NATIVE_UI, "Toggle Control", - fields=fields, - description="Toggle a native control (e.g. a checkbox).", - )) - specs.append(CommandSpec( - "AC_read_table", _NATIVE_UI, "Read Table / Grid", - fields=fields, - description="Read a grid/table/list control as rows of cell strings.", - )) - specs.append(CommandSpec( - "AC_expand_control", _NATIVE_UI, "Expand Control", - fields=fields, - description="Expand a tree node / combobox (ExpandCollapsePattern).", - )) - specs.append(CommandSpec( - "AC_collapse_control", _NATIVE_UI, "Collapse Control", - fields=fields, - description="Collapse a tree node / combobox (ExpandCollapsePattern).", - )) - specs.append(CommandSpec( - "AC_control_expand_state", _NATIVE_UI, "Control Expand State", - fields=fields, - description="Read expanded/collapsed/partial/leaf state of a control.", - )) - specs.append(CommandSpec( - "AC_select_control_item", _NATIVE_UI, "Select Control Item", - fields=fields, - description="Select a list / tree / tab item (SelectionItemPattern).", - )) - specs.append(CommandSpec( - "AC_control_range", _NATIVE_UI, "Get Control Range", - fields=fields, - description="Read a slider / progress range (RangeValuePattern).", - )) - specs.append(CommandSpec( - "AC_set_control_range", _NATIVE_UI, "Set Control Range", - fields=(FieldSpec("value", FieldType.FLOAT),) + fields, - description="Set a slider / progress / spinner value (RangeValuePattern).", - )) - specs.append(CommandSpec( - "AC_scroll_control_into_view", _NATIVE_UI, "Scroll Control Into View", - fields=fields, - description="Scroll a control into view (ScrollItemPattern).", - )) - specs.append(CommandSpec( - "AC_realize_item", _NATIVE_UI, "Realize Virtualized Item", - fields=( - FieldSpec("item_name", FieldType.STRING), - FieldSpec("by", FieldType.ENUM, optional=True, default="name", - choices=("name", "automation_id")), - FieldSpec("container_name", FieldType.STRING, optional=True), - FieldSpec("container_role", FieldType.STRING, optional=True), - FieldSpec("app_name", FieldType.STRING, optional=True), - FieldSpec("automation_id", FieldType.STRING, optional=True), - ), - description="Realize an off-screen item in a virtualized list/grid.", - )) - specs.append(CommandSpec( - "AC_get_element_properties", _NATIVE_UI, "Get Element Properties", - fields=fields, - description="Read rich UIA props (enabled/offscreen/help/status/keys).", - )) - specs.append(CommandSpec( - "AC_table_headers", _NATIVE_UI, "Get Table Headers", - fields=fields, - description="Read a table's row/column header labels (TablePattern).", - )) - specs.append(CommandSpec( - "AC_table_cell", _NATIVE_UI, "Get Table Cell (by index)", - fields=(FieldSpec("row", FieldType.INT), - FieldSpec("column", FieldType.INT)) + fields, - description="Read the cell at (row, column) with its span.", - )) - specs.append(CommandSpec( - "AC_cell_by_header", _NATIVE_UI, "Get Table Cell (by header)", - fields=(FieldSpec("row", FieldType.INT), - FieldSpec("column_header", FieldType.STRING)) + fields, - description="Read the cell at (row, named column) — assert by header.", - )) - specs.append(CommandSpec( - "AC_move_element", _NATIVE_UI, "Move Element (Transform)", - fields=(FieldSpec("x", FieldType.FLOAT), - FieldSpec("y", FieldType.FLOAT)) + fields, - description="Move a UIA element to (x, y) (TransformPattern).", - )) - specs.append(CommandSpec( - "AC_resize_element", _NATIVE_UI, "Resize Element (Transform)", - fields=(FieldSpec("width", FieldType.FLOAT), - FieldSpec("height", FieldType.FLOAT)) + fields, - description="Resize a UIA element (TransformPattern).", - )) - specs.append(CommandSpec( - "AC_set_window_state", _NATIVE_UI, "Set Window State", - fields=(FieldSpec("state", FieldType.ENUM, default="normal", - choices=("normal", "maximized", "minimized")),) + fields, - description="Minimize / maximize / restore a window (WindowPattern).", - )) - specs.append(CommandSpec( - "AC_window_interaction_state", _NATIVE_UI, "Window Interaction State", - fields=fields, - description="Read window readiness (ready/blocked_by_modal/...).", - )) - specs.append(CommandSpec( - "AC_legacy_info", _NATIVE_UI, "Legacy (MSAA) Info", - fields=fields, - description="Read an old control's MSAA info (LegacyIAccessible).", - )) - specs.append(CommandSpec( - "AC_legacy_default_action", _NATIVE_UI, "Legacy (MSAA) Default Action", - fields=fields, - description="Fire an old control's MSAA default action (fallback).", - )) - specs.append(CommandSpec( - "AC_get_selection", _NATIVE_UI, "Get Container Selection", - fields=fields, - description="Read a container's selection (SelectionPattern).", - )) - specs.append(CommandSpec( - "AC_list_views", _NATIVE_UI, "List Control Views", - fields=fields, - description="List a control's selectable views (MultipleViewPattern).", - )) - specs.append(CommandSpec( - "AC_set_view", _NATIVE_UI, "Set Control View", - fields=(FieldSpec("view", FieldType.STRING),) + fields, - description="Switch a control to the named view (MultipleViewPattern).", - )) - specs.append(CommandSpec( - "AC_a11y_focused", _NATIVE_UI, "Get Focused Element", - fields=(FieldSpec("app_name", FieldType.STRING, optional=True),), - description="Read the element holding keyboard focus (None if none).", - )) - specs.append(CommandSpec( - "AC_wait_for_focus_change", _NATIVE_UI, "Wait for Focus Change", - fields=(FieldSpec("timeout", FieldType.FLOAT, optional=True, - default=5.0),), - description="Block until keyboard focus moves (real UIA focus event).", - )) - specs.append(CommandSpec( - "AC_get_control_text", _NATIVE_UI, "Get Control Text", - fields=fields, - description="Read full text via TextPattern (multiline / document safe).", - )) - specs.append(CommandSpec( - "AC_find_control_text", _NATIVE_UI, "Find Text in Control", - fields=(FieldSpec("text", FieldType.STRING), - FieldSpec("ignore_case", FieldType.BOOL, optional=True, - default=True)) + fields, - description="Whether text occurs in a control (TextPattern.FindText).", - )) - specs.append(CommandSpec( - "AC_select_control_text", _NATIVE_UI, "Select Text in Control", - fields=(FieldSpec("text", FieldType.STRING), - FieldSpec("ignore_case", FieldType.BOOL, optional=True, - default=True)) + fields, - description="Find + select text in a control (FindText + Select).", - )) - specs.append(CommandSpec( - "AC_control_text_attributes", _NATIVE_UI, "Get Text Attributes", - fields=fields, - description="Read selection formatting (font/size/bold/italic/colour).", - )) - specs.append(CommandSpec( - "AC_get_selected_text", _NATIVE_UI, "Get Selected Text", - fields=fields, - description="Read the currently selected text via TextPattern.", - )) - specs.append(CommandSpec( - "AC_get_visible_text", _NATIVE_UI, "Get Visible Text", - fields=fields, - description="Read only the on-screen text via TextPattern.GetVisibleRanges.", - )) - specs.append(CommandSpec( - "AC_walk_tree", _NATIVE_UI, "Walk Accessibility Tree", - fields=(FieldSpec("app_name", FieldType.STRING, optional=True), - FieldSpec("max_results", FieldType.INT, optional=True, - default=500)), - description="Dump the a11y tree with friendly roles + a path per node.", - )) - specs.append(CommandSpec( - "AC_humanize_role", _NATIVE_UI, "Humanize UIA Role", - fields=(FieldSpec("role", FieldType.STRING),), - description="Translate a raw UIA role (ControlType_50000) to a name.", - )) + specs.extend([ + CommandSpec( + "AC_control_get_value", _NATIVE_UI, "Get Control Value", + fields=fields, + description="Read a native control's value via the accessibility API.", + ), + CommandSpec( + "AC_control_set_value", _NATIVE_UI, "Set Control Value", + fields=(FieldSpec("value", FieldType.STRING),) + fields, + description="Set a native control's value directly (no per-key typing).", + ), + CommandSpec( + "AC_control_invoke", _NATIVE_UI, "Invoke Control", + fields=fields, + description="Invoke a native control (e.g. press a button).", + ), + CommandSpec( + "AC_control_toggle", _NATIVE_UI, "Toggle Control", + fields=fields, + description="Toggle a native control (e.g. a checkbox).", + ), + CommandSpec( + "AC_read_table", _NATIVE_UI, "Read Table / Grid", + fields=fields, + description="Read a grid/table/list control as rows of cell strings.", + ), + CommandSpec( + "AC_expand_control", _NATIVE_UI, "Expand Control", + fields=fields, + description="Expand a tree node / combobox (ExpandCollapsePattern).", + ), + CommandSpec( + "AC_collapse_control", _NATIVE_UI, "Collapse Control", + fields=fields, + description="Collapse a tree node / combobox (ExpandCollapsePattern).", + ), + CommandSpec( + "AC_control_expand_state", _NATIVE_UI, "Control Expand State", + fields=fields, + description="Read expanded/collapsed/partial/leaf state of a control.", + ), + CommandSpec( + "AC_select_control_item", _NATIVE_UI, "Select Control Item", + fields=fields, + description="Select a list / tree / tab item (SelectionItemPattern).", + ), + CommandSpec( + "AC_control_range", _NATIVE_UI, "Get Control Range", + fields=fields, + description="Read a slider / progress range (RangeValuePattern).", + ), + CommandSpec( + "AC_set_control_range", _NATIVE_UI, "Set Control Range", + fields=(FieldSpec("value", FieldType.FLOAT),) + fields, + description="Set a slider / progress / spinner value (RangeValuePattern).", + ), + CommandSpec( + "AC_scroll_control_into_view", _NATIVE_UI, "Scroll Control Into View", + fields=fields, + description="Scroll a control into view (ScrollItemPattern).", + ), + CommandSpec( + "AC_realize_item", _NATIVE_UI, "Realize Virtualized Item", + fields=( + FieldSpec("item_name", FieldType.STRING), + FieldSpec("by", FieldType.ENUM, optional=True, default="name", + choices=("name", "automation_id")), + FieldSpec("container_name", FieldType.STRING, optional=True), + FieldSpec("container_role", FieldType.STRING, optional=True), + FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("automation_id", FieldType.STRING, optional=True), + ), + description="Realize an off-screen item in a virtualized list/grid.", + ), + CommandSpec( + "AC_get_element_properties", _NATIVE_UI, "Get Element Properties", + fields=fields, + description="Read rich UIA props (enabled/offscreen/help/status/keys).", + ), + CommandSpec( + "AC_table_headers", _NATIVE_UI, "Get Table Headers", + fields=fields, + description="Read a table's row/column header labels (TablePattern).", + ), + CommandSpec( + "AC_table_cell", _NATIVE_UI, "Get Table Cell (by index)", + fields=(FieldSpec("row", FieldType.INT), + FieldSpec("column", FieldType.INT)) + fields, + description="Read the cell at (row, column) with its span.", + ), + CommandSpec( + "AC_cell_by_header", _NATIVE_UI, "Get Table Cell (by header)", + fields=(FieldSpec("row", FieldType.INT), + FieldSpec("column_header", FieldType.STRING)) + fields, + description="Read the cell at (row, named column) — assert by header.", + ), + CommandSpec( + "AC_move_element", _NATIVE_UI, "Move Element (Transform)", + fields=(FieldSpec("x", FieldType.FLOAT), + FieldSpec("y", FieldType.FLOAT)) + fields, + description="Move a UIA element to (x, y) (TransformPattern).", + ), + CommandSpec( + "AC_resize_element", _NATIVE_UI, "Resize Element (Transform)", + fields=(FieldSpec("width", FieldType.FLOAT), + FieldSpec("height", FieldType.FLOAT)) + fields, + description="Resize a UIA element (TransformPattern).", + ), + CommandSpec( + "AC_set_window_state", _NATIVE_UI, "Set Window State", + fields=(FieldSpec("state", FieldType.ENUM, default="normal", + choices=("normal", "maximized", "minimized")),) + fields, + description="Minimize / maximize / restore a window (WindowPattern).", + ), + CommandSpec( + "AC_window_interaction_state", _NATIVE_UI, "Window Interaction State", + fields=fields, + description="Read window readiness (ready/blocked_by_modal/...).", + ), + CommandSpec( + "AC_legacy_info", _NATIVE_UI, "Legacy (MSAA) Info", + fields=fields, + description="Read an old control's MSAA info (LegacyIAccessible).", + ), + CommandSpec( + "AC_legacy_default_action", _NATIVE_UI, "Legacy (MSAA) Default Action", + fields=fields, + description="Fire an old control's MSAA default action (fallback).", + ), + CommandSpec( + "AC_get_selection", _NATIVE_UI, "Get Container Selection", + fields=fields, + description="Read a container's selection (SelectionPattern).", + ), + CommandSpec( + "AC_list_views", _NATIVE_UI, "List Control Views", + fields=fields, + description="List a control's selectable views (MultipleViewPattern).", + ), + CommandSpec( + "AC_set_view", _NATIVE_UI, "Set Control View", + fields=(FieldSpec("view", FieldType.STRING),) + fields, + description="Switch a control to the named view (MultipleViewPattern).", + ), + CommandSpec( + "AC_a11y_focused", _NATIVE_UI, "Get Focused Element", + fields=(FieldSpec("app_name", FieldType.STRING, optional=True),), + description="Read the element holding keyboard focus (None if none).", + ), + CommandSpec( + "AC_wait_for_focus_change", _NATIVE_UI, "Wait for Focus Change", + fields=(FieldSpec("timeout", FieldType.FLOAT, optional=True, + default=5.0),), + description="Block until keyboard focus moves (real UIA focus event).", + ), + CommandSpec( + "AC_get_control_text", _NATIVE_UI, "Get Control Text", + fields=fields, + description="Read full text via TextPattern (multiline / document safe).", + ), + CommandSpec( + "AC_find_control_text", _NATIVE_UI, "Find Text in Control", + fields=(FieldSpec("text", FieldType.STRING), + FieldSpec("ignore_case", FieldType.BOOL, optional=True, + default=True)) + fields, + description="Whether text occurs in a control (TextPattern.FindText).", + ), + CommandSpec( + "AC_select_control_text", _NATIVE_UI, "Select Text in Control", + fields=(FieldSpec("text", FieldType.STRING), + FieldSpec("ignore_case", FieldType.BOOL, optional=True, + default=True)) + fields, + description="Find + select text in a control (FindText + Select).", + ), + CommandSpec( + "AC_control_text_attributes", _NATIVE_UI, "Get Text Attributes", + fields=fields, + description="Read selection formatting (font/size/bold/italic/colour).", + ), + CommandSpec( + "AC_get_selected_text", _NATIVE_UI, "Get Selected Text", + fields=fields, + description="Read the currently selected text via TextPattern.", + ), + CommandSpec( + "AC_get_visible_text", _NATIVE_UI, "Get Visible Text", + fields=fields, + description="Read only the on-screen text via TextPattern.GetVisibleRanges.", + ), + CommandSpec( + "AC_walk_tree", _NATIVE_UI, "Walk Accessibility Tree", + fields=(FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("max_results", FieldType.INT, optional=True, + default=500)), + description="Dump the a11y tree with friendly roles + a path per node.", + ), + CommandSpec( + "AC_humanize_role", _NATIVE_UI, "Humanize UIA Role", + fields=(FieldSpec("role", FieldType.STRING),), + description="Translate a raw UIA role (ControlType_50000) to a name.", + ), + ]) tree_fields = (FieldSpec("app_name", FieldType.STRING, optional=True), FieldSpec("max_results", FieldType.INT, optional=True, default=500)) - specs.append(CommandSpec( - "AC_tab_order", _NATIVE_UI, "Keyboard Tab Order", - fields=tree_fields, - description="List focusable controls in keyboard Tab (reading) order.", - )) - specs.append(CommandSpec( - "AC_audit_focus_order", _NATIVE_UI, "Audit Focus Order (WCAG)", - fields=tree_fields, - description="WCAG 2.4.x focus-order audit: tab sequence + flagged issues.", - )) - specs.append(CommandSpec( - "AC_focus_control", _NATIVE_UI, "Set Keyboard Focus", - fields=fields, - description="Set keyboard focus on a control natively (UIA SetFocus).", - )) + specs.extend([ + CommandSpec( + "AC_tab_order", _NATIVE_UI, "Keyboard Tab Order", + fields=tree_fields, + description="List focusable controls in keyboard Tab (reading) order.", + ), + CommandSpec( + "AC_audit_focus_order", _NATIVE_UI, "Audit Focus Order (WCAG)", + fields=tree_fields, + description="WCAG 2.4.x focus-order audit: tab sequence + flagged issues.", + ), + CommandSpec( + "AC_focus_control", _NATIVE_UI, "Set Keyboard Focus", + fields=fields, + description="Set keyboard focus on a control natively (UIA SetFocus).", + ), + ]) def _add_misc_specs(specs: List[CommandSpec]) -> None: _add_native_control_specs(specs) - specs.append(CommandSpec( - "AC_probe_capabilities", "System", "Inspect Input and Capture Capabilities", - description="Passive prerequisites and last local authority state; no capture, input or permission request.", - )) - specs.append(CommandSpec( - "AC_set_clipboard_html", "Data", "Set Clipboard HTML", - fields=( - FieldSpec("html", FieldType.STRING, - placeholder="Bold rich text"), - FieldSpec("fragment_plaintext", FieldType.STRING, optional=True), + specs.extend([ + CommandSpec( + "AC_probe_capabilities", "System", "Inspect Input and Capture Capabilities", + description=("Passive prerequisites and last local authority state; no capture, input or " + "permission request."), ), - description="Put rich HTML on the clipboard (CF_HTML, Windows).", - )) - specs.append(CommandSpec( - "AC_get_clipboard_html", "Data", "Get Clipboard HTML", - description="Read the clipboard's HTML fragment (CF_HTML, Windows).", - )) - specs.append(CommandSpec( - "AC_set_clipboard_files", "Data", "Set Clipboard Files", - fields=( - FieldSpec("paths", FieldType.STRING, - placeholder='["C:\\\\a\\\\one.txt", "C:\\\\b\\\\two.png"]'), + CommandSpec( + "AC_set_clipboard_html", "Data", "Set Clipboard HTML", + fields=( + FieldSpec("html", FieldType.STRING, + placeholder="Bold rich text"), + FieldSpec("fragment_plaintext", FieldType.STRING, optional=True), + ), + description="Put rich HTML on the clipboard (CF_HTML, Windows).", ), - description="Put a file-drop list on the clipboard (CF_HDROP, Windows).", - )) - specs.append(CommandSpec( - "AC_get_clipboard_files", "Data", "Get Clipboard Files", - description="Read the clipboard's file-drop list (CF_HDROP, Windows).", - )) - specs.append(CommandSpec( - "AC_set_clipboard_rtf", "Data", "Set Clipboard RTF", - fields=(FieldSpec("text", FieldType.STRING, - placeholder="Styled paste text"),), - description="Put text on the clipboard as Rich Text Format (Windows).", - )) - specs.append(CommandSpec( - "AC_get_clipboard_rtf", "Data", "Get Clipboard RTF", - description="Read the clipboard's RTF document string (Windows).", - )) - specs.append(CommandSpec( - "AC_set_clipboard_csv", "Data", "Set Clipboard CSV/TSV", - fields=( - FieldSpec("rows", FieldType.STRING, - placeholder='[["a", "b"], ["1", "2"]]'), - FieldSpec("delimiter", FieldType.STRING, optional=True, default=","), + CommandSpec( + "AC_get_clipboard_html", "Data", "Get Clipboard HTML", + description="Read the clipboard's HTML fragment (CF_HTML, Windows).", + ), + CommandSpec( + "AC_set_clipboard_files", "Data", "Set Clipboard Files", + fields=( + FieldSpec("paths", FieldType.STRING, + placeholder='["C:\\\\a\\\\one.txt", "C:\\\\b\\\\two.png"]'), + ), + description="Put a file-drop list on the clipboard (CF_HDROP, Windows).", + ), + CommandSpec( + "AC_get_clipboard_files", "Data", "Get Clipboard Files", + description="Read the clipboard's file-drop list (CF_HDROP, Windows).", + ), + CommandSpec( + "AC_set_clipboard_rtf", "Data", "Set Clipboard RTF", + fields=(FieldSpec("text", FieldType.STRING, + placeholder="Styled paste text"),), + description="Put text on the clipboard as Rich Text Format (Windows).", + ), + CommandSpec( + "AC_get_clipboard_rtf", "Data", "Get Clipboard RTF", + description="Read the clipboard's RTF document string (Windows).", + ), + CommandSpec( + "AC_set_clipboard_csv", "Data", "Set Clipboard CSV/TSV", + fields=( + FieldSpec("rows", FieldType.STRING, + placeholder='[["a", "b"], ["1", "2"]]'), + FieldSpec("delimiter", FieldType.STRING, optional=True, default=","), + ), + description="Put a table on the clipboard as the Csv format (Windows).", + ), + CommandSpec( + "AC_get_clipboard_csv", "Data", "Get Clipboard CSV/TSV", + fields=(FieldSpec("delimiter", FieldType.STRING, optional=True, + default=","),), + description="Read the clipboard's Csv content as rows (Windows).", + ), + CommandSpec( + "AC_clipboard_formats", "Data", "List Clipboard Formats", + description="Enumerate + classify the clipboard's formats (Windows).", + ), + CommandSpec( + "AC_clipboard_get_image", "Data", "Clipboard: Save Image", + fields=(FieldSpec("path", FieldType.FILE_PATH, + placeholder="clipboard.png"),), + description="Save the clipboard's image to a PNG file, if it has one.", + ), + CommandSpec( + "AC_clipboard_set_image", "Data", "Clipboard: Set Image", + fields=(FieldSpec("path", FieldType.FILE_PATH),), + description="Put an image file onto the clipboard.", + ), + 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).", + ), + 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).", + ), + CommandSpec( + "AC_watchdog_add", "Flow", "Watchdog: Add Popup Rule", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("action", FieldType.STRING, optional=True, + default="close", placeholder="close / enter / esc"), + FieldSpec("case_sensitive", FieldType.BOOL, optional=True, + default=False), + FieldSpec("name", FieldType.STRING, optional=True), + ), + description="Auto-dismiss an unexpected window when it appears.", + ), + CommandSpec( + "AC_watchdog_start", "Flow", "Watchdog: Start"), + CommandSpec( + "AC_watchdog_stop", "Flow", "Watchdog: Stop"), + CommandSpec( + "AC_watchdog_list", "Flow", "Watchdog: List Rules / Hits"), + CommandSpec( + "AC_otp_to_var", "Flow", "OTP (TOTP) into Variable", + fields=( + FieldSpec("secret", FieldType.STRING), + FieldSpec("var", FieldType.STRING, default="otp"), + FieldSpec("digits", FieldType.INT, optional=True, default=6), + FieldSpec("step", FieldType.INT, optional=True, default=30), + ), + description="Generate a TOTP 2FA code from a base32 secret.", ), - description="Put a table on the clipboard as the Csv format (Windows).", - )) - specs.append(CommandSpec( - "AC_get_clipboard_csv", "Data", "Get Clipboard CSV/TSV", - fields=(FieldSpec("delimiter", FieldType.STRING, optional=True, - 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_clipboard_get_image", "Data", "Clipboard: Save Image", - fields=(FieldSpec("path", FieldType.FILE_PATH, - placeholder="clipboard.png"),), - description="Save the clipboard's image to a PNG file, if it has one.", - )) - specs.append(CommandSpec( - "AC_clipboard_set_image", "Data", "Clipboard: Set Image", - fields=(FieldSpec("path", FieldType.FILE_PATH),), - description="Put an image file onto the clipboard.", - )) - 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]"), + CommandSpec( + "AC_handle_file_dialog", _NATIVE_UI, "Handle File Dialog", + fields=( + FieldSpec("path", FieldType.STRING), + FieldSpec("action", FieldType.ENUM, + choices=("open", "save", "folder"), + optional=True, default="open"), + FieldSpec("window_title", FieldType.STRING, optional=True), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=10.0), + FieldSpec("confirm_key", FieldType.STRING, optional=True, + default="enter"), + ), + description="Wait for a native file dialog, type a path, confirm.", ), - description="Diff two clipboard-format snapshots (pure).", - )) - specs.append(CommandSpec( - "AC_watchdog_add", "Flow", "Watchdog: Add Popup Rule", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("action", FieldType.STRING, optional=True, - default="close", placeholder="close / enter / esc"), - FieldSpec("case_sensitive", FieldType.BOOL, optional=True, - default=False), - FieldSpec("name", FieldType.STRING, optional=True), - ), - description="Auto-dismiss an unexpected window when it appears.", - )) - specs.append(CommandSpec( - "AC_watchdog_start", "Flow", "Watchdog: Start")) - specs.append(CommandSpec( - "AC_watchdog_stop", "Flow", "Watchdog: Stop")) - specs.append(CommandSpec( - "AC_watchdog_list", "Flow", "Watchdog: List Rules / Hits")) - specs.append(CommandSpec( - "AC_otp_to_var", "Flow", "OTP (TOTP) into Variable", - fields=( - FieldSpec("secret", FieldType.STRING), - FieldSpec("var", FieldType.STRING, default="otp"), - FieldSpec("digits", FieldType.INT, optional=True, default=6), - FieldSpec("step", FieldType.INT, optional=True, default=30), + CommandSpec( + "AC_assert_session_active", "Flow", "Assert Session Active", + description="Fail if the session is locked / non-interactive.", ), - description="Generate a TOTP 2FA code from a base32 secret.", - )) - specs.append(CommandSpec( - "AC_handle_file_dialog", _NATIVE_UI, "Handle File Dialog", - fields=( - FieldSpec("path", FieldType.STRING), - FieldSpec("action", FieldType.ENUM, - choices=("open", "save", "folder"), - optional=True, default="open"), - FieldSpec("window_title", FieldType.STRING, optional=True), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=10.0), - FieldSpec("confirm_key", FieldType.STRING, optional=True, - default="enter"), - ), - description="Wait for a native file dialog, type a path, confirm.", - )) - specs.append(CommandSpec( - "AC_assert_session_active", "Flow", "Assert Session Active", - description="Fail if the session is locked / non-interactive.", - )) + ]) _add_work_queue_specs(specs) _add_tooling_specs(specs) _add_authoring_specs(specs) @@ -2240,3137 +2279,3175 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None: _add_devex_specs(specs) _add_audit_specs(specs) _add_webrunner_specs(specs) - specs.append(CommandSpec( - "AC_tween_drag", "Mouse", "Tweened Drag", - fields=( - FieldSpec("steps", FieldType.INT, optional=True, default=30), - FieldSpec("easing", FieldType.ENUM, - choices=("linear", "ease_in_out_quad", "ease_out_cubic", - "ease_in_cubic"), - optional=True, default="ease_in_out_quad"), - FieldSpec("button", FieldType.ENUM, choices=_MOUSE_BUTTONS, - optional=True, default="mouse_left"), - FieldSpec("step_delay_s", FieldType.FLOAT, optional=True, - default=0.0, min_value=0.0), - FieldSpec("settle_s", FieldType.FLOAT, optional=True, - default=0.0, min_value=0.0), - ), - description="Drag along an eased path; 'start'/'end' [x,y] via JSON " - "view.", - )) - specs.append(CommandSpec( - "AC_move_along_path", "Mouse", "Move Along Path", - fields=( - FieldSpec("waypoints", FieldType.STRING, - placeholder="[[100,100],[400,150],[400,500]]"), - FieldSpec("easing", FieldType.ENUM, - choices=("linear", "ease_in_out_quad", "ease_out_cubic", - "ease_in_cubic"), - optional=True, default="linear"), - FieldSpec("per_segment_steps", FieldType.INT, optional=True, - default=20), - ), - description="Move the pointer through a polyline of waypoints.", - )) - specs.append(CommandSpec( - "AC_drag_path", "Mouse", "Drag Along Path", - fields=( - FieldSpec("waypoints", FieldType.STRING, - placeholder="[[50,50],[300,50],[300,300]]"), - FieldSpec("button", FieldType.ENUM, choices=_MOUSE_BUTTONS, - optional=True, default="mouse_left"), - FieldSpec("easing", FieldType.ENUM, - choices=("linear", "ease_in_out_quad", "ease_out_cubic", - "ease_in_cubic"), - optional=True, default="linear"), - FieldSpec("per_segment_steps", FieldType.INT, optional=True, - default=20), - FieldSpec("step_delay_s", FieldType.FLOAT, optional=True, - default=0.0, min_value=0.0), - FieldSpec("settle_s", FieldType.FLOAT, optional=True, - default=0.0, min_value=0.0), - ), - description="Press, drag through a polyline of waypoints, release.", - )) - specs.append(CommandSpec( - "AC_move_mouse_relative", "Mouse", "Move Relative", - fields=( - FieldSpec("dx", FieldType.INT, placeholder="-40"), - FieldSpec("dy", FieldType.INT, placeholder="12"), + specs.extend([ + CommandSpec( + "AC_tween_drag", "Mouse", "Tweened Drag", + fields=( + FieldSpec("steps", FieldType.INT, optional=True, default=30), + FieldSpec("easing", FieldType.ENUM, + choices=("linear", "ease_in_out_quad", "ease_out_cubic", + "ease_in_cubic"), + optional=True, default="ease_in_out_quad"), + FieldSpec("button", FieldType.ENUM, choices=_MOUSE_BUTTONS, + optional=True, default="mouse_left"), + FieldSpec("step_delay_s", FieldType.FLOAT, optional=True, + default=0.0, min_value=0.0), + FieldSpec("settle_s", FieldType.FLOAT, optional=True, + default=0.0, min_value=0.0), + ), + description="Drag along an eased path; 'start'/'end' [x,y] via JSON " + "view.", ), - description="Move the pointer by (dx, dy) from its current position.", - )) - specs.append(CommandSpec( - "AC_grid_cell", "Mouse", "Grid Cell (row / col)", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder="[[10,100,20,10],[110,100,20,10], ...]"), - FieldSpec("row", FieldType.INT, placeholder="0"), - FieldSpec("col", FieldType.INT, placeholder="0"), - FieldSpec("row_tolerance", FieldType.INT, optional=True, default=10), - FieldSpec("box_format", FieldType.ENUM, optional=True, default="xywh", - choices=("xywh", "ltrb")), - ), - description="Resolve a table cell centre by row/col from cell boxes.", - )) - specs.append(CommandSpec( - "AC_list_plugins", "Tools", "List Plugin Commands", - fields=(FieldSpec("group", FieldType.STRING, optional=True, - default="je_auto_control.commands"),), - description="Discover third-party AC_* commands from entry points.", - )) - specs.append(CommandSpec( - "AC_load_plugins", "Tools", "Load Plugin Commands", - fields=(FieldSpec("group", FieldType.STRING, optional=True, - default="je_auto_control.commands"),), - description="Discover + register third-party plugin commands.", - )) - specs.append(CommandSpec( - "AC_approval_request", "Tools", "Approval: Request", - fields=( - FieldSpec("action", FieldType.STRING), - FieldSpec("requester", FieldType.STRING, optional=True), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_move_along_path", "Mouse", "Move Along Path", + fields=( + FieldSpec("waypoints", FieldType.STRING, + placeholder="[[100,100],[400,150],[400,500]]"), + FieldSpec("easing", FieldType.ENUM, + choices=("linear", "ease_in_out_quad", "ease_out_cubic", + "ease_in_cubic"), + optional=True, default="linear"), + FieldSpec("per_segment_steps", FieldType.INT, optional=True, + default=20), + ), + description="Move the pointer through a polyline of waypoints.", ), - description="Maker-checker: file a high-risk action for approval; returns {token}, the " - "request id to hand to the checker (shown, not masked).", - )) - specs.append(CommandSpec( - "AC_approval_approve", "Tools", "Approval: Approve", - fields=( - FieldSpec("token", FieldType.STRING), - FieldSpec("approver", FieldType.STRING), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_drag_path", "Mouse", "Drag Along Path", + fields=( + FieldSpec("waypoints", FieldType.STRING, + placeholder="[[50,50],[300,50],[300,300]]"), + FieldSpec("button", FieldType.ENUM, choices=_MOUSE_BUTTONS, + optional=True, default="mouse_left"), + FieldSpec("easing", FieldType.ENUM, + choices=("linear", "ease_in_out_quad", "ease_out_cubic", + "ease_in_cubic"), + optional=True, default="linear"), + FieldSpec("per_segment_steps", FieldType.INT, optional=True, + default=20), + FieldSpec("step_delay_s", FieldType.FLOAT, optional=True, + default=0.0, min_value=0.0), + FieldSpec("settle_s", FieldType.FLOAT, optional=True, + default=0.0, min_value=0.0), + ), + description="Press, drag through a polyline of waypoints, release.", ), - description="Approve a request (approver must differ from requester).", - )) - specs.append(CommandSpec( - "AC_approval_reject", "Tools", "Approval: Reject", - fields=( - FieldSpec("token", FieldType.STRING), - FieldSpec("approver", FieldType.STRING), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_move_mouse_relative", "Mouse", "Move Relative", + fields=( + FieldSpec("dx", FieldType.INT, placeholder="-40"), + FieldSpec("dy", FieldType.INT, placeholder="12"), + ), + description="Move the pointer by (dx, dy) from its current position.", ), - description="Reject a request (approver must differ from requester).", - )) - specs.append(CommandSpec( - "AC_approval_status", "Tools", "Approval: Status", - fields=( - FieldSpec("token", FieldType.STRING), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_grid_cell", "Mouse", "Grid Cell (row / col)", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder="[[10,100,20,10],[110,100,20,10], ...]"), + FieldSpec("row", FieldType.INT, placeholder="0"), + FieldSpec("col", FieldType.INT, placeholder="0"), + FieldSpec("row_tolerance", FieldType.INT, optional=True, default=10), + FieldSpec("box_format", FieldType.ENUM, optional=True, default="xywh", + choices=("xywh", "ltrb")), + ), + description="Resolve a table cell centre by row/col from cell boxes.", ), - description="Report a request's status and approved flag.", - )) - specs.append(CommandSpec( - "AC_lease_secret", "Tools", "Lease: Issue", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("ttl", FieldType.FLOAT, optional=True, default=300.0), + CommandSpec( + "AC_list_plugins", "Tools", "List Plugin Commands", + fields=(FieldSpec("group", FieldType.STRING, optional=True, + default="je_auto_control.commands"),), + description="Discover third-party AC_* commands from entry points.", ), - description="Issue a short-lived JIT lease for a secret (no value); returns {token, ttl}. " - "This builder masks the token -- read it with 'One-time values' after the run.", - sensitive_result=True, - )) - specs.append(CommandSpec( - "AC_lease_valid", "Tools", "Lease: Valid?", - fields=(FieldSpec("token", FieldType.STRING),), - description="Report whether a lease token is still valid.", - )) - specs.append(CommandSpec( - "AC_revoke_lease", "Tools", "Lease: Revoke", - fields=(FieldSpec("token", FieldType.STRING),), - description="Revoke a lease token immediately.", - )) - specs.append(CommandSpec( - "AC_lease_active", "Tools", "Lease: List Active", - fields=(), - description="List active leases (token, name, ttl_remaining). This builder masks each " - "token -- read them with 'One-time values' after the run.", - sensitive_result=True, - )) - specs.append(CommandSpec( - "AC_egress_allow", "Tools", "Egress: Set Allowlist", - fields=( - FieldSpec("allow", FieldType.STRING, optional=True, - placeholder="*.example.com, api.foo.com"), - FieldSpec("deny", FieldType.STRING, optional=True, - placeholder="bad.example.com"), + CommandSpec( + "AC_load_plugins", "Tools", "Load Plugin Commands", + fields=(FieldSpec("group", FieldType.STRING, optional=True, + default="je_auto_control.commands"),), + description="Discover + register third-party plugin commands.", ), - description="Lock the HTTP client to an egress allow/deny policy.", - )) - specs.append(CommandSpec( - "AC_egress_check", "Tools", "Egress: Check URL", - fields=(FieldSpec("url", FieldType.STRING, - placeholder="https://api.example.com"),), - description="Report whether a URL is permitted by the egress policy.", - )) - specs.append(CommandSpec( - "AC_egress_reset", "Tools", "Egress: Reset (allow-all)", - fields=(), - description="Clear the egress policy back to allow-all.", - )) - specs.append(CommandSpec( - "AC_verify_artifact", "Testing", "Approval: Verify Artifact", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="login_screen"), - FieldSpec("content", FieldType.STRING), - FieldSpec("approvals_dir", FieldType.STRING, optional=True, - default=_APPROVALS_DIR), - FieldSpec("extension", FieldType.STRING, optional=True, - default="txt"), - ), - description="Compare content to its approved baseline (snapshot test).", - )) - specs.append(CommandSpec( - "AC_approve_artifact", "Testing", "Approval: Promote Received", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("approvals_dir", FieldType.STRING, optional=True, - default=_APPROVALS_DIR), - FieldSpec("extension", FieldType.STRING, optional=True, - default="txt"), + CommandSpec( + "AC_approval_request", "Tools", "Approval: Request", + fields=( + FieldSpec("action", FieldType.STRING), + FieldSpec("requester", FieldType.STRING, optional=True), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Maker-checker: file a high-risk action for approval; returns {token}, the " + "request id to hand to the checker (shown, not masked).", + ), + CommandSpec( + "AC_approval_approve", "Tools", "Approval: Approve", + fields=( + FieldSpec("token", FieldType.STRING), + FieldSpec("approver", FieldType.STRING), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Approve a request (approver must differ from requester).", + ), + CommandSpec( + "AC_approval_reject", "Tools", "Approval: Reject", + fields=( + FieldSpec("token", FieldType.STRING), + FieldSpec("approver", FieldType.STRING), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Reject a request (approver must differ from requester).", + ), + CommandSpec( + "AC_approval_status", "Tools", "Approval: Status", + fields=( + FieldSpec("token", FieldType.STRING), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Report a request's status and approved flag.", + ), + CommandSpec( + "AC_lease_secret", "Tools", "Lease: Issue", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("ttl", FieldType.FLOAT, optional=True, default=300.0), + ), + description="Issue a short-lived JIT lease for a secret (no value); returns {token, ttl}. " + "This builder masks the token -- read it with 'One-time values' after the run.", + sensitive_result=True, + ), + CommandSpec( + "AC_lease_valid", "Tools", "Lease: Valid?", + fields=(FieldSpec("token", FieldType.STRING),), + description="Report whether a lease token is still valid.", + ), + CommandSpec( + "AC_revoke_lease", "Tools", "Lease: Revoke", + fields=(FieldSpec("token", FieldType.STRING),), + description="Revoke a lease token immediately.", + ), + CommandSpec( + "AC_lease_active", "Tools", "Lease: List Active", + fields=(), + description="List active leases (token, name, ttl_remaining). This builder masks each " + "token -- read them with 'One-time values' after the run.", + sensitive_result=True, + ), + CommandSpec( + "AC_egress_allow", "Tools", "Egress: Set Allowlist", + fields=( + FieldSpec("allow", FieldType.STRING, optional=True, + placeholder="*.example.com, api.foo.com"), + FieldSpec("deny", FieldType.STRING, optional=True, + placeholder="bad.example.com"), + ), + description="Lock the HTTP client to an egress allow/deny policy.", + ), + CommandSpec( + "AC_egress_check", "Tools", "Egress: Check URL", + fields=(FieldSpec("url", FieldType.STRING, + placeholder="https://api.example.com"),), + description="Report whether a URL is permitted by the egress policy.", + ), + CommandSpec( + "AC_egress_reset", "Tools", "Egress: Reset (allow-all)", + fields=(), + description="Clear the egress policy back to allow-all.", + ), + CommandSpec( + "AC_verify_artifact", "Testing", "Approval: Verify Artifact", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="login_screen"), + FieldSpec("content", FieldType.STRING), + FieldSpec("approvals_dir", FieldType.STRING, optional=True, + default=_APPROVALS_DIR), + FieldSpec("extension", FieldType.STRING, optional=True, + default="txt"), + ), + description="Compare content to its approved baseline (snapshot test).", + ), + CommandSpec( + "AC_approve_artifact", "Testing", "Approval: Promote Received", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("approvals_dir", FieldType.STRING, optional=True, + default=_APPROVALS_DIR), + FieldSpec("extension", FieldType.STRING, optional=True, + default="txt"), + ), + description="Promote a received artifact to the approved baseline.", + ), + CommandSpec( + "AC_pending_artifacts", "Testing", "Approval: List Pending", + fields=(FieldSpec("approvals_dir", FieldType.STRING, optional=True, + default=_APPROVALS_DIR),), + description="List artifacts awaiting approval.", + ), + CommandSpec( + "AC_evaluate_trajectory", "Agent", "Evaluate Trajectory", + fields=( + FieldSpec("trajectory", FieldType.STRING, + placeholder='[{"action": "AC_click_mouse"}]'), + FieldSpec("rubric", FieldType.STRING, + placeholder='{"required_actions": ["AC_type_text"]}'), + ), + description="Score an agent trajectory against a rubric (JSON).", + ), + CommandSpec( + "AC_compliance_report", "Report", "Compliance Control Report", + fields=( + FieldSpec("evidence", FieldType.STRING, + placeholder='{"egress_allowlist_enforced": true}'), + FieldSpec("frameworks", FieldType.STRING, optional=True, + placeholder="SOC2, ISO27001"), + FieldSpec("path", FieldType.STRING, optional=True), + FieldSpec("fmt", FieldType.ENUM, optional=True, default="json", + choices=("json", "html")), + ), + description="Map governance evidence to SOC2/ISO 27001 controls.", + ), + CommandSpec( + "AC_trace_record", "Agent", "Trace: Record Span", + fields=( + FieldSpec("operation", FieldType.STRING, placeholder="chat"), + FieldSpec("model", FieldType.STRING, optional=True), + FieldSpec("system", FieldType.STRING, optional=True), + FieldSpec("input_tokens", FieldType.INT, optional=True), + FieldSpec("output_tokens", FieldType.INT, optional=True), + FieldSpec("tool_name", FieldType.STRING, optional=True), + FieldSpec("duration_s", FieldType.FLOAT, optional=True, + default=0.0), + FieldSpec("status", FieldType.ENUM, optional=True, default="ok", + choices=("ok", "error")), + ), + description="Record a GenAI-convention span on the default trace.", + ), + CommandSpec( + "AC_trace_summary", "Agent", "Trace: Summary", + fields=(), + description="Roll up the default agent trace (count/tokens/duration).", + ), + CommandSpec( + "AC_trace_export", "Agent", "Trace: Export (OTLP)", + fields=(), + description="Export the default agent trace as OTLP-friendly spans.", + ), + CommandSpec( + "AC_trace_reset", "Agent", "Trace: Reset", + fields=(), + description="Clear the default agent trace.", + ), + CommandSpec( + "AC_write_step_video", "Report", "Step-Overlay Video", + fields=( + FieldSpec("steps", FieldType.STRING, + placeholder='[{"image": "s1.png", "caption": "Step 1"}]'), + FieldSpec("output", FieldType.STRING, default="walkthrough.mp4"), + FieldSpec("fps", FieldType.INT, optional=True, default=10), + FieldSpec("seconds_per_step", FieldType.FLOAT, optional=True, + default=2.0), + ), + description="Render captioned screenshots into a walkthrough video.", + ), + CommandSpec( + "AC_fuzzy_ratio", "Data", "Fuzzy: Similarity Ratio", + fields=( + FieldSpec("left", FieldType.STRING), + FieldSpec("right", FieldType.STRING), + FieldSpec("ignore_case", FieldType.BOOL, optional=True, + default=True), + ), + description="Similarity score (0..1) between two strings.", + ), + CommandSpec( + "AC_fuzzy_best_match", "Data", "Fuzzy: Best Match", + fields=( + FieldSpec("query", FieldType.STRING), + FieldSpec("choices", FieldType.STRING, + placeholder='["Save", "Cancel", "Submit"]'), + FieldSpec("score_cutoff", FieldType.FLOAT, optional=True, + default=0.0), + FieldSpec("ignore_case", FieldType.BOOL, optional=True, + default=True), + ), + description="Best fuzzy match of query within choices (JSON list).", + ), + CommandSpec( + "AC_fuzzy_dedupe", "Data", "Fuzzy: Dedupe", + fields=( + FieldSpec("items", FieldType.STRING, + placeholder='["foo", "foo ", "bar"]'), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=0.9), + FieldSpec("ignore_case", FieldType.BOOL, optional=True, + default=True), + ), + description="Collapse near-duplicate strings (JSON list).", + ), + CommandSpec( + "AC_s3_upload", "Tools", "S3: Upload Artifact", + fields=( + FieldSpec("local_path", FieldType.FILE_PATH), + FieldSpec("key", FieldType.STRING, optional=True), + ), + description="Upload a file to the configured default S3 store.", + ), + CommandSpec( + "AC_s3_download", "Tools", "S3: Download Artifact", + fields=( + FieldSpec("key", FieldType.STRING), + FieldSpec("local_path", FieldType.STRING), + ), + description="Download an object from the default S3 store.", + ), + CommandSpec( + "AC_s3_list", "Tools", "S3: List Artifacts", + fields=(FieldSpec("prefix", FieldType.STRING, optional=True),), + description="List object keys in the default S3 store.", + ), + CommandSpec( + "AC_s3_delete", "Tools", "S3: Delete Artifact", + fields=(FieldSpec("key", FieldType.STRING),), + description="Delete an object from the default S3 store.", + ), + CommandSpec( + "AC_image_hash", "Image", "Perceptual Hash", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("algo", FieldType.ENUM, optional=True, default="average", + choices=("average", "dhash")), + ), + description="Perceptual hash of an image (average or dhash).", + ), + CommandSpec( + "AC_dedupe_images", "Image", "Dedupe Near-Identical Images", + fields=( + FieldSpec("paths", FieldType.STRING, + placeholder='["a.png", "b.png"]'), + FieldSpec("max_distance", FieldType.INT, optional=True, default=5), + ), + description="Collapse near-duplicate images by perceptual hash.", + ), + CommandSpec( + "AC_canonicalize_url", "Data", "URL: Canonicalize", + fields=( + FieldSpec("url", FieldType.STRING, + placeholder="HTTP://Example.COM:80/a/../b?b=2&a=1#frag"), + ), + description="Canonical form of a URL, for equality and de-duplication.", + ), + CommandSpec( + "AC_normalize_url", "Data", "URL: Normalize", + fields=( + FieldSpec("url", FieldType.STRING, + placeholder="https://EXAMPLE.com:443/p%2fx/"), + FieldSpec("sort_query", FieldType.BOOL, optional=True, + default=False), + FieldSpec("drop_fragment", FieldType.BOOL, optional=True, + default=False), + ), + description="RFC 3986 syntax-based normalisation of a URL.", + ), + CommandSpec( + "AC_urls_equal", "Data", "URL: Compare", + fields=( + # https, because nothing in this example depends on the scheme — + # it shows that query order and the fragment are ignored. The + # canonicalize placeholder above keeps http on purpose: it needs + # port 80 to demonstrate default-port removal. + FieldSpec("first", FieldType.STRING, + placeholder="https://x.com/a?b=1&a=2"), + FieldSpec("second", FieldType.STRING, + placeholder="https://x.com/a?a=2&b=1#top"), + ), + description="Whether two URLs are equivalent after canonicalisation.", + ), + CommandSpec( + "AC_parse_decimal", "Data", "Locale: Parse Decimal", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="1.234,56"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Parse a locale-formatted decimal string to a float.", + ), + CommandSpec( + "AC_parse_number", "Data", "Locale: Parse Number", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="1,234"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Parse a locale-formatted integer string to an int.", + ), + CommandSpec( + "AC_format_decimal", "Data", "Locale: Format Decimal", + fields=( + FieldSpec("value", FieldType.FLOAT), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Format a number for a locale.", + ), + CommandSpec( + "AC_format_currency", "Data", "Locale: Format Currency", + fields=( + FieldSpec("value", FieldType.FLOAT), + FieldSpec("currency", FieldType.STRING, placeholder="USD"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Format a value as currency (ISO 4217) for a locale.", + ), + CommandSpec( + "AC_format_date", "Data", "Locale: Format Date", + fields=( + FieldSpec("value", FieldType.STRING, placeholder="2026-06-20"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + FieldSpec("fmt", FieldType.ENUM, optional=True, default="medium", + choices=("short", "medium", "long", "full")), + ), + description="Format an ISO date string for a locale.", + ), + CommandSpec( + "AC_voice_register", "Agent", "Voice: Register Command", + fields=( + FieldSpec("phrase", FieldType.STRING, placeholder="save file"), + FieldSpec("actions", FieldType.STRING, + placeholder='[["AC_hotkey", {"keys": ["ctrl", "s"]}]]'), + ), + description="Map a spoken phrase to an action list (JSON).", + ), + CommandSpec( + "AC_voice_dispatch", "Agent", "Voice: Dispatch Text", + fields=(FieldSpec("text", FieldType.STRING, + placeholder="save the file"),), + description="Run the command best matching recognized text.", + ), + CommandSpec( + "AC_voice_list", "Agent", "Voice: List Commands", + fields=(), + description="List registered voice-command phrases.", ), - description="Promote a received artifact to the approved baseline.", - )) - specs.append(CommandSpec( - "AC_pending_artifacts", "Testing", "Approval: List Pending", - fields=(FieldSpec("approvals_dir", FieldType.STRING, optional=True, - default=_APPROVALS_DIR),), - description="List artifacts awaiting approval.", - )) - specs.append(CommandSpec( - "AC_evaluate_trajectory", "Agent", "Evaluate Trajectory", - fields=( - FieldSpec("trajectory", FieldType.STRING, - placeholder='[{"action": "AC_click_mouse"}]'), - FieldSpec("rubric", FieldType.STRING, - placeholder='{"required_actions": ["AC_type_text"]}'), + CommandSpec( + "AC_voice_clear", "Agent", "Voice: Clear Commands", + fields=(), + description="Remove all registered voice commands.", ), - description="Score an agent trajectory against a rubric (JSON).", - )) - specs.append(CommandSpec( - "AC_compliance_report", "Report", "Compliance Control Report", - fields=( - FieldSpec("evidence", FieldType.STRING, - placeholder='{"egress_allowlist_enforced": true}'), - FieldSpec("frameworks", FieldType.STRING, optional=True, - placeholder="SOC2, ISO27001"), - FieldSpec("path", FieldType.STRING, optional=True), - FieldSpec("fmt", FieldType.ENUM, optional=True, default="json", - choices=("json", "html")), - ), - description="Map governance evidence to SOC2/ISO 27001 controls.", - )) - specs.append(CommandSpec( - "AC_trace_record", "Agent", "Trace: Record Span", - fields=( - FieldSpec("operation", FieldType.STRING, placeholder="chat"), - FieldSpec("model", FieldType.STRING, optional=True), - FieldSpec("system", FieldType.STRING, optional=True), - FieldSpec("input_tokens", FieldType.INT, optional=True), - FieldSpec("output_tokens", FieldType.INT, optional=True), - FieldSpec("tool_name", FieldType.STRING, optional=True), - FieldSpec("duration_s", FieldType.FLOAT, optional=True, - default=0.0), - FieldSpec("status", FieldType.ENUM, optional=True, default="ok", - choices=("ok", "error")), - ), - description="Record a GenAI-convention span on the default trace.", - )) - specs.append(CommandSpec( - "AC_trace_summary", "Agent", "Trace: Summary", - fields=(), - description="Roll up the default agent trace (count/tokens/duration).", - )) - specs.append(CommandSpec( - "AC_trace_export", "Agent", "Trace: Export (OTLP)", - fields=(), - description="Export the default agent trace as OTLP-friendly spans.", - )) - specs.append(CommandSpec( - "AC_trace_reset", "Agent", "Trace: Reset", - fields=(), - description="Clear the default agent trace.", - )) - specs.append(CommandSpec( - "AC_write_step_video", "Report", "Step-Overlay Video", - fields=( - FieldSpec("steps", FieldType.STRING, - placeholder='[{"image": "s1.png", "caption": "Step 1"}]'), - FieldSpec("output", FieldType.STRING, default="walkthrough.mp4"), - FieldSpec("fps", FieldType.INT, optional=True, default=10), - FieldSpec("seconds_per_step", FieldType.FLOAT, optional=True, - default=2.0), - ), - description="Render captioned screenshots into a walkthrough video.", - )) - specs.append(CommandSpec( - "AC_fuzzy_ratio", "Data", "Fuzzy: Similarity Ratio", - fields=( - FieldSpec("left", FieldType.STRING), - FieldSpec("right", FieldType.STRING), - FieldSpec("ignore_case", FieldType.BOOL, optional=True, - default=True), + CommandSpec( + "AC_to_physical", "Agent", "Coords: Model -> Physical", + fields=( + FieldSpec("x", FieldType.FLOAT), FieldSpec("y", FieldType.FLOAT), + FieldSpec("physical_w", FieldType.INT), + FieldSpec("physical_h", FieldType.INT), + FieldSpec("model_w", FieldType.INT), + FieldSpec("model_h", FieldType.INT), + ), + description="Map a model-grid coordinate to physical pixels.", ), - description="Similarity score (0..1) between two strings.", - )) - specs.append(CommandSpec( - "AC_fuzzy_best_match", "Data", "Fuzzy: Best Match", - fields=( - FieldSpec("query", FieldType.STRING), - FieldSpec("choices", FieldType.STRING, - placeholder='["Save", "Cancel", "Submit"]'), - FieldSpec("score_cutoff", FieldType.FLOAT, optional=True, - default=0.0), - FieldSpec("ignore_case", FieldType.BOOL, optional=True, - default=True), - ), - description="Best fuzzy match of query within choices (JSON list).", - )) - specs.append(CommandSpec( - "AC_fuzzy_dedupe", "Data", "Fuzzy: Dedupe", - fields=( - FieldSpec("items", FieldType.STRING, - placeholder='["foo", "foo ", "bar"]'), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=0.9), - FieldSpec("ignore_case", FieldType.BOOL, optional=True, - default=True), - ), - description="Collapse near-duplicate strings (JSON list).", - )) - specs.append(CommandSpec( - "AC_s3_upload", "Tools", "S3: Upload Artifact", - fields=( - FieldSpec("local_path", FieldType.FILE_PATH), - FieldSpec("key", FieldType.STRING, optional=True), + CommandSpec( + "AC_to_model", "Agent", "Coords: Physical -> Model", + fields=( + FieldSpec("x", FieldType.INT), FieldSpec("y", FieldType.INT), + FieldSpec("physical_w", FieldType.INT), + FieldSpec("physical_h", FieldType.INT), + FieldSpec("model_w", FieldType.INT), + FieldSpec("model_h", FieldType.INT), + ), + description="Map a physical-pixel coordinate to a model grid.", ), - description="Upload a file to the configured default S3 store.", - )) - specs.append(CommandSpec( - "AC_s3_download", "Tools", "S3: Download Artifact", - fields=( - FieldSpec("key", FieldType.STRING), - FieldSpec("local_path", FieldType.STRING), + CommandSpec( + "AC_loop_guard_observe", "Agent", "Loop Guard: Observe Step", + fields=( + FieldSpec("tool", FieldType.STRING, placeholder="AC_click_mouse"), + FieldSpec("args", FieldType.STRING, optional=True, + placeholder='{"x": 10, "y": 20}'), + FieldSpec("result_digest", FieldType.STRING, optional=True), + ), + description="Detect repeat/ping-pong/no-op stuck-loop patterns.", ), - description="Download an object from the default S3 store.", - )) - specs.append(CommandSpec( - "AC_s3_list", "Tools", "S3: List Artifacts", - fields=(FieldSpec("prefix", FieldType.STRING, optional=True),), - description="List object keys in the default S3 store.", - )) - specs.append(CommandSpec( - "AC_s3_delete", "Tools", "S3: Delete Artifact", - fields=(FieldSpec("key", FieldType.STRING),), - description="Delete an object from the default S3 store.", - )) - specs.append(CommandSpec( - "AC_image_hash", "Image", "Perceptual Hash", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("algo", FieldType.ENUM, optional=True, default="average", - choices=("average", "dhash")), + CommandSpec( + "AC_loop_guard_reset", "Agent", "Loop Guard: Reset", + fields=(), + description="Clear the default loop guard's history.", ), - description="Perceptual hash of an image (average or dhash).", - )) - specs.append(CommandSpec( - "AC_dedupe_images", "Image", "Dedupe Near-Identical Images", - fields=( - FieldSpec("paths", FieldType.STRING, - placeholder='["a.png", "b.png"]'), - FieldSpec("max_distance", FieldType.INT, optional=True, default=5), + CommandSpec( + "AC_mine_actions", "Report", "Mine Action Log", + fields=( + FieldSpec("actions", FieldType.STRING, + placeholder='[["AC_click_mouse", {}], ...]'), + FieldSpec("min_len", FieldType.INT, optional=True, default=2), + FieldSpec("max_len", FieldType.INT, optional=True, default=5), + FieldSpec("min_count", FieldType.INT, optional=True, default=3), + ), + description="Find repeated sequences + rank automation candidates.", ), - description="Collapse near-duplicate images by perceptual hash.", - )) - specs.append(CommandSpec( - "AC_canonicalize_url", "Data", "URL: Canonicalize", - fields=( - FieldSpec("url", FieldType.STRING, - placeholder="HTTP://Example.COM:80/a/../b?b=2&a=1#frag"), + CommandSpec( + "AC_set_asset", "Data", "Asset: Set", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("value", FieldType.STRING), + FieldSpec("asset_type", FieldType.ENUM, optional=True, + default="text", + choices=("text", "int", "bool", "credential")), + FieldSpec("environment", FieldType.STRING, optional=True, + default="default"), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Store a typed, environment-scoped asset.", ), - description="Canonical form of a URL, for equality and de-duplication.", - )) - specs.append(CommandSpec( - "AC_normalize_url", "Data", "URL: Normalize", - fields=( - FieldSpec("url", FieldType.STRING, - placeholder="https://EXAMPLE.com:443/p%2fx/"), - FieldSpec("sort_query", FieldType.BOOL, optional=True, - default=False), - FieldSpec("drop_fragment", FieldType.BOOL, optional=True, - default=False), - ), - description="RFC 3986 syntax-based normalisation of a URL.", - )) - specs.append(CommandSpec( - "AC_urls_equal", "Data", "URL: Compare", - fields=( - # https, because nothing in this example depends on the scheme — - # it shows that query order and the fragment are ignored. The - # canonicalize placeholder above keeps http on purpose: it needs - # port 80 to demonstrate default-port removal. - FieldSpec("first", FieldType.STRING, - placeholder="https://x.com/a?b=1&a=2"), - FieldSpec("second", FieldType.STRING, - placeholder="https://x.com/a?a=2&b=1#top"), - ), - description="Whether two URLs are equivalent after canonicalisation.", - )) - specs.append(CommandSpec( - "AC_parse_decimal", "Data", "Locale: Parse Decimal", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="1.234,56"), - FieldSpec("locale", FieldType.STRING, optional=True, - default="en_US"), + CommandSpec( + "AC_get_asset", "Data", "Asset: Get", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("environment", FieldType.STRING, optional=True, + default="default"), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Read a typed asset (credential stays a reference).", ), - description="Parse a locale-formatted decimal string to a float.", - )) - specs.append(CommandSpec( - "AC_parse_number", "Data", "Locale: Parse Number", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="1,234"), - FieldSpec("locale", FieldType.STRING, optional=True, - default="en_US"), + CommandSpec( + "AC_list_assets", "Data", "Asset: List", + fields=( + FieldSpec("environment", FieldType.STRING, optional=True), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="List assets (name/type/environment, no values).", ), - description="Parse a locale-formatted integer string to an int.", - )) - specs.append(CommandSpec( - "AC_format_decimal", "Data", "Locale: Format Decimal", - fields=( - FieldSpec("value", FieldType.FLOAT), - FieldSpec("locale", FieldType.STRING, optional=True, - default="en_US"), + CommandSpec( + "AC_emit_event", "Tools", "Emit CloudEvent", + fields=( + FieldSpec("event_type", FieldType.STRING, + placeholder="com.example.run.finished"), + FieldSpec("data", FieldType.STRING, optional=True, + placeholder='{"run_id": "42"}'), + FieldSpec("source", FieldType.STRING, optional=True, + default="je_auto_control"), + FieldSpec("subject", FieldType.STRING, optional=True), + FieldSpec("url", FieldType.STRING, optional=True, + placeholder="https://hooks.example.com/ce"), + ), + description="Wrap data in a CloudEvents envelope; optionally POST it.", ), - description="Format a number for a locale.", - )) - specs.append(CommandSpec( - "AC_format_currency", "Data", "Locale: Format Currency", - fields=( - FieldSpec("value", FieldType.FLOAT), - FieldSpec("currency", FieldType.STRING, placeholder="USD"), - FieldSpec("locale", FieldType.STRING, optional=True, - default="en_US"), + CommandSpec( + "AC_notify_webhook", "Tools", "Notify: Webhook/Chat", + fields=( + FieldSpec("url", FieldType.STRING, + placeholder="https://hooks.example.com/..."), + FieldSpec("text", FieldType.STRING), + FieldSpec("transport", FieldType.ENUM, optional=True, + default="raw", + choices=("raw", "slack", "discord", "teams")), + FieldSpec("title", FieldType.STRING, optional=True), + ), + description="Send a Slack/Discord/Teams/raw webhook notification.", ), - description="Format a value as currency (ISO 4217) for a locale.", - )) - specs.append(CommandSpec( - "AC_format_date", "Data", "Locale: Format Date", - fields=( - FieldSpec("value", FieldType.STRING, placeholder="2026-06-20"), - FieldSpec("locale", FieldType.STRING, optional=True, - default="en_US"), - FieldSpec("fmt", FieldType.ENUM, optional=True, default="medium", - choices=("short", "medium", "long", "full")), + CommandSpec( + "AC_json_query", "Data", "JSONPath: Query", + fields=( + FieldSpec("data", FieldType.STRING, + placeholder='{"a": [1, 2]}'), + FieldSpec("path", FieldType.STRING, placeholder="$.a[*]"), + ), + description="Query parsed JSON with a JSONPath subset (all matches).", ), - description="Format an ISO date string for a locale.", - )) - specs.append(CommandSpec( - "AC_voice_register", "Agent", "Voice: Register Command", - fields=( - FieldSpec("phrase", FieldType.STRING, placeholder="save file"), - FieldSpec("actions", FieldType.STRING, - placeholder='[["AC_hotkey", {"keys": ["ctrl", "s"]}]]'), + CommandSpec( + "AC_json_extract", "Data", "JSONPath: Extract Mapping", + fields=( + FieldSpec("data", FieldType.STRING), + FieldSpec("mapping", FieldType.STRING, + placeholder='{"name": "$.user.name"}'), + ), + description="Extract a {key: jsonpath} mapping into a flat object.", ), - description="Map a spoken phrase to an action list (JSON).", - )) - specs.append(CommandSpec( - "AC_voice_dispatch", "Agent", "Voice: Dispatch Text", - fields=(FieldSpec("text", FieldType.STRING, - placeholder="save the file"),), - description="Run the command best matching recognized text.", - )) - specs.append(CommandSpec( - "AC_voice_list", "Agent", "Voice: List Commands", - fields=(), - description="List registered voice-command phrases.", - )) - specs.append(CommandSpec( - "AC_voice_clear", "Agent", "Voice: Clear Commands", - fields=(), - description="Remove all registered voice commands.", - )) - specs.append(CommandSpec( - "AC_to_physical", "Agent", "Coords: Model -> Physical", - fields=( - FieldSpec("x", FieldType.FLOAT), FieldSpec("y", FieldType.FLOAT), - FieldSpec("physical_w", FieldType.INT), - FieldSpec("physical_h", FieldType.INT), - FieldSpec("model_w", FieldType.INT), - FieldSpec("model_h", FieldType.INT), + CommandSpec( + "AC_validate_json", "Data", "JSON Schema: Validate", + fields=( + FieldSpec("data", FieldType.STRING, + placeholder='{"name": "Jo", "age": 30}'), + FieldSpec("schema", FieldType.STRING, + placeholder='{"type": "object", "required": ["name"]}'), + ), + description="Validate JSON against a JSON Schema; returns {ok, errors}.", ), - description="Map a model-grid coordinate to physical pixels.", - )) - specs.append(CommandSpec( - "AC_to_model", "Agent", "Coords: Physical -> Model", - fields=( - FieldSpec("x", FieldType.INT), FieldSpec("y", FieldType.INT), - FieldSpec("physical_w", FieldType.INT), - FieldSpec("physical_h", FieldType.INT), - FieldSpec("model_w", FieldType.INT), - FieldSpec("model_h", FieldType.INT), + CommandSpec( + "AC_match_json", "Data", "JSON Contract: Match", + fields=( + FieldSpec("actual", FieldType.STRING, + placeholder='{"id": 1, "name": "Ada"}'), + FieldSpec("expected", FieldType.STRING, + placeholder='{"id": 1, "name": "Ada"}'), + FieldSpec("partial", FieldType.BOOL, optional=True, default=False), + FieldSpec("match_type", FieldType.BOOL, optional=True, + default=False), + ), + description="Match JSON against expected (partial/type); {ok, mismatches}.", ), - description="Map a physical-pixel coordinate to a model grid.", - )) - specs.append(CommandSpec( - "AC_loop_guard_observe", "Agent", "Loop Guard: Observe Step", - fields=( - FieldSpec("tool", FieldType.STRING, placeholder="AC_click_mouse"), - FieldSpec("args", FieldType.STRING, optional=True, - placeholder='{"x": 10, "y": 20}'), - FieldSpec("result_digest", FieldType.STRING, optional=True), + CommandSpec( + "AC_diff_json", "Data", "JSON Contract: Diff", + fields=( + FieldSpec("actual", FieldType.STRING, placeholder='[1, 2, 3]'), + FieldSpec("expected", FieldType.STRING, placeholder='[1, 2]'), + ), + description="Path-tagged diff between two JSON payloads; {diffs}.", ), - description="Detect repeat/ping-pong/no-op stuck-loop patterns.", - )) - specs.append(CommandSpec( - "AC_loop_guard_reset", "Agent", "Loop Guard: Reset", - fields=(), - description="Clear the default loop guard's history.", - )) - specs.append(CommandSpec( - "AC_mine_actions", "Report", "Mine Action Log", - fields=( - FieldSpec("actions", FieldType.STRING, - placeholder='[["AC_click_mouse", {}], ...]'), - FieldSpec("min_len", FieldType.INT, optional=True, default=2), - FieldSpec("max_len", FieldType.INT, optional=True, default=5), - FieldSpec("min_count", FieldType.INT, optional=True, default=3), + CommandSpec( + "AC_evaluate_flag", "Flow", "Feature Flag: Evaluate", + fields=( + FieldSpec("flags", FieldType.STRING, + placeholder='{"flags": {"f": {"variants": {...}}}}'), + FieldSpec("key", FieldType.STRING, placeholder="new-checkout"), + FieldSpec("context", FieldType.STRING, optional=True, + placeholder='{"targeting_key": "user1", "country": "US"}'), + ), + description="Evaluate a feature flag; returns {value, variant, reason}.", ), - description="Find repeated sequences + rank automation candidates.", - )) - specs.append(CommandSpec( - "AC_set_asset", "Data", "Asset: Set", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("value", FieldType.STRING), - FieldSpec("asset_type", FieldType.ENUM, optional=True, - default="text", - choices=("text", "int", "bool", "credential")), - FieldSpec("environment", FieldType.STRING, optional=True, - default="default"), - FieldSpec("db", FieldType.STRING, optional=True), - ), - description="Store a typed, environment-scoped asset.", - )) - specs.append(CommandSpec( - "AC_get_asset", "Data", "Asset: Get", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("environment", FieldType.STRING, optional=True, - default="default"), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_flag_enabled", "Flow", "Feature Flag: Enabled?", + fields=( + FieldSpec("flags", FieldType.STRING), + FieldSpec("key", FieldType.STRING, placeholder="new-checkout"), + FieldSpec("context", FieldType.STRING, optional=True), + ), + description="Boolean feature-flag check; returns {enabled}.", ), - description="Read a typed asset (credential stays a reference).", - )) - specs.append(CommandSpec( - "AC_list_assets", "Data", "Asset: List", - fields=( - FieldSpec("environment", FieldType.STRING, optional=True), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_unified_diff", "Data", "Text: Unified Diff", + fields=( + FieldSpec("a", FieldType.STRING, placeholder="original text"), + FieldSpec("b", FieldType.STRING, placeholder="changed text"), + ), + description="Unified diff transforming a into b; returns {diff}.", ), - description="List assets (name/type/environment, no values).", - )) - specs.append(CommandSpec( - "AC_emit_event", "Tools", "Emit CloudEvent", - fields=( - FieldSpec("event_type", FieldType.STRING, - placeholder="com.example.run.finished"), - FieldSpec("data", FieldType.STRING, optional=True, - placeholder='{"run_id": "42"}'), - FieldSpec("source", FieldType.STRING, optional=True, - default="je_auto_control"), - FieldSpec("subject", FieldType.STRING, optional=True), - FieldSpec("url", FieldType.STRING, optional=True, - placeholder="https://hooks.example.com/ce"), - ), - description="Wrap data in a CloudEvents envelope; optionally POST it.", - )) - specs.append(CommandSpec( - "AC_notify_webhook", "Tools", "Notify: Webhook/Chat", - fields=( - FieldSpec("url", FieldType.STRING, - placeholder="https://hooks.example.com/..."), - FieldSpec("text", FieldType.STRING), - FieldSpec("transport", FieldType.ENUM, optional=True, - default="raw", - choices=("raw", "slack", "discord", "teams")), - FieldSpec("title", FieldType.STRING, optional=True), - ), - description="Send a Slack/Discord/Teams/raw webhook notification.", - )) - specs.append(CommandSpec( - "AC_json_query", "Data", "JSONPath: Query", - fields=( - FieldSpec("data", FieldType.STRING, - placeholder='{"a": [1, 2]}'), - FieldSpec("path", FieldType.STRING, placeholder="$.a[*]"), + CommandSpec( + "AC_apply_unified", "Data", "Text: Apply Diff", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="original text"), + FieldSpec("diff", FieldType.STRING, placeholder="@@ -1 +1 @@ ..."), + ), + description="Apply a unified diff to text; returns {result}.", ), - description="Query parsed JSON with a JSONPath subset (all matches).", - )) - specs.append(CommandSpec( - "AC_json_extract", "Data", "JSONPath: Extract Mapping", - fields=( - FieldSpec("data", FieldType.STRING), - FieldSpec("mapping", FieldType.STRING, - placeholder='{"name": "$.user.name"}'), + CommandSpec( + "AC_three_way_merge", "Data", "Text: Three-Way Merge", + fields=( + FieldSpec("base", FieldType.STRING, placeholder="base text"), + FieldSpec("ours", FieldType.STRING, placeholder="our text"), + FieldSpec("theirs", FieldType.STRING, placeholder="their text"), + ), + description="Merge ours/theirs against base; returns {text, clean, conflicts}.", ), - description="Extract a {key: jsonpath} mapping into a flat object.", - )) - specs.append(CommandSpec( - "AC_validate_json", "Data", "JSON Schema: Validate", - fields=( - FieldSpec("data", FieldType.STRING, - placeholder='{"name": "Jo", "age": 30}'), - FieldSpec("schema", FieldType.STRING, - placeholder='{"type": "object", "required": ["name"]}'), + CommandSpec( + "AC_rrule_occurrences", "Flow", "Recurrence: Expand (RRULE)", + fields=( + FieldSpec("rule", FieldType.STRING, + placeholder="FREQ=MONTHLY;BYDAY=2TU"), + FieldSpec("dtstart", FieldType.STRING, + placeholder="2026-01-01T09:00:00"), + FieldSpec("count", FieldType.INT, optional=True, default=10), + ), + description="Expand an RFC 5545 RRULE into ISO datetimes.", ), - description="Validate JSON against a JSON Schema; returns {ok, errors}.", - )) - specs.append(CommandSpec( - "AC_match_json", "Data", "JSON Contract: Match", - fields=( - FieldSpec("actual", FieldType.STRING, - placeholder='{"id": 1, "name": "Ada"}'), - FieldSpec("expected", FieldType.STRING, - placeholder='{"id": 1, "name": "Ada"}'), - FieldSpec("partial", FieldType.BOOL, optional=True, default=False), - FieldSpec("match_type", FieldType.BOOL, optional=True, - default=False), - ), - description="Match JSON against expected (partial/type); {ok, mismatches}.", - )) - specs.append(CommandSpec( - "AC_diff_json", "Data", "JSON Contract: Diff", - fields=( - FieldSpec("actual", FieldType.STRING, placeholder='[1, 2, 3]'), - FieldSpec("expected", FieldType.STRING, placeholder='[1, 2]'), + CommandSpec( + "AC_rrule_next", "Flow", "Recurrence: Next Occurrence", + fields=( + FieldSpec("rule", FieldType.STRING, + placeholder="FREQ=WEEKLY;BYDAY=MO,WE,FR"), + FieldSpec("dtstart", FieldType.STRING, + placeholder="2026-01-01T09:00:00"), + FieldSpec("now", FieldType.STRING, optional=True, + placeholder="2026-03-15T00:00:00"), + ), + description="Next RRULE occurrence at/after now; returns {next}.", ), - description="Path-tagged diff between two JSON payloads; {diffs}.", - )) - specs.append(CommandSpec( - "AC_evaluate_flag", "Flow", "Feature Flag: Evaluate", - fields=( - FieldSpec("flags", FieldType.STRING, - placeholder='{"flags": {"f": {"variants": {...}}}}'), - FieldSpec("key", FieldType.STRING, placeholder="new-checkout"), - FieldSpec("context", FieldType.STRING, optional=True, - placeholder='{"targeting_key": "user1", "country": "US"}'), + CommandSpec( + "AC_describe_stats", "Data", "Describe Statistics", + fields=( + FieldSpec("values", FieldType.STRING, + placeholder="[12.0, 9.5, 14.2, 11.1]"), + ), + description="Summary stats + percentiles of a numeric list.", ), - description="Evaluate a feature flag; returns {value, variant, reason}.", - )) - specs.append(CommandSpec( - "AC_flag_enabled", "Flow", "Feature Flag: Enabled?", - fields=( - FieldSpec("flags", FieldType.STRING), - FieldSpec("key", FieldType.STRING, placeholder="new-checkout"), - FieldSpec("context", FieldType.STRING, optional=True), + CommandSpec( + "AC_ab_significance", "Data", "A/B Significance (z-test)", + fields=( + FieldSpec("a_conv", FieldType.INT, placeholder="90"), + FieldSpec("a_n", FieldType.INT, placeholder="200"), + FieldSpec("b_conv", FieldType.INT, placeholder="110"), + FieldSpec("b_n", FieldType.INT, placeholder="200"), + ), + description="Two-proportion z-test; returns {z, p_value, significant, ci}.", ), - description="Boolean feature-flag check; returns {enabled}.", - )) - specs.append(CommandSpec( - "AC_unified_diff", "Data", "Text: Unified Diff", - fields=( - FieldSpec("a", FieldType.STRING, placeholder="original text"), - FieldSpec("b", FieldType.STRING, placeholder="changed text"), + CommandSpec( + "AC_search_documents", "Data", "Full-Text Search (BM25)", + fields=( + FieldSpec("docs", FieldType.STRING, + placeholder='{"d1": "quick brown fox", "d2": "lazy dog"}'), + FieldSpec("query", FieldType.STRING, placeholder="quick fox"), + FieldSpec("top_k", FieldType.INT, optional=True, default=10), + FieldSpec("mode", FieldType.STRING, optional=True, + placeholder="bm25", choices=("bm25", "tfidf")), + ), + description="Rank a {id: text} corpus for a query; returns {hits}.", ), - description="Unified diff transforming a into b; returns {diff}.", - )) - specs.append(CommandSpec( - "AC_apply_unified", "Data", "Text: Apply Diff", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="original text"), - FieldSpec("diff", FieldType.STRING, placeholder="@@ -1 +1 @@ ..."), + CommandSpec( + "AC_resolve_pointer", "Data", "JSON Pointer: Resolve", + fields=( + FieldSpec("doc", FieldType.STRING, placeholder='{"a": {"b": [1, 2]}}'), + FieldSpec("pointer", FieldType.STRING, placeholder="/a/b/0"), + ), + description="Resolve an RFC 6901 JSON Pointer; returns {value}.", ), - description="Apply a unified diff to text; returns {result}.", - )) - specs.append(CommandSpec( - "AC_three_way_merge", "Data", "Text: Three-Way Merge", - fields=( - FieldSpec("base", FieldType.STRING, placeholder="base text"), - FieldSpec("ours", FieldType.STRING, placeholder="our text"), - FieldSpec("theirs", FieldType.STRING, placeholder="their text"), + CommandSpec( + "AC_apply_json_patch", "Data", "JSON Patch: Apply", + fields=( + FieldSpec("doc", FieldType.STRING, placeholder='{"a": 1}'), + FieldSpec("patch", FieldType.STRING, + placeholder='[{"op": "add", "path": "/b", "value": 2}]'), + ), + description="Apply an RFC 6902 JSON Patch; returns {result}.", ), - description="Merge ours/theirs against base; returns {text, clean, conflicts}.", - )) - specs.append(CommandSpec( - "AC_rrule_occurrences", "Flow", "Recurrence: Expand (RRULE)", - fields=( - FieldSpec("rule", FieldType.STRING, - placeholder="FREQ=MONTHLY;BYDAY=2TU"), - FieldSpec("dtstart", FieldType.STRING, - placeholder="2026-01-01T09:00:00"), - FieldSpec("count", FieldType.INT, optional=True, default=10), + CommandSpec( + "AC_make_json_patch", "Data", "JSON Patch: Diff", + fields=( + FieldSpec("old", FieldType.STRING, placeholder='{"a": 1}'), + FieldSpec("new", FieldType.STRING, placeholder='{"a": 2}'), + ), + description="Compute an RFC 6902 patch from old to new; returns {patch}.", ), - description="Expand an RFC 5545 RRULE into ISO datetimes.", - )) - specs.append(CommandSpec( - "AC_rrule_next", "Flow", "Recurrence: Next Occurrence", - fields=( - FieldSpec("rule", FieldType.STRING, - placeholder="FREQ=WEEKLY;BYDAY=MO,WE,FR"), - FieldSpec("dtstart", FieldType.STRING, - placeholder="2026-01-01T09:00:00"), - FieldSpec("now", FieldType.STRING, optional=True, - placeholder="2026-03-15T00:00:00"), - ), - description="Next RRULE occurrence at/after now; returns {next}.", - )) - specs.append(CommandSpec( - "AC_describe_stats", "Data", "Describe Statistics", - fields=( - FieldSpec("values", FieldType.STRING, - placeholder="[12.0, 9.5, 14.2, 11.1]"), + CommandSpec( + "AC_merge_patch", "Data", "JSON Merge Patch: Apply", + fields=( + FieldSpec("doc", FieldType.STRING, placeholder='{"a": 1, "b": 2}'), + FieldSpec("patch", FieldType.STRING, + placeholder='{"b": null, "c": 3}'), + ), + description="Apply an RFC 7386 merge patch (null deletes); returns {result}.", ), - description="Summary stats + percentiles of a numeric list.", - )) - specs.append(CommandSpec( - "AC_ab_significance", "Data", "A/B Significance (z-test)", - fields=( - FieldSpec("a_conv", FieldType.INT, placeholder="90"), - FieldSpec("a_n", FieldType.INT, placeholder="200"), - FieldSpec("b_conv", FieldType.INT, placeholder="110"), - FieldSpec("b_n", FieldType.INT, placeholder="200"), + CommandSpec( + "AC_scan_vulns", "Security", "Scan Dependencies for Vulnerabilities", + fields=( + FieldSpec("components", FieldType.STRING, + placeholder='{"components": [{"name": "foo", ' + '"version": "1.0", "purl": "pkg:pypi/foo@1.0"}]}'), + FieldSpec("advisories", FieldType.STRING, + placeholder='[{"id": "GHSA-...", "affected": [...]}]'), + FieldSpec("sarif_path", FieldType.STRING, optional=True, + placeholder="vulns.sarif"), + ), + description="Match SBOM components against an OSV advisory database.", ), - description="Two-proportion z-test; returns {z, p_value, significant, ci}.", - )) - specs.append(CommandSpec( - "AC_search_documents", "Data", "Full-Text Search (BM25)", - fields=( - FieldSpec("docs", FieldType.STRING, - placeholder='{"d1": "quick brown fox", "d2": "lazy dog"}'), - FieldSpec("query", FieldType.STRING, placeholder="quick fox"), - FieldSpec("top_k", FieldType.INT, optional=True, default=10), - FieldSpec("mode", FieldType.STRING, optional=True, - placeholder="bm25", choices=("bm25", "tfidf")), - ), - description="Rank a {id: text} corpus for a query; returns {hits}.", - )) - specs.append(CommandSpec( - "AC_resolve_pointer", "Data", "JSON Pointer: Resolve", - fields=( - FieldSpec("doc", FieldType.STRING, placeholder='{"a": {"b": [1, 2]}}'), - FieldSpec("pointer", FieldType.STRING, placeholder="/a/b/0"), + CommandSpec( + "AC_apply_vex", "Security", "Apply VEX Triage to Findings", + fields=( + FieldSpec("findings", FieldType.STRING, + placeholder='[{"id": "GHSA-...", "package": "foo"}]'), + FieldSpec("vex", FieldType.STRING, + placeholder='{"statements": [{"vulnerability": ' + '{"name": "CVE-..."}, "status": "not_affected"}]}'), + ), + description="Suppress not_affected/fixed vulns via an OpenVEX document.", + ), + CommandSpec( + "AC_check_licenses", "Security", "Check Dependency Licenses", + fields=( + FieldSpec("components", FieldType.STRING, + placeholder='{"components": [{"name": "x", ' + '"licenses": [{"license": {"name": "MIT"}}]}]}'), + FieldSpec("allow", FieldType.STRING, optional=True, + placeholder='["MIT", "Apache-2.0", "BSD-3-Clause"]'), + FieldSpec("deny", FieldType.STRING, optional=True, + placeholder='["GPL-3.0-only", "AGPL-3.0-only"]'), + ), + description="Evaluate SBOM licenses against allow/deny SPDX lists.", + ), + CommandSpec( + "AC_build_provenance", "Security", "Provenance: Build (SLSA)", + fields=( + FieldSpec("paths", FieldType.STRING, + placeholder='["dist/app.whl", "sbom.cdx.json"]'), + FieldSpec("builder_id", FieldType.STRING, optional=True, + placeholder="je_auto_control"), + ), + description="Build a SLSA in-toto provenance statement over files.", ), - description="Resolve an RFC 6901 JSON Pointer; returns {value}.", - )) - specs.append(CommandSpec( - "AC_apply_json_patch", "Data", "JSON Patch: Apply", - fields=( - FieldSpec("doc", FieldType.STRING, placeholder='{"a": 1}'), - FieldSpec("patch", FieldType.STRING, - placeholder='[{"op": "add", "path": "/b", "value": 2}]'), + CommandSpec( + "AC_verify_provenance", "Security", "Provenance: Verify", + fields=( + FieldSpec("statement", FieldType.STRING, + placeholder='{"subject": [...], "predicate": {...}}'), + FieldSpec("files", FieldType.STRING, + placeholder='{"app.whl": "dist/app.whl"}'), + ), + description="Re-hash files against a provenance statement; {ok, mismatches}.", ), - description="Apply an RFC 6902 JSON Patch; returns {result}.", - )) - specs.append(CommandSpec( - "AC_make_json_patch", "Data", "JSON Patch: Diff", - fields=( - FieldSpec("old", FieldType.STRING, placeholder='{"a": 1}'), - FieldSpec("new", FieldType.STRING, placeholder='{"a": 2}'), + CommandSpec( + "AC_jwt_encode", "Security", "JWT: Sign Token", + fields=( + FieldSpec("claims", FieldType.STRING, + placeholder='{"sub": "user1", "exp": 1893456000}'), + FieldSpec("key", FieldType.STRING, placeholder="shared secret"), + FieldSpec("alg", FieldType.STRING, optional=True, + placeholder="HS256", + choices=("HS256", "HS384", "HS512")), + ), + description="Sign a compact JWT (HMAC) from claims; returns {token}.", + sensitive_result=True, ), - description="Compute an RFC 6902 patch from old to new; returns {patch}.", - )) - specs.append(CommandSpec( - "AC_merge_patch", "Data", "JSON Merge Patch: Apply", - fields=( - FieldSpec("doc", FieldType.STRING, placeholder='{"a": 1, "b": 2}'), - FieldSpec("patch", FieldType.STRING, - placeholder='{"b": null, "c": 3}'), + CommandSpec( + "AC_jwt_decode", "Security", "JWT: Verify Token", + fields=( + FieldSpec("token", FieldType.STRING, placeholder="eyJhbGci..."), + FieldSpec("key", FieldType.STRING, placeholder="shared secret"), + FieldSpec("algorithms", FieldType.STRING, optional=True, + placeholder='["HS256"]'), + FieldSpec("audience", FieldType.STRING, optional=True), + ), + description="Verify a JWT (alg allowlist + exp/nbf/aud); returns {ok, claims}.", ), - description="Apply an RFC 7386 merge patch (null deletes); returns {result}.", - )) - specs.append(CommandSpec( - "AC_scan_vulns", "Security", "Scan Dependencies for Vulnerabilities", - fields=( - FieldSpec("components", FieldType.STRING, - placeholder='{"components": [{"name": "foo", ' - '"version": "1.0", "purl": "pkg:pypi/foo@1.0"}]}'), - FieldSpec("advisories", FieldType.STRING, - placeholder='[{"id": "GHSA-...", "affected": [...]}]'), - FieldSpec("sarif_path", FieldType.STRING, optional=True, - placeholder="vulns.sarif"), - ), - description="Match SBOM components against an OSV advisory database.", - )) - specs.append(CommandSpec( - "AC_apply_vex", "Security", "Apply VEX Triage to Findings", - fields=( - FieldSpec("findings", FieldType.STRING, - placeholder='[{"id": "GHSA-...", "package": "foo"}]'), - FieldSpec("vex", FieldType.STRING, - placeholder='{"statements": [{"vulnerability": ' - '{"name": "CVE-..."}, "status": "not_affected"}]}'), + CommandSpec( + "AC_percentiles", "Report", "Percentiles", + fields=( + FieldSpec("samples", FieldType.STRING, + placeholder="[12.0, 9.5, 14.2, 11.1]"), + FieldSpec("qs", FieldType.STRING, optional=True, + placeholder="[50, 90, 99]"), + ), + description="Exact percentiles of a numeric sample list.", ), - description="Suppress not_affected/fixed vulns via an OpenVEX document.", - )) - specs.append(CommandSpec( - "AC_check_licenses", "Security", "Check Dependency Licenses", - fields=( - FieldSpec("components", FieldType.STRING, - placeholder='{"components": [{"name": "x", ' - '"licenses": [{"license": {"name": "MIT"}}]}]}'), - FieldSpec("allow", FieldType.STRING, optional=True, - placeholder='["MIT", "Apache-2.0", "BSD-3-Clause"]'), - FieldSpec("deny", FieldType.STRING, optional=True, - placeholder='["GPL-3.0-only", "AGPL-3.0-only"]'), - ), - description="Evaluate SBOM licenses against allow/deny SPDX lists.", - )) - specs.append(CommandSpec( - "AC_build_provenance", "Security", "Provenance: Build (SLSA)", - fields=( - FieldSpec("paths", FieldType.STRING, - placeholder='["dist/app.whl", "sbom.cdx.json"]'), - FieldSpec("builder_id", FieldType.STRING, optional=True, - placeholder="je_auto_control"), + CommandSpec( + "AC_evaluate_slo", "Report", "SLO: Evaluate (SLI + Error Budget)", + fields=( + FieldSpec("records", FieldType.STRING, + placeholder='[{"timestamp": 1700000000, "ok": true}]'), + FieldSpec("target", FieldType.FLOAT, placeholder="0.99"), + FieldSpec("window_s", FieldType.FLOAT, optional=True), + ), + description="SLI + error budget for outcome records vs a target.", ), - description="Build a SLSA in-toto provenance statement over files.", - )) - specs.append(CommandSpec( - "AC_verify_provenance", "Security", "Provenance: Verify", - fields=( - FieldSpec("statement", FieldType.STRING, - placeholder='{"subject": [...], "predicate": {...}}'), - FieldSpec("files", FieldType.STRING, - placeholder='{"app.whl": "dist/app.whl"}'), + CommandSpec( + "AC_burn_alerts", "Report", "SLO: Burn-Rate Alerts", + fields=( + FieldSpec("records", FieldType.STRING, + placeholder='[{"timestamp": 1700000000, "ok": false}]'), + FieldSpec("target", FieldType.FLOAT, placeholder="0.99"), + ), + description="Multi-window burn-rate alerts (Google SRE tiers).", ), - description="Re-hash files against a provenance statement; {ok, mismatches}.", - )) - specs.append(CommandSpec( - "AC_jwt_encode", "Security", "JWT: Sign Token", - fields=( - FieldSpec("claims", FieldType.STRING, - placeholder='{"sub": "user1", "exp": 1893456000}'), - FieldSpec("key", FieldType.STRING, placeholder="shared secret"), - FieldSpec("alg", FieldType.STRING, optional=True, - placeholder="HS256", - choices=("HS256", "HS384", "HS512")), - ), - description="Sign a compact JWT (HMAC) from claims; returns {token}.", - sensitive_result=True, - )) - specs.append(CommandSpec( - "AC_jwt_decode", "Security", "JWT: Verify Token", - fields=( - FieldSpec("token", FieldType.STRING, placeholder="eyJhbGci..."), - FieldSpec("key", FieldType.STRING, placeholder="shared secret"), - FieldSpec("algorithms", FieldType.STRING, optional=True, - placeholder='["HS256"]'), - FieldSpec("audience", FieldType.STRING, optional=True), + CommandSpec( + "AC_run_chaos", "Flow", "Run Chaos Experiment", + fields=( + FieldSpec("spec", FieldType.STRING, + placeholder='{"title": "...", "probes": [{"name": "p", ' + '"action": [...]}], "method": [{"name": "f", ' + '"action": [...]}], "rollbacks": [[...]]}'), + ), + description="Verify steady state, inject faults, re-verify, roll back.", ), - description="Verify a JWT (alg allowlist + exp/nbf/aud); returns {ok, claims}.", - )) - specs.append(CommandSpec( - "AC_percentiles", "Report", "Percentiles", - fields=( - FieldSpec("samples", FieldType.STRING, - placeholder="[12.0, 9.5, 14.2, 11.1]"), - FieldSpec("qs", FieldType.STRING, optional=True, - placeholder="[50, 90, 99]"), + CommandSpec( + "AC_run_saga", "Flow", "Run Saga (Compensating Rollback)", + fields=( + FieldSpec("steps", FieldType.STRING, + placeholder='[{"name": "s1", "action": [...], ' + '"compensation": [...]}]'), + ), + description="Run steps; on failure undo completed steps LIFO.", ), - description="Exact percentiles of a numeric sample list.", - )) - specs.append(CommandSpec( - "AC_evaluate_slo", "Report", "SLO: Evaluate (SLI + Error Budget)", - fields=( - FieldSpec("records", FieldType.STRING, - placeholder='[{"timestamp": 1700000000, "ok": true}]'), - FieldSpec("target", FieldType.FLOAT, placeholder="0.99"), - FieldSpec("window_s", FieldType.FLOAT, optional=True), + CommandSpec( + "AC_decision_table", "Flow", "Decision Table (DMN)", + fields=( + FieldSpec("spec", FieldType.STRING, + placeholder='{"inputs": ["age"], "hit_policy": "FIRST", ' + '"rules": [...]}'), + FieldSpec("context", FieldType.STRING, + placeholder='{"age": 30}'), + ), + description="Evaluate inputs against a rule table (hit policy).", ), - description="SLI + error budget for outcome records vs a target.", - )) - specs.append(CommandSpec( - "AC_burn_alerts", "Report", "SLO: Burn-Rate Alerts", - fields=( - FieldSpec("records", FieldType.STRING, - placeholder='[{"timestamp": 1700000000, "ok": false}]'), - FieldSpec("target", FieldType.FLOAT, placeholder="0.99"), + CommandSpec( + "AC_repair_record", "Tools", "Locator Repair: Record", + fields=( + FieldSpec("key", FieldType.STRING), + FieldSpec("method", FieldType.STRING, placeholder="vlm/image"), + FieldSpec("coordinates", FieldType.STRING, optional=True, + placeholder=_POINT_PLACEHOLDER), + FieldSpec("description", FieldType.STRING, optional=True), + FieldSpec("confidence", FieldType.FLOAT, optional=True, + default=1.0), + FieldSpec("auto_threshold", FieldType.FLOAT, optional=True, + default=0.9), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Persist a corrected locator from a heal (auto/queue).", ), - description="Multi-window burn-rate alerts (Google SRE tiers).", - )) - specs.append(CommandSpec( - "AC_run_chaos", "Flow", "Run Chaos Experiment", - fields=( - FieldSpec("spec", FieldType.STRING, - placeholder='{"title": "...", "probes": [{"name": "p", ' - '"action": [...]}], "method": [{"name": "f", ' - '"action": [...]}], "rollbacks": [[...]]}'), + CommandSpec( + "AC_repair_resolved", "Tools", "Locator Repair: Resolved", + fields=( + FieldSpec("key", FieldType.STRING), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Get the learned corrected locator for a key.", ), - description="Verify steady state, inject faults, re-verify, roll back.", - )) - specs.append(CommandSpec( - "AC_run_saga", "Flow", "Run Saga (Compensating Rollback)", - fields=( - FieldSpec("steps", FieldType.STRING, - placeholder='[{"name": "s1", "action": [...], ' - '"compensation": [...]}]'), + CommandSpec( + "AC_repair_pending", "Tools", "Locator Repair: Pending", + fields=(FieldSpec("db", FieldType.STRING, optional=True),), + description="List locator-repair suggestions awaiting review.", ), - description="Run steps; on failure undo completed steps LIFO.", - )) - specs.append(CommandSpec( - "AC_decision_table", "Flow", "Decision Table (DMN)", - fields=( - FieldSpec("spec", FieldType.STRING, - placeholder='{"inputs": ["age"], "hit_policy": "FIRST", ' - '"rules": [...]}'), - FieldSpec("context", FieldType.STRING, - placeholder='{"age": 30}'), + CommandSpec( + "AC_repair_approve", "Tools", "Locator Repair: Approve", + fields=( + FieldSpec("suggestion_id", FieldType.STRING), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Approve a pending locator-repair suggestion.", ), - description="Evaluate inputs against a rule table (hit policy).", - )) - specs.append(CommandSpec( - "AC_repair_record", "Tools", "Locator Repair: Record", - fields=( - FieldSpec("key", FieldType.STRING), - FieldSpec("method", FieldType.STRING, placeholder="vlm/image"), - FieldSpec("coordinates", FieldType.STRING, optional=True, - placeholder=_POINT_PLACEHOLDER), - FieldSpec("description", FieldType.STRING, optional=True), - FieldSpec("confidence", FieldType.FLOAT, optional=True, - default=1.0), - FieldSpec("auto_threshold", FieldType.FLOAT, optional=True, - default=0.9), - FieldSpec("db", FieldType.STRING, optional=True), - ), - description="Persist a corrected locator from a heal (auto/queue).", - )) - specs.append(CommandSpec( - "AC_repair_resolved", "Tools", "Locator Repair: Resolved", - fields=( - FieldSpec("key", FieldType.STRING), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_detect_pii", "Data", "PII: Detect", + fields=( + FieldSpec("text", FieldType.STRING), + FieldSpec("kinds", FieldType.STRING, optional=True, + placeholder='["email", "phone"]'), + ), + description="Detect PII spans (email/phone/ssn/card/ip/iban) in text.", ), - description="Get the learned corrected locator for a key.", - )) - specs.append(CommandSpec( - "AC_repair_pending", "Tools", "Locator Repair: Pending", - fields=(FieldSpec("db", FieldType.STRING, optional=True),), - description="List locator-repair suggestions awaiting review.", - )) - specs.append(CommandSpec( - "AC_repair_approve", "Tools", "Locator Repair: Approve", - fields=( - FieldSpec("suggestion_id", FieldType.STRING), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_redact_pii", "Data", "PII: Redact", + fields=( + FieldSpec("text", FieldType.STRING), + FieldSpec("kinds", FieldType.STRING, optional=True, + placeholder='["email"]'), + FieldSpec("mode", FieldType.ENUM, optional=True, default="label", + choices=("label", "mask", "partial", "hash")), + FieldSpec("mask_char", FieldType.STRING, optional=True, + default="*"), + ), + description="Redact PII in text (label/mask/partial/hash).", ), - description="Approve a pending locator-repair suggestion.", - )) - specs.append(CommandSpec( - "AC_detect_pii", "Data", "PII: Detect", - fields=( - FieldSpec("text", FieldType.STRING), - FieldSpec("kinds", FieldType.STRING, optional=True, - placeholder='["email", "phone"]'), + CommandSpec( + "AC_export_sarif", "Report", "Export SARIF (Code Scanning)", + fields=( + FieldSpec("findings", FieldType.STRING, + placeholder='[{"rule_id": "AC1", "message": "...", ' + '"level": "error"}]'), + FieldSpec("path", FieldType.STRING, optional=True, + placeholder="results.sarif"), + FieldSpec("tool_name", FieldType.STRING, optional=True, + default="AutoControl"), + ), + description="Unify findings into a SARIF 2.1.0 document.", ), - description="Detect PII spans (email/phone/ssn/card/ip/iban) in text.", - )) - specs.append(CommandSpec( - "AC_redact_pii", "Data", "PII: Redact", - fields=( - FieldSpec("text", FieldType.STRING), - FieldSpec("kinds", FieldType.STRING, optional=True, - placeholder='["email"]'), - FieldSpec("mode", FieldType.ENUM, optional=True, default="label", - choices=("label", "mask", "partial", "hash")), - FieldSpec("mask_char", FieldType.STRING, optional=True, - default="*"), - ), - description="Redact PII in text (label/mask/partial/hash).", - )) - specs.append(CommandSpec( - "AC_export_sarif", "Report", "Export SARIF (Code Scanning)", - fields=( - FieldSpec("findings", FieldType.STRING, - placeholder='[{"rule_id": "AC1", "message": "...", ' - '"level": "error"}]'), - FieldSpec("path", FieldType.STRING, optional=True, - placeholder="results.sarif"), - FieldSpec("tool_name", FieldType.STRING, optional=True, - default="AutoControl"), - ), - description="Unify findings into a SARIF 2.1.0 document.", - )) - specs.append(CommandSpec( - "AC_generate_sop", "Report", "Generate SOP Document", - fields=( - FieldSpec("title", FieldType.STRING, optional=True, - default="Automation Procedure"), - FieldSpec("path", FieldType.FILE_PATH, optional=True), + CommandSpec( + "AC_generate_sop", "Report", "Generate SOP Document", + fields=( + FieldSpec("title", FieldType.STRING, optional=True, + default="Automation Procedure"), + FieldSpec("path", FieldType.FILE_PATH, optional=True), + ), + description="Build a step-by-step SOP (HTML) from 'actions' (JSON " + "view).", ), - description="Build a step-by-step SOP (HTML) from 'actions' (JSON " - "view).", - )) + ]) def _add_audit_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_heal_stats", "Testing", "Self-Heal Analytics", - fields=(FieldSpec("limit", FieldType.INT, optional=True, - default=200),), - description="Aggregate the self-heal log (heal rate, brittle " - "locators).", - )) - specs.append(CommandSpec( - "AC_failure_signature", "Testing", "Failure Signature", - fields=( - FieldSpec("error", FieldType.STRING, - placeholder="Timeout at C:\\app.py line 42 (0x7ff..)"), - FieldSpec("length", FieldType.INT, optional=True, default=12), + specs.extend([ + CommandSpec( + "AC_heal_stats", "Testing", "Self-Heal Analytics", + fields=(FieldSpec("limit", FieldType.INT, optional=True, + default=200),), + description="Aggregate the self-heal log (heal rate, brittle " + "locators).", ), - description="Normalise + hash an error to a stable failure signature.", - )) - specs.append(CommandSpec( - "AC_group_failures", "Testing", "Group Failures by Signature", - fields=(FieldSpec("errors", FieldType.STRING, - placeholder='["err one", "err two"]'),), - description="Group error messages by failure signature (most frequent).", - )) - specs.append(CommandSpec( - "AC_diff_runs", "Testing", "Diff Two Run Traces", - fields=( - FieldSpec("before", FieldType.STRING, - placeholder='[{"name": "login", "status": "ok"}]'), - FieldSpec("after", FieldType.STRING, - placeholder='[{"name": "login", "status": "error"}]'), - FieldSpec("key", FieldType.STRING, optional=True, default="name"), - FieldSpec("regress_factor", FieldType.FLOAT, optional=True, - default=1.5), - ), - description="LCS-align two run step-traces: added/removed/flips/regress.", - )) - specs.append(CommandSpec( - "AC_failure_clusters", "Testing", "Cluster Co-Failing Tests", - fields=( - FieldSpec("runs", FieldType.STRING, - placeholder='[["test_a", "test_b"], ["test_a", "test_b"]]'), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.5), - FieldSpec("min_size", FieldType.INT, optional=True, default=2), + CommandSpec( + "AC_failure_signature", "Testing", "Failure Signature", + fields=( + FieldSpec("error", FieldType.STRING, + placeholder="Timeout at C:\\app.py line 42 (0x7ff..)"), + FieldSpec("length", FieldType.INT, optional=True, default=12), + ), + description="Normalise + hash an error to a stable failure signature.", ), - description="Cluster tests that flake together (co-failure Jaccard).", - )) - specs.append(CommandSpec( - "AC_cofailure_pairs", "Testing", "Co-Failing Test Pairs", - fields=( - FieldSpec("runs", FieldType.STRING, - placeholder='[["test_a", "test_b"]]'), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.5), + CommandSpec( + "AC_group_failures", "Testing", "Group Failures by Signature", + fields=(FieldSpec("errors", FieldType.STRING, + placeholder='["err one", "err two"]'),), + description="Group error messages by failure signature (most frequent).", ), - description="Test pairs that fail together above a Jaccard threshold.", - )) - specs.append(CommandSpec( - "AC_build_timeline", "Testing", "Step Timeline (waterfall)", - fields=(FieldSpec("steps", FieldType.STRING, - placeholder='[{"name": "login", "duration": 1.2}]'),), - description="Per-run step waterfall: offsets, durations, bottleneck.", - )) - specs.append(CommandSpec( - "AC_critical_steps", "Testing", "Critical (Bottleneck) Steps", - fields=( - FieldSpec("steps", FieldType.STRING, - placeholder='[{"name": "login", "duration": 1.2}]'), - FieldSpec("top", FieldType.INT, optional=True, default=3), + CommandSpec( + "AC_diff_runs", "Testing", "Diff Two Run Traces", + fields=( + FieldSpec("before", FieldType.STRING, + placeholder='[{"name": "login", "status": "ok"}]'), + FieldSpec("after", FieldType.STRING, + placeholder='[{"name": "login", "status": "error"}]'), + FieldSpec("key", FieldType.STRING, optional=True, default="name"), + FieldSpec("regress_factor", FieldType.FLOAT, optional=True, + default=1.5), + ), + description="LCS-align two run step-traces: added/removed/flips/regress.", ), - description="The steps that dominate a run's time, longest first.", - )) - specs.append(CommandSpec( - "AC_scan_secrets", "Tools", "Scan for Hardcoded Secrets", - description="Scan 'data' (JSON view) for hardcoded secrets that " - "should use ${secrets.*}.", - )) + CommandSpec( + "AC_failure_clusters", "Testing", "Cluster Co-Failing Tests", + fields=( + FieldSpec("runs", FieldType.STRING, + placeholder='[["test_a", "test_b"], ["test_a", "test_b"]]'), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.5), + FieldSpec("min_size", FieldType.INT, optional=True, default=2), + ), + description="Cluster tests that flake together (co-failure Jaccard).", + ), + CommandSpec( + "AC_cofailure_pairs", "Testing", "Co-Failing Test Pairs", + fields=( + FieldSpec("runs", FieldType.STRING, + placeholder='[["test_a", "test_b"]]'), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.5), + ), + description="Test pairs that fail together above a Jaccard threshold.", + ), + CommandSpec( + "AC_build_timeline", "Testing", "Step Timeline (waterfall)", + fields=(FieldSpec("steps", FieldType.STRING, + placeholder='[{"name": "login", "duration": 1.2}]'),), + description="Per-run step waterfall: offsets, durations, bottleneck.", + ), + CommandSpec( + "AC_critical_steps", "Testing", "Critical (Bottleneck) Steps", + fields=( + FieldSpec("steps", FieldType.STRING, + placeholder='[{"name": "login", "duration": 1.2}]'), + FieldSpec("top", FieldType.INT, optional=True, default=3), + ), + description="The steps that dominate a run's time, longest first.", + ), + CommandSpec( + "AC_scan_secrets", "Tools", "Scan for Hardcoded Secrets", + description="Scan 'data' (JSON view) for hardcoded secrets that " + "should use ${secrets.*}.", + ), + ]) def _add_devex_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_ci_annotations", "Tools", "Emit CI Annotations", - description="Emit GitHub Actions annotations from 'annotations' " - "(JSON view).", - )) - specs.append(CommandSpec( - "AC_clip_history_capture", "Misc", "Clipboard History: Capture")) - specs.append(CommandSpec( - "AC_clip_history_list", "Misc", "Clipboard History: List")) - specs.append(CommandSpec( - "AC_clip_history_search", "Misc", "Clipboard History: Search", - fields=(FieldSpec("query", FieldType.STRING),))) - specs.append(CommandSpec( - "AC_clip_history_start", "Misc", "Clipboard History: Start")) - specs.append(CommandSpec( - "AC_clip_history_stop", "Misc", "Clipboard History: Stop")) + specs.extend([ + CommandSpec( + "AC_ci_annotations", "Tools", "Emit CI Annotations", + description="Emit GitHub Actions annotations from 'annotations' " + "(JSON view).", + ), + CommandSpec( + "AC_clip_history_capture", "Misc", "Clipboard History: Capture"), + CommandSpec( + "AC_clip_history_list", "Misc", "Clipboard History: List"), + CommandSpec( + "AC_clip_history_search", "Misc", "Clipboard History: Search", + fields=(FieldSpec("query", FieldType.STRING),)), + CommandSpec( + "AC_clip_history_start", "Misc", "Clipboard History: Start"), + CommandSpec( + "AC_clip_history_stop", "Misc", "Clipboard History: Stop"), + ]) def _add_resilience_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_circuit_call", "Flow", "Circuit Breaker Call", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("threshold", FieldType.INT, optional=True, default=5), - FieldSpec("reset_s", FieldType.FLOAT, optional=True, default=30.0), + specs.extend([ + CommandSpec( + "AC_circuit_call", "Flow", "Circuit Breaker Call", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("threshold", FieldType.INT, optional=True, default=5), + FieldSpec("reset_s", FieldType.FLOAT, optional=True, default=30.0), + ), + description="Run 'actions' (JSON view) via a named circuit breaker.", ), - description="Run 'actions' (JSON view) via a named circuit breaker.", - )) - specs.append(CommandSpec( - "AC_bulkhead_run", "Flow", "Bulkhead (Bounded Concurrency)", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("max_concurrent", FieldType.INT, default=4), - FieldSpec("actions", FieldType.STRING, - placeholder="[[\"AC_click_mouse\", {...}]]"), + CommandSpec( + "AC_bulkhead_run", "Flow", "Bulkhead (Bounded Concurrency)", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("max_concurrent", FieldType.INT, default=4), + FieldSpec("actions", FieldType.STRING, + placeholder="[[\"AC_click_mouse\", {...}]]"), + ), + description="Run 'actions' (JSON view) under a named bulkhead permit.", ), - description="Run 'actions' (JSON view) under a named bulkhead permit.", - )) - specs.append(CommandSpec( - "AC_retry_after", "Flow", "Parse Retry-After / RateLimit", - fields=( - FieldSpec("response", FieldType.STRING, - placeholder='{"status": 429, "headers": {"Retry-After": "30"}}'), + CommandSpec( + "AC_retry_after", "Flow", "Parse Retry-After / RateLimit", + fields=( + FieldSpec("response", FieldType.STRING, + placeholder='{"status": 429, "headers": {"Retry-After": "30"}}'), + ), + description="Server-advised wait (seconds) from an HTTP response; {delay}.", ), - description="Server-advised wait (seconds) from an HTTP response; {delay}.", - )) - specs.append(CommandSpec( - "AC_http_replay", "Data", "HTTP Cassette: Replay", - fields=( - FieldSpec("cassette", FieldType.STRING, - placeholder='{"interactions": [{"request": {...}, ' - '"response": {...}}]}'), - FieldSpec("url", FieldType.STRING, - placeholder="https://api.example.com/users/1"), - FieldSpec("method", FieldType.STRING, optional=True, - placeholder="GET"), - ), - description="Replay a recorded HTTP response from a cassette (no network).", - )) - specs.append(CommandSpec( - "AC_trace_inject", "Data", "Trace Context: Inject", - fields=( - FieldSpec("headers", FieldType.STRING, optional=True, - placeholder='{"accept": "application/json"}'), - FieldSpec("traceparent", FieldType.STRING, optional=True, - placeholder="00-<32 hex>-<16 hex>-01 (parent; omit for root)"), + CommandSpec( + "AC_http_replay", "Data", "HTTP Cassette: Replay", + fields=( + FieldSpec("cassette", FieldType.STRING, + placeholder='{"interactions": [{"request": {...}, ' + '"response": {...}}]}'), + FieldSpec("url", FieldType.STRING, + placeholder="https://api.example.com/users/1"), + FieldSpec("method", FieldType.STRING, optional=True, + placeholder="GET"), + ), + description="Replay a recorded HTTP response from a cassette (no network).", + ), + CommandSpec( + "AC_trace_inject", "Data", "Trace Context: Inject", + fields=( + FieldSpec("headers", FieldType.STRING, optional=True, + placeholder='{"accept": "application/json"}'), + FieldSpec("traceparent", FieldType.STRING, optional=True, + placeholder="00-<32 hex>-<16 hex>-01 (parent; omit for root)"), + ), + description="Set a W3C traceparent on outgoing headers (root or child).", + ), + CommandSpec( + "AC_trace_extract", "Data", "Trace Context: Extract", + fields=( + FieldSpec("headers", FieldType.STRING, + placeholder='{"traceparent": "00-...-...-01"}'), + ), + description="Extract the W3C trace context from request headers.", + ), + CommandSpec( + "AC_baggage_parse", "Data", "Baggage: Parse Header", + fields=( + FieldSpec("header", FieldType.STRING, + placeholder="tenant=acme,run=42"), + ), + description="Parse a W3C baggage header into key-value items.", + ), + CommandSpec( + "AC_baggage_format", "Data", "Baggage: Format Header", + fields=( + FieldSpec("items", FieldType.STRING, + placeholder='{"tenant": "acme", "run": "42"}'), + ), + description="Serialise items into a percent-encoded baggage header.", + ), + CommandSpec( + "AC_canonical_log", "Report", "Canonical Log: Build Line", + fields=( + FieldSpec("fields", FieldType.STRING, + placeholder='{"event": "run", "ok": true, "ms": 42}'), + ), + description="Build a canonical wide-event log line (rendered as JSON).", + ), + CommandSpec( + "AC_normalize_text", "Data", "Text: Normalize (Unicode)", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="Café Menu"), + FieldSpec("form", FieldType.STRING, optional=True, + placeholder="NFKC"), + FieldSpec("casefold", FieldType.BOOL, optional=True, default=True), + FieldSpec("collapse_ws", FieldType.BOOL, optional=True, + default=True), + ), + description="Unicode-normalise text (form + casefold + ws fold).", + ), + CommandSpec( + "AC_slugify", "Data", "Text: Slugify", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="Café Menu!"), + FieldSpec("sep", FieldType.STRING, optional=True, placeholder="-"), + ), + description="Produce an ASCII slug (de-accent, lowercase, join).", + ), + CommandSpec( + "AC_text_similarity", "Data", "Text: Similarity", + fields=( + FieldSpec("a", FieldType.STRING, placeholder="login"), + FieldSpec("b", FieldType.STRING, placeholder="lgoin"), + FieldSpec("metric", FieldType.STRING, optional=True, + placeholder="jaro_winkler | levenshtein | jaccard | dice"), + ), + description="Normalised string similarity (Jaro-Winkler / edit / Jaccard).", + ), + CommandSpec( + "AC_simhash", "Data", "Near-Dup: SimHash", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="some text"), + FieldSpec("bits", FieldType.INT, optional=True, default=64), + ), + description="SimHash fingerprint (int) of text.", + ), + CommandSpec( + "AC_near_duplicates", "Data", "Near-Dup: Cluster Texts", + fields=( + FieldSpec("texts", FieldType.STRING, + placeholder='["the cat sat", "the cat sat down", "dog"]'), + FieldSpec("max_distance", FieldType.INT, optional=True, default=3), + ), + description="Cluster near-duplicate texts by SimHash distance.", ), - description="Set a W3C traceparent on outgoing headers (root or child).", - )) - specs.append(CommandSpec( - "AC_trace_extract", "Data", "Trace Context: Extract", - fields=( - FieldSpec("headers", FieldType.STRING, - placeholder='{"traceparent": "00-...-...-01"}'), + CommandSpec( + "AC_spans_to_otlp", "Report", "OTLP: Export Spans", + fields=( + FieldSpec("spans", FieldType.STRING, + placeholder='[{"trace_id": "...", "span_id": "...", ' + '"name": "step", "start_unix_nano": 1, ' + '"end_unix_nano": 2}]'), + FieldSpec("resource_attrs", FieldType.STRING, optional=True, + placeholder='{"service.name": "autocontrol"}'), + ), + description="Wrap spans in an OTLP/JSON resourceSpans envelope.", ), - description="Extract the W3C trace context from request headers.", - )) - specs.append(CommandSpec( - "AC_baggage_parse", "Data", "Baggage: Parse Header", - fields=( - FieldSpec("header", FieldType.STRING, - placeholder="tenant=acme,run=42"), + CommandSpec( + "AC_resolve_ref", "Security", "Secret Ref: Resolve", + fields=( + FieldSpec("ref", FieldType.STRING, + placeholder="env://TOKEN | file://./token | secret://api-key"), + ), + description="Resolve an env:// / file:// / secret:// value reference.", ), - description="Parse a W3C baggage header into key-value items.", - )) - specs.append(CommandSpec( - "AC_baggage_format", "Data", "Baggage: Format Header", - fields=( - FieldSpec("items", FieldType.STRING, - placeholder='{"tenant": "acme", "run": "42"}'), + CommandSpec( + "AC_resolve_refs", "Security", "Secret Ref: Resolve In Structure", + fields=( + FieldSpec("obj", FieldType.STRING, + placeholder='{"token": "env://TOKEN", "url": "..."}'), + ), + description="Recursively resolve references inside a JSON structure.", ), - description="Serialise items into a percent-encoded baggage header.", - )) - specs.append(CommandSpec( - "AC_canonical_log", "Report", "Canonical Log: Build Line", - fields=( - FieldSpec("fields", FieldType.STRING, - placeholder='{"event": "run", "ok": true, "ms": 42}'), + CommandSpec( + "AC_validate_config", "Data", "Config Schema: Validate", + fields=( + FieldSpec("schema", FieldType.STRING, + placeholder='{"port": {"type": "int", "required": true}}'), + FieldSpec("config", FieldType.STRING, + placeholder='{"port": "8080"}'), + ), + description="Validate a config mapping against a typed schema spec.", ), - description="Build a canonical wide-event log line (rendered as JSON).", - )) - specs.append(CommandSpec( - "AC_normalize_text", "Data", "Text: Normalize (Unicode)", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="Café Menu"), - FieldSpec("form", FieldType.STRING, optional=True, - placeholder="NFKC"), - FieldSpec("casefold", FieldType.BOOL, optional=True, default=True), - FieldSpec("collapse_ws", FieldType.BOOL, optional=True, - default=True), - ), - description="Unicode-normalise text (form + casefold + ws fold).", - )) - specs.append(CommandSpec( - "AC_slugify", "Data", "Text: Slugify", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="Café Menu!"), - FieldSpec("sep", FieldType.STRING, optional=True, placeholder="-"), + CommandSpec( + "AC_redact_config", "Security", "Redaction: Redact Config", + fields=( + FieldSpec("obj", FieldType.STRING, + placeholder='{"db": {"password": "hunter2longvalue"}}'), + FieldSpec("mask", FieldType.STRING, optional=True, default="***"), + ), + description="Mask secret-looking values in a JSON config structure.", ), - description="Produce an ASCII slug (de-accent, lowercase, join).", - )) - specs.append(CommandSpec( - "AC_text_similarity", "Data", "Text: Similarity", - fields=( - FieldSpec("a", FieldType.STRING, placeholder="login"), - FieldSpec("b", FieldType.STRING, placeholder="lgoin"), - FieldSpec("metric", FieldType.STRING, optional=True, - placeholder="jaro_winkler | levenshtein | jaccard | dice"), + CommandSpec( + "AC_redact_secret_text", "Security", "Redaction: Redact Secret Text", + fields=( + FieldSpec("text", FieldType.STRING, + placeholder="log line with AKIA... or a bearer token"), + FieldSpec("mask", FieldType.STRING, optional=True, default="***"), + ), + description="Mask secret-looking tokens within a free-text string.", ), - description="Normalised string similarity (Jaro-Winkler / edit / Jaccard).", - )) - specs.append(CommandSpec( - "AC_simhash", "Data", "Near-Dup: SimHash", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="some text"), - FieldSpec("bits", FieldType.INT, optional=True, default=64), + CommandSpec( + "AC_profile_rows", "Data", "Data Profile: Profile Rows", + fields=( + FieldSpec("rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "a"}, {"id": 2}]'), + FieldSpec("columns", FieldType.STRING, optional=True, + placeholder='["id", "name"] (omit for all)'), + ), + description="Per-column stats: null fraction, cardinality, type, ranges.", ), - description="SimHash fingerprint (int) of text.", - )) - specs.append(CommandSpec( - "AC_near_duplicates", "Data", "Near-Dup: Cluster Texts", - fields=( - FieldSpec("texts", FieldType.STRING, - placeholder='["the cat sat", "the cat sat down", "dog"]'), - FieldSpec("max_distance", FieldType.INT, optional=True, default=3), + CommandSpec( + "AC_infer_schema", "Data", "Data Profile: Infer Schema", + fields=( + FieldSpec("rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "a"}, {"id": 2}]'), + FieldSpec("columns", FieldType.STRING, optional=True, + placeholder='["id", "name"] (omit for all)'), + ), + description="Infer a validate_rows-compatible schema from observed rows.", ), - description="Cluster near-duplicate texts by SimHash distance.", - )) - specs.append(CommandSpec( - "AC_spans_to_otlp", "Report", "OTLP: Export Spans", - fields=( - FieldSpec("spans", FieldType.STRING, - placeholder='[{"trace_id": "...", "span_id": "...", ' - '"name": "step", "start_unix_nano": 1, ' - '"end_unix_nano": 2}]'), - FieldSpec("resource_attrs", FieldType.STRING, optional=True, - placeholder='{"service.name": "autocontrol"}'), - ), - description="Wrap spans in an OTLP/JSON resourceSpans envelope.", - )) - specs.append(CommandSpec( - "AC_resolve_ref", "Security", "Secret Ref: Resolve", - fields=( - FieldSpec("ref", FieldType.STRING, - placeholder="env://TOKEN | file://./token | secret://api-key"), + CommandSpec( + "AC_parse_problem", "Data", "HTTP Problem (RFC 9457): Parse", + fields=( + FieldSpec("response", FieldType.STRING, + placeholder='{"status": 400, "headers": ' + '{"Content-Type": "application/problem+json"}, ' + '"json": {"title": "Bad Request"}}'), + ), + description="Parse an application/problem+json error response.", ), - description="Resolve an env:// / file:// / secret:// value reference.", - )) - specs.append(CommandSpec( - "AC_resolve_refs", "Security", "Secret Ref: Resolve In Structure", - fields=( - FieldSpec("obj", FieldType.STRING, - placeholder='{"token": "env://TOKEN", "url": "..."}'), + CommandSpec( + "AC_parse_dotenv", "Data", "Dotenv: Parse Text", + fields=( + FieldSpec("text", FieldType.STRING, + placeholder='KEY=value\nexport TOKEN="abc" # comment'), + ), + description="Parse .env text (KEY=VALUE, quotes, escapes) into values.", ), - description="Recursively resolve references inside a JSON structure.", - )) - specs.append(CommandSpec( - "AC_validate_config", "Data", "Config Schema: Validate", - fields=( - FieldSpec("schema", FieldType.STRING, - placeholder='{"port": {"type": "int", "required": true}}'), - FieldSpec("config", FieldType.STRING, - placeholder='{"port": "8080"}'), + CommandSpec( + "AC_load_dotenv", "Data", "Dotenv: Load File", + fields=( + FieldSpec("path", FieldType.STRING, placeholder=".env"), + FieldSpec("override", FieldType.BOOL, optional=True, default=False), + ), + description="Load a .env file into a values dict.", ), - description="Validate a config mapping against a typed schema spec.", - )) - specs.append(CommandSpec( - "AC_redact_config", "Security", "Redaction: Redact Config", - fields=( - FieldSpec("obj", FieldType.STRING, - placeholder='{"db": {"password": "hunter2longvalue"}}'), - FieldSpec("mask", FieldType.STRING, optional=True, default="***"), + CommandSpec( + "AC_parse_sse", "Data", "SSE: Parse Event Stream", + fields=( + FieldSpec("text", FieldType.STRING, + placeholder='event: ping\ndata: {"x": 1}\n\n'), + ), + description="Parse a text/event-stream blob into events.", ), - description="Mask secret-looking values in a JSON config structure.", - )) - specs.append(CommandSpec( - "AC_redact_secret_text", "Security", "Redaction: Redact Secret Text", - fields=( - FieldSpec("text", FieldType.STRING, - placeholder="log line with AKIA... or a bearer token"), - FieldSpec("mask", FieldType.STRING, optional=True, default="***"), + CommandSpec( + "AC_parse_link_header", "Data", "Link Header: Parse", + fields=( + FieldSpec("value", FieldType.STRING, + placeholder='; rel="next"'), + ), + description="Parse an RFC 8288 Link header into links.", ), - description="Mask secret-looking tokens within a free-text string.", - )) - specs.append(CommandSpec( - "AC_profile_rows", "Data", "Data Profile: Profile Rows", - fields=( - FieldSpec("rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "a"}, {"id": 2}]'), - FieldSpec("columns", FieldType.STRING, optional=True, - placeholder='["id", "name"] (omit for all)'), + CommandSpec( + "AC_next_url", "Data", "Link Header: Next URL", + fields=( + FieldSpec("value", FieldType.STRING, + placeholder='; rel="next"'), + ), + description="Return the rel=next URL from a Link header.", ), - description="Per-column stats: null fraction, cardinality, type, ranges.", - )) - specs.append(CommandSpec( - "AC_infer_schema", "Data", "Data Profile: Infer Schema", - fields=( - FieldSpec("rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "a"}, {"id": 2}]'), - FieldSpec("columns", FieldType.STRING, optional=True, - placeholder='["id", "name"] (omit for all)'), + CommandSpec( + "AC_build_multipart", "Data", "Multipart: Build Body", + fields=( + FieldSpec("fields", FieldType.STRING, optional=True, + placeholder='{"name": "report", "tag": "v1"}'), + FieldSpec("files", FieldType.STRING, optional=True, + placeholder='[{"name": "f", "filename": "a.txt", ' + '"content": "hi"}]'), + FieldSpec("boundary", FieldType.STRING, optional=True), + ), + description="Build a multipart/form-data body (base64) for upload.", ), - description="Infer a validate_rows-compatible schema from observed rows.", - )) - specs.append(CommandSpec( - "AC_parse_problem", "Data", "HTTP Problem (RFC 9457): Parse", - fields=( - FieldSpec("response", FieldType.STRING, - placeholder='{"status": 400, "headers": ' - '{"Content-Type": "application/problem+json"}, ' - '"json": {"title": "Bad Request"}}'), + CommandSpec( + "AC_parse_multipart", "Data", "Multipart: Parse Body", + fields=( + FieldSpec("content_type", FieldType.STRING, + placeholder="multipart/form-data; boundary=..."), + FieldSpec("body_base64", FieldType.STRING, + placeholder=""), + ), + description="Parse a base64 multipart body into fields and files.", ), - description="Parse an application/problem+json error response.", - )) - specs.append(CommandSpec( - "AC_parse_dotenv", "Data", "Dotenv: Parse Text", - fields=( - FieldSpec("text", FieldType.STRING, - placeholder='KEY=value\nexport TOKEN="abc" # comment'), + CommandSpec( + "AC_decode_body", "Data", "HTTP Content: Decode Body", + fields=( + FieldSpec("headers", FieldType.STRING, + placeholder='{"Content-Encoding": "gzip"}'), + FieldSpec("body_base64", FieldType.STRING, + placeholder=""), + ), + description="Decode a gzip/deflate response body by Content-Encoding.", ), - description="Parse .env text (KEY=VALUE, quotes, escapes) into values.", - )) - specs.append(CommandSpec( - "AC_load_dotenv", "Data", "Dotenv: Load File", - fields=( - FieldSpec("path", FieldType.STRING, placeholder=".env"), - FieldSpec("override", FieldType.BOOL, optional=True, default=False), + CommandSpec( + "AC_parse_quality_values", "Data", "HTTP Content: Quality Values", + fields=( + FieldSpec("header", FieldType.STRING, + placeholder="text/html;q=0.8, application/json"), + ), + description="Parse an Accept/Accept-Encoding header by q-value.", ), - description="Load a .env file into a values dict.", - )) - specs.append(CommandSpec( - "AC_parse_sse", "Data", "SSE: Parse Event Stream", - fields=( - FieldSpec("text", FieldType.STRING, - placeholder='event: ping\ndata: {"x": 1}\n\n'), + CommandSpec( + "AC_cookie_header", "Data", "Cookie Jar: Build Cookie Header", + fields=( + FieldSpec("set_cookies", FieldType.STRING, + placeholder='sid=abc; Path=/ (or ["a=1", "b=2"])'), + ), + description="Build a Cookie request header from Set-Cookie value(s).", ), - description="Parse a text/event-stream blob into events.", - )) - specs.append(CommandSpec( - "AC_parse_link_header", "Data", "Link Header: Parse", - fields=( - FieldSpec("value", FieldType.STRING, - placeholder='; rel="next"'), + CommandSpec( + "AC_parse_set_cookie", "Data", "Cookie Jar: Parse Set-Cookie", + fields=( + FieldSpec("header", FieldType.STRING, + placeholder="sid=abc; Path=/; Max-Age=3600"), + ), + description="Parse one Set-Cookie header into name/value/attributes.", ), - description="Parse an RFC 8288 Link header into links.", - )) - specs.append(CommandSpec( - "AC_next_url", "Data", "Link Header: Next URL", - fields=( - FieldSpec("value", FieldType.STRING, - placeholder='; rel="next"'), + CommandSpec( + "AC_parse_cache_control", "Data", "HTTP Conditional: Cache-Control", + fields=( + FieldSpec("headers", FieldType.STRING, + placeholder='{"Cache-Control": "max-age=60, public"}'), + ), + description="Parse a Cache-Control header into directives.", ), - description="Return the rel=next URL from a Link header.", - )) - specs.append(CommandSpec( - "AC_build_multipart", "Data", "Multipart: Build Body", - fields=( - FieldSpec("fields", FieldType.STRING, optional=True, - placeholder='{"name": "report", "tag": "v1"}'), - FieldSpec("files", FieldType.STRING, optional=True, - placeholder='[{"name": "f", "filename": "a.txt", ' - '"content": "hi"}]'), - FieldSpec("boundary", FieldType.STRING, optional=True), - ), - description="Build a multipart/form-data body (base64) for upload.", - )) - specs.append(CommandSpec( - "AC_parse_multipart", "Data", "Multipart: Parse Body", - fields=( - FieldSpec("content_type", FieldType.STRING, - placeholder="multipart/form-data; boundary=..."), - FieldSpec("body_base64", FieldType.STRING, - placeholder=""), + CommandSpec( + "AC_store_validators", "Data", "HTTP Conditional: Store Validators", + fields=( + FieldSpec("response", FieldType.STRING, + placeholder='{"headers": {"ETag": "\\"abc\\""}}'), + ), + description="Extract ETag / Last-Modified / Cache-Control validators.", ), - description="Parse a base64 multipart body into fields and files.", - )) - specs.append(CommandSpec( - "AC_decode_body", "Data", "HTTP Content: Decode Body", - fields=( - FieldSpec("headers", FieldType.STRING, - placeholder='{"Content-Encoding": "gzip"}'), - FieldSpec("body_base64", FieldType.STRING, - placeholder=""), + CommandSpec( + "AC_resolve_config", "Data", "Layered Config: Resolve", + fields=( + FieldSpec("layers", FieldType.STRING, + placeholder='[{"name": "defaults", "mapping": {}}, ' + '{"name": "env", "mapping": {}, ' + '"priority": 10}]'), + ), + description="Deep-merge ordered config layers into one config.", ), - description="Decode a gzip/deflate response body by Content-Encoding.", - )) - specs.append(CommandSpec( - "AC_parse_quality_values", "Data", "HTTP Content: Quality Values", - fields=( - FieldSpec("header", FieldType.STRING, - placeholder="text/html;q=0.8, application/json"), + CommandSpec( + "AC_explain_config", "Data", "Layered Config: Explain Key", + fields=( + FieldSpec("layers", FieldType.STRING, + placeholder='[{"name": "defaults", "mapping": {}}]'), + FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), + ), + description="Show the value and winning layer for a dotted config key.", ), - description="Parse an Accept/Accept-Encoding header by q-value.", - )) - specs.append(CommandSpec( - "AC_cookie_header", "Data", "Cookie Jar: Build Cookie Header", - fields=( - FieldSpec("set_cookies", FieldType.STRING, - placeholder='sid=abc; Path=/ (or ["a=1", "b=2"])'), + CommandSpec( + "AC_detect_drift", "Data", "Data Drift: Detect (PSI + KS)", + fields=( + FieldSpec("reference", FieldType.STRING, + placeholder="[1.0, 2.0, 3.0, ...]"), + FieldSpec("current", FieldType.STRING, + placeholder="[1.1, 2.2, 9.9, ...]"), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=0.25), + FieldSpec("bins", FieldType.INT, optional=True, default=10), + ), + description="Numeric drift: Population Stability Index + KS two-sample.", ), - description="Build a Cookie request header from Set-Cookie value(s).", - )) - specs.append(CommandSpec( - "AC_parse_set_cookie", "Data", "Cookie Jar: Parse Set-Cookie", - fields=( - FieldSpec("header", FieldType.STRING, - placeholder="sid=abc; Path=/; Max-Age=3600"), + CommandSpec( + "AC_categorical_drift", "Data", "Data Drift: Categorical", + fields=( + FieldSpec("reference", FieldType.STRING, + placeholder='["a", "b", "a", ...]'), + FieldSpec("current", FieldType.STRING, + placeholder='["a", "c", "c", ...]'), + ), + description="Categorical drift: chi-square + total-variation distance.", ), - description="Parse one Set-Cookie header into name/value/attributes.", - )) - specs.append(CommandSpec( - "AC_parse_cache_control", "Data", "HTTP Conditional: Cache-Control", - fields=( - FieldSpec("headers", FieldType.STRING, - placeholder='{"Cache-Control": "max-age=60, public"}'), + CommandSpec( + "AC_check_compatibility", "Data", "Schema Compat: Check", + fields=( + FieldSpec("old", FieldType.STRING, + placeholder='{"properties": {"id": {"type": "integer"}}}'), + FieldSpec("new", FieldType.STRING, + placeholder='{"properties": {"id": {"type": "integer"}}, ' + '"required": ["id"]}'), + FieldSpec("mode", FieldType.STRING, optional=True, + placeholder="backward | forward | full"), + ), + description="Classify JSON-Schema changes as backward/forward/full.", ), - description="Parse a Cache-Control header into directives.", - )) - specs.append(CommandSpec( - "AC_store_validators", "Data", "HTTP Conditional: Store Validators", - fields=( - FieldSpec("response", FieldType.STRING, - placeholder='{"headers": {"ETag": "\\"abc\\""}}'), + CommandSpec( + "AC_ts_rate", "Data", "Time-Series: Counter Rate", + fields=( + FieldSpec("series", FieldType.STRING, + placeholder="[[0, 0], [10, 50], [20, 120]]"), + FieldSpec("window_s", FieldType.FLOAT, optional=True), + ), + description="Per-second counter rate (reset-aware) over a series.", ), - description="Extract ETag / Last-Modified / Cache-Control validators.", - )) - specs.append(CommandSpec( - "AC_resolve_config", "Data", "Layered Config: Resolve", - fields=( - FieldSpec("layers", FieldType.STRING, - placeholder='[{"name": "defaults", "mapping": {}}, ' - '{"name": "env", "mapping": {}, ' - '"priority": 10}]'), + CommandSpec( + "AC_ts_downsample", "Data", "Time-Series: Downsample", + fields=( + FieldSpec("series", FieldType.STRING, + placeholder="[[0, 1], [5, 3], [12, 9]]"), + FieldSpec("bucket_s", FieldType.FLOAT, placeholder="10"), + FieldSpec("agg", FieldType.STRING, optional=True, + placeholder="avg|sum|min|max|first|last|count"), + ), + description="Roll a series into tumbling buckets by aggregate.", ), - description="Deep-merge ordered config layers into one config.", - )) - specs.append(CommandSpec( - "AC_explain_config", "Data", "Layered Config: Explain Key", - fields=( - FieldSpec("layers", FieldType.STRING, - placeholder='[{"name": "defaults", "mapping": {}}]'), - FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), + CommandSpec( + "AC_detect_anomalies", "Data", "Anomaly: Detect in Series", + fields=( + FieldSpec("values", FieldType.STRING, + placeholder="[10, 11, 9, 10, 95, 10]"), + FieldSpec("method", FieldType.STRING, optional=True, + placeholder="mad | zscore"), + FieldSpec("threshold", FieldType.FLOAT, optional=True), + ), + description="Flag outliers in a numeric series (MAD / z-score).", ), - description="Show the value and winning layer for a dotted config key.", - )) - specs.append(CommandSpec( - "AC_detect_drift", "Data", "Data Drift: Detect (PSI + KS)", - fields=( - FieldSpec("reference", FieldType.STRING, - placeholder="[1.0, 2.0, 3.0, ...]"), - FieldSpec("current", FieldType.STRING, - placeholder="[1.1, 2.2, 9.9, ...]"), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=0.25), - FieldSpec("bins", FieldType.INT, optional=True, default=10), - ), - description="Numeric drift: Population Stability Index + KS two-sample.", - )) - specs.append(CommandSpec( - "AC_categorical_drift", "Data", "Data Drift: Categorical", - fields=( - FieldSpec("reference", FieldType.STRING, - placeholder='["a", "b", "a", ...]'), - FieldSpec("current", FieldType.STRING, - placeholder='["a", "c", "c", ...]'), + CommandSpec( + "AC_sma", "Data", "Smoothing: Simple Moving Average", + fields=( + FieldSpec("values", FieldType.STRING, placeholder="[1, 2, 3, 4, 5]"), + FieldSpec("window", FieldType.INT, placeholder="3"), + ), + description="Trailing simple moving average over a window.", ), - description="Categorical drift: chi-square + total-variation distance.", - )) - specs.append(CommandSpec( - "AC_check_compatibility", "Data", "Schema Compat: Check", - fields=( - FieldSpec("old", FieldType.STRING, - placeholder='{"properties": {"id": {"type": "integer"}}}'), - FieldSpec("new", FieldType.STRING, - placeholder='{"properties": {"id": {"type": "integer"}}, ' - '"required": ["id"]}'), - FieldSpec("mode", FieldType.STRING, optional=True, - placeholder="backward | forward | full"), - ), - description="Classify JSON-Schema changes as backward/forward/full.", - )) - specs.append(CommandSpec( - "AC_ts_rate", "Data", "Time-Series: Counter Rate", - fields=( - FieldSpec("series", FieldType.STRING, - placeholder="[[0, 0], [10, 50], [20, 120]]"), - FieldSpec("window_s", FieldType.FLOAT, optional=True), + CommandSpec( + "AC_ewma", "Data", "Smoothing: EWMA", + fields=( + FieldSpec("values", FieldType.STRING, placeholder="[1, 2, 3, 4, 5]"), + FieldSpec("alpha", FieldType.FLOAT, optional=True, default=0.3), + ), + description="Exponentially-weighted moving average of a series.", ), - description="Per-second counter rate (reset-aware) over a series.", - )) - specs.append(CommandSpec( - "AC_ts_downsample", "Data", "Time-Series: Downsample", - fields=( - FieldSpec("series", FieldType.STRING, - placeholder="[[0, 1], [5, 3], [12, 9]]"), - FieldSpec("bucket_s", FieldType.FLOAT, placeholder="10"), - FieldSpec("agg", FieldType.STRING, optional=True, - placeholder="avg|sum|min|max|first|last|count"), + CommandSpec( + "AC_idempotency_begin", "Flow", "Idempotency: Begin", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="payments"), + FieldSpec("key", FieldType.STRING, placeholder="order-42"), + FieldSpec("request", FieldType.STRING, optional=True, + placeholder='{"amount": 100}'), + ), + description="Register/look up an idempotency key (new/in_progress/done).", ), - description="Roll a series into tumbling buckets by aggregate.", - )) - specs.append(CommandSpec( - "AC_detect_anomalies", "Data", "Anomaly: Detect in Series", - fields=( - FieldSpec("values", FieldType.STRING, - placeholder="[10, 11, 9, 10, 95, 10]"), - FieldSpec("method", FieldType.STRING, optional=True, - placeholder="mad | zscore"), - FieldSpec("threshold", FieldType.FLOAT, optional=True), + CommandSpec( + "AC_idempotency_complete", "Flow", "Idempotency: Complete", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="payments"), + FieldSpec("key", FieldType.STRING, placeholder="order-42"), + FieldSpec("response", FieldType.STRING, placeholder='{"ok": true}'), + ), + description="Store the completed response for an idempotency key.", ), - description="Flag outliers in a numeric series (MAD / z-score).", - )) - specs.append(CommandSpec( - "AC_sma", "Data", "Smoothing: Simple Moving Average", - fields=( - FieldSpec("values", FieldType.STRING, placeholder="[1, 2, 3, 4, 5]"), - FieldSpec("window", FieldType.INT, placeholder="3"), + CommandSpec( + "AC_idempotency_release", "Flow", "Idempotency: Release", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="payments"), + FieldSpec("key", FieldType.STRING, placeholder="order-42"), + ), + description="Drop an in-progress idempotency key whose work failed.", ), - description="Trailing simple moving average over a window.", - )) - specs.append(CommandSpec( - "AC_ewma", "Data", "Smoothing: EWMA", - fields=( - FieldSpec("values", FieldType.STRING, placeholder="[1, 2, 3, 4, 5]"), - FieldSpec("alpha", FieldType.FLOAT, optional=True, default=0.3), + CommandSpec( + "AC_dedup_check", "Flow", "Dedup Window: Check", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="webhooks"), + FieldSpec("message_id", FieldType.STRING, placeholder="evt-123"), + FieldSpec("ttl_s", FieldType.FLOAT, optional=True, default=3600), + ), + description="Check-and-mark a message id; first_seen false on duplicate.", ), - description="Exponentially-weighted moving average of a series.", - )) - specs.append(CommandSpec( - "AC_idempotency_begin", "Flow", "Idempotency: Begin", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="payments"), - FieldSpec("key", FieldType.STRING, placeholder="order-42"), - FieldSpec("request", FieldType.STRING, optional=True, - placeholder='{"amount": 100}'), + CommandSpec( + "AC_sequence_observe", "Flow", "Sequence: Observe", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="ingest"), + FieldSpec("stream_id", FieldType.STRING, placeholder="orders"), + FieldSpec("seq", FieldType.INT, placeholder="14"), + ), + description="Classify a sequence number (ok/duplicate/gap/reorder).", ), - description="Register/look up an idempotency key (new/in_progress/done).", - )) - specs.append(CommandSpec( - "AC_idempotency_complete", "Flow", "Idempotency: Complete", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="payments"), - FieldSpec("key", FieldType.STRING, placeholder="order-42"), - FieldSpec("response", FieldType.STRING, placeholder='{"ok": true}'), + CommandSpec( + "AC_cas_put", "Flow", "Optimistic: Put (CAS)", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="config"), + FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), + FieldSpec("value", FieldType.STRING, placeholder='"prod-1"'), + FieldSpec("expected_version", FieldType.INT, optional=True), + ), + description="Put only if expected_version matches (returns new version).", ), - description="Store the completed response for an idempotency key.", - )) - specs.append(CommandSpec( - "AC_idempotency_release", "Flow", "Idempotency: Release", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="payments"), - FieldSpec("key", FieldType.STRING, placeholder="order-42"), + CommandSpec( + "AC_cas_get", "Flow", "Optimistic: Get", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="config"), + FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), + ), + description="Read a versioned record {value, version}.", ), - description="Drop an in-progress idempotency key whose work failed.", - )) - specs.append(CommandSpec( - "AC_dedup_check", "Flow", "Dedup Window: Check", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="webhooks"), - FieldSpec("message_id", FieldType.STRING, placeholder="evt-123"), - FieldSpec("ttl_s", FieldType.FLOAT, optional=True, default=3600), + CommandSpec( + "AC_outbox_enqueue", "Flow", "Outbox: Enqueue", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="orders"), + FieldSpec("event", FieldType.STRING, + placeholder='{"type": "order.created", "id": 7}'), + ), + description="Durably buffer an event for at-least-once delivery.", ), - description="Check-and-mark a message id; first_seen false on duplicate.", - )) - specs.append(CommandSpec( - "AC_sequence_observe", "Flow", "Sequence: Observe", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="ingest"), - FieldSpec("stream_id", FieldType.STRING, placeholder="orders"), - FieldSpec("seq", FieldType.INT, placeholder="14"), + CommandSpec( + "AC_outbox_pending", "Flow", "Outbox: Pending", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="orders"), + ), + description="List events still awaiting successful delivery.", ), - description="Classify a sequence number (ok/duplicate/gap/reorder).", - )) - specs.append(CommandSpec( - "AC_cas_put", "Flow", "Optimistic: Put (CAS)", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="config"), - FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), - FieldSpec("value", FieldType.STRING, placeholder='"prod-1"'), - FieldSpec("expected_version", FieldType.INT, optional=True), + CommandSpec( + "AC_collation_sort", "Data", "Text: Collation Sort", + fields=( + FieldSpec("items", FieldType.STRING, + placeholder='["zebra", "apple", "Äpple"]'), + FieldSpec("strength", FieldType.STRING, optional=True, + placeholder="tertiary"), + FieldSpec("tailoring", FieldType.STRING, optional=True, + placeholder="abc...xyzåäö"), + FieldSpec("reverse", FieldType.BOOL, optional=True), + ), + description="Locale-aware sort (base letter, then accent, then case).", ), - description="Put only if expected_version matches (returns new version).", - )) - specs.append(CommandSpec( - "AC_cas_get", "Flow", "Optimistic: Get", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="config"), - FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), + CommandSpec( + "AC_collation_compare", "Data", "Text: Collation Compare", + fields=( + FieldSpec("first", FieldType.STRING, placeholder="apple"), + FieldSpec("second", FieldType.STRING, placeholder="Äpple"), + FieldSpec("strength", FieldType.STRING, optional=True, + placeholder="tertiary"), + FieldSpec("tailoring", FieldType.STRING, optional=True), + ), + description="Locale-aware compare; returns order -1/0/1.", ), - description="Read a versioned record {value, version}.", - )) - specs.append(CommandSpec( - "AC_outbox_enqueue", "Flow", "Outbox: Enqueue", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="orders"), - FieldSpec("event", FieldType.STRING, - placeholder='{"type": "order.created", "id": 7}'), + CommandSpec( + "AC_confusable_scan", "Data", "Text: Confusable Scan", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="pаypal.com"), + ), + description="Homoglyph / mixed-script spoofing report for a string.", ), - description="Durably buffer an event for at-least-once delivery.", - )) - specs.append(CommandSpec( - "AC_outbox_pending", "Flow", "Outbox: Pending", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="orders"), + CommandSpec( + "AC_confusable_compare", "Data", "Text: Confusable Compare", + fields=( + FieldSpec("first", FieldType.STRING, placeholder="paypal"), + FieldSpec("second", FieldType.STRING, placeholder="pаypal"), + ), + description="Whether two strings share the same confusable skeleton.", ), - description="List events still awaiting successful delivery.", - )) - specs.append(CommandSpec( - "AC_collation_sort", "Data", "Text: Collation Sort", - fields=( - FieldSpec("items", FieldType.STRING, - placeholder='["zebra", "apple", "Äpple"]'), - FieldSpec("strength", FieldType.STRING, optional=True, - placeholder="tertiary"), - FieldSpec("tailoring", FieldType.STRING, optional=True, - placeholder="abc...xyzåäö"), - FieldSpec("reverse", FieldType.BOOL, optional=True), - ), - description="Locale-aware sort (base letter, then accent, then case).", - )) - specs.append(CommandSpec( - "AC_collation_compare", "Data", "Text: Collation Compare", - fields=( - FieldSpec("first", FieldType.STRING, placeholder="apple"), - FieldSpec("second", FieldType.STRING, placeholder="Äpple"), - FieldSpec("strength", FieldType.STRING, optional=True, - placeholder="tertiary"), - FieldSpec("tailoring", FieldType.STRING, optional=True), + CommandSpec( + "AC_readability_report", "Data", "Text: Readability Report", + fields=( + FieldSpec("text", FieldType.STRING, + placeholder="The cat sat on the mat."), + ), + description="Flesch / Flesch-Kincaid / Fog / SMOG / ARI scores + counts.", ), - description="Locale-aware compare; returns order -1/0/1.", - )) - specs.append(CommandSpec( - "AC_confusable_scan", "Data", "Text: Confusable Scan", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="pаypal.com"), + CommandSpec( + "AC_bidi_check", "Data", "Text: Bidi / Trojan-Source Check", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="value = admin"), + ), + description="Bidi controls, nesting balance, base dir, Trojan-source flag.", ), - description="Homoglyph / mixed-script spoofing report for a string.", - )) - specs.append(CommandSpec( - "AC_confusable_compare", "Data", "Text: Confusable Compare", - fields=( - FieldSpec("first", FieldType.STRING, placeholder="paypal"), - FieldSpec("second", FieldType.STRING, placeholder="pаypal"), + CommandSpec( + "AC_bidi_strip", "Data", "Text: Strip Bidi Controls", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="value = admin"), + ), + description="Remove all bidirectional control characters from a string.", ), - description="Whether two strings share the same confusable skeleton.", - )) - specs.append(CommandSpec( - "AC_readability_report", "Data", "Text: Readability Report", - fields=( - FieldSpec("text", FieldType.STRING, - placeholder="The cat sat on the mat."), + CommandSpec( + "AC_format_list", "Data", "Text: Format List", + fields=( + FieldSpec("items", FieldType.STRING, + placeholder='["apple", "pear", "grape"]'), + FieldSpec("style", FieldType.STRING, optional=True, + placeholder="and | or | unit"), + FieldSpec("locale", FieldType.STRING, optional=True, + placeholder="en | es | fr | de | pt"), + ), + description="Join items into a localised list ('A, B, and C').", ), - description="Flesch / Flesch-Kincaid / Fog / SMOG / ARI scores + counts.", - )) - specs.append(CommandSpec( - "AC_bidi_check", "Data", "Text: Bidi / Trojan-Source Check", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="value = admin"), + CommandSpec( + "AC_format_message", "Data", "Text: Format Message (ICU)", + fields=( + FieldSpec("pattern", FieldType.STRING, + placeholder="{count, plural, one {# item} other {# items}}"), + FieldSpec("args", FieldType.STRING, placeholder='{"count": 3}'), + FieldSpec("locale", FieldType.STRING, optional=True, + placeholder="en | fr"), + ), + description="Render ICU plural/select/selectordinal message.", ), - description="Bidi controls, nesting balance, base dir, Trojan-source flag.", - )) - specs.append(CommandSpec( - "AC_bidi_strip", "Data", "Text: Strip Bidi Controls", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="value = admin"), + CommandSpec( + "AC_gettext_translate", "Data", "Text: gettext Translate (.po)", + fields=( + FieldSpec("po", FieldType.STRING, + placeholder='msgid "Hello"\\nmsgstr "Hola"'), + FieldSpec("msgid", FieldType.STRING, placeholder="Hello"), + FieldSpec("context", FieldType.STRING, optional=True), + ), + description="Look up a singular translation in a gettext .po catalog.", ), - description="Remove all bidirectional control characters from a string.", - )) - specs.append(CommandSpec( - "AC_format_list", "Data", "Text: Format List", - fields=( - FieldSpec("items", FieldType.STRING, - placeholder='["apple", "pear", "grape"]'), - FieldSpec("style", FieldType.STRING, optional=True, - placeholder="and | or | unit"), - FieldSpec("locale", FieldType.STRING, optional=True, - placeholder="en | es | fr | de | pt"), - ), - description="Join items into a localised list ('A, B, and C').", - )) - specs.append(CommandSpec( - "AC_format_message", "Data", "Text: Format Message (ICU)", - fields=( - FieldSpec("pattern", FieldType.STRING, - placeholder="{count, plural, one {# item} other {# items}}"), - FieldSpec("args", FieldType.STRING, placeholder='{"count": 3}'), - FieldSpec("locale", FieldType.STRING, optional=True, - placeholder="en | fr"), + CommandSpec( + "AC_gettext_ngettext", "Data", "Text: gettext Plural (.po)", + fields=( + FieldSpec("po", FieldType.STRING, placeholder="(.po source)"), + FieldSpec("msgid", FieldType.STRING, placeholder="file"), + FieldSpec("msgid_plural", FieldType.STRING, placeholder="files"), + FieldSpec("n", FieldType.INT, placeholder="3"), + ), + description="Pick the plural-correct translation for count n.", ), - description="Render ICU plural/select/selectordinal message.", - )) - specs.append(CommandSpec( - "AC_gettext_translate", "Data", "Text: gettext Translate (.po)", - fields=( - FieldSpec("po", FieldType.STRING, - placeholder='msgid "Hello"\\nmsgstr "Hola"'), - FieldSpec("msgid", FieldType.STRING, placeholder="Hello"), - FieldSpec("context", FieldType.STRING, optional=True), + CommandSpec( + "AC_checksum_validate", "Data", "Checksum: Validate", + fields=( + FieldSpec("scheme", FieldType.STRING, + placeholder="luhn | verhoeff | damm | mod97"), + FieldSpec("number", FieldType.STRING, placeholder="4111111111111111"), + ), + description="Validate a number's check digit (Luhn/Verhoeff/Damm/mod97).", ), - description="Look up a singular translation in a gettext .po catalog.", - )) - specs.append(CommandSpec( - "AC_gettext_ngettext", "Data", "Text: gettext Plural (.po)", - fields=( - FieldSpec("po", FieldType.STRING, placeholder="(.po source)"), - FieldSpec("msgid", FieldType.STRING, placeholder="file"), - FieldSpec("msgid_plural", FieldType.STRING, placeholder="files"), - FieldSpec("n", FieldType.INT, placeholder="3"), + CommandSpec( + "AC_checksum_digit", "Data", "Checksum: Check Digit", + fields=( + FieldSpec("scheme", FieldType.STRING, + placeholder="luhn | verhoeff | damm | mod97"), + FieldSpec("partial", FieldType.STRING, placeholder="799273987"), + ), + description="Compute the check digit(s) to append to a value.", ), - description="Pick the plural-correct translation for count n.", - )) - specs.append(CommandSpec( - "AC_checksum_validate", "Data", "Checksum: Validate", - fields=( - FieldSpec("scheme", FieldType.STRING, - placeholder="luhn | verhoeff | damm | mod97"), - FieldSpec("number", FieldType.STRING, placeholder="4111111111111111"), + CommandSpec( + "AC_diff_rows", "Data", "Dataset Diff: Rows by Key", + fields=( + FieldSpec("old_rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "a"}]'), + FieldSpec("new_rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "b"}]'), + FieldSpec("key", FieldType.STRING, + placeholder='id (or ["id", "region"])'), + ), + description="Diff two row-sets by key: added/removed/changed/unchanged.", ), - description="Validate a number's check digit (Luhn/Verhoeff/Damm/mod97).", - )) - specs.append(CommandSpec( - "AC_checksum_digit", "Data", "Checksum: Check Digit", - fields=( - FieldSpec("scheme", FieldType.STRING, - placeholder="luhn | verhoeff | damm | mod97"), - FieldSpec("partial", FieldType.STRING, placeholder="799273987"), + CommandSpec( + "AC_cell_changes", "Data", "Dataset Diff: Cell Changes", + fields=( + FieldSpec("old_rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "a"}]'), + FieldSpec("new_rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "b"}]'), + FieldSpec("key", FieldType.STRING, placeholder="id"), + ), + description="Per-cell {key, column, old, new} changes between row-sets.", ), - description="Compute the check digit(s) to append to a value.", - )) - specs.append(CommandSpec( - "AC_diff_rows", "Data", "Dataset Diff: Rows by Key", - fields=( - FieldSpec("old_rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "a"}]'), - FieldSpec("new_rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "b"}]'), - FieldSpec("key", FieldType.STRING, - placeholder='id (or ["id", "region"])'), - ), - description="Diff two row-sets by key: added/removed/changed/unchanged.", - )) - specs.append(CommandSpec( - "AC_cell_changes", "Data", "Dataset Diff: Cell Changes", - fields=( - FieldSpec("old_rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "a"}]'), - FieldSpec("new_rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "b"}]'), - FieldSpec("key", FieldType.STRING, placeholder="id"), + CommandSpec( + "AC_check_foreign_key", "Data", "Referential: Foreign Key", + fields=( + FieldSpec("child_rows", FieldType.STRING, + placeholder='[{"user_id": 1}]'), + FieldSpec("child_col", FieldType.STRING, placeholder="user_id"), + FieldSpec("parent_rows", FieldType.STRING, + placeholder='[{"id": 1}]'), + FieldSpec("parent_col", FieldType.STRING, placeholder="id"), + ), + description="Every child value must exist in the parent column.", ), - description="Per-cell {key, column, old, new} changes between row-sets.", - )) - specs.append(CommandSpec( - "AC_check_foreign_key", "Data", "Referential: Foreign Key", - fields=( - FieldSpec("child_rows", FieldType.STRING, - placeholder='[{"user_id": 1}]'), - FieldSpec("child_col", FieldType.STRING, placeholder="user_id"), - FieldSpec("parent_rows", FieldType.STRING, - placeholder='[{"id": 1}]'), - FieldSpec("parent_col", FieldType.STRING, placeholder="id"), - ), - description="Every child value must exist in the parent column.", - )) - specs.append(CommandSpec( - "AC_check_unique_key", "Data", "Referential: Unique Key", - fields=( - FieldSpec("rows", FieldType.STRING, - placeholder='[{"id": 1}, {"id": 1}]'), - FieldSpec("cols", FieldType.STRING, - placeholder='id (or ["region", "id"])'), + CommandSpec( + "AC_check_unique_key", "Data", "Referential: Unique Key", + fields=( + FieldSpec("rows", FieldType.STRING, + placeholder='[{"id": 1}, {"id": 1}]'), + FieldSpec("cols", FieldType.STRING, + placeholder='id (or ["region", "id"])'), + ), + description="A single or composite key must be unique across rows.", ), - description="A single or composite key must be unique across rows.", - )) - specs.append(CommandSpec( - "AC_check_accepted_values", "Data", "Referential: Accepted Values", - fields=( - FieldSpec("rows", FieldType.STRING, - placeholder='[{"status": "open"}]'), - FieldSpec("col", FieldType.STRING, placeholder="status"), - FieldSpec("allowed", FieldType.STRING, - placeholder='["open", "closed"]'), + CommandSpec( + "AC_check_accepted_values", "Data", "Referential: Accepted Values", + fields=( + FieldSpec("rows", FieldType.STRING, + placeholder='[{"status": "open"}]'), + FieldSpec("col", FieldType.STRING, placeholder="status"), + FieldSpec("allowed", FieldType.STRING, + placeholder='["open", "closed"]'), + ), + description="Every non-null column value must be in the allowed set.", ), - description="Every non-null column value must be in the allowed set.", - )) - specs.append(CommandSpec( - "AC_check_row_count", "Data", "Referential: Row Count", - fields=( - FieldSpec("rows", FieldType.STRING, placeholder='[{"id": 1}]'), - FieldSpec("minimum", FieldType.INT, optional=True), - FieldSpec("maximum", FieldType.INT, optional=True), + CommandSpec( + "AC_check_row_count", "Data", "Referential: Row Count", + fields=( + FieldSpec("rows", FieldType.STRING, placeholder='[{"id": 1}]'), + FieldSpec("minimum", FieldType.INT, optional=True), + FieldSpec("maximum", FieldType.INT, optional=True), + ), + description="The row count must fall within the given bounds.", ), - description="The row count must fall within the given bounds.", - )) - specs.append(CommandSpec( - "AC_rate_limit", "Flow", "Rate Limit (Token Bucket)", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("rate", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("capacity", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("n", FieldType.FLOAT, optional=True, default=1.0), + CommandSpec( + "AC_rate_limit", "Flow", "Rate Limit (Token Bucket)", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("rate", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("capacity", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("n", FieldType.FLOAT, optional=True, default=1.0), + ), + description="Try to take 'n' tokens from a named limiter; {acquired, wait}.", ), - description="Try to take 'n' tokens from a named limiter; {acquired, wait}.", - )) + ]) def _add_input_macro_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_replay_timeline", "Flow", "Replay Timed Events", - fields=(FieldSpec("speed", FieldType.FLOAT, optional=True, - default=1.0),), - description="Replay 'events' (JSON view) honoring delta_ms, scaled by " - "speed.", - )) - specs.append(CommandSpec( - "AC_input_sequence", "Flow", "Run Input Sequence (DSL)", - description="Run 'steps' (JSON view): press/hold/release/repeat/wait.", - )) + specs.extend([ + CommandSpec( + "AC_replay_timeline", "Flow", "Replay Timed Events", + fields=(FieldSpec("speed", FieldType.FLOAT, optional=True, + default=1.0),), + description="Replay 'events' (JSON view) honoring delta_ms, scaled by " + "speed.", + ), + CommandSpec( + "AC_input_sequence", "Flow", "Run Input Sequence (DSL)", + description="Run 'steps' (JSON view): press/hold/release/repeat/wait.", + ), + ]) def _add_screen_state_specs(specs: List[CommandSpec]) -> None: app = FieldSpec("app_name", FieldType.STRING, optional=True) - specs.append(CommandSpec( - "AC_screen_snapshot", _NATIVE_UI, "Screen: Snapshot Baseline", - fields=(app,), - description="Snapshot the a11y tree as a semantic-diff baseline.", - )) - specs.append(CommandSpec( - "AC_screen_diff", _NATIVE_UI, "Screen: Diff Snapshots", - description="Semantic diff of 'before'/'after' snapshots (JSON view).", - )) - specs.append(CommandSpec( - "AC_screen_changed", _NATIVE_UI, "Screen: What Changed", - fields=(app,), - description="Diff the live screen against the last snapshot baseline.", - )) - specs.append(CommandSpec( - "AC_describe_screen", _NATIVE_UI, "Screen: Describe", - fields=(app,), - description="Structured 'where am I' (role counts + control labels).", - )) + specs.extend([ + CommandSpec( + "AC_screen_snapshot", _NATIVE_UI, "Screen: Snapshot Baseline", + fields=(app,), + description="Snapshot the a11y tree as a semantic-diff baseline.", + ), + CommandSpec( + "AC_screen_diff", _NATIVE_UI, "Screen: Diff Snapshots", + description="Semantic diff of 'before'/'after' snapshots (JSON view).", + ), + CommandSpec( + "AC_screen_changed", _NATIVE_UI, "Screen: What Changed", + fields=(app,), + description="Diff the live screen against the last snapshot baseline.", + ), + CommandSpec( + "AC_describe_screen", _NATIVE_UI, "Screen: Describe", + fields=(app,), + description="Structured 'where am I' (role counts + control labels).", + ), + ]) def _add_set_of_marks_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_cua_command", _NATIVE_UI, "Computer-Use: Map Action", - fields=( - FieldSpec("payload", FieldType.STRING, - placeholder='{"action":"left_click","coordinate":[x,y]}'), - FieldSpec("source", FieldType.ENUM, optional=True, default="canonical", - choices=("canonical", "anthropic", "openai")), + specs.extend([ + CommandSpec( + "AC_cua_command", _NATIVE_UI, "Computer-Use: Map Action", + fields=( + FieldSpec("payload", FieldType.STRING, + placeholder='{"action":"left_click","coordinate":[x,y]}'), + FieldSpec("source", FieldType.ENUM, optional=True, default="canonical", + choices=("canonical", "anthropic", "openai")), + ), + description="Map an Anthropic / OpenAI computer-use action to an AC command.", ), - description="Map an Anthropic / OpenAI computer-use action to an AC command.", - )) - specs.append(CommandSpec( - "AC_serialize_observation", _NATIVE_UI, "Observation: Serialize Elements", - fields=( - FieldSpec("elements", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("viewport", FieldType.STRING, optional=True, - placeholder=_RECT4_PLACEHOLDER), - FieldSpec("max_elements", FieldType.INT, optional=True, default=80), + CommandSpec( + "AC_serialize_observation", _NATIVE_UI, "Observation: Serialize Elements", + fields=( + FieldSpec("elements", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("viewport", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + FieldSpec("max_elements", FieldType.INT, optional=True, default=80), + ), + description="Indexed text observation of UI elements for a VLM (act by index).", ), - description="Indexed text observation of UI elements for a VLM (act by index).", - )) - specs.append(CommandSpec( - "AC_observation_index", _NATIVE_UI, "Observation: Index Elements", - fields=( - FieldSpec("elements", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("viewport", FieldType.STRING, optional=True, - placeholder=_RECT4_PLACEHOLDER), - FieldSpec("max_elements", FieldType.INT, optional=True, default=80), + CommandSpec( + "AC_observation_index", _NATIVE_UI, "Observation: Index Elements", + fields=( + FieldSpec("elements", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("viewport", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + FieldSpec("max_elements", FieldType.INT, optional=True, default=80), + ), + description="Reading-ordered, viewport-clipped, indexed element list.", ), - description="Reading-ordered, viewport-clipped, indexed element list.", - )) - specs.append(CommandSpec( - "AC_delta_observation", _NATIVE_UI, "Observation: Delta (what changed)", - fields=( - FieldSpec("prev", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("curr", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("viewport", FieldType.STRING, optional=True, - placeholder=_RECT4_PLACEHOLDER), - FieldSpec("max_elements", FieldType.INT, optional=True, default=80), - FieldSpec("max_lines", FieldType.INT, optional=True, default=40), - ), - description="Token-budgeted '+/~/-' summary of what changed between frames.", - )) - specs.append(CommandSpec( - "AC_classify_effect", _NATIVE_UI, "Classify Action Effect", - fields=( - FieldSpec("before", FieldType.STRING, - placeholder='[{"role":"button","name":"OK","x":0,"y":0}]'), - FieldSpec("after", FieldType.STRING, - placeholder='[{"role":"button","name":"OK","x":0,"y":0}]'), - FieldSpec("action", FieldType.STRING, - placeholder='{"type":"click","x":50,"y":50}'), - FieldSpec("radius", FieldType.INT, optional=True, default=64), - ), - description="Did the action change the screen near its target? (no_op/…).", - )) - specs.append(CommandSpec( - "AC_effect_near_point", _NATIVE_UI, "Effect Near Point?", - fields=( - FieldSpec("before", FieldType.STRING, - placeholder=_BOXES_JSON_PLACEHOLDER), - FieldSpec("after", FieldType.STRING, - placeholder=_BOXES_JSON_PLACEHOLDER), - FieldSpec("point", FieldType.STRING, placeholder="[50, 50]"), - FieldSpec("radius", FieldType.INT, optional=True, default=64), - ), - description="Did any before/after change land within radius of a point?", - )) - specs.append(CommandSpec( - "AC_check_postcondition", _NATIVE_UI, "Check Postcondition", - fields=( - FieldSpec("after", FieldType.STRING, - placeholder='[{"role":"dialog","name":"Saved"}]'), - FieldSpec("spec", FieldType.STRING, - placeholder='{"appears":{"role":"dialog"},' - '"disabled":{"name":"Submit"}}'), - FieldSpec("before", FieldType.STRING, optional=True, - placeholder='[{"role":"button","name":"Submit"}]'), - ), - description="Check expected outcome clauses against after/before frames.", - )) - specs.append(CommandSpec( - "AC_plan_repair", _NATIVE_UI, "Plan Repair Tactics", - fields=( - FieldSpec("verdict", FieldType.STRING, - placeholder="no_op / changed_elsewhere / changed"), - FieldSpec("max_attempts", FieldType.INT, optional=True, default=3), + CommandSpec( + "AC_delta_observation", _NATIVE_UI, "Observation: Delta (what changed)", + fields=( + FieldSpec("prev", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("curr", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("viewport", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + FieldSpec("max_elements", FieldType.INT, optional=True, default=80), + FieldSpec("max_lines", FieldType.INT, optional=True, default=40), + ), + description="Token-budgeted '+/~/-' summary of what changed between frames.", ), - description="Ordered repair tactics for a failed/no-effect action verdict.", - )) - specs.append(CommandSpec( - "AC_consensus_point", _NATIVE_UI, "Grounding Consensus Point", - fields=( - FieldSpec("candidates", FieldType.STRING, - placeholder="[[100, 100], [104, 98], [97, 103]]"), - FieldSpec("cluster_radius", FieldType.INT, optional=True, default=24), + CommandSpec( + "AC_classify_effect", _NATIVE_UI, "Classify Action Effect", + fields=( + FieldSpec("before", FieldType.STRING, + placeholder='[{"role":"button","name":"OK","x":0,"y":0}]'), + FieldSpec("after", FieldType.STRING, + placeholder='[{"role":"button","name":"OK","x":0,"y":0}]'), + FieldSpec("action", FieldType.STRING, + placeholder='{"type":"click","x":50,"y":50}'), + FieldSpec("radius", FieldType.INT, optional=True, default=64), + ), + description="Did the action change the screen near its target? (no_op/…).", ), - description="Agreed target point from clustered grounding proposals.", - )) - specs.append(CommandSpec( - "AC_settle_point", "Flow", "Settle Point (churn series)", - fields=( - FieldSpec("churns", FieldType.STRING, - placeholder="[5, 4, 0.5, 0.3, 0.2]"), - FieldSpec("quiet_samples", FieldType.INT, optional=True, default=3), - FieldSpec("max_churn", FieldType.FLOAT, optional=True, default=1.0), + CommandSpec( + "AC_effect_near_point", _NATIVE_UI, "Effect Near Point?", + fields=( + FieldSpec("before", FieldType.STRING, + placeholder=_BOXES_JSON_PLACEHOLDER), + FieldSpec("after", FieldType.STRING, + placeholder=_BOXES_JSON_PLACEHOLDER), + FieldSpec("point", FieldType.STRING, placeholder="[50, 50]"), + FieldSpec("radius", FieldType.INT, optional=True, default=64), + ), + description="Did any before/after change land within radius of a point?", + ), + CommandSpec( + "AC_check_postcondition", _NATIVE_UI, "Check Postcondition", + fields=( + FieldSpec("after", FieldType.STRING, + placeholder='[{"role":"dialog","name":"Saved"}]'), + FieldSpec("spec", FieldType.STRING, + placeholder='{"appears":{"role":"dialog"},' + '"disabled":{"name":"Submit"}}'), + FieldSpec("before", FieldType.STRING, optional=True, + placeholder='[{"role":"button","name":"Submit"}]'), + ), + description="Check expected outcome clauses against after/before frames.", + ), + CommandSpec( + "AC_plan_repair", _NATIVE_UI, "Plan Repair Tactics", + fields=( + FieldSpec("verdict", FieldType.STRING, + placeholder="no_op / changed_elsewhere / changed"), + FieldSpec("max_attempts", FieldType.INT, optional=True, default=3), + ), + description="Ordered repair tactics for a failed/no-effect action verdict.", + ), + CommandSpec( + "AC_consensus_point", _NATIVE_UI, "Grounding Consensus Point", + fields=( + FieldSpec("candidates", FieldType.STRING, + placeholder="[[100, 100], [104, 98], [97, 103]]"), + FieldSpec("cluster_radius", FieldType.INT, optional=True, default=24), + ), + description="Agreed target point from clustered grounding proposals.", + ), + CommandSpec( + "AC_settle_point", "Flow", "Settle Point (churn series)", + fields=( + FieldSpec("churns", FieldType.STRING, + placeholder="[5, 4, 0.5, 0.3, 0.2]"), + FieldSpec("quiet_samples", FieldType.INT, optional=True, default=3), + FieldSpec("max_churn", FieldType.FLOAT, optional=True, default=1.0), + ), + description="Index where a churn series first settles (offline settle check).", + ), + CommandSpec( + "AC_build_critic_record", _NATIVE_UI, "Build Critic Record", + fields=( + FieldSpec("action", FieldType.STRING, + placeholder='{"type":"click","x":50,"y":50}'), + FieldSpec("before", FieldType.STRING, + placeholder=_BOXES_JSON_PLACEHOLDER), + FieldSpec("after", FieldType.STRING, + placeholder='[{"role":"dialog","x":40,"y":40}]'), + FieldSpec("postcondition", FieldType.STRING, optional=True, + placeholder='{"appears":{"role":"dialog"}}'), + FieldSpec("radius", FieldType.INT, optional=True, default=64), + ), + description="Per-step critic evidence (effect + delta + postcondition).", + ), + CommandSpec( + "AC_score_step", _NATIVE_UI, "Score Step (rule-based)", + fields=( + FieldSpec("record", FieldType.STRING, + placeholder='{"effect":{"effect":"changed_near_target"}}'), + ), + description="Rule-based outcome + process score of a critic record.", + ), + CommandSpec( + "AC_consensus_element", _NATIVE_UI, "Grounding Consensus Element", + fields=( + FieldSpec("candidates", FieldType.STRING, + placeholder="[[8, 8], [12, 10]]"), + FieldSpec("elements", FieldType.STRING, + placeholder=_BOXES_JSON_PLACEHOLDER), + ), + description="Vote grounding proposals to the nearest element.", + ), + CommandSpec( + "AC_validate_action", _NATIVE_UI, "Validate / Snap Action", + fields=( + FieldSpec("action", FieldType.STRING, + placeholder='{"type":"click","x":..,"y":..}'), + FieldSpec("screen", FieldType.STRING, optional=True, + placeholder=_SIZE_PLACEHOLDER), + FieldSpec("targets", FieldType.STRING, optional=True, + placeholder=_POINTS_JSON_PLACEHOLDER), + ), + description="Reject out-of-bounds clicks; snap a near-miss to the nearest " + "element.", + ), + CommandSpec( + "AC_match_elements", _NATIVE_UI, "Match Elements (frames)", + fields=( + FieldSpec("before", FieldType.STRING, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("after", FieldType.STRING, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.5, + min_value=0.0, max_value=1.0), + ), + description="Match element boxes across two frames by overlap (move/rename).", + ), + CommandSpec( + "AC_assign_stable_ids", _NATIVE_UI, "Assign Stable Element IDs", + fields=( + FieldSpec("elements", FieldType.STRING, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("prior", FieldType.STRING, optional=True, + placeholder="prior frame's elements (with ids)"), + FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.5, + min_value=0.0, max_value=1.0), + ), + description="Tag elements with IDs carried across frames by overlap.", ), - description="Index where a churn series first settles (offline settle check).", - )) - specs.append(CommandSpec( - "AC_build_critic_record", _NATIVE_UI, "Build Critic Record", - fields=( - FieldSpec("action", FieldType.STRING, - placeholder='{"type":"click","x":50,"y":50}'), - FieldSpec("before", FieldType.STRING, - placeholder=_BOXES_JSON_PLACEHOLDER), - FieldSpec("after", FieldType.STRING, - placeholder='[{"role":"dialog","x":40,"y":40}]'), - FieldSpec("postcondition", FieldType.STRING, optional=True, - placeholder='{"appears":{"role":"dialog"}}'), - FieldSpec("radius", FieldType.INT, optional=True, default=64), - ), - description="Per-step critic evidence (effect + delta + postcondition).", - )) - specs.append(CommandSpec( - "AC_score_step", _NATIVE_UI, "Score Step (rule-based)", - fields=( - FieldSpec("record", FieldType.STRING, - placeholder='{"effect":{"effect":"changed_near_target"}}'), + CommandSpec( + "AC_score_candidates", _NATIVE_UI, "Score Candidates", + fields=( + FieldSpec("candidates", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("want_role", FieldType.STRING, optional=True), + FieldSpec("want_name", FieldType.STRING, optional=True), + FieldSpec("anchor", FieldType.STRING, optional=True, + placeholder="[x, y]"), + ), + description="Rank candidate elements by role / name / proximity confidence.", ), - description="Rule-based outcome + process score of a critic record.", - )) - specs.append(CommandSpec( - "AC_consensus_element", _NATIVE_UI, "Grounding Consensus Element", - fields=( - FieldSpec("candidates", FieldType.STRING, - placeholder="[[8, 8], [12, 10]]"), - FieldSpec("elements", FieldType.STRING, - placeholder=_BOXES_JSON_PLACEHOLDER), + CommandSpec( + "AC_best_candidate", _NATIVE_UI, "Best Candidate", + fields=( + FieldSpec("candidates", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("want_role", FieldType.STRING, optional=True), + FieldSpec("want_name", FieldType.STRING, optional=True), + FieldSpec("anchor", FieldType.STRING, optional=True, + placeholder="[x, y]"), + ), + description="The single highest-scoring candidate element.", ), - description="Vote grounding proposals to the nearest element.", - )) - specs.append(CommandSpec( - "AC_validate_action", _NATIVE_UI, "Validate / Snap Action", - fields=( - FieldSpec("action", FieldType.STRING, - placeholder='{"type":"click","x":..,"y":..}'), - FieldSpec("screen", FieldType.STRING, optional=True, - placeholder="[width, height]"), - FieldSpec("targets", FieldType.STRING, optional=True, - placeholder=_POINTS_JSON_PLACEHOLDER), - ), - description="Reject out-of-bounds clicks; snap a near-miss to the nearest " - "element.", - )) - specs.append(CommandSpec( - "AC_match_elements", _NATIVE_UI, "Match Elements (frames)", - fields=( - FieldSpec("before", FieldType.STRING, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("after", FieldType.STRING, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.5, - min_value=0.0, max_value=1.0), - ), - description="Match element boxes across two frames by overlap (move/rename).", - )) - specs.append(CommandSpec( - "AC_assign_stable_ids", _NATIVE_UI, "Assign Stable Element IDs", - fields=( - FieldSpec("elements", FieldType.STRING, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("prior", FieldType.STRING, optional=True, - placeholder="prior frame's elements (with ids)"), - FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.5, - min_value=0.0, max_value=1.0), - ), - description="Tag elements with IDs carried across frames by overlap.", - )) - specs.append(CommandSpec( - "AC_score_candidates", _NATIVE_UI, "Score Candidates", - fields=( - FieldSpec("candidates", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("want_role", FieldType.STRING, optional=True), - FieldSpec("want_name", FieldType.STRING, optional=True), - FieldSpec("anchor", FieldType.STRING, optional=True, - placeholder="[x, y]"), - ), - description="Rank candidate elements by role / name / proximity confidence.", - )) - specs.append(CommandSpec( - "AC_best_candidate", _NATIVE_UI, "Best Candidate", - fields=( - FieldSpec("candidates", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("want_role", FieldType.STRING, optional=True), - FieldSpec("want_name", FieldType.STRING, optional=True), - FieldSpec("anchor", FieldType.STRING, optional=True, - placeholder="[x, y]"), - ), - description="The single highest-scoring candidate element.", - )) - specs.append(CommandSpec( - "AC_mark_screen", _NATIVE_UI, "Set-of-Marks: Number Elements", - fields=( - FieldSpec("app_name", FieldType.STRING, optional=True), - FieldSpec("render_path", FieldType.FILE_PATH, optional=True), + CommandSpec( + "AC_mark_screen", _NATIVE_UI, "Set-of-Marks: Number Elements", + fields=( + FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("render_path", FieldType.FILE_PATH, optional=True), + ), + description="Number live UI elements (id->bbox legend) for VLM " + "grounding; optional numbered-box overlay screenshot.", ), - description="Number live UI elements (id->bbox legend) for VLM " - "grounding; optional numbered-box overlay screenshot.", - )) - specs.append(CommandSpec( - "AC_mark_click", _NATIVE_UI, "Set-of-Marks: Click Number", - fields=(FieldSpec("mark_id", FieldType.INT),), - description="Click the element behind a numbered mark.", - )) + CommandSpec( + "AC_mark_click", _NATIVE_UI, "Set-of-Marks: Click Number", + fields=(FieldSpec("mark_id", FieldType.INT),), + description="Click the element behind a numbered mark.", + ), + ]) def _add_checkpoint_specs(specs: List[CommandSpec]) -> None: run_id = FieldSpec("run_id", FieldType.STRING) db = FieldSpec("db", FieldType.FILE_PATH) - specs.append(CommandSpec( - "AC_run_resumable", "Flow", "Run Resumable (checkpoint)", - fields=(run_id, db), - description="Run 'actions' (JSON view) with checkpoint/resume keyed " - "by run_id; resumes past completed steps after a crash.", - )) - specs.append(CommandSpec( - "AC_checkpoint_status", "Flow", "Checkpoint: Status", - fields=(run_id, db), - description="Return the saved checkpoint for a run (step + variables).", - )) - specs.append(CommandSpec( - "AC_checkpoint_clear", "Flow", "Checkpoint: Clear", - fields=(run_id, db), - description="Delete a run's checkpoint.", - )) - specs.append(CommandSpec( - "AC_wcag_audit", "Accessibility", "WCAG 2.2 Conformance Audit", - fields=( - FieldSpec("app_name", FieldType.STRING, optional=True), - FieldSpec("level", FieldType.ENUM, choices=("A", "AA", "AAA"), - optional=True, default="AA"), - FieldSpec("min_target_px", FieldType.INT, optional=True, - default=24), + specs.extend([ + CommandSpec( + "AC_run_resumable", "Flow", "Run Resumable (checkpoint)", + fields=(run_id, db), + description="Run 'actions' (JSON view) with checkpoint/resume keyed " + "by run_id; resumes past completed steps after a crash.", ), - description="WCAG 2.2 audit: SC-tagged findings + Target Size 2.5.8.", - )) + CommandSpec( + "AC_checkpoint_status", "Flow", "Checkpoint: Status", + fields=(run_id, db), + description="Return the saved checkpoint for a run (step + variables).", + ), + CommandSpec( + "AC_checkpoint_clear", "Flow", "Checkpoint: Clear", + fields=(run_id, db), + description="Delete a run's checkpoint.", + ), + CommandSpec( + "AC_wcag_audit", "Accessibility", "WCAG 2.2 Conformance Audit", + fields=( + FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("level", FieldType.ENUM, choices=("A", "AA", "AAA"), + optional=True, default="AA"), + FieldSpec("min_target_px", FieldType.INT, optional=True, + default=24), + ), + description="WCAG 2.2 audit: SC-tagged findings + Target Size 2.5.8.", + ), + ]) _add_observer_specs(specs) _add_ops_specs(specs) def _add_ops_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_generate_sbom", "Tools", "Generate SBOM (CycloneDX)", - fields=( - FieldSpec("path", FieldType.FILE_PATH, optional=True, - placeholder="sbom.cdx.json"), - FieldSpec("root", FieldType.STRING, optional=True, - default="je_auto_control"), + specs.extend([ + CommandSpec( + "AC_generate_sbom", "Tools", "Generate SBOM (CycloneDX)", + fields=( + FieldSpec("path", FieldType.FILE_PATH, optional=True, + placeholder="sbom.cdx.json"), + FieldSpec("root", FieldType.STRING, optional=True, + default="je_auto_control"), + ), + description="Emit a CycloneDX 1.6 dependency SBOM for the project.", ), - description="Emit a CycloneDX 1.6 dependency SBOM for the project.", - )) - specs.append(CommandSpec( - "AC_shard_suite", "Testing", "Shard Suite (duration-aware)", - fields=( - FieldSpec("shards", FieldType.INT, default=2), - FieldSpec("history_path", FieldType.FILE_PATH, optional=True), - FieldSpec("window", FieldType.INT, optional=True, default=20), + CommandSpec( + "AC_shard_suite", "Testing", "Shard Suite (duration-aware)", + fields=( + FieldSpec("shards", FieldType.INT, default=2), + FieldSpec("history_path", FieldType.FILE_PATH, optional=True), + FieldSpec("window", FieldType.INT, optional=True, default=20), + ), + description="Balance 'flows' (JSON view) into N shards by duration.", ), - description="Balance 'flows' (JSON view) into N shards by duration.", - )) - specs.append(CommandSpec( - "AC_merge_results", "Testing", "Merge Shard Results", - description="Merge per-shard 'reports' (JSON view) into one report.", - )) + CommandSpec( + "AC_merge_results", "Testing", "Merge Shard Results", + description="Merge per-shard 'reports' (JSON view) into one report.", + ), + ]) def _add_observer_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_observe_add", "Flow", "Observe: Add Watch", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("kind", FieldType.ENUM, - choices=("image", "text", "pixel"), default="image"), - FieldSpec("event", FieldType.ENUM, - choices=("appear", "vanish", "change"), - default="appear"), - FieldSpec("image", FieldType.FILE_PATH, optional=True), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=0.8), - FieldSpec("text", FieldType.STRING, optional=True), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), - ), - description="Run 'actions' (JSON view) on appear/vanish/change of an " - "image/text/pixel.", - )) - specs.append(CommandSpec( - "AC_observe_remove", "Flow", "Observe: Remove Watch", - fields=(FieldSpec("name", FieldType.STRING),), - description="Remove a registered watch.", - )) - specs.append(CommandSpec( - "AC_observe_list", "Flow", "Observe: List Watches", - description="List registered watch names.")) - specs.append(CommandSpec( - "AC_observe_poll", "Flow", "Observe: Poll Once", - description="Evaluate all watches once; return fired events.")) - specs.append(CommandSpec( - "AC_observe_start", "Flow", "Observe: Start", - description="Start the background observer thread.")) - specs.append(CommandSpec( - "AC_observe_stop", "Flow", "Observe: Stop", - description="Stop the background observer thread.")) + specs.extend([ + CommandSpec( + "AC_observe_add", "Flow", "Observe: Add Watch", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("kind", FieldType.ENUM, + choices=("image", "text", "pixel"), default="image"), + FieldSpec("event", FieldType.ENUM, + choices=("appear", "vanish", "change"), + default="appear"), + FieldSpec("image", FieldType.FILE_PATH, optional=True), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=0.8), + FieldSpec("text", FieldType.STRING, optional=True), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + ), + description="Run 'actions' (JSON view) on appear/vanish/change of an " + "image/text/pixel.", + ), + CommandSpec( + "AC_observe_remove", "Flow", "Observe: Remove Watch", + fields=(FieldSpec("name", FieldType.STRING),), + description="Remove a registered watch.", + ), + CommandSpec( + "AC_observe_list", "Flow", "Observe: List Watches", + description="List registered watch names."), + CommandSpec( + "AC_observe_poll", "Flow", "Observe: Poll Once", + description="Evaluate all watches once; return fired events."), + CommandSpec( + "AC_observe_start", "Flow", "Observe: Start", + description="Start the background observer thread."), + CommandSpec( + "AC_observe_stop", "Flow", "Observe: Stop", + description="Stop the background observer thread."), + ]) def _add_i18n_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_pseudo_localize", "Data", "Pseudo-Localize", - fields=( - FieldSpec("text", FieldType.STRING, optional=True), - FieldSpec("expansion", FieldType.FLOAT, optional=True, - default=0.4), + specs.extend([ + CommandSpec( + "AC_pseudo_localize", "Data", "Pseudo-Localize", + fields=( + FieldSpec("text", FieldType.STRING, optional=True), + FieldSpec("expansion", FieldType.FLOAT, optional=True, + default=0.4), + ), + description="Accent+pad a string (or 'mapping' via JSON view) for " + "i18n stress testing.", ), - description="Accent+pad a string (or 'mapping' via JSON view) for " - "i18n stress testing.", - )) - specs.append(CommandSpec( - "AC_check_overflow", "Data", "Check Text Overflow", - fields=( - FieldSpec("app_name", FieldType.STRING, optional=True), - FieldSpec("avg_char_px", FieldType.FLOAT, optional=True, - default=7.0), + CommandSpec( + "AC_check_overflow", "Data", "Check Text Overflow", + fields=( + FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("avg_char_px", FieldType.FLOAT, optional=True, + default=7.0), + ), + description="Flag text wider than its widget (translation overflow).", ), - description="Flag text wider than its widget (translation overflow).", - )) - specs.append(CommandSpec( - "AC_check_catalog", "Data", "Check Translation Catalog", - description="Diff 'target' vs 'base' catalog (JSON view): missing / " - "empty / placeholder mismatch.", - )) + CommandSpec( + "AC_check_catalog", "Data", "Check Translation Catalog", + description="Diff 'target' vs 'base' catalog (JSON view): missing / " + "empty / placeholder mismatch.", + ), + ]) def _add_data_quality_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_validate_rows", "Data", "Validate Rows (schema)", - description="Validate 'rows' against a 'schema' (both via JSON view).", - )) - specs.append(CommandSpec( - "AC_extract_fields", "Data", "Extract Fields (regex)", - fields=(FieldSpec("text", FieldType.STRING),), - description="Pull email/url/phone/amount/... from text; 'fields' / " - "'patterns' via JSON view.", - )) - specs.append(CommandSpec( - "AC_mask_rows", "Data", "Mask Rows", - description="Mask columns in 'rows' per 'rules' (redact/hash/partial)," - " via JSON view.", - )) + specs.extend([ + CommandSpec( + "AC_validate_rows", "Data", "Validate Rows (schema)", + description="Validate 'rows' against a 'schema' (both via JSON view).", + ), + CommandSpec( + "AC_extract_fields", "Data", "Extract Fields (regex)", + fields=(FieldSpec("text", FieldType.STRING),), + description="Pull email/url/phone/amount/... from text; 'fields' / " + "'patterns' via JSON view.", + ), + CommandSpec( + "AC_mask_rows", "Data", "Mask Rows", + description="Mask columns in 'rows' per 'rules' (redact/hash/partial)," + " via JSON view.", + ), + ]) def _add_memory_specs(specs: List[CommandSpec]) -> None: db = FieldSpec("db", FieldType.FILE_PATH) - specs.append(CommandSpec( - "AC_memory_remember", "Agent", "Memory: Remember Episode", - fields=(db, FieldSpec("goal", FieldType.STRING), - FieldSpec("outcome", FieldType.STRING, optional=True)), - description="Store an episode (goal -> 'steps' via JSON view -> " - "outcome).", - )) - specs.append(CommandSpec( - "AC_memory_recall", "Agent", "Memory: Recall", - fields=(db, FieldSpec("query", FieldType.STRING), - FieldSpec("limit", FieldType.INT, optional=True, default=5)), - description="Recall episodes most relevant to a query.", - )) - specs.append(CommandSpec( - "AC_memory_recent", "Agent", "Memory: Recent", - fields=(db, FieldSpec("limit", FieldType.INT, optional=True, - default=10)), - description="List the most recent episodes.", - )) - specs.append(CommandSpec( - "AC_memory_forget", "Agent", "Memory: Forget", - fields=(db, FieldSpec("episode_id", FieldType.INT)), - description="Delete an episode by id.", - )) - specs.append(CommandSpec( - "AC_memory_stats", "Agent", "Memory: Stats", - fields=(db,), - description="Episode count for a memory store.", - )) - specs.append(CommandSpec( - "AC_seed_everything", "Flow", "Seed RNG (deterministic)", - fields=(FieldSpec("seed", FieldType.INT, optional=True, default=0),), - description="Seed all RNG run-wide for reproducible runs.", - )) + specs.extend([ + CommandSpec( + "AC_memory_remember", "Agent", "Memory: Remember Episode", + fields=(db, FieldSpec("goal", FieldType.STRING), + FieldSpec("outcome", FieldType.STRING, optional=True)), + description="Store an episode (goal -> 'steps' via JSON view -> " + "outcome).", + ), + CommandSpec( + "AC_memory_recall", "Agent", "Memory: Recall", + fields=(db, FieldSpec("query", FieldType.STRING), + FieldSpec("limit", FieldType.INT, optional=True, default=5)), + description="Recall episodes most relevant to a query.", + ), + CommandSpec( + "AC_memory_recent", "Agent", "Memory: Recent", + fields=(db, FieldSpec("limit", FieldType.INT, optional=True, + default=10)), + description="List the most recent episodes.", + ), + CommandSpec( + "AC_memory_forget", "Agent", "Memory: Forget", + fields=(db, FieldSpec("episode_id", FieldType.INT)), + description="Delete an episode by id.", + ), + CommandSpec( + "AC_memory_stats", "Agent", "Memory: Stats", + fields=(db,), + description="Episode count for a memory store.", + ), + CommandSpec( + "AC_seed_everything", "Flow", "Seed RNG (deterministic)", + fields=(FieldSpec("seed", FieldType.INT, optional=True, default=0),), + description="Seed all RNG run-wide for reproducible runs.", + ), + ]) def _add_office_specs(specs: List[CommandSpec]) -> None: xlsx = FieldSpec("path", FieldType.FILE_PATH) - specs.append(CommandSpec( - "AC_read_workbook", "Office", "Excel: Read Workbook", - fields=(xlsx, FieldSpec("sheet", FieldType.STRING, optional=True)), - description="Read an .xlsx worksheet into rows (needs [office] extra).", - )) - specs.append(CommandSpec( - "AC_write_workbook", "Office", "Excel: Write Workbook", - fields=(xlsx, FieldSpec("sheet", FieldType.STRING, optional=True, - default="Sheet1")), - description="Write 'rows' (JSON view) to an .xlsx file.", - )) - specs.append(CommandSpec( - "AC_read_document", "Office", "Word: Read Document", - fields=(xlsx,), - description="Read a .docx file's paragraphs (needs [office] extra).", - )) - specs.append(CommandSpec( - "AC_write_document", "Office", "Word: Write Document", - fields=(xlsx,), - description="Write 'paragraphs' (JSON view) to a .docx file.", - )) - specs.append(CommandSpec( - "AC_read_presentation", "Office", "PowerPoint: Read", - fields=(xlsx,), - description="Read a .pptx file's per-slide text (needs [office]).", - )) - specs.append(CommandSpec( - "AC_write_presentation", "Office", "PowerPoint: Write", - fields=(xlsx,), - description="Write 'slides' (JSON view) to a .pptx file.", - )) + specs.extend([ + CommandSpec( + "AC_read_workbook", "Office", "Excel: Read Workbook", + fields=(xlsx, FieldSpec("sheet", FieldType.STRING, optional=True)), + description="Read an .xlsx worksheet into rows (needs [office] extra).", + ), + CommandSpec( + "AC_write_workbook", "Office", "Excel: Write Workbook", + fields=(xlsx, FieldSpec("sheet", FieldType.STRING, optional=True, + default="Sheet1")), + description="Write 'rows' (JSON view) to an .xlsx file.", + ), + CommandSpec( + "AC_read_document", "Office", "Word: Read Document", + fields=(xlsx,), + description="Read a .docx file's paragraphs (needs [office] extra).", + ), + CommandSpec( + "AC_write_document", "Office", "Word: Write Document", + fields=(xlsx,), + description="Write 'paragraphs' (JSON view) to a .docx file.", + ), + CommandSpec( + "AC_read_presentation", "Office", "PowerPoint: Read", + fields=(xlsx,), + description="Read a .pptx file's per-slide text (needs [office]).", + ), + CommandSpec( + "AC_write_presentation", "Office", "PowerPoint: Write", + fields=(xlsx,), + description="Write 'slides' (JSON view) to a .pptx file.", + ), + ]) def _add_authoring_specs(specs: List[CommandSpec]) -> None: path = FieldSpec("path", FieldType.FILE_PATH) key = FieldSpec("key", FieldType.STRING) - specs.append(CommandSpec( - "AC_element_save", _NATIVE_UI, "Element: Save Locator", - fields=(path, key, - FieldSpec("name", FieldType.STRING, optional=True), - FieldSpec("role", FieldType.STRING, optional=True), - FieldSpec("app_name", FieldType.STRING, optional=True)), - description="Save a named native-UI locator (object repository).", - )) - specs.append(CommandSpec( - "AC_element_find", _NATIVE_UI, "Element: Find Saved", - fields=(path, key), - description="Resolve a saved locator to a live element summary.", - )) - specs.append(CommandSpec( - "AC_element_click", _NATIVE_UI, "Element: Click Saved", - fields=(path, key), - description="Click the element behind a saved locator.", - )) - specs.append(CommandSpec( - "AC_element_remove", _NATIVE_UI, "Element: Remove Saved", - fields=(path, key), - description="Delete a saved locator.", - )) - specs.append(CommandSpec( - "AC_element_list", _NATIVE_UI, "Element: List Saved", - fields=(path,), - description="List saved locator names in a repository file.", - )) - specs.append(CommandSpec( - "AC_debug_trace", "Flow", "Debug: Trace Actions", - fields=(FieldSpec("dry_run", FieldType.BOOL, optional=True, - default=False),), - description="Run 'actions' (JSON view) and return a per-step trace.", - )) + specs.extend([ + CommandSpec( + "AC_element_save", _NATIVE_UI, "Element: Save Locator", + fields=(path, key, + FieldSpec("name", FieldType.STRING, optional=True), + FieldSpec("role", FieldType.STRING, optional=True), + FieldSpec("app_name", FieldType.STRING, optional=True)), + description="Save a named native-UI locator (object repository).", + ), + CommandSpec( + "AC_element_find", _NATIVE_UI, "Element: Find Saved", + fields=(path, key), + description="Resolve a saved locator to a live element summary.", + ), + CommandSpec( + "AC_element_click", _NATIVE_UI, "Element: Click Saved", + fields=(path, key), + description="Click the element behind a saved locator.", + ), + CommandSpec( + "AC_element_remove", _NATIVE_UI, "Element: Remove Saved", + fields=(path, key), + description="Delete a saved locator.", + ), + CommandSpec( + "AC_element_list", _NATIVE_UI, "Element: List Saved", + fields=(path,), + description="List saved locator names in a repository file.", + ), + CommandSpec( + "AC_debug_trace", "Flow", "Debug: Trace Actions", + fields=(FieldSpec("dry_run", FieldType.BOOL, optional=True, + default=False),), + description="Run 'actions' (JSON view) and return a per-step trace.", + ), + ]) def _add_agent_specs(specs: List[CommandSpec]) -> None: path = FieldSpec("path", FieldType.FILE_PATH) name = FieldSpec("name", FieldType.STRING) - specs.append(CommandSpec( - "AC_skill_save", "Agent", "Skill: Save Playbook", - fields=(path, name, - FieldSpec("description", FieldType.STRING, optional=True), - FieldSpec("tags", FieldType.STRING, optional=True)), - description="Save a reusable action sequence ('actions' via JSON " - "view) under a name.", - )) - specs.append(CommandSpec( - "AC_skill_run", "Agent", "Skill: Run Playbook", - fields=(path, name), - description="Execute a stored skill's actions.", - )) - specs.append(CommandSpec( - "AC_skill_list", "Agent", "Skill: List", - fields=(path,), - description="List saved skill names.", - )) - specs.append(CommandSpec( - "AC_skill_remove", "Agent", "Skill: Remove", - fields=(path, name), - description="Delete a saved skill.", - )) - specs.append(CommandSpec( - "AC_skill_search", "Agent", "Skill: Search", - fields=(path, FieldSpec("query", FieldType.STRING)), - description="Search skills by name/description/tags.", - )) - specs.append(CommandSpec( - "AC_guard_text", "Agent", "Guardrail: Scan Text", - fields=(FieldSpec("text", FieldType.STRING), - FieldSpec("threshold", FieldType.INT, optional=True, - default=2)), - description="Scan untrusted text for prompt-injection patterns.", - )) - specs.append(CommandSpec( - "AC_agent_card", "Agent", "A2A Agent Card", - fields=(FieldSpec("path", FieldType.FILE_PATH, optional=True, - placeholder="agent-card.json"),), - description="Write an A2A agent card describing AutoControl's skills.", - )) + specs.extend([ + CommandSpec( + "AC_skill_save", "Agent", "Skill: Save Playbook", + fields=(path, name, + FieldSpec("description", FieldType.STRING, optional=True), + FieldSpec("tags", FieldType.STRING, optional=True)), + description="Save a reusable action sequence ('actions' via JSON " + "view) under a name.", + ), + CommandSpec( + "AC_skill_run", "Agent", "Skill: Run Playbook", + fields=(path, name), + description="Execute a stored skill's actions.", + ), + CommandSpec( + "AC_skill_list", "Agent", "Skill: List", + fields=(path,), + description="List saved skill names.", + ), + CommandSpec( + "AC_skill_remove", "Agent", "Skill: Remove", + fields=(path, name), + description="Delete a saved skill.", + ), + CommandSpec( + "AC_skill_search", "Agent", "Skill: Search", + fields=(path, FieldSpec("query", FieldType.STRING)), + description="Search skills by name/description/tags.", + ), + CommandSpec( + "AC_guard_text", "Agent", "Guardrail: Scan Text", + fields=(FieldSpec("text", FieldType.STRING), + FieldSpec("threshold", FieldType.INT, optional=True, + default=2)), + description="Scan untrusted text for prompt-injection patterns.", + ), + CommandSpec( + "AC_agent_card", "Agent", "A2A Agent Card", + fields=(FieldSpec("path", FieldType.FILE_PATH, optional=True, + placeholder="agent-card.json"),), + description="Write an A2A agent card describing AutoControl's skills.", + ), + ]) def _add_tooling_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_generate_data", "Data", "Generate Synthetic Data", - fields=( - FieldSpec("count", FieldType.INT, optional=True, default=10), - FieldSpec("path", FieldType.FILE_PATH, optional=True), - FieldSpec("fmt", FieldType.ENUM, choices=("json", "csv"), - optional=True), - FieldSpec("seed", FieldType.INT, optional=True), - ), - description="Generate seeded fake rows from a 'schema' (JSON view); " - "writes a file when 'path' is set.", - )) - specs.append(CommandSpec( - "AC_start_mcp_server", "Tools", "Start MCP Stdio Server", - fields=(FieldSpec("read_only", FieldType.BOOL, optional=True), - FieldSpec("disclosure", FieldType.ENUM, optional=True, choices=("full", "progressive", "static")), - FieldSpec("tool_profile", FieldType.JSON, optional=True), - FieldSpec("page_size", FieldType.INT, optional=True, default=50, min_value=1, max_value=100)), - description="Start the foreground MCP stdio server with a reviewed tool availability profile.", - )) - specs.append(CommandSpec( - "AC_mcp_tool_search", "Tools", "Search MCP Tools", - fields=(FieldSpec("query", FieldType.STRING), - FieldSpec("limit", FieldType.INT, optional=True, default=10), - FieldSpec("read_only", FieldType.BOOL, optional=True)), - description="Search authorized registry summaries without executing a tool.", - )) - specs.append(CommandSpec( - "AC_mcp_tool_schema", "Tools", "Inspect MCP Tool Schema", - fields=(FieldSpec("name", FieldType.STRING), FieldSpec("read_only", FieldType.BOOL, optional=True)), - description="Read one authorized tool descriptor from the default registry.", - )) - specs.append(CommandSpec( - "AC_mcp_manifest", "Tools", "MCP Registry Manifest", - fields=( - FieldSpec("path", FieldType.FILE_PATH, optional=True, - placeholder="server.json"), - FieldSpec("include_tools", FieldType.BOOL, optional=True, - default=False), + specs.extend([ + CommandSpec( + "AC_generate_data", "Data", "Generate Synthetic Data", + fields=( + FieldSpec("count", FieldType.INT, optional=True, default=10), + FieldSpec("path", FieldType.FILE_PATH, optional=True), + FieldSpec("fmt", FieldType.ENUM, choices=("json", "csv"), + optional=True), + FieldSpec("seed", FieldType.INT, optional=True), + ), + description="Generate seeded fake rows from a 'schema' (JSON view); " + "writes a file when 'path' is set.", ), - description="Write an MCP registry server.json for this server.", - )) - specs.append(CommandSpec( - "AC_rank_tests", "Testing", "Rank Tests by Risk", - fields=( - FieldSpec("history_path", FieldType.FILE_PATH, optional=True), - FieldSpec("window", FieldType.INT, optional=True, default=10), + CommandSpec( + "AC_start_mcp_server", "Tools", "Start MCP Stdio Server", + fields=(FieldSpec("read_only", FieldType.BOOL, optional=True), + FieldSpec("disclosure", FieldType.ENUM, optional=True, choices=("full", "progressive", "static")), + FieldSpec("tool_profile", FieldType.JSON, optional=True), + FieldSpec("page_size", FieldType.INT, optional=True, default=50, min_value=1, max_value=100)), + description="Start the foreground MCP stdio server with a reviewed tool availability profile.", ), - description="Score 'flows' (JSON view) by risk from run history.", - )) - specs.append(CommandSpec( - "AC_select_tests", "Testing", "Select Risky Tests", - fields=( - FieldSpec("k", FieldType.INT, optional=True), - FieldSpec("threshold", FieldType.FLOAT, optional=True), - FieldSpec("history_path", FieldType.FILE_PATH, optional=True), - FieldSpec("window", FieldType.INT, optional=True, default=10), + CommandSpec( + "AC_mcp_tool_search", "Tools", "Search MCP Tools", + fields=(FieldSpec("query", FieldType.STRING), + FieldSpec("limit", FieldType.INT, optional=True, default=10), + FieldSpec("read_only", FieldType.BOOL, optional=True)), + description="Search authorized registry summaries without executing a tool.", ), - description="Pick riskiest 'flows' (JSON view): top-k or threshold.", - )) + CommandSpec( + "AC_mcp_tool_schema", "Tools", "Inspect MCP Tool Schema", + fields=(FieldSpec("name", FieldType.STRING), FieldSpec("read_only", FieldType.BOOL, optional=True)), + description="Read one authorized tool descriptor from the default registry.", + ), + CommandSpec( + "AC_mcp_manifest", "Tools", "MCP Registry Manifest", + fields=( + FieldSpec("path", FieldType.FILE_PATH, optional=True, + placeholder="server.json"), + FieldSpec("include_tools", FieldType.BOOL, optional=True, + default=False), + ), + description="Write an MCP registry server.json for this server.", + ), + CommandSpec( + "AC_rank_tests", "Testing", "Rank Tests by Risk", + fields=( + FieldSpec("history_path", FieldType.FILE_PATH, optional=True), + FieldSpec("window", FieldType.INT, optional=True, default=10), + ), + description="Score 'flows' (JSON view) by risk from run history.", + ), + CommandSpec( + "AC_select_tests", "Testing", "Select Risky Tests", + fields=( + FieldSpec("k", FieldType.INT, optional=True), + FieldSpec("threshold", FieldType.FLOAT, optional=True), + FieldSpec("history_path", FieldType.FILE_PATH, optional=True), + FieldSpec("window", FieldType.INT, optional=True, default=10), + ), + description="Pick riskiest 'flows' (JSON view): top-k or threshold.", + ), + ]) def _add_webrunner_specs(specs: List[CommandSpec]) -> None: """Browser automation through the optional ``je_web_runner`` bridge.""" - specs.append(CommandSpec( - "AC_web_open", "Browser", "Open Browser at URL", - fields=( - FieldSpec("url", FieldType.STRING, placeholder="https://example.com"), - FieldSpec("browser", FieldType.ENUM, optional=True, default="chrome", - choices=("chrome", "chromium", "firefox", "edge", "safari")), + specs.extend([ + CommandSpec( + "AC_web_open", "Browser", "Open Browser at URL", + fields=( + FieldSpec("url", FieldType.STRING, placeholder="https://example.com"), + FieldSpec("browser", FieldType.ENUM, optional=True, default="chrome", + choices=("chrome", "chromium", "firefox", "edge", "safari")), + ), + description="Start a WebRunner Selenium browser and open the URL (needs je_web_runner).", ), - description="Start a WebRunner Selenium browser and open the URL (needs je_web_runner).", - )) - specs.append(CommandSpec( - "AC_web_run", "Browser", "Run WebRunner Command", - fields=(FieldSpec("action", FieldType.STRING, placeholder="WR_to_url"),), - description="Run one WR_* command; its arguments go in 'params' (JSON view).", - )) - specs.append(CommandSpec( - "AC_web_run_actions", "Browser", "Run WebRunner Commands", - description="Run 'actions' (JSON view), a list of {action, params}; stops at the first failure.", - )) - specs.append(CommandSpec( - "AC_web_screenshot", "Browser", "Browser Screenshot", - fields=(FieldSpec("file_path", FieldType.FILE_PATH, placeholder="page.png"),), - )) - specs.append(CommandSpec("AC_web_current_url", "Browser", "Current Browser URL")) - specs.append(CommandSpec("AC_web_quit", "Browser", "Quit Browser")) - specs.append(CommandSpec("AC_web_available", "Browser", "Is WebRunner Installed")) - specs.append(CommandSpec("AC_web_list_commands", "Browser", "List WebRunner Commands")) + CommandSpec( + "AC_web_run", "Browser", "Run WebRunner Command", + fields=(FieldSpec("action", FieldType.STRING, placeholder="WR_to_url"),), + description="Run one WR_* command; its arguments go in 'params' (JSON view).", + ), + CommandSpec( + "AC_web_run_actions", "Browser", "Run WebRunner Commands", + description="Run 'actions' (JSON view), a list of {action, params}; stops at the first failure.", + ), + CommandSpec( + "AC_web_screenshot", "Browser", "Browser Screenshot", + fields=(FieldSpec("file_path", FieldType.FILE_PATH, placeholder="page.png"),), + ), + CommandSpec("AC_web_current_url", "Browser", "Current Browser URL"), + CommandSpec("AC_web_quit", "Browser", "Quit Browser"), + CommandSpec("AC_web_available", "Browser", "Is WebRunner Installed"), + CommandSpec("AC_web_list_commands", "Browser", "List WebRunner Commands"), + ]) def _add_work_queue_specs(specs: List[CommandSpec]) -> None: db = FieldSpec("db", FieldType.FILE_PATH) name = FieldSpec("name", FieldType.STRING, optional=True, default="default") - specs.append(CommandSpec( - "AC_queue_add", "Queue", "Queue: Add Item", - fields=(db, FieldSpec("reference", FieldType.STRING, optional=True), - name), - description="Enqueue a work item (data via JSON view); dedupes by " - "reference.", - )) - specs.append(CommandSpec( - "AC_queue_next", "Queue", "Queue: Get Next Item", - fields=(db, name, FieldSpec("stale_after_s", FieldType.FLOAT, optional=True)), - description="Atomically claim the next work item (performer).", - )) - specs.append(CommandSpec( - "AC_queue_complete", "Queue", "Queue: Complete Item", - fields=(db, FieldSpec("item_id", FieldType.INT), name, - FieldSpec("claim", FieldType.INT, optional=True)), - description="Mark a work item successfully processed.", - )) - specs.append(CommandSpec( - "AC_queue_fail", "Queue", "Queue: Fail Item", - fields=(db, FieldSpec("item_id", FieldType.INT), - FieldSpec("error", FieldType.STRING), - FieldSpec("kind", FieldType.ENUM, - choices=("application", "business"), - optional=True, default="application"), - FieldSpec("max_retries", FieldType.INT, optional=True, + specs.extend([ + CommandSpec( + "AC_queue_add", "Queue", "Queue: Add Item", + fields=(db, FieldSpec("reference", FieldType.STRING, optional=True), + name), + description="Enqueue a work item (data via JSON view); dedupes by " + "reference.", + ), + CommandSpec( + "AC_queue_next", "Queue", "Queue: Get Next Item", + fields=(db, name, FieldSpec("stale_after_s", FieldType.FLOAT, optional=True)), + description="Atomically claim the next work item (performer).", + ), + CommandSpec( + "AC_queue_complete", "Queue", "Queue: Complete Item", + fields=(db, FieldSpec("item_id", FieldType.INT), name, + FieldSpec("claim", FieldType.INT, optional=True)), + description="Mark a work item successfully processed.", + ), + CommandSpec( + "AC_queue_fail", "Queue", "Queue: Fail Item", + fields=(db, FieldSpec("item_id", FieldType.INT), + FieldSpec("error", FieldType.STRING), + FieldSpec("kind", FieldType.ENUM, + choices=("application", "business"), + optional=True, default="application"), + FieldSpec("max_retries", FieldType.INT, optional=True, + default=3), + name, FieldSpec("claim", FieldType.INT, optional=True)), + description="Fail an item; application errors retry, business don't.", + ), + CommandSpec( + "AC_queue_stats", "Queue", "Queue: Stats", + fields=(db, name), + description="Per-status counts for a work queue.", + ), + CommandSpec( + "AC_shell_command", "Shell", "Shell Command", + fields=(FieldSpec("shell_command", FieldType.STRING),), + ), + CommandSpec( + "AC_open_path", "Shell", "Open File / WebRunner URL", + fields=( + FieldSpec("target", FieldType.STRING, + placeholder="report.pdf or https://example.com"), + FieldSpec("verb", FieldType.STRING, optional=True, default="open", + placeholder="open / print / edit"), + ), + description="Open a file with its registered app, or a URL through je_web_runner.", + ), + CommandSpec( + "AC_plan_open", "Shell", "Plan Open (classify)", + fields=( + FieldSpec("target", FieldType.STRING), + FieldSpec("verb", FieldType.STRING, optional=True, default="open"), + ), + description="Classify how a file/URL would be opened (pure, no launch).", + ), + CommandSpec( + "AC_idle_seconds", "Shell", "Idle Seconds", + fields=(), + description="Seconds since the last user keyboard / mouse input.", + ), + CommandSpec( + "AC_is_idle", "Shell", "Is User Idle", + fields=( + FieldSpec("threshold", FieldType.FLOAT, default=300.0, + placeholder="idle seconds threshold"), + ), + description="True if the user has been idle for >= threshold seconds.", + ), + CommandSpec( + "AC_plan_keep_awake", "Shell", "Plan Keep Awake", + fields=( + FieldSpec("display", FieldType.BOOL, optional=True, default=True), + FieldSpec("system", FieldType.BOOL, optional=True, default=True), + ), + description="Describe a keep-awake request (pure, no OS call).", + ), + CommandSpec( + "AC_keep_awake_on", "Shell", "Keep Machine Awake", + fields=( + FieldSpec("display", FieldType.BOOL, optional=True, default=True), + FieldSpec("system", FieldType.BOOL, optional=True, default=True), + ), + description="Keep the machine awake until Allow Sleep is run.", + ), + CommandSpec( + "AC_allow_sleep", "Shell", "Allow Machine to Sleep", + fields=(), + description="Release a previously-started keep-awake.", + ), + CommandSpec( + "AC_get_volume", "Shell", "Get System Volume", + fields=(), + description="Read the master volume percent and mute state.", + ), + CommandSpec( + "AC_set_volume", "Shell", "Set System Volume", + fields=( + FieldSpec("level", FieldType.INT, default=50, + placeholder="volume percent 0-100"), + ), + description="Set the master volume to level percent (clamped 0-100).", + ), + CommandSpec( + "AC_change_volume", "Shell", "Change System Volume", + fields=( + FieldSpec("delta", FieldType.INT, default=10, + placeholder="percent delta (may be negative)"), + ), + description="Add delta percent to the master volume (clamped 0-100).", + ), + CommandSpec( + "AC_set_mute", "Shell", "Set Mute", + fields=( + FieldSpec("muted", FieldType.BOOL, optional=True, default=True), + ), + description="Mute or unmute the master output.", + ), + CommandSpec( + "AC_toggle_mute", "Shell", "Toggle Mute", + fields=(), + description="Flip the master mute flag.", + ), + CommandSpec( + "AC_lock_session", "Shell", "Lock Workstation", + fields=(), + description="Lock the workstation now (interrupts the session).", + ), + CommandSpec( + "AC_plan_lock_session", "Shell", "Plan Lock Workstation", + fields=(), + description="Describe how the workstation would be locked (pure).", + ), + CommandSpec( + "AC_wait_for_unlock", "Shell", "Wait for Unlock", + fields=( + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0, + placeholder=_TIMEOUT_PLACEHOLDER), + FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.5, + placeholder=_POLL_INTERVAL_PLACEHOLDER), + ), + description="Block until the session is unlocked or timeout.", + ), + CommandSpec( + "AC_classify_lock_transitions", "Shell", "Classify Lock Transitions", + fields=( + FieldSpec("states", FieldType.STRING, + placeholder="JSON list of booleans"), + ), + description="Reduce lock-state samples to lock / unlock events.", + ), + CommandSpec( + "AC_ime_state", "Shell", "IME State", + fields=(), + description="Read the focused window's live IME composition state.", + ), + CommandSpec( + "AC_is_composing", "Shell", "Is IME Composing", + fields=(), + description="True while the IME has an uncommitted composition.", + ), + CommandSpec( + "AC_wait_for_composition_commit", "Shell", "Wait for IME Commit", + fields=( + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=5.0, + placeholder=_TIMEOUT_PLACEHOLDER), + FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.1, + placeholder=_POLL_INTERVAL_PLACEHOLDER), + ), + description="Block until the IME finishes composing or timeout.", + ), + CommandSpec( + "AC_decode_conversion_mode", "Shell", "Decode IME Conversion Mode", + fields=( + FieldSpec("flags", FieldType.INT, default=0, + placeholder="IMM32 conversion bitmask"), + ), + description="Decode an IMM32 conversion bitmask into named flags.", + ), + CommandSpec( + "AC_retry_delay", "Flow", "Retry Backoff Delay", + fields=( + FieldSpec("attempt", FieldType.INT, default=1, + placeholder="1-based retry attempt"), + FieldSpec("base", FieldType.FLOAT, optional=True, default=0.1), + FieldSpec("max_delay", FieldType.FLOAT, optional=True, + default=5.0), + FieldSpec("multiplier", FieldType.FLOAT, optional=True, + default=2.0), + FieldSpec("jitter", FieldType.STRING, optional=True, + default="none", placeholder="none / full / equal"), + ), + description="Capped exponential backoff delay before a retry attempt.", + ), + CommandSpec( + "AC_plan_retry_delays", "Flow", "Plan Retry Delays", + fields=( + FieldSpec("attempts", FieldType.INT, default=5, + placeholder="number of retries"), + FieldSpec("base", FieldType.FLOAT, optional=True, default=0.1), + FieldSpec("max_delay", FieldType.FLOAT, optional=True, + default=5.0), + FieldSpec("multiplier", FieldType.FLOAT, optional=True, + default=2.0), + FieldSpec("jitter", FieldType.STRING, optional=True, + default="none", placeholder="none / full / equal"), + ), + description="The backoff delay schedule for the first N retries.", + ), + 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.", + ), + 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.", + ), + CommandSpec( + "AC_adaptive_timeout", "Flow", "Adaptive Timeout", + fields=( + FieldSpec("durations", FieldType.STRING, + placeholder="JSON list of durations (seconds)"), + FieldSpec("percentile_q", FieldType.FLOAT, optional=True, + default=95.0), + FieldSpec("factor", FieldType.FLOAT, optional=True, default=1.5), + FieldSpec("min_s", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("max_s", FieldType.FLOAT, optional=True, default=60.0), + ), + description="Recommend a wait timeout from observed step durations.", + ), + CommandSpec( + "AC_timeout_stats", "Flow", "Timeout Stats", + fields=( + FieldSpec("durations", FieldType.STRING, + placeholder="JSON list of durations (seconds)"), + FieldSpec("percentile_q", FieldType.FLOAT, optional=True, + default=95.0), + FieldSpec("factor", FieldType.FLOAT, optional=True, default=1.5), + FieldSpec("min_s", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("max_s", FieldType.FLOAT, optional=True, default=60.0), + ), + description="Timeout recommendation plus percentiles and clamp flags.", + ), + CommandSpec( + "AC_ensure_field_value", "Flow", "Ensure Field Value", + fields=( + FieldSpec("desired", FieldType.STRING, placeholder="desired value"), + 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("attempts", FieldType.INT, optional=True, default=2), + ), + description="Idempotently set a control's value (no-op if already set).", + ), + CommandSpec( + "AC_wait_until_app_idle", "Flow", "Wait Until App Idle", + fields=( + FieldSpec("quiet_samples", FieldType.INT, optional=True, + default=3, placeholder="consecutive idle polls"), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0, + placeholder=_TIMEOUT_PLACEHOLDER), + FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.1, + placeholder=_POLL_INTERVAL_PLACEHOLDER), + ), + description="Block until the app's busy cursor settles idle or timeout.", + ), + CommandSpec( + "AC_idle_point", "Flow", "Idle Point", + fields=( + FieldSpec("busy_samples", FieldType.STRING, + placeholder="JSON list of busy booleans"), + FieldSpec("quiet_samples", FieldType.INT, optional=True, default=3), - name, FieldSpec("claim", FieldType.INT, optional=True)), - description="Fail an item; application errors retry, business don't.", - )) - specs.append(CommandSpec( - "AC_queue_stats", "Queue", "Queue: Stats", - fields=(db, name), - description="Per-status counts for a work queue.", - )) - specs.append(CommandSpec( - "AC_shell_command", "Shell", "Shell Command", - fields=(FieldSpec("shell_command", FieldType.STRING),), - )) - specs.append(CommandSpec( - "AC_open_path", "Shell", "Open File / WebRunner URL", - fields=( - FieldSpec("target", FieldType.STRING, - placeholder="report.pdf or https://example.com"), - FieldSpec("verb", FieldType.STRING, optional=True, default="open", - placeholder="open / print / edit"), + ), + description="Index where a busy/idle series first settles idle.", ), - description="Open a file with its registered app, or a URL through je_web_runner.", - )) - specs.append(CommandSpec( - "AC_plan_open", "Shell", "Plan Open (classify)", - fields=( - FieldSpec("target", FieldType.STRING), - FieldSpec("verb", FieldType.STRING, optional=True, default="open"), + CommandSpec( + "AC_act_in_view", "Flow", "Act In View (scroll + click)", + fields=( + FieldSpec("target", FieldType.STRING, + placeholder="template path or text"), + FieldSpec("kind", FieldType.STRING, optional=True, + default="image", placeholder="image / text"), + FieldSpec("direction", FieldType.STRING, optional=True, + default="down", placeholder="up / down"), + FieldSpec("max_scrolls", FieldType.INT, optional=True, default=10), + FieldSpec("scroll_amount", FieldType.INT, optional=True, + default=3), + FieldSpec("button", FieldType.STRING, optional=True, + default="left"), + ), + description="Scroll a target into view, then click it when actionable.", ), - description="Classify how a file/URL would be opened (pure, no launch).", - )) - specs.append(CommandSpec( - "AC_idle_seconds", "Shell", "Idle Seconds", - fields=(), - description="Seconds since the last user keyboard / mouse input.", - )) - specs.append(CommandSpec( - "AC_is_idle", "Shell", "Is User Idle", - fields=( - FieldSpec("threshold", FieldType.FLOAT, default=300.0, - placeholder="idle seconds threshold"), + CommandSpec( + "AC_act_with_mode", "Flow", "Click with Mode (auto/trial/force)", + fields=( + FieldSpec("x", FieldType.INT, placeholder="x"), + FieldSpec("y", FieldType.INT, placeholder="y"), + FieldSpec("mode", FieldType.STRING, optional=True, default="auto", + placeholder="auto / trial / force"), + FieldSpec("button", FieldType.STRING, optional=True, + default="left"), + ), + description="Click a point under an action mode (gate / dry-run / force).", ), - description="True if the user has been idle for >= threshold seconds.", - )) - specs.append(CommandSpec( - "AC_plan_keep_awake", "Shell", "Plan Keep Awake", - fields=( - FieldSpec("display", FieldType.BOOL, optional=True, default=True), - FieldSpec("system", FieldType.BOOL, optional=True, default=True), + CommandSpec( + "AC_simulate_cvd", "Image", "Simulate Colour-Vision Deficiency", + fields=( + FieldSpec("rgb", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + FieldSpec("kind", FieldType.STRING, optional=True, + default="deuteranopia", + placeholder="protanopia / deuteranopia / tritanopia"), + FieldSpec("severity", FieldType.FLOAT, optional=True, default=1.0), + ), + description="Map an RGB colour through a CVD simulation matrix.", ), - description="Describe a keep-awake request (pure, no OS call).", - )) - specs.append(CommandSpec( - "AC_keep_awake_on", "Shell", "Keep Machine Awake", - fields=( - FieldSpec("display", FieldType.BOOL, optional=True, default=True), - FieldSpec("system", FieldType.BOOL, optional=True, default=True), + CommandSpec( + "AC_colors_collide", "Image", "Colours Collide Under CVD", + fields=( + FieldSpec("left", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + FieldSpec("right", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + FieldSpec("kind", FieldType.STRING, optional=True, + default="deuteranopia", + placeholder="protanopia / deuteranopia / tritanopia"), + FieldSpec("severity", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=40.0), + ), + description="Whether two colours become confusable under a CVD type.", ), - description="Keep the machine awake until Allow Sleep is run.", - )) - specs.append(CommandSpec( - "AC_allow_sleep", "Shell", "Allow Machine to Sleep", - fields=(), - description="Release a previously-started keep-awake.", - )) - specs.append(CommandSpec( - "AC_get_volume", "Shell", "Get System Volume", - fields=(), - description="Read the master volume percent and mute state.", - )) - specs.append(CommandSpec( - "AC_set_volume", "Shell", "Set System Volume", - fields=( - FieldSpec("level", FieldType.INT, default=50, - placeholder="volume percent 0-100"), + CommandSpec( + "AC_place_labels", "Image", "Place Mark Labels", + fields=( + FieldSpec("marks", FieldType.STRING, + placeholder="JSON list of {id, bbox}"), + FieldSpec("label_width", FieldType.INT, optional=True, default=22), + FieldSpec("label_height", FieldType.INT, optional=True, + default=16), + FieldSpec("bounds", FieldType.STRING, optional=True, + placeholder=_SIZE_PLACEHOLDER), + ), + description="Lay out non-overlapping Set-of-Marks label boxes.", ), - description="Set the master volume to level percent (clamped 0-100).", - )) - specs.append(CommandSpec( - "AC_change_volume", "Shell", "Change System Volume", - fields=( - FieldSpec("delta", FieldType.INT, default=10, - placeholder="percent delta (may be negative)"), + CommandSpec( + "AC_label_color", "Image", "Label Colour for Background", + fields=( + FieldSpec("background", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + ), + description="Higher-contrast label colour (black/white) for a background.", ), - description="Add delta percent to the master volume (clamped 0-100).", - )) - specs.append(CommandSpec( - "AC_set_mute", "Shell", "Set Mute", - fields=( - FieldSpec("muted", FieldType.BOOL, optional=True, default=True), + CommandSpec( + "AC_grade_contrast", "Image", "Grade Contrast (WCAG)", + fields=( + FieldSpec("foreground", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + FieldSpec("background", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + ), + description="Grade a foreground/background colour pair against WCAG.", ), - description="Mute or unmute the master output.", - )) - specs.append(CommandSpec( - "AC_toggle_mute", "Shell", "Toggle Mute", - fields=(), - description="Flip the master mute flag.", - )) - specs.append(CommandSpec( - "AC_lock_session", "Shell", "Lock Workstation", - fields=(), - description="Lock the workstation now (interrupts the session).", - )) - specs.append(CommandSpec( - "AC_plan_lock_session", "Shell", "Plan Lock Workstation", - fields=(), - description="Describe how the workstation would be locked (pure).", - )) - specs.append(CommandSpec( - "AC_wait_for_unlock", "Shell", "Wait for Unlock", - fields=( - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0, - placeholder="timeout seconds"), - FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.5, - placeholder="poll interval seconds"), + CommandSpec( + "AC_dominant_pair", "Image", "Dominant FG/BG Colours", + fields=( + FieldSpec("pixels", FieldType.STRING, + placeholder="JSON list of [r, g, b] pixels"), + ), + description="Split sampled pixels into dominant foreground/background.", + ), + CommandSpec( + "AC_region_contrast", "Image", "Region Text Contrast", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Sample a screen region and grade its text contrast.", + ), + CommandSpec( + "AC_match_theme", "Image", "Match (Theme-Invariant)", + fields=( + FieldSpec("template", FieldType.STRING, + placeholder="template image path"), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + FieldSpec("method", FieldType.STRING, optional=True, + default="sobel", + placeholder="sobel / laplacian / zscore"), + FieldSpec("min_score", FieldType.FLOAT, optional=True, + default=0.5), + ), + description="Locate a template across a light/dark theme flip.", + ), + CommandSpec( + "AC_rank_changes", "Image", "Rank Changed Boxes", + fields=( + FieldSpec("scored_boxes", FieldType.STRING, + placeholder="JSON list of {box, score}"), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=0.1), + ), + description="Rank scored element boxes by how much they changed.", ), - description="Block until the session is unlocked or timeout.", - )) - specs.append(CommandSpec( - "AC_classify_lock_transitions", "Shell", "Classify Lock Transitions", - fields=( - FieldSpec("states", FieldType.STRING, - placeholder="JSON list of booleans"), + CommandSpec( + "AC_localize_changes", "Image", "Localize Changed Elements", + fields=( + FieldSpec("reference", FieldType.STRING, + placeholder="reference image path"), + FieldSpec("boxes", FieldType.STRING, + placeholder="JSON list of [x, y, w, h]"), + FieldSpec("current", FieldType.STRING, optional=True, + placeholder="current image path (else screen)"), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=0.1), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + ), + description="Rank which element boxes changed between two frames.", ), - description="Reduce lock-state samples to lock / unlock events.", - )) - specs.append(CommandSpec( - "AC_ime_state", "Shell", "IME State", - fields=(), - description="Read the focused window's live IME composition state.", - )) - specs.append(CommandSpec( - "AC_is_composing", "Shell", "Is IME Composing", - fields=(), - description="True while the IME has an uncommitted composition.", - )) - specs.append(CommandSpec( - "AC_wait_for_composition_commit", "Shell", "Wait for IME Commit", - fields=( - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=5.0, - placeholder="timeout seconds"), - FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.1, - placeholder="poll interval seconds"), + CommandSpec( + "AC_classify_widget", "Image", "Classify Widget (features)", + fields=( + FieldSpec("features", FieldType.STRING, + placeholder="JSON {aspect, circularity, fill}"), + ), + description="Map geometric features to a widget type.", ), - description="Block until the IME finishes composing or timeout.", - )) - specs.append(CommandSpec( - "AC_decode_conversion_mode", "Shell", "Decode IME Conversion Mode", - fields=( - FieldSpec("flags", FieldType.INT, default=0, - placeholder="IMM32 conversion bitmask"), + CommandSpec( + "AC_classify_icon", "Image", "Classify Icon (box)", + fields=( + FieldSpec("source", FieldType.STRING, placeholder="image path"), + FieldSpec("box", FieldType.STRING, placeholder=_RECT4_PLACEHOLDER), + ), + description="Classify the widget in an image box from its pixels.", ), - description="Decode an IMM32 conversion bitmask into named flags.", - )) - specs.append(CommandSpec( - "AC_retry_delay", "Flow", "Retry Backoff Delay", - fields=( - FieldSpec("attempt", FieldType.INT, default=1, - placeholder="1-based retry attempt"), - FieldSpec("base", FieldType.FLOAT, optional=True, default=0.1), - FieldSpec("max_delay", FieldType.FLOAT, optional=True, - default=5.0), - FieldSpec("multiplier", FieldType.FLOAT, optional=True, - default=2.0), - FieldSpec("jitter", FieldType.STRING, optional=True, - default="none", placeholder="none / full / equal"), - ), - description="Capped exponential backoff delay before a retry attempt.", - )) - specs.append(CommandSpec( - "AC_plan_retry_delays", "Flow", "Plan Retry Delays", - fields=( - FieldSpec("attempts", FieldType.INT, default=5, - placeholder="number of retries"), - FieldSpec("base", FieldType.FLOAT, optional=True, default=0.1), - FieldSpec("max_delay", FieldType.FLOAT, optional=True, - default=5.0), - FieldSpec("multiplier", FieldType.FLOAT, optional=True, - default=2.0), - FieldSpec("jitter", FieldType.STRING, optional=True, - default="none", placeholder="none / full / equal"), - ), - 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"), + CommandSpec( + "AC_propose_elements", "Image", "Propose Elements (template-free)", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + FieldSpec("min_area", FieldType.INT, optional=True, default=80), + FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, + default=0.5), + ), + description="Propose text/widget element boxes from raw screen pixels.", ), - 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_adaptive_timeout", "Flow", "Adaptive Timeout", - fields=( - FieldSpec("durations", FieldType.STRING, - placeholder="JSON list of durations (seconds)"), - FieldSpec("percentile_q", FieldType.FLOAT, optional=True, - default=95.0), - FieldSpec("factor", FieldType.FLOAT, optional=True, default=1.5), - FieldSpec("min_s", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("max_s", FieldType.FLOAT, optional=True, default=60.0), - ), - description="Recommend a wait timeout from observed step durations.", - )) - specs.append(CommandSpec( - "AC_timeout_stats", "Flow", "Timeout Stats", - fields=( - FieldSpec("durations", FieldType.STRING, - placeholder="JSON list of durations (seconds)"), - FieldSpec("percentile_q", FieldType.FLOAT, optional=True, - default=95.0), - FieldSpec("factor", FieldType.FLOAT, optional=True, default=1.5), - FieldSpec("min_s", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("max_s", FieldType.FLOAT, optional=True, default=60.0), - ), - description="Timeout recommendation plus percentiles and clamp flags.", - )) - specs.append(CommandSpec( - "AC_ensure_field_value", "Flow", "Ensure Field Value", - fields=( - FieldSpec("desired", FieldType.STRING, placeholder="desired value"), - 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("attempts", FieldType.INT, optional=True, default=2), - ), - description="Idempotently set a control's value (no-op if already set).", - )) - specs.append(CommandSpec( - "AC_wait_until_app_idle", "Flow", "Wait Until App Idle", - fields=( - FieldSpec("quiet_samples", FieldType.INT, optional=True, - default=3, placeholder="consecutive idle polls"), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0, - placeholder="timeout seconds"), - FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.1, - placeholder="poll interval seconds"), - ), - description="Block until the app's busy cursor settles idle or timeout.", - )) - specs.append(CommandSpec( - "AC_idle_point", "Flow", "Idle Point", - fields=( - FieldSpec("busy_samples", FieldType.STRING, - placeholder="JSON list of busy booleans"), - FieldSpec("quiet_samples", FieldType.INT, optional=True, - default=3), + CommandSpec( + "AC_tag_kinds", "Image", "Tag Element Kinds", + fields=( + FieldSpec("elements", FieldType.STRING, + placeholder="JSON list of fused boxes"), + ), + description="Label fused element boxes text/widget by source.", ), - description="Index where a busy/idle series first settles idle.", - )) - specs.append(CommandSpec( - "AC_act_in_view", "Flow", "Act In View (scroll + click)", - fields=( - FieldSpec("target", FieldType.STRING, - placeholder="template path or text"), - FieldSpec("kind", FieldType.STRING, optional=True, - default="image", placeholder="image / text"), - FieldSpec("direction", FieldType.STRING, optional=True, - default="down", placeholder="up / down"), - FieldSpec("max_scrolls", FieldType.INT, optional=True, default=10), - FieldSpec("scroll_amount", FieldType.INT, optional=True, - default=3), - FieldSpec("button", FieldType.STRING, optional=True, - default="left"), - ), - description="Scroll a target into view, then click it when actionable.", - )) - specs.append(CommandSpec( - "AC_act_with_mode", "Flow", "Click with Mode (auto/trial/force)", - fields=( - FieldSpec("x", FieldType.INT, placeholder="x"), - FieldSpec("y", FieldType.INT, placeholder="y"), - FieldSpec("mode", FieldType.STRING, optional=True, default="auto", - placeholder="auto / trial / force"), - FieldSpec("button", FieldType.STRING, optional=True, - default="left"), - ), - description="Click a point under an action mode (gate / dry-run / force).", - )) - specs.append(CommandSpec( - "AC_simulate_cvd", "Image", "Simulate Colour-Vision Deficiency", - fields=( - FieldSpec("rgb", FieldType.STRING, placeholder="[r, g, b]"), - FieldSpec("kind", FieldType.STRING, optional=True, - default="deuteranopia", - placeholder="protanopia / deuteranopia / tritanopia"), - FieldSpec("severity", FieldType.FLOAT, optional=True, default=1.0), + CommandSpec( + "AC_normalize_ext", "Shell", "Normalize Extension", + fields=( + FieldSpec("target", FieldType.STRING, + placeholder="report.pdf or .pdf or pdf"), + ), + description="Lowercased file extension (with dot) of a path / ext.", ), - description="Map an RGB colour through a CVD simulation matrix.", - )) - specs.append(CommandSpec( - "AC_colors_collide", "Image", "Colours Collide Under CVD", - fields=( - FieldSpec("left", FieldType.STRING, placeholder="[r, g, b]"), - FieldSpec("right", FieldType.STRING, placeholder="[r, g, b]"), - FieldSpec("kind", FieldType.STRING, optional=True, - default="deuteranopia", - placeholder="protanopia / deuteranopia / tritanopia"), - FieldSpec("severity", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=40.0), - ), - description="Whether two colours become confusable under a CVD type.", - )) - specs.append(CommandSpec( - "AC_place_labels", "Image", "Place Mark Labels", - fields=( - FieldSpec("marks", FieldType.STRING, - placeholder="JSON list of {id, bbox}"), - FieldSpec("label_width", FieldType.INT, optional=True, default=22), - FieldSpec("label_height", FieldType.INT, optional=True, - default=16), - FieldSpec("bounds", FieldType.STRING, optional=True, - placeholder="[width, height]"), - ), - description="Lay out non-overlapping Set-of-Marks label boxes.", - )) - specs.append(CommandSpec( - "AC_label_color", "Image", "Label Colour for Background", - fields=( - FieldSpec("background", FieldType.STRING, placeholder="[r, g, b]"), + CommandSpec( + "AC_file_association", "Shell", "File Association (default app)", + fields=( + FieldSpec("target", FieldType.STRING, + placeholder="report.pdf or .pdf"), + ), + description="Which app is registered to open a file type (Windows).", ), - description="Higher-contrast label colour (black/white) for a background.", - )) - specs.append(CommandSpec( - "AC_grade_contrast", "Image", "Grade Contrast (WCAG)", - fields=( - FieldSpec("foreground", FieldType.STRING, placeholder="[r, g, b]"), - FieldSpec("background", FieldType.STRING, placeholder="[r, g, b]"), + CommandSpec( + "AC_take_golden", "Report", "Capture Golden Image", + fields=(FieldSpec("path", FieldType.FILE_PATH),), + description="Capture and save a baseline image for visual regression.", ), - description="Grade a foreground/background colour pair against WCAG.", - )) - specs.append(CommandSpec( - "AC_dominant_pair", "Image", "Dominant FG/BG Colours", - fields=( - FieldSpec("pixels", FieldType.STRING, - placeholder="JSON list of [r, g, b] pixels"), + CommandSpec( + "AC_assert_visual", "Report", "Assert Visual (Golden)", + fields=( + FieldSpec("golden_path", FieldType.FILE_PATH), + FieldSpec("tolerance", FieldType.FLOAT, optional=True, default=0.0, + min_value=0.0), + FieldSpec("per_pixel_threshold", FieldType.INT, optional=True, + default=16, min_value=0), + FieldSpec("diff_path", FieldType.FILE_PATH, optional=True), + ), + description=("Compare the screen to a golden image; first run creates " + "the baseline. Use the JSON view for a region / masks."), ), - description="Split sampled pixels into dominant foreground/background.", - )) - specs.append(CommandSpec( - "AC_region_contrast", "Image", "Region Text Contrast", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + CommandSpec( + "AC_run_state_machine", "Flow", "Run State Machine", + description=("Run a finite-state-machine; configure the 'spec' " + "{initial, states} dict in the JSON view."), ), - description="Sample a screen region and grade its text contrast.", - )) - specs.append(CommandSpec( - "AC_match_theme", "Image", "Match (Theme-Invariant)", - fields=( - FieldSpec("template", FieldType.STRING, - placeholder="template image path"), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder="[x, y, w, h]"), - FieldSpec("method", FieldType.STRING, optional=True, - default="sobel", - placeholder="sobel / laplacian / zscore"), - FieldSpec("min_score", FieldType.FLOAT, optional=True, - default=0.5), - ), - description="Locate a template across a light/dark theme flip.", - )) - specs.append(CommandSpec( - "AC_rank_changes", "Image", "Rank Changed Boxes", - fields=( - FieldSpec("scored_boxes", FieldType.STRING, - placeholder="JSON list of {box, score}"), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=0.1), + CommandSpec( + "AC_shell_to_var", "Shell", "Shell Output into Variable", + fields=( + FieldSpec("command", FieldType.STRING), + FieldSpec("var", FieldType.STRING, default="shell_output"), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), + FieldSpec("encoding", FieldType.STRING, optional=True), + ), + description="Run a command and store its stdout in a flow variable.", ), - description="Rank scored element boxes by how much they changed.", - )) - specs.append(CommandSpec( - "AC_localize_changes", "Image", "Localize Changed Elements", - fields=( - FieldSpec("reference", FieldType.STRING, - placeholder="reference image path"), - FieldSpec("boxes", FieldType.STRING, - placeholder="JSON list of [x, y, w, h]"), - FieldSpec("current", FieldType.STRING, optional=True, - placeholder="current image path (else screen)"), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=0.1), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder="[x, y, w, h]"), - ), - description="Rank which element boxes changed between two frames.", - )) - specs.append(CommandSpec( - "AC_classify_widget", "Image", "Classify Widget (features)", - fields=( - FieldSpec("features", FieldType.STRING, - placeholder="JSON {aspect, circularity, fill}"), + CommandSpec( + "AC_read_file_to_var", "Shell", "Read File into Variable", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("var", FieldType.STRING, default="file_content"), + FieldSpec("encoding", FieldType.STRING, optional=True, + default="utf-8"), + ), + description="Read a file's text content into a flow variable.", ), - description="Map geometric features to a widget type.", - )) - specs.append(CommandSpec( - "AC_classify_icon", "Image", "Classify Icon (box)", - fields=( - FieldSpec("source", FieldType.STRING, placeholder="image path"), - FieldSpec("box", FieldType.STRING, placeholder="[x, y, w, h]"), + CommandSpec( + "AC_sql_to_var", "Report", "SQL Query into Variable", + fields=( + FieldSpec("database", FieldType.FILE_PATH), + FieldSpec("query", FieldType.STRING, + placeholder="SELECT name FROM users WHERE id = ?"), + FieldSpec("var", FieldType.STRING, default="sql_result"), + FieldSpec("fetch", FieldType.ENUM, + choices=("all", "one", "scalar"), + optional=True, default="all"), + ), + description=("Run a read-only SQLite SELECT; store rows / a row / a " + "scalar in a variable. Bind values via params (JSON view)."), ), - description="Classify the widget in an image box from its pixels.", - )) - specs.append(CommandSpec( - "AC_propose_elements", "Image", "Propose Elements (template-free)", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder="[x, y, w, h]"), - FieldSpec("min_area", FieldType.INT, optional=True, default=80), - FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, - default=0.5), + CommandSpec( + "AC_assert_db", "Report", "Assert SQL Result", + fields=( + FieldSpec("database", FieldType.FILE_PATH), + FieldSpec("query", FieldType.STRING, + placeholder="SELECT COUNT(*) FROM users"), + FieldSpec("op", FieldType.ENUM, + choices=("eq", "ne", "lt", "le", "gt", "ge", + "contains", "startswith", "endswith"), + optional=True, default="eq"), + FieldSpec("expected", FieldType.STRING, optional=True), + ), + description=("Run a scalar SELECT and assert its value (use the JSON " + "view for non-string expected values / params)."), ), - description="Propose text/widget element boxes from raw screen pixels.", - )) - specs.append(CommandSpec( - "AC_tag_kinds", "Image", "Tag Element Kinds", - fields=( - FieldSpec("elements", FieldType.STRING, - placeholder="JSON list of fused boxes"), + CommandSpec( + "AC_http_to_var", "Report", "HTTP Request into Variable", + fields=( + FieldSpec("url", FieldType.STRING, placeholder="https://..."), + FieldSpec("method", FieldType.ENUM, + choices=("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"), + optional=True, default="GET"), + FieldSpec("var", FieldType.STRING, default="http_response"), + FieldSpec("json_path", FieldType.STRING, optional=True, + placeholder="data.0.name"), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), + ), + description="Request a URL; store the body or a JSON field in a variable.", ), - description="Label fused element boxes text/widget by source.", - )) - specs.append(CommandSpec( - "AC_normalize_ext", "Shell", "Normalize Extension", - fields=( - FieldSpec("target", FieldType.STRING, - placeholder="report.pdf or .pdf or pdf"), + CommandSpec( + "AC_pdf_to_var", "Report", "PDF Text into Variable", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("var", FieldType.STRING, default="pdf_text"), + FieldSpec("page", FieldType.INT, optional=True, min_value=1), + ), + description="Extract a PDF's text (all pages or one) into a variable.", ), - description="Lowercased file extension (with dot) of a path / ext.", - )) - specs.append(CommandSpec( - "AC_file_association", "Shell", "File Association (default app)", - fields=( - FieldSpec("target", FieldType.STRING, - placeholder="report.pdf or .pdf"), + CommandSpec( + "AC_assert_pdf_text", "Report", "Assert PDF Text", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("text", FieldType.STRING), + FieldSpec("present", FieldType.BOOL, optional=True, default=True), + FieldSpec("page", FieldType.INT, optional=True, min_value=1), + FieldSpec("case_sensitive", FieldType.BOOL, optional=True, + default=True), + ), + description="Assert text is present (or absent) in a PDF document.", ), - description="Which app is registered to open a file type (Windows).", - )) - specs.append(CommandSpec( - "AC_take_golden", "Report", "Capture Golden Image", - fields=(FieldSpec("path", FieldType.FILE_PATH),), - description="Capture and save a baseline image for visual regression.", - )) - specs.append(CommandSpec( - "AC_assert_visual", "Report", "Assert Visual (Golden)", - fields=( - FieldSpec("golden_path", FieldType.FILE_PATH), - FieldSpec("tolerance", FieldType.FLOAT, optional=True, default=0.0, - min_value=0.0), - FieldSpec("per_pixel_threshold", FieldType.INT, optional=True, - default=16, min_value=0), - FieldSpec("diff_path", FieldType.FILE_PATH, optional=True), - ), - description=("Compare the screen to a golden image; first run creates " - "the baseline. Use the JSON view for a region / masks."), - )) - specs.append(CommandSpec( - "AC_run_state_machine", "Flow", "Run State Machine", - description=("Run a finite-state-machine; configure the 'spec' " - "{initial, states} dict in the JSON view."), - )) - specs.append(CommandSpec( - "AC_shell_to_var", "Shell", "Shell Output into Variable", - fields=( - FieldSpec("command", FieldType.STRING), - FieldSpec("var", FieldType.STRING, default="shell_output"), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), - FieldSpec("encoding", FieldType.STRING, optional=True), + CommandSpec( + "AC_send_email", "Report", "Send Email", + description=("Send an email via SMTP. Configure the 'message' " + "{sender,to,subject,body,attachments} and 'smtp' " + "{host,port,username,password} dicts in the JSON view."), ), - description="Run a command and store its stdout in a flow variable.", - )) - specs.append(CommandSpec( - "AC_read_file_to_var", "Shell", "Read File into Variable", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("var", FieldType.STRING, default="file_content"), - FieldSpec("encoding", FieldType.STRING, optional=True, - default="utf-8"), + CommandSpec( + "AC_http_request", "Report", "HTTP Request", + fields=( + FieldSpec("url", FieldType.STRING, placeholder="https://..."), + FieldSpec("method", FieldType.ENUM, + choices=("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"), + default="GET"), + FieldSpec("data", FieldType.STRING, optional=True, + placeholder="raw request body"), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), + ), + description=("Perform an HTTP(S) request; returns status/headers/text/" + "json. Use the JSON view for headers/json_body/auth."), ), - description="Read a file's text content into a flow variable.", - )) - specs.append(CommandSpec( - "AC_sql_to_var", "Report", "SQL Query into Variable", - fields=( - FieldSpec("database", FieldType.FILE_PATH), - FieldSpec("query", FieldType.STRING, - placeholder="SELECT name FROM users WHERE id = ?"), - FieldSpec("var", FieldType.STRING, default="sql_result"), - FieldSpec("fetch", FieldType.ENUM, - choices=("all", "one", "scalar"), - optional=True, default="all"), - ), - description=("Run a read-only SQLite SELECT; store rows / a row / a " - "scalar in a variable. Bind values via params (JSON view)."), - )) - specs.append(CommandSpec( - "AC_assert_db", "Report", "Assert SQL Result", - fields=( - FieldSpec("database", FieldType.FILE_PATH), - FieldSpec("query", FieldType.STRING, - placeholder="SELECT COUNT(*) FROM users"), - FieldSpec("op", FieldType.ENUM, - choices=("eq", "ne", "lt", "le", "gt", "ge", - "contains", "startswith", "endswith"), - optional=True, default="eq"), - FieldSpec("expected", FieldType.STRING, optional=True), - ), - description=("Run a scalar SELECT and assert its value (use the JSON " - "view for non-string expected values / params)."), - )) - specs.append(CommandSpec( - "AC_http_to_var", "Report", "HTTP Request into Variable", - fields=( - FieldSpec("url", FieldType.STRING, placeholder="https://..."), - FieldSpec("method", FieldType.ENUM, - choices=("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"), - optional=True, default="GET"), - FieldSpec("var", FieldType.STRING, default="http_response"), - FieldSpec("json_path", FieldType.STRING, optional=True, - placeholder="data.0.name"), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), - ), - description="Request a URL; store the body or a JSON field in a variable.", - )) - specs.append(CommandSpec( - "AC_pdf_to_var", "Report", "PDF Text into Variable", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("var", FieldType.STRING, default="pdf_text"), - FieldSpec("page", FieldType.INT, optional=True, min_value=1), + CommandSpec( + "AC_execute_process", "Shell", "Start Executable", + # Must match start_exe(exe_path); the executor dispatches + # event(**params), so a renamed field is a guaranteed TypeError. + fields=(FieldSpec("exe_path", FieldType.FILE_PATH),), ), - description="Extract a PDF's text (all pages or one) into a variable.", - )) - specs.append(CommandSpec( - "AC_assert_pdf_text", "Report", "Assert PDF Text", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("text", FieldType.STRING), - FieldSpec("present", FieldType.BOOL, optional=True, default=True), - FieldSpec("page", FieldType.INT, optional=True, min_value=1), - FieldSpec("case_sensitive", FieldType.BOOL, optional=True, - default=True), - ), - description="Assert text is present (or absent) in a PDF document.", - )) - specs.append(CommandSpec( - "AC_send_email", "Report", "Send Email", - description=("Send an email via SMTP. Configure the 'message' " - "{sender,to,subject,body,attachments} and 'smtp' " - "{host,port,username,password} dicts in the JSON view."), - )) - specs.append(CommandSpec( - "AC_http_request", "Report", "HTTP Request", - fields=( - FieldSpec("url", FieldType.STRING, placeholder="https://..."), - FieldSpec("method", FieldType.ENUM, - choices=("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"), - default="GET"), - FieldSpec("data", FieldType.STRING, optional=True, - placeholder="raw request body"), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), - ), - description=("Perform an HTTP(S) request; returns status/headers/text/" - "json. Use the JSON view for headers/json_body/auth."), - )) - specs.append(CommandSpec( - "AC_execute_process", "Shell", "Start Executable", - # Must match start_exe(exe_path); the executor dispatches - # event(**params), so a renamed field is a guaranteed TypeError. - fields=(FieldSpec("exe_path", FieldType.FILE_PATH),), - )) - specs.append(CommandSpec( - "AC_move_to_trash", "Shell", "Move File to Recycle Bin", - fields=(FieldSpec("path", FieldType.FILE_PATH),), - description="Delete a file to the OS recycle bin (recoverable).", - )) - specs.append(CommandSpec( - "AC_create_signing_keypair", "Security", "Create Signing Key Pair", - fields=( - FieldSpec("private_path", FieldType.STRING), - FieldSpec("public_path", FieldType.STRING), - FieldSpec("passphrase", FieldType.STRING, optional=True, - placeholder="encrypts the private key file"), + CommandSpec( + "AC_move_to_trash", "Shell", "Move File to Recycle Bin", + fields=(FieldSpec("path", FieldType.FILE_PATH),), + description="Delete a file to the OS recycle bin (recoverable).", ), - description="Create an Ed25519 key pair: keep the private key on the " - "signing machine, give execution endpoints the public key.", - )) - specs.append(CommandSpec( - "AC_sign_action_file", "Security", "Sign Action File", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("key", FieldType.STRING, optional=True), - FieldSpec("private_key_path", FieldType.FILE_PATH, optional=True), + CommandSpec( + "AC_create_signing_keypair", "Security", "Create Signing Key Pair", + fields=( + FieldSpec("private_path", FieldType.STRING), + FieldSpec("public_path", FieldType.STRING), + FieldSpec("passphrase", FieldType.STRING, optional=True, + placeholder="encrypts the private key file"), + ), + description="Create an Ed25519 key pair: keep the private key on the " + "signing machine, give execution endpoints the public key.", ), - description="Write a signature sidecar for an action file " - "(Ed25519 with a private key, else HMAC-SHA256).", - )) - specs.append(CommandSpec( - "AC_verify_action_file", "Security", "Verify Action File", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("key", FieldType.STRING, optional=True), - FieldSpec("raise_on_fail", FieldType.BOOL, optional=True, - default=False), - FieldSpec("public_key_path", FieldType.FILE_PATH, optional=True), + CommandSpec( + "AC_sign_action_file", "Security", "Sign Action File", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("key", FieldType.STRING, optional=True), + FieldSpec("private_key_path", FieldType.FILE_PATH, optional=True), + ), + description="Write a signature sidecar for an action file " + "(Ed25519 with a private key, else HMAC-SHA256).", ), - description="Verify an action file against its signature sidecar.", - )) - specs.append(CommandSpec( - "AC_encrypt_action_file", "Security", "Encrypt Action File", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("key", FieldType.STRING, optional=True), + CommandSpec( + "AC_verify_action_file", "Security", "Verify Action File", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("key", FieldType.STRING, optional=True), + FieldSpec("raise_on_fail", FieldType.BOOL, optional=True, + default=False), + FieldSpec("public_key_path", FieldType.FILE_PATH, optional=True), + ), + description="Verify an action file against its signature sidecar.", ), - description="Fernet-encrypt an action file to .enc.", - )) - specs.append(CommandSpec( - "AC_decrypt_action_file", "Security", "Decrypt Action File", - fields=( - FieldSpec("enc_path", FieldType.FILE_PATH), - FieldSpec("key", FieldType.STRING, optional=True), - FieldSpec("output_path", FieldType.STRING, optional=True), + CommandSpec( + "AC_encrypt_action_file", "Security", "Encrypt Action File", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("key", FieldType.STRING, optional=True), + ), + description="Fernet-encrypt an action file to .enc.", ), - description="Decrypt a Fernet-encrypted action file.", - )) - specs.append(CommandSpec( - "AC_annotate_screenshot", "Report", "Annotate Screenshot", - fields=( - FieldSpec("source", FieldType.FILE_PATH), - FieldSpec("output_path", FieldType.STRING), - FieldSpec("annotations", FieldType.STRING, - placeholder='[{"type":"box","rect":[10,10,80,40]}]'), + CommandSpec( + "AC_decrypt_action_file", "Security", "Decrypt Action File", + fields=( + FieldSpec("enc_path", FieldType.FILE_PATH), + FieldSpec("key", FieldType.STRING, optional=True), + FieldSpec("output_path", FieldType.STRING, optional=True), + ), + description="Decrypt a Fernet-encrypted action file.", ), - description="Draw boxes / highlights / arrows / labels onto an image.", - )) - specs.append(CommandSpec( - "AC_notify", "Report", "Desktop Notification", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("message", FieldType.STRING, optional=True), + CommandSpec( + "AC_annotate_screenshot", "Report", "Annotate Screenshot", + fields=( + FieldSpec("source", FieldType.FILE_PATH), + FieldSpec("output_path", FieldType.STRING), + FieldSpec("annotations", FieldType.STRING, + placeholder='[{"type":"box","rect":[10,10,80,40]}]'), + ), + description="Draw boxes / highlights / arrows / labels onto an image.", ), - description="Show a cross-platform desktop notification.", - )) - specs.append(CommandSpec( - "AC_region_color_stats", "Report", "Region Colour Stats", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - FieldSpec("buckets", FieldType.INT, optional=True, default=8, - min_value=1), + CommandSpec( + "AC_notify", "Report", "Desktop Notification", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("message", FieldType.STRING, optional=True), + ), + description="Show a cross-platform desktop notification.", ), - description="Average + dominant colour of a screen region.", - )) + CommandSpec( + "AC_region_color_stats", "Report", "Region Colour Stats", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + FieldSpec("buckets", FieldType.INT, optional=True, default=8, + min_value=1), + ), + description="Average + dominant colour of a screen region.", + ), + ]) def _add_mobile_specs(specs: List[CommandSpec]) -> None: @@ -5392,26 +5469,28 @@ def _add_mobile_specs(specs: List[CommandSpec]) -> None: def _add_run_control_specs(specs: List[CommandSpec]) -> None: """Stoppable runs: name a run, list the ones in progress, stop one at its next checkpoint.""" - specs.append(CommandSpec( - "AC_run_stoppable", "Flow", "Stoppable Run", - fields=( - FieldSpec("run_id", FieldType.STRING, optional=True, placeholder="nightly"), + specs.extend([ + CommandSpec( + "AC_run_stoppable", "Flow", "Stoppable Run", + fields=( + FieldSpec("run_id", FieldType.STRING, optional=True, placeholder="nightly"), + ), + body_keys=("body",), + description="Run the body as a named run that AC_stop_execution can end.", ), - body_keys=("body",), - description="Run the body as a named run that AC_stop_execution can end.", - )) - specs.append(CommandSpec( - "AC_stop_execution", "Flow", "Stop Execution", - fields=( - FieldSpec("run_id", FieldType.STRING, optional=True, placeholder="nightly"), - FieldSpec("reason", FieldType.STRING, optional=True), + CommandSpec( + "AC_stop_execution", "Flow", "Stop Execution", + fields=( + FieldSpec("run_id", FieldType.STRING, optional=True, placeholder="nightly"), + FieldSpec("reason", FieldType.STRING, optional=True), + ), + description="Ask a stoppable run (or every other one) to stop at its next checkpoint.", ), - description="Ask a stoppable run (or every other one) to stop at its next checkpoint.", - )) - specs.append(CommandSpec( - "AC_list_executions", "Flow", "List Stoppable Runs", - description="List the stoppable runs in progress.", - )) + CommandSpec( + "AC_list_executions", "Flow", "List Stoppable Runs", + description="List the stoppable runs in progress.", + ), + ]) def _add_user_specs(specs: List[CommandSpec]) -> None: """RBAC user management; under RBAC these need the ``manage_users`` capability.""" diff --git a/je_auto_control/linux_wayland/_select_input.py b/je_auto_control/linux_wayland/_select_input.py index 6fcdf86fa..d91394417 100644 --- a/je_auto_control/linux_wayland/_select_input.py +++ b/je_auto_control/linux_wayland/_select_input.py @@ -92,7 +92,7 @@ def emitted(backend, send) -> bool: from je_auto_control.linux_wayland.libei import LibeiUnavailable try: send(backend) - except (LibeiUnavailable, RuntimeError, OSError) as error: + except (RuntimeError, OSError) as error: # LibeiUnavailable is a RuntimeError _refuse_required(error) if not isinstance(error, LibeiUnavailable): raise diff --git a/je_auto_control/utils/action_journal/events.py b/je_auto_control/utils/action_journal/events.py index 9be34d80e..75d01228c 100644 --- a/je_auto_control/utils/action_journal/events.py +++ b/je_auto_control/utils/action_journal/events.py @@ -9,6 +9,7 @@ EventStatus = Literal["incomplete", "ok", "error", "cancelled", "skipped", "dry_run"] _STATUSES = frozenset({"incomplete", "ok", "error", "cancelled", "skipped", "dry_run"}) +_NON_FINITE_TIMESTAMP = "journal timestamps must be finite numbers" OUTCOME_TYPES = frozenset({"none", "boolean", "integer", "number", "text", "object", "array", "unsupported"}) @@ -34,13 +35,13 @@ def _integer(value: object) -> int: def _number(value: object) -> float: if isinstance(value, bool) or not isinstance(value, (int, float)): - raise ActionJournalError("journal timestamps must be finite numbers") + raise ActionJournalError(_NON_FINITE_TIMESTAMP) try: number = float(value) except OverflowError: - raise ActionJournalError("journal timestamps must be finite numbers") from None + raise ActionJournalError(_NON_FINITE_TIMESTAMP) from None if not math.isfinite(number): - raise ActionJournalError("journal timestamps must be finite numbers") + raise ActionJournalError(_NON_FINITE_TIMESTAMP) return number diff --git a/je_auto_control/utils/action_journal/store.py b/je_auto_control/utils/action_journal/store.py index c228c46e7..df100bd69 100644 --- a/je_auto_control/utils/action_journal/store.py +++ b/je_auto_control/utils/action_journal/store.py @@ -117,7 +117,7 @@ def _decode(raw: bytes, line_number: int) -> ActionEvent: try: payload: object = json.loads(raw.decode("utf-8")) return ActionEvent.from_dict(payload) - except (ValueError, TypeError, UnicodeError): + except (ValueError, TypeError): # UnicodeError is a ValueError # Do not include raw data or JSON decoder messages: external journals # may contain secrets even though this writer never emits those bytes. raise ActionJournalError(f"invalid action journal record at line {line_number}") from None diff --git a/je_auto_control/utils/agent_runtime/android_hierarchy.py b/je_auto_control/utils/agent_runtime/android_hierarchy.py index d382352d6..4a7bdd693 100644 --- a/je_auto_control/utils/agent_runtime/android_hierarchy.py +++ b/je_auto_control/utils/agent_runtime/android_hierarchy.py @@ -22,10 +22,10 @@ 'long-clickable', 'password', 'selected'}) _FIELDS = _FLAGS | {'index', 'text', 'resource-id', 'class', 'package', 'content-desc', 'bounds'} _OPTIONAL = frozenset({'NAF', 'editable', 'visible-to-user'}) -_INDEX = re.compile(r'0|[1-9][0-9]{0,4}') -_PATH = re.compile(r'0(?:/(?:0|[1-9][0-9]{0,4})){0,31}') -_BOUNDS = re.compile(r'\[(0|[1-9][0-9]{0,4}),(0|[1-9][0-9]{0,4})\]' - r'\[(0|[1-9][0-9]{0,4}),(0|[1-9][0-9]{0,4})\]') +_INDEX = re.compile(r'0|[1-9]\d{0,4}', re.ASCII) +_PATH = re.compile(r'0(?:/(?:0|[1-9]\d{0,4})){0,31}', re.ASCII) +_BOUNDS = re.compile(r'\[(0|[1-9]\d{0,4}),(0|[1-9]\d{0,4})\]' + r'\[(0|[1-9]\d{0,4}),(0|[1-9]\d{0,4})\]', re.ASCII) _ROLES = {'android.widget.Button': 'button', 'android.widget.ImageButton': 'button', 'android.widget.CheckBox': 'checkbox', 'android.widget.RadioButton': 'radio', 'android.widget.Switch': 'switch', 'android.widget.EditText': 'textfield', @@ -215,7 +215,7 @@ def _element(self, attrs: dict, flags: dict, frame: _Frame, parent_visible: bool def finish(self) -> list[dict]: """Never accept missing grants, empty/multiple roots or unfinished XML state.""" - if self.stack or not self.root_seen or not self.elements or not self.grants <= self.seen: + if self.stack or not self.root_seen or not self.elements or not self.grants.issubset(self.seen): raise AgentTaskError('Android hierarchy is incomplete or has stale text grants') return self.elements diff --git a/je_auto_control/utils/agent_runtime/android_input_evidence.py b/je_auto_control/utils/agent_runtime/android_input_evidence.py index 8882d24c7..1f1f7c91d 100644 --- a/je_auto_control/utils/agent_runtime/android_input_evidence.py +++ b/je_auto_control/utils/agent_runtime/android_input_evidence.py @@ -15,6 +15,7 @@ INPUT_END = " RecentQueue:" POLICY_START = " KeyguardServiceDelegate\n" POLICY_END = " Looper state:\n" +_EMPTY = "" def _text(value: bytes) -> str: @@ -96,8 +97,8 @@ def _routing(prefix: str, snapshot: AndroidSnapshot) -> None: def _queues(current: str, snapshot: AndroidSnapshot) -> str: block = _section(current, " PendingEvent:", " AppSwitch:") + _one(current, r"^( AppSwitch: [^\n]*\n)") - _required(block, {"PendingEvent": "", "InboundQueue": "", "ReplacedKeys": "", - "CommandQueue": "", "AppSwitch": "not pending"}, indent=2, separator=": ") + _required(block, {"PendingEvent": "", "InboundQueue": _EMPTY, "ReplacedKeys": _EMPTY, + "CommandQueue": _EMPTY, "AppSwitch": "not pending"}, indent=2, separator=": ") _connections(block, snapshot) return block @@ -137,7 +138,7 @@ def _monitors(lines: list[str]) -> bool: def _display(prefix: str) -> tuple[int, int, list[str], bool]: - if re.findall(r"^ Display: ([^\n]*)$", prefix, re.MULTILINE) != ["0"]: + if re.findall(r"^ {2}Display: ([^\n]*)$", prefix, re.MULTILINE) != ["0"]: raise AgentTaskError("Android input proof requires one identified default display") match = re.search(r"^ Display: 0\n logicalSize=(\d{1,5})x(\d{1,5})\n" r" transform \(ROT_0\) \(IDENTITY\)\n Windows:\n", prefix, re.MULTILINE) diff --git a/je_auto_control/utils/agent_runtime/android_input_windows.py b/je_auto_control/utils/agent_runtime/android_input_windows.py index ebd11ea5b..1ad92a10c 100644 --- a/je_auto_control/utils/agent_runtime/android_input_windows.py +++ b/je_auto_control/utils/agent_runtime/android_input_windows.py @@ -23,7 +23,7 @@ r"touchOcclusionMode=(?:BLOCK_UNTRUSTED|USE_OPACITY|ALLOW)") _TRANSFORM = re.compile(r" transform \((?:ROT_0|ROT_90|ROT_180|ROT_270|FLIP_H|FLIP_V)\) " r"\((?:IDENTITY|(?:SCALE |ROTATE |TRANSLATE){1,3})\)") -_MATRIX = re.compile(r" -?\d{1,10}\.\d{4} -?\d{1,10}\.\d{4} -?\d{1,10}\.\d{4}") +_MATRIX = re.compile(r" {12}-?\d{1,10}\.\d{4} {2}-?\d{1,10}\.\d{4} {2}-?\d{1,10}\.\d{4}") def android_input_name(snapshot: AndroidSnapshot) -> str: diff --git a/je_auto_control/utils/agent_runtime/android_scope.py b/je_auto_control/utils/agent_runtime/android_scope.py index a000cbea7..9df0937e2 100644 --- a/je_auto_control/utils/agent_runtime/android_scope.py +++ b/je_auto_control/utils/agent_runtime/android_scope.py @@ -7,10 +7,10 @@ from je_auto_control.utils.agent_runtime.errors import AgentTaskError _DEVICE = re.compile(r"[A-Za-z0-9._:-]{1,128}") -_PACKAGE = re.compile(r"[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+") +_PACKAGE = re.compile(r"[A-Za-z]\w*(?:\.[A-Za-z]\w*)+", re.ASCII) _BOOT = re.compile(r"[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}") -_WINDOW = re.compile(r"Window\{([0-9a-f]+) u([0-9]+) ([A-Za-z0-9_.]+)/([A-Za-z0-9_.$]+)\}") -_ACTIVITY = re.compile(r"ActivityRecord\{[0-9a-f]+ u([0-9]+) ([A-Za-z0-9_.]+)/[A-Za-z0-9_.$]+ t[0-9]+\}") +_WINDOW = re.compile(r"Window\{([0-9a-f]+) u(\d+) ([A-Za-z0-9_.]+)/([A-Za-z0-9_.$]+)\}", re.ASCII) +_ACTIVITY = re.compile(r"ActivityRecord\{[0-9a-f]+ u(\d+) ([A-Za-z0-9_.]+)/[A-Za-z0-9_.$]+ t\d+\}", re.ASCII) IDENTITY_MARKER = "__JE_ANDROID_IDENTITY_V1__" IDENTITY_PROBE = ( f"/system/bin/printf '{IDENTITY_MARKER}\\n'; " @@ -77,7 +77,7 @@ def parse_android_snapshot(output: bytes, physical_id: str, package: str, *, use raise AgentTaskError("Android identity evidence exceeds its closed byte format") try: marker, device, boot, dump = output.decode("utf-8").split("\n", 3) - except (UnicodeError, ValueError): + except ValueError: # includes UnicodeError raise AgentTaskError("Android identity evidence is incomplete or invalid UTF-8") from None if marker != IDENTITY_MARKER or device != physical_id: raise AgentTaskError("Android endpoint does not identify the reviewed physical device") diff --git a/je_auto_control/utils/agent_runtime/android_session.py b/je_auto_control/utils/agent_runtime/android_session.py index 009b87f15..400fa4488 100644 --- a/je_auto_control/utils/agent_runtime/android_session.py +++ b/je_auto_control/utils/agent_runtime/android_session.py @@ -50,7 +50,7 @@ def _select(self, timeout: float, check: Callable[[], None]) -> int: if len(rows) != 1 or rows[0].state != "device": raise AgentTaskError("reviewed Android endpoint is ambiguous, offline or unauthorized") value = rows[0].transport_id - if (not isinstance(value, str) or re.fullmatch(r"[1-9][0-9]{0,19}", value) is None + if (not isinstance(value, str) or re.fullmatch(r"[1-9]\d{0,19}", value, re.ASCII) is None or int(value) > 2**64 - 1): raise AgentTaskError("Android endpoint lacks an exact transport identity") return int(value) diff --git a/je_auto_control/utils/agent_runtime/browser_environment.py b/je_auto_control/utils/agent_runtime/browser_environment.py index 1281ceb6e..1e316d7f4 100644 --- a/je_auto_control/utils/agent_runtime/browser_environment.py +++ b/je_auto_control/utils/agent_runtime/browser_environment.py @@ -69,7 +69,7 @@ def validate_browser_probe(value: Any, transfer: bool) -> dict[str, Any]: if not isinstance(value, dict) or set(value) != _KEYS: raise AgentTaskError("invalid browser dependency probe response") python = value["python"] - if not isinstance(python, str) or re.fullmatch(r"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}", python) is None: + if not isinstance(python, str) or re.fullmatch(r"\d{1,3}\.\d{1,3}\.\d{1,3}", python, re.ASCII) is None: raise AgentTaskError("invalid browser dependency probe Python version") if any(not isinstance(value[key], bool) for key in _FLAGS): raise AgentTaskError("invalid browser dependency probe flags") diff --git a/je_auto_control/utils/agent_runtime/browser_retirement.py b/je_auto_control/utils/agent_runtime/browser_retirement.py index ee12bf091..4980917cb 100644 --- a/je_auto_control/utils/agent_runtime/browser_retirement.py +++ b/je_auto_control/utils/agent_runtime/browser_retirement.py @@ -6,17 +6,19 @@ from je_auto_control.utils.agent_runtime.errors import AgentTaskError +_UNCONFIRMED = "browser retirement is unconfirmed" + def retirement_proof(value: Any, context_id: str | None = None) -> dict: """Validate a closed proof from the already owned helper, optionally pinning its actual context.""" fields = {"contract", "input_supported", "network_retired", "context_id"} if not isinstance(value, dict) or set(value) != fields: - raise AgentTaskError("browser retirement is unconfirmed") + raise AgentTaskError(_UNCONFIRMED) # pylint: disable-next=unidiomatic-typecheck # reason: protocol integers must exclude boolean versions if type(value["contract"]) is not int or value["contract"] != 1: - raise AgentTaskError("browser retirement is unconfirmed") + raise AgentTaskError(_UNCONFIRMED) if value["input_supported"] is not False or value["network_retired"] is not True: - raise AgentTaskError("browser retirement is unconfirmed") + raise AgentTaskError(_UNCONFIRMED) _require_context(value["context_id"], context_id) return dict(value) diff --git a/je_auto_control/utils/agent_runtime/browser_target.py b/je_auto_control/utils/agent_runtime/browser_target.py index 3e9b6e263..42f42c499 100644 --- a/je_auto_control/utils/agent_runtime/browser_target.py +++ b/je_auto_control/utils/agent_runtime/browser_target.py @@ -16,6 +16,8 @@ from je_auto_control.utils.agent_runtime.browser_retirement import retirement_proof from je_auto_control.utils.agent_runtime.browser_readonly_scope import readonly_verification_scope +_RETIREMENT_UNCONFIRMED = "browser retirement is unconfirmed" + def _open_session(origins: tuple[str, ...], *, transfer: bool = False) -> Any: try: @@ -108,19 +110,19 @@ def freeze_readonly(self) -> dict: self._session = self._session.freeze_readonly() result = retirement_proof(self._session.capabilities(), identity) except Exception: # noqa: BLE001 # reason: contain optional SDK failures without input revival or data leaks - raise AgentTaskError("browser retirement is unconfirmed") from None + raise AgentTaskError(_RETIREMENT_UNCONFIRMED) from None self._readonly = result["context_id"] return result def readonly_capabilities(self) -> dict: """Check the current actual SDK view rather than trusting a saved description.""" if not self._readonly or self._session is None: - raise AgentTaskError("browser retirement is unconfirmed") + raise AgentTaskError(_RETIREMENT_UNCONFIRMED) try: return retirement_proof(self._session.capabilities(), self._readonly) except Exception: # noqa: BLE001 # reason: SDK faults stay bounded and leave this target permanently read-only self._readonly = "" - raise AgentTaskError("browser retirement is unconfirmed") from None + raise AgentTaskError(_RETIREMENT_UNCONFIRMED) from None @property def readonly_spec(self) -> AgentTaskSpec: diff --git a/je_auto_control/utils/agent_runtime/browser_workflows.py b/je_auto_control/utils/agent_runtime/browser_workflows.py index 68ec85c7d..2b4c20f36 100644 --- a/je_auto_control/utils/agent_runtime/browser_workflows.py +++ b/je_auto_control/utils/agent_runtime/browser_workflows.py @@ -135,9 +135,11 @@ def _commit(actions: list[dict], config: dict, confirmation: Any = None) -> None expected = _text(confirmation if confirmation is not None else config.get("confirmation_value"), "confirmation") locator = _locator(config.get("confirmation_selector"), config.get("confirmation_frame", "")) submit = _locator(config.get("submit_selector"), config.get("submit_frame", "")) - actions.append({**_action("AC_browser_read", {**locator, "operation": "text"}), "expect_not_result": expected}) - actions.append({**_action("AC_browser_click", submit, True), "side_effect": "submit"}) - actions.append(_action("AC_browser_wait", _wait_arguments(config, locator, "text", expected), expected)) + actions.extend([ + {**_action("AC_browser_read", {**locator, "operation": "text"}), "expect_not_result": expected}, + {**_action("AC_browser_click", submit, True), "side_effect": "submit"}, + _action("AC_browser_wait", _wait_arguments(config, locator, "text", expected), expected), + ]) def _wait_arguments(config: dict, locator: dict, operation: str, expected: Any) -> dict: diff --git a/je_auto_control/utils/agent_runtime/client.py b/je_auto_control/utils/agent_runtime/client.py index ae1aeac3f..d7efa556a 100644 --- a/je_auto_control/utils/agent_runtime/client.py +++ b/je_auto_control/utils/agent_runtime/client.py @@ -12,6 +12,8 @@ from je_auto_control.utils.agent_runtime.transport import request_json from je_auto_control.utils.agent_runtime.control_admission import control_request +_INVALID_METADATA = "invalid local runtime connection metadata" + def _connection_identity(metadata: dict[str, Any]) -> str: generation = metadata.get("generation") @@ -24,16 +26,16 @@ def _connection_identity(metadata: dict[str, Any]) -> str: def _validate_local_metadata(value: Any) -> dict[str, Any]: if (not isinstance(value, dict) or not isinstance(value.get("url"), str) or not isinstance(value.get("token"), str) or not value["token"]): - raise AgentTaskError("invalid local runtime connection metadata") + raise AgentTaskError(_INVALID_METADATA) try: parsed = urlparse(value["url"]) valid = all((parsed.scheme == "http", parsed.hostname == "127.0.0.1", not parsed.path, parsed.username is None, parsed.password is None, not parsed.query, not parsed.fragment, parsed.port != 0)) except ValueError: - raise AgentTaskError("invalid local runtime connection metadata") from None + raise AgentTaskError(_INVALID_METADATA) from None if not valid: - raise AgentTaskError("invalid local runtime connection metadata") + raise AgentTaskError(_INVALID_METADATA) return value diff --git a/je_auto_control/utils/agent_runtime/completion_recovery.py b/je_auto_control/utils/agent_runtime/completion_recovery.py index fb89b64cd..2df44cd66 100644 --- a/je_auto_control/utils/agent_runtime/completion_recovery.py +++ b/je_auto_control/utils/agent_runtime/completion_recovery.py @@ -100,6 +100,7 @@ def _principals(details: dict) -> None: if not owns_task(owner, current_authorization()): raise AuthorizationError("completion reviewer can no longer manage the source owner") with DeferredAuthorization(AuthorizationContext(**owner) if owner else None).scope(): + # reason: entering the scope re-checks the owner's current rights; nothing runs inside it pass @@ -139,7 +140,7 @@ def completion_metadata(store: Any, task_id: str, *, runtime: Any = None) -> dic try: check_completion_authority(runtime, task_id) result['completion_authorization'] = 'active' - except (AuthorizationError, OSError, TypeError, ValueError, LookupError): + except (OSError, TypeError, ValueError, LookupError): # AuthorizationError is a PermissionError result.update(completion_contract=0, completion_authorization='unavailable') return result @@ -225,7 +226,10 @@ def _verification_template(value: dict) -> dict: def _details(runtime: Any, source: dict, effect: dict, ownership: dict | None, *, connection: Any = None) -> dict: actor = current_authorization() - reviewer = ownership["actor"] if ownership is not None else (asdict(actor) if actor else {}) + if ownership is not None: + reviewer = ownership["actor"] + else: + reviewer = asdict(actor) if actor else {} details = {"contract": 1, "source_task": source["id"], "source_snapshot": _snapshot(runtime, source, connection=connection), "effect_task": effect["id"], "effect_snapshot": _snapshot(runtime, effect, connection=connection), diff --git a/je_auto_control/utils/agent_runtime/dag_admission.py b/je_auto_control/utils/agent_runtime/dag_admission.py index f010a49da..3f465a897 100644 --- a/je_auto_control/utils/agent_runtime/dag_admission.py +++ b/je_auto_control/utils/agent_runtime/dag_admission.py @@ -76,4 +76,5 @@ def check_admission(runtime: Any, task_id: str) -> None: if not owns_task(owner, current_authorization()): raise AuthorizationError("local DAG caller can no longer manage the original owner's work") with DeferredAuthorization(AuthorizationContext(**owner) if owner else None).scope(): + # Entering the scope is the check: it refuses a revoked or read-only original owner. pass diff --git a/je_auto_control/utils/agent_runtime/dag_jobs.py b/je_auto_control/utils/agent_runtime/dag_jobs.py index 500485ce9..29bbf731e 100644 --- a/je_auto_control/utils/agent_runtime/dag_jobs.py +++ b/je_auto_control/utils/agent_runtime/dag_jobs.py @@ -20,11 +20,10 @@ def fence_delivery(runtime: Any, started: dict) -> None: """Retire only this delivery's scheduling attempt, preserving all node delivery evidence.""" # pylint: disable-next=protected-access # reason: serialize internal retirement with public batch transitions - with runtime._lock("dag:" + started["batch_id"]): - with runtime.store.connection(write=True) as connection: - connection.execute("UPDATE agent_dag_batches SET state='waiting_review',attempt='',updated=? " - "WHERE id=? AND attempt=? AND state='running'", - (time.time(), started["batch_id"], started["attempt"])) + with runtime._lock("dag:" + started["batch_id"]), runtime.store.connection(write=True) as connection: + connection.execute("UPDATE agent_dag_batches SET state='waiting_review',attempt='',updated=? " + "WHERE id=? AND attempt=? AND state='running'", + (time.time(), started["batch_id"], started["attempt"])) class BackgroundDags: diff --git a/je_auto_control/utils/agent_runtime/dag_service_client.py b/je_auto_control/utils/agent_runtime/dag_service_client.py index 03136e698..d1b11a32f 100644 --- a/je_auto_control/utils/agent_runtime/dag_service_client.py +++ b/je_auto_control/utils/agent_runtime/dag_service_client.py @@ -18,10 +18,9 @@ def __init__(self, runtime: Any, delivery: dict) -> None: def check(self) -> None: """Refresh the registrant and approver, and reject a fenced or replaced coordinator attempt.""" - with self.actor.scope(), self.owner.scope(): - with self.runtime.store.connection() as connection: - row = connection.execute("SELECT state,attempt FROM agent_dag_batches WHERE id=?", - (self.started["batch_id"],)).fetchone() + with self.actor.scope(), self.owner.scope(), self.runtime.store.connection() as connection: + row = connection.execute("SELECT state,attempt FROM agent_dag_batches WHERE id=?", + (self.started["batch_id"],)).fetchone() if row is None or row["state"] != "running" or row["attempt"] != self.started["attempt"]: raise AgentTaskError("background DAG attempt is inactive") diff --git a/je_auto_control/utils/agent_runtime/dag_transfer_stop.py b/je_auto_control/utils/agent_runtime/dag_transfer_stop.py index 264540428..92dc69991 100644 --- a/je_auto_control/utils/agent_runtime/dag_transfer_stop.py +++ b/je_auto_control/utils/agent_runtime/dag_transfer_stop.py @@ -68,8 +68,9 @@ def request(self, node: dict, receipt: dict) -> dict: result['task_id'] = identifier self._call(node, 'pause', {'task_id': identifier}) except _ERRORS: - # Missing acknowledgement cannot clear the claim or certify remote input retirement. - return result + # Missing acknowledgement cannot clear the claim or certify remote input retirement, + # so the result keeps its requires_review outcome. + pass return result def _local_admission(self, node: dict, identifier: str) -> None: diff --git a/je_auto_control/utils/agent_runtime/evidence.py b/je_auto_control/utils/agent_runtime/evidence.py index 030712b35..507ae1394 100644 --- a/je_auto_control/utils/agent_runtime/evidence.py +++ b/je_auto_control/utils/agent_runtime/evidence.py @@ -20,6 +20,8 @@ from je_auto_control.utils.run_history.history_store import HistoryStore from je_auto_control.utils.sqlite_support import last_row_id +_JOURNAL_NAME = 'journal.jsonl' + class TaskEvidence: """Own only artifacts under this runtime's cache; user output files and external journals stay separate.""" @@ -41,7 +43,7 @@ def directory(self, task_id: str) -> Path: def ensure(self, task_id: str) -> dict[str, Any]: """Create one history link and private artifact directory, idempotently after restart.""" - journal = self.cache.file(task_id, 'journal.jsonl') + journal = self.cache.file(task_id, _JOURNAL_NAME) with self.store.connection() as connection: current = self._current_link(connection, task_id, journal) if current is not None: @@ -51,7 +53,7 @@ def ensure(self, task_id: str) -> dict[str, Any]: self.history.list_runs(limit=1) # initialize the existing history schema before the shared transaction directory = self.directory(task_id) directory.mkdir(parents=True, exist_ok=True, mode=0o700) - journal = self.cache.file(task_id, 'journal.jsonl') + journal = self.cache.file(task_id, _JOURNAL_NAME) journal.touch(mode=0o600, exist_ok=True) with self.store.connection(write=True) as connection: self.cache.check_identity(connection) @@ -95,7 +97,7 @@ def metadata(self, task_id: str) -> dict[str, Any]: artifacts = [str(path) for path in paths if path.is_file() and not path.is_symlink()] return {"history_path": self.history.path, "history_id": row["history_id"], "journal_path": row["journal_path"], "artifacts": sorted(artifacts), - "artifact_cache_state": "owned" if Path(row['journal_path']) == directory / 'journal.jsonl' + "artifact_cache_state": "owned" if Path(row['journal_path']) == directory / _JOURNAL_NAME else "historical", "report_pending": pending is not None, "completion_contract": completion[0] if completion else 0} @@ -116,10 +118,11 @@ def flush(self, limit: int = 8) -> None: def action(self, task_id: str, receipt: dict[str, Any], *, finished: bool = False) -> None: """Mirror receipt certainty into the existing append-only action journal.""" self.ensure(task_id) - journal = self.cache.file(task_id, 'journal.jsonl') + journal = self.cache.file(task_id, _JOURNAL_NAME) self.cache.file(task_id, 'journal.jsonl.lock') - status: EventStatus = "incomplete" if not finished else ( - "ok" if receipt["status"] == "confirmed" else "error") + status: EventStatus = "incomplete" + if finished: + status = "ok" if receipt["status"] == "confirmed" else "error" ActionJournal(journal).append(ActionEvent( run_id=task_id, step_id=receipt["id"], command=receipt["command"], parameters=durable_payload(receipt["arguments"]), started_at=receipt["created"], @@ -188,7 +191,7 @@ def remove(self, task_id: str) -> None: def _owned_artifact(path: Path) -> bool: if path.is_symlink() or not path.is_file(): return False - return path.name in {"journal.jsonl", "journal.jsonl.lock", "report.json"} or ( + return path.name in {_JOURNAL_NAME, "journal.jsonl.lock", "report.json"} or ( re.fullmatch(r"[0-9a-f]{32}\.jpg", path.name) is not None) diff --git a/je_auto_control/utils/agent_runtime/evidence_handoff.py b/je_auto_control/utils/agent_runtime/evidence_handoff.py index 3a7862140..8fbcebe87 100644 --- a/je_auto_control/utils/agent_runtime/evidence_handoff.py +++ b/je_auto_control/utils/agent_runtime/evidence_handoff.py @@ -165,9 +165,12 @@ def _proof(self, connection: Any, request: dict, actor: dict, *, issuing: bool = issuer_policies: list[str] | None = None, authorization: dict | None = None) -> tuple[dict, list[dict], dict]: context = self._context(request, connection=connection, issuer_policies=issuer_policies) - allowed = ({'paused', 'waiting_user', 'recovering', 'cancelled', 'timed_out', 'failed', 'succeeded'} - if self.records.standalone else - {'waiting_review'} if issuing else {'waiting_review', 'approved', 'running'}) + if self.records.standalone: + allowed = {'paused', 'waiting_user', 'recovering', 'cancelled', 'timed_out', 'failed', 'succeeded'} + elif issuing: + allowed = {'waiting_review'} + else: + allowed = {'waiting_review', 'approved', 'running'} if context['state'] not in allowed: raise AgentTaskError('handoff batch is not in a reviewed evidence state') _manager(actor, context['binding']['owner']) diff --git a/je_auto_control/utils/agent_runtime/images.py b/je_auto_control/utils/agent_runtime/images.py index 7f70b6fb1..39bb1c86e 100644 --- a/je_auto_control/utils/agent_runtime/images.py +++ b/je_auto_control/utils/agent_runtime/images.py @@ -2,7 +2,6 @@ from __future__ import annotations import base64 -import binascii import io from typing import Any, Mapping @@ -34,7 +33,7 @@ def observation_image_bytes(image: Mapping[str, Any]) -> bytes: raise AgentTaskError("observation image exceeds its bounded encoding") try: data = base64.b64decode(encoded, validate=True) - except (ValueError, binascii.Error) as error: + except ValueError as error: # includes binascii.Error raise AgentTaskError("observation image encoding is invalid") from error dimensions = verify_jpeg(data) expected = image.get("image_size") diff --git a/je_auto_control/utils/agent_runtime/local_evidence.py b/je_auto_control/utils/agent_runtime/local_evidence.py index aeb13e634..34364fe21 100644 --- a/je_auto_control/utils/agent_runtime/local_evidence.py +++ b/je_auto_control/utils/agent_runtime/local_evidence.py @@ -14,6 +14,7 @@ def runtime_evidence_scope(runtime: Any) -> Iterator[None]: def check() -> None: with caller.scope(): + # Entering the scope is the check: it refuses a revoked or read-only caller. pass def read(operation: str, body: dict, _timeout: float) -> dict: diff --git a/je_auto_control/utils/agent_runtime/native_target.py b/je_auto_control/utils/agent_runtime/native_target.py index 1a0494302..d1b2731e0 100644 --- a/je_auto_control/utils/agent_runtime/native_target.py +++ b/je_auto_control/utils/agent_runtime/native_target.py @@ -86,8 +86,10 @@ def _ensure(self) -> HelperChannel: def check_resume(self, spec: AgentTaskSpec, receipts: list[dict]) -> None: """Fence UI prefixes before launching a replacement helper; file recovery uses persistent evidence.""" - kind = self._profile.kind if self._profile is not None else ( - "desktop" if spec.target == "local-desktop" else "") + if self._profile is not None: + kind = self._profile.kind + else: + kind = "desktop" if spec.target == "local-desktop" else "" if kind in {"browser", "desktop"}: connected = self._channel is not None and not self._channel.retired require_context_continuity(spec, receipts, connected, kind=kind) diff --git a/je_auto_control/utils/agent_runtime/presence_broker.py b/je_auto_control/utils/agent_runtime/presence_broker.py index d521755bb..39649c410 100644 --- a/je_auto_control/utils/agent_runtime/presence_broker.py +++ b/je_auto_control/utils/agent_runtime/presence_broker.py @@ -152,7 +152,7 @@ def run(source: Any, sink: Any, broker: PresenceBroker | None = None) -> None: return try: result = broker.dispatch(request['operation'], request['payload']) - except (OSError, EOFError, ValueError, AgentTaskError) as error: + except (OSError, EOFError, ValueError) as error: # ValueError covers AgentTaskError send_error(sink, request['id'], error) return send_result(sink, request['id'], result) diff --git a/je_auto_control/utils/agent_runtime/presence_frames.py b/je_auto_control/utils/agent_runtime/presence_frames.py index 89ef82c7a..7348eb1df 100644 --- a/je_auto_control/utils/agent_runtime/presence_frames.py +++ b/je_auto_control/utils/agent_runtime/presence_frames.py @@ -19,7 +19,7 @@ def encode_frame(value: dict) -> bytes: raise AgentTaskError('physical presence frame must be an object') try: result = json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(',', ':')).encode('utf-8') - except (TypeError, ValueError, UnicodeError, RecursionError) as error: + except (TypeError, ValueError, RecursionError) as error: raise AgentTaskError('physical presence frame must be finite UTF-8 JSON') from error if len(result) > 4096: raise AgentTaskError('physical presence frame exceeds4096bytes') @@ -43,7 +43,7 @@ def decode_frame(raw: bytes) -> dict: value = json.loads(raw.decode('utf-8'), object_pairs_hook=_unique) encode_frame(value) return value - except (TypeError, ValueError, UnicodeError, RecursionError) as error: + except (TypeError, ValueError, RecursionError) as error: raise AgentTaskError('physical presence frame is invalid') from error diff --git a/je_auto_control/utils/agent_runtime/presence_messages_linux.py b/je_auto_control/utils/agent_runtime/presence_messages_linux.py index 5d7558cdf..a9132502b 100644 --- a/je_auto_control/utils/agent_runtime/presence_messages_linux.py +++ b/je_auto_control/utils/agent_runtime/presence_messages_linux.py @@ -96,7 +96,7 @@ def _receive(connection: socket.socket, peer: LinuxPeerProcess) -> dict: return decode_frame(raw) except AgentTaskError: raise - except (OSError, UnicodeError, ValueError): + except (OSError, ValueError): raise AgentTaskError('Linux packet writer evidence cannot be verified') from None finally: _release(descriptors) diff --git a/je_auto_control/utils/agent_runtime/presence_peer_linux.py b/je_auto_control/utils/agent_runtime/presence_peer_linux.py index 9e1bd1ffe..67f88b140 100644 --- a/je_auto_control/utils/agent_runtime/presence_peer_linux.py +++ b/je_auto_control/utils/agent_runtime/presence_peer_linux.py @@ -65,7 +65,7 @@ def _field(text: str, name: str) -> str: def _decimal(value: str, minimum: int = 0) -> int: - if not re.fullmatch('[0-9]{1,20}', value): + if not re.fullmatch(r'\d{1,20}', value, re.ASCII): raise AgentTaskError('Linux connected-peer metadata integer is invalid') number = int(value) if not minimum <= number <= (1 << 64) - 1: @@ -156,7 +156,7 @@ def alive(self) -> bool: return True except AgentTaskError: raise - except (OSError, UnicodeError, ValueError): + except (OSError, ValueError): # ValueError covers UnicodeError raise AgentTaskError('Linux connected-peer identity cannot be verified') from None def close(self) -> None: @@ -211,7 +211,7 @@ def retain_connected_peer(connection: socket.socket) -> LinuxPeerProcess: return peer except AgentTaskError: raise - except (OSError, UnicodeError, ValueError, struct.error): + except (OSError, ValueError, struct.error): # ValueError covers UnicodeError raise AgentTaskError('Linux connected-peer process evidence cannot be verified') from None finally: if descriptor is not None and not retained: diff --git a/je_auto_control/utils/agent_runtime/presence_pool.py b/je_auto_control/utils/agent_runtime/presence_pool.py index ea066b526..8680b12cf 100644 --- a/je_auto_control/utils/agent_runtime/presence_pool.py +++ b/je_auto_control/utils/agent_runtime/presence_pool.py @@ -312,7 +312,8 @@ def _needs_release(self, grant: _Grant) -> bool: def _run(self) -> None: try: self._loop() - except (OSError, EOFError, ValueError, TypeError, RuntimeError, AgentTaskError, CancelledError) as error: + # ValueError covers AgentTaskError. + except (OSError, EOFError, ValueError, TypeError, RuntimeError, CancelledError) as error: self._owner.failure = error self._owner.stop.set() finally: @@ -346,7 +347,7 @@ def _retire(self) -> None: reference.tokens.clear() self._references.difference_update(ref for ref in tuple(self._references) if ref.closed.is_set()) self._owner.retired.set() - except (OSError, RuntimeError, ValueError, TypeError, AgentTaskError) as error: + except (OSError, RuntimeError, ValueError, TypeError) as error: # ValueError covers AgentTaskError self._owner.failure = error diff --git a/je_auto_control/utils/agent_runtime/presence_security_windows.py b/je_auto_control/utils/agent_runtime/presence_security_windows.py index 39cecf59c..ac56bf859 100644 --- a/je_auto_control/utils/agent_runtime/presence_security_windows.py +++ b/je_auto_control/utils/agent_runtime/presence_security_windows.py @@ -120,7 +120,7 @@ def _sid_text(self, sid: int) -> str: if not pointer.value: raise AgentTaskError('physical presence native SID is empty') text = ctypes.wstring_at(pointer.value) - if len(text) > 192 or not re.fullmatch(r'S-1-(?:[0-9]+-)*[0-9]+', text): + if len(text) > 192 or not re.fullmatch(r'S-1-(?:\d+-)*\d+', text, re.ASCII): raise AgentTaskError('physical presence native SID is invalid') return text finally: diff --git a/je_auto_control/utils/agent_runtime/presence_server.py b/je_auto_control/utils/agent_runtime/presence_server.py index 7177295b9..0bb9e6775 100644 --- a/je_auto_control/utils/agent_runtime/presence_server.py +++ b/je_auto_control/utils/agent_runtime/presence_server.py @@ -81,7 +81,7 @@ def _failure(self, error: BaseException) -> None: if self._life.source is not None: try: self._life.source.close() - except (OSError, RuntimeError, ValueError, AgentTaskError) as cleanup: + except (OSError, RuntimeError, ValueError) as cleanup: with self._mutex: self._life.failures.append(type(cleanup).__name__) @@ -93,7 +93,7 @@ def _accept(self) -> None: except queue.Full: continue self._accept_one() - except (OSError, EOFError, RuntimeError, ValueError, AgentTaskError) as error: + except (OSError, EOFError, RuntimeError, ValueError) as error: if not self._life.stop.is_set(): self._failure(error) @@ -126,7 +126,7 @@ def _client(self, connection: Any) -> None: protocol = PresenceProtocol(self._life.source) peer = server_handshake(connection, self._security, protocol.generation) self._exchange(connection, protocol, peer) - except (OSError, EOFError, ValueError, AgentTaskError): + except (OSError, EOFError, ValueError): pass # reason: malformed/disconnected clients retire their own tokens, not the shared source finally: self._retire_client(connection, protocol, peer) @@ -149,7 +149,7 @@ def _retire_client(self, connection: Any, protocol: Any, peer: Any) -> None: cleanup.callback(peer.close) if protocol is not None: cleanup.callback(protocol.close) - except (OSError, RuntimeError, ValueError, AgentTaskError) as error: + except (OSError, RuntimeError, ValueError) as error: self._failure(error) finally: with self._mutex: @@ -167,7 +167,7 @@ def close(self) -> None: try: if resource is not None: resource.close() - except (OSError, RuntimeError, ValueError, AgentTaskError) as error: + except (OSError, RuntimeError, ValueError) as error: errors.append(type(error).__name__) self._join([self._life.acceptor, *clients.values()]) if errors or self._life.failures: diff --git a/je_auto_control/utils/agent_runtime/presence_windows.py b/je_auto_control/utils/agent_runtime/presence_windows.py index f6a503cee..7de8f4063 100644 --- a/je_auto_control/utils/agent_runtime/presence_windows.py +++ b/je_auto_control/utils/agent_runtime/presence_windows.py @@ -18,6 +18,8 @@ from je_auto_control.utils.agent_runtime.presence_platform import default_presence_api from je_auto_control.utils.agent_runtime.presence_frames import presence_body as _body +_DEADLINE_EXHAUSTED = 'physical presence request deadline exhausted' + @dataclass class _Ticket: @@ -153,12 +155,12 @@ def _serve(self, api: Any) -> None: except queue.Empty: return if self._expired(ticket): - ticket.reply.put_nowait(AgentTaskError('physical presence request deadline exhausted')) + ticket.reply.put_nowait(AgentTaskError(_DEADLINE_EXHAUSTED)) continue if not self._fresh(api, ticket): continue if self._expired(ticket): - ticket.reply.put_nowait(AgentTaskError('physical presence request deadline exhausted')) + ticket.reply.put_nowait(AgentTaskError(_DEADLINE_EXHAUSTED)) continue try: result: Any = self._dispatch(ticket) @@ -187,10 +189,10 @@ def _reap(self) -> None: @contextmanager def _locked(self, deadline: float) -> Iterator[None]: if not self._mutex.acquire(timeout=max(0, deadline - time.monotonic())): - raise AgentTaskError('physical presence request deadline exhausted') + raise AgentTaskError(_DEADLINE_EXHAUSTED) try: if time.monotonic() >= deadline: - raise AgentTaskError('physical presence request deadline exhausted') + raise AgentTaskError(_DEADLINE_EXHAUSTED) yield finally: self._mutex.release() @@ -220,10 +222,10 @@ def request(self, operation: str, body: dict, timeout: float = 0.25) -> dict: result = ticket.reply.get(timeout=max(0, ticket.deadline - time.monotonic())) except queue.Empty as error: ticket.abandoned.set() - raise AgentTaskError('physical presence request deadline exhausted') from error + raise AgentTaskError(_DEADLINE_EXHAUSTED) from error if time.monotonic() >= ticket.deadline: ticket.abandoned.set() - raise AgentTaskError('physical presence request deadline exhausted') + raise AgentTaskError(_DEADLINE_EXHAUSTED) if isinstance(result, AgentTaskError): raise result ticket.accepted.set() diff --git a/je_auto_control/utils/agent_runtime/presence_worker.py b/je_auto_control/utils/agent_runtime/presence_worker.py index a183c28c7..70c2e77b5 100644 --- a/je_auto_control/utils/agent_runtime/presence_worker.py +++ b/je_auto_control/utils/agent_runtime/presence_worker.py @@ -67,7 +67,7 @@ def run(source: Any, sink: Any, *, source_factory: Callable[[], Any] = WindowsPr namespace = _namespace(request['payload']) server = PresenceServer(source_factory, pipe=pipe_factory(namespace=namespace)) result = server.start() - except (OSError, ValueError, AgentTaskError) as error: + except (OSError, ValueError) as error: send_error(sink, request['id'], error) return send_result(sink, request['id'], result) diff --git a/je_auto_control/utils/agent_runtime/presence_x11.py b/je_auto_control/utils/agent_runtime/presence_x11.py index e617c2ec9..327d83e62 100644 --- a/je_auto_control/utils/agent_runtime/presence_x11.py +++ b/je_auto_control/utils/agent_runtime/presence_x11.py @@ -53,7 +53,11 @@ def _query(self) -> bool: capture_output=True, text=True, timeout=1, check=False, shell=False) if result.returncode != 0: return False - values = dict(line.split('=', 1) for line in result.stdout.splitlines() if '=' in line) + values: dict[str, str] = {} + for line in result.stdout.splitlines(): + key, separator, value = line.partition('=') + if separator: + values[key] = value return values.get('Active') == 'yes' and values.get('LockedHint') == 'no' def interactive(self) -> bool: diff --git a/je_auto_control/utils/agent_runtime/retained_browser.py b/je_auto_control/utils/agent_runtime/retained_browser.py index af93b54fe..c1507e92c 100644 --- a/je_auto_control/utils/agent_runtime/retained_browser.py +++ b/je_auto_control/utils/agent_runtime/retained_browser.py @@ -14,6 +14,7 @@ _CAPACITY = 16 _LIFETIME = 300 +_UNAVAILABLE = "retained browser context is unavailable" def _binding(row: Mapping) -> dict: @@ -74,7 +75,7 @@ def commit(self, entry: RetainedBrowser, channel: Any, proof: dict) -> None: description = retirement_proof(proof) with self._lock: if self._closed or entry.invalid or channel.retired or self._clock() >= entry.expires: - raise AgentTaskError("retained browser context is unavailable") + raise AgentTaskError(_UNAVAILABLE) entry.proof = description def require(self, row: Mapping) -> RetainedBrowser: @@ -83,7 +84,7 @@ def require(self, row: Mapping) -> RetainedBrowser: with self._lock: entry = self._records.get(source["id"]) if entry is None or (entry.proof is None and not entry.invalid): - raise AgentTaskError("retained browser context is unavailable") + raise AgentTaskError(_UNAVAILABLE) same_source = (json.dumps(entry.source, sort_keys=True, allow_nan=False) == json.dumps(source, sort_keys=True, allow_nan=False)) invalid = (self._closed or entry.invalid or not same_source @@ -92,14 +93,14 @@ def require(self, row: Mapping) -> RetainedBrowser: return entry entry.invalid = True self.discard(source["id"]) - raise AgentTaskError("retained browser context is unavailable") + raise AgentTaskError(_UNAVAILABLE) def borrow(self, row: Mapping) -> RetainedBrowser: """Grant one transient read borrower while process lifetime stays with this collection.""" entry = self.require(row) with self._lock: if self._records.get(entry.source["id"]) is not entry or entry.invalid: - raise AgentTaskError("retained browser context is unavailable") + raise AgentTaskError(_UNAVAILABLE) if entry.busy: raise AgentTaskError("retained browser context is busy") entry.busy = True diff --git a/je_auto_control/utils/agent_runtime/runner.py b/je_auto_control/utils/agent_runtime/runner.py index 6c1bb464f..ce9e511d3 100644 --- a/je_auto_control/utils/agent_runtime/runner.py +++ b/je_auto_control/utils/agent_runtime/runner.py @@ -39,18 +39,15 @@ def run_task(runtime: Any, task_id: str, *, backend: Any = None) -> AgentTaskRes def _loop(runtime: Any, task_id: str, spec: AgentTaskSpec, backend: Any, guard: LoopGuard) -> AgentTaskResult: stale_attempts = 0 + step_guard = None if spec.mode == "skill" else guard while runtime.get(task_id).state in {"queued", "running"}: observation = runtime.observe(task_id) receipts = effective_actions(runtime.store.actions(task_id)) if spec.mode == "skill": - if not verified_prefix(spec, receipts): - raise AgentTaskError("previous workflow read-back did not match; review before continuing") - if len(receipts) >= len(spec.actions): - return runtime.finish(task_id) - proposal = thaw(spec.actions[len(receipts)]) + proposal = _skill_proposal(spec, receipts) else: proposal = _decision(runtime, task_id, spec, backend, observation.data, receipts) - if proposal.get("stop"): + if proposal is None or proposal.get("stop"): return runtime.finish(task_id) values = captures(spec, receipts) action = _grounded_action(proposal, values, observation.observation_id) @@ -62,10 +59,19 @@ def _loop(runtime: Any, task_id: str, spec: AgentTaskSpec, backend: Any, guard: stale_attempts += 1 continue stale_attempts = 0 - _check_step(proposal, values, result.result, action, guard if spec.mode != "skill" else None) + _check_step(proposal, values, result.result, action, step_guard) return runtime.get(task_id) +def _skill_proposal(spec: AgentTaskSpec, receipts: list[dict]) -> dict | None: + """Return the next declared workflow action, or None once every action has a receipt.""" + if not verified_prefix(spec, receipts): + raise AgentTaskError("previous workflow read-back did not match; review before continuing") + if len(receipts) >= len(spec.actions): + return None + return thaw(spec.actions[len(receipts)]) + + def _grounded_action(proposal: dict, values: dict, observation_id: str) -> dict: action = {"tool": proposal["tool"], "arguments": resolve(proposal.get("arguments", {}), values), "observation_id": observation_id} diff --git a/je_auto_control/utils/agent_runtime/runtime.py b/je_auto_control/utils/agent_runtime/runtime.py index 4a8bcdd79..8a845101d 100644 --- a/je_auto_control/utils/agent_runtime/runtime.py +++ b/je_auto_control/utils/agent_runtime/runtime.py @@ -47,8 +47,7 @@ ) from je_auto_control.utils.agent_runtime.completion_recovery import ( completion_target, completion_evidence, completion_metadata, require_executable_task, - check_completion_authority, completion_spec, completion_details, - retainable_browser, + check_completion_authority, completion_spec, completion_details, retainable_browser, ) from je_auto_control.utils.agent_runtime.verification import verify from je_auto_control.utils.agent_runtime.resume_gate import ResumeGate, validate_task_conditions @@ -134,8 +133,7 @@ def retired(task_id: str) -> bool: policy = (lambda value: TaskPolicy(bind_origin(AgentTaskSpec.from_dict(value))), submitted_digest, request_spec_digest, recorded_recovery_owner, self.batches.transfer_private_files, self.target_profiles.transfer_profile, origin_ceilings) - host = TransferHost(self, policy, check_completion_authority, - lambda: not self._closed, retired) + host = TransferHost(self, policy, check_completion_authority, lambda: not self._closed, retired) self._transfers = AgentTransfers(self, TransferPeer(self.batches.transfer_call), host=host, task_active=lambda: current_operation() is not None) return self._transfers @@ -215,7 +213,7 @@ def _request_match(self, record: dict[str, Any] | None, spec: AgentTaskSpec) -> digest = request_spec_digest(expected.to_dict()) except AuthorizationError: raise - except (AgentTaskError, OSError, ValueError): + except (OSError, ValueError): return "unverifiable" return "matches" if digest == record["spec_hash"] else "differs" @@ -307,6 +305,7 @@ def checkpoint() -> None: self.target_profiles.check(spec) check_task_authority(self, row["id"]) with DeferredAuthorization(owner).scope(): + # reason: entering the scope re-checks the owner's current rights; nothing runs inside it pass def guard(name: str, arguments: Any) -> None: @@ -325,11 +324,11 @@ def takeover() -> None: with self._mutex: self._executing.add(task_id) try: - with DeferredAuthorization(owner).scope(), cancellation_scope(self._token(task_id)): - with (command_guard(guard), execution_scope(thaw(spec.inputs)), - operation_scope(TaskOperation(spec, checkpoint, current_authorization(), takeover, - self._presence.reference))): - yield policy + with (DeferredAuthorization(owner).scope(), cancellation_scope(self._token(task_id)), + command_guard(guard), execution_scope(thaw(spec.inputs)), + operation_scope(TaskOperation(spec, checkpoint, current_authorization(), takeover, + self._presence.reference))): + yield policy except AuthorizationError: self.cancel(task_id) raise @@ -685,6 +684,7 @@ def maintain(self) -> None: self.target_profiles.check(AgentTaskSpec.from_dict(row["spec"])) check_task_authority(self, task_id) with DeferredAuthorization(owner).scope(): + # reason: entering the scope re-checks the owner's current rights; nothing runs inside it pass except (AutoControlException, OSError, ValueError): self._stop_owned(task_id, lease, "cancelled", @@ -732,8 +732,8 @@ def _retained_source(self, task_id: str) -> dict: spec = AgentTaskSpec.from_dict(row["spec"]) validate_root_snapshot(spec.roots) self.target_profiles.check(spec) - owner = AuthorizationContext(**row["owner"]) if row["owner"] else None - with DeferredAuthorization(owner).scope(): + with DeferredAuthorization(AuthorizationContext(**row["owner"]) if row["owner"] else None).scope(): + # reason: entering the scope re-checks the owner's current rights; nothing runs inside it pass return row diff --git a/je_auto_control/utils/agent_runtime/skill_parameters.py b/je_auto_control/utils/agent_runtime/skill_parameters.py index ca1c8aa79..12a94030d 100644 --- a/je_auto_control/utils/agent_runtime/skill_parameters.py +++ b/je_auto_control/utils/agent_runtime/skill_parameters.py @@ -9,7 +9,7 @@ from je_auto_control.utils.agent_runtime.errors import AgentTaskError from je_auto_control.utils.agent_runtime.models import detached -_NAME = re.compile(r"[A-Za-z][A-Za-z0-9_]{0,63}") +_NAME = re.compile(r"[A-Za-z]\w{0,63}", re.ASCII) _DATA = re.compile(r"/(?:actions/[0-9]+/(?:arguments/[^/]+(?:/[^/]+)*|expect_result|expect_not_result)|" r"inputs/[^/]+(?:/[^/]+)*|" r"success_criteria/[0-9]+/value)") @@ -103,7 +103,7 @@ def repair_data(spec: dict, changes: Any) -> dict: raise AgentTaskError("repair changes require path and value") pointer = change["path"] if not isinstance(pointer, str) or re.fullmatch( - r"/actions/[0-9]+/arguments/selector/(?:name|role|automation_id)", pointer) is None: + r"/actions/\d+/arguments/selector/(?:name|role|automation_id)", pointer, re.ASCII) is None: raise AgentTaskError("repair may change only existing semantic selector names/roles/IDs") parent, key = _slot(result, pointer) parent[key] = _typed(change["value"], "string") diff --git a/je_auto_control/utils/agent_runtime/target_profiles.py b/je_auto_control/utils/agent_runtime/target_profiles.py index c76ceabf7..7f5f835ef 100644 --- a/je_auto_control/utils/agent_runtime/target_profiles.py +++ b/je_auto_control/utils/agent_runtime/target_profiles.py @@ -169,10 +169,11 @@ def transfer_profile(self, name: str, *, connection: Any = None) -> dict: def _summary(self, name: str) -> dict: profile = self._profiles.get(name) - kind = profile.kind if profile is not None else {"local-desktop": "desktop", "local-files": "files"}.get( - name, "injected") - tools = list(profile.allowed_tools) if profile is not None else sorted( - reviewed_tools(kind)) if kind in {"desktop", "files", "browser"} else [] + if profile is not None: + kind, tools = profile.kind, list(profile.allowed_tools) + else: + kind = {"local-desktop": "desktop", "local-files": "files"}.get(name, "injected") + tools = sorted(reviewed_tools(kind)) if kind in {"desktop", "files", "browser"} else [] result: dict[str, Any] = {"name": name, "kind": kind, "enabled": self._enabled.get(name, True), "declared_tools": tools, "managed_profile": profile is not None, "readiness": "not_probed", "native_evidence": "not_recorded", diff --git a/je_auto_control/utils/agent_runtime/task_authority.py b/je_auto_control/utils/agent_runtime/task_authority.py index ed569d56d..20a9607ac 100644 --- a/je_auto_control/utils/agent_runtime/task_authority.py +++ b/je_auto_control/utils/agent_runtime/task_authority.py @@ -14,6 +14,8 @@ from je_auto_control.utils.rbac.authorization import AuthorizationContext, AuthorizationError, current_authorization from je_auto_control.utils.rbac.deferred import DeferredAuthorization +_INVALID_PRINCIPAL = 'invalid recovery execution principal' + @dataclass(frozen=True) class RecoveryBinding: @@ -137,14 +139,14 @@ def check_task_authority(runtime: Any, task_id: str) -> None: def _principal(value: Any) -> None: if not isinstance(value, dict): - raise AgentTaskError('invalid recovery execution principal') + raise AgentTaskError(_INVALID_PRINCIPAL) if not value: return if not {'user_id', 'role'} <= set(value) or set(value) - {'user_id', 'role', 'store_path'}: - raise AgentTaskError('invalid recovery execution principal') + raise AgentTaskError(_INVALID_PRINCIPAL) if (not all(isinstance(value[key], str) for key in ('user_id', 'role')) or value.get('store_path') is not None and not isinstance(value['store_path'], str)): - raise AgentTaskError('invalid recovery execution principal') + raise AgentTaskError(_INVALID_PRINCIPAL) def recorded_recovery_owner(connection: Any, task_id: str) -> dict | None: @@ -200,6 +202,7 @@ def _task_effect_identity(runtime: Any, connection: Any, row: dict, historical: else: check_task_authority(runtime, row['id']) with DeferredAuthorization(AuthorizationContext(**row['owner']) if row['owner'] else None).scope(): + # reason: entering the scope re-checks the owner's current rights; nothing runs inside it pass @@ -309,4 +312,5 @@ def _check_pair(value: dict) -> None: if not owns_task(owner, current_authorization()): raise AuthorizationError("recovery reviewer can no longer manage the source owner's work") with DeferredAuthorization(AuthorizationContext(**owner) if owner else None).scope(): + # reason: entering the scope re-checks the owner's current rights; nothing runs inside it pass diff --git a/je_auto_control/utils/agent_runtime/task_recovery.py b/je_auto_control/utils/agent_runtime/task_recovery.py index e24072f81..7ec36bfbf 100644 --- a/je_auto_control/utils/agent_runtime/task_recovery.py +++ b/je_auto_control/utils/agent_runtime/task_recovery.py @@ -35,6 +35,7 @@ "create_authorized_completion": {"task_id", "snapshot", "authorization_review", "note"}, } OPERATIONS = tuple(_REQUEST_FIELDS) + HANDOFF_OPERATIONS +_CAPTURE_KEY = "$capture" _STOPPED = frozenset({"paused", "waiting_user", "recovering", "cancelled", "timed_out", "failed"}) _POINTER_TOOLS = ("AC_click_mouse", "AC_set_mouse_position", "AC_mouse_scroll", "AC_press_mouse", "AC_release_mouse", "AC_android_tap", "AC_android_swipe") @@ -384,7 +385,7 @@ def _source_issues(spec: AgentTaskSpec, receipts: list[dict]) -> list[str]: return [] try: return [] if verified_prefix(spec, receipts) else ["source_readback_mismatch"] - except (KeyError, TypeError, ValueError, AgentTaskError): + except (KeyError, TypeError, ValueError): return ["source_readback_mismatch"] @@ -422,12 +423,12 @@ def _capture_names(value: Any) -> set[str]: def _capture_reference(value: dict) -> set[str]: - if "$capture" not in value: + if _CAPTURE_KEY not in value: return set() - if set(value) - {"$capture", "transform"} or not isinstance(value["$capture"], str) or not value["$capture"]: + if set(value) - {_CAPTURE_KEY, "transform"} or not isinstance(value[_CAPTURE_KEY], str) or not value[_CAPTURE_KEY]: raise AgentTaskError("capture references require a fresh named capture") _check_transform(value.get("transform", "identity")) - return {value["$capture"]} + return {value[_CAPTURE_KEY]} def _action_issues(actions: list[dict]) -> list[str]: diff --git a/je_auto_control/utils/agent_runtime/task_triggers.py b/je_auto_control/utils/agent_runtime/task_triggers.py index 085ca5663..4bc04d002 100644 --- a/je_auto_control/utils/agent_runtime/task_triggers.py +++ b/je_auto_control/utils/agent_runtime/task_triggers.py @@ -22,6 +22,7 @@ from je_auto_control.utils.rbac.deferred import DeferredAuthorization from je_auto_control.utils.exception.exceptions import AutoControlException +_TRIGGER_PREFIX = "trigger:" OPERATIONS = ("register", "list", "get", "enable", "disable", "fire", "remove") + AUTHORIZATION_OPERATIONS @@ -61,11 +62,10 @@ def register(self, request: Mapping[str, Any]) -> dict[str, Any]: spec = self.runtime._validated_spec(value["spec"]) config = validate_config(value["kind"], value.get("config", {}), TaskPolicy(spec)) identifier, now = uuid.uuid4().hex, time.time() - with DeferredAuthorization.capture().context_scope() as owner: - with DeferredAuthorization(owner).scope(): - metadata = {"user_id": owner.user_id, "role": owner.role, - "store_path": owner.store_path} if owner else {} - self._insert(identifier, value, spec, config, metadata, now) + with DeferredAuthorization.capture().context_scope() as owner, DeferredAuthorization(owner).scope(): + metadata = {"user_id": owner.user_id, "role": owner.role, + "store_path": owner.store_path} if owner else {} + self._insert(identifier, value, spec, config, metadata, now) return {"trigger_id": identifier, "enabled": True, "kind": value["kind"]} def _insert(self, identifier: str, value: dict, spec: AgentTaskSpec, config: dict, owner: dict, now: float) -> None: @@ -113,7 +113,7 @@ def _control_existing(self, operation: str, value: dict) -> dict: with self.runtime.store.connection() as connection: metadata = authorization_metadata(connection, row["id"]) return {"trigger": {**{key: item for key, item in row.items() if key != "owner"}, **metadata}} - with self.runtime._lock("trigger:" + row["id"]): + with self.runtime._lock(_TRIGGER_PREFIX + row["id"]): row = self._row(row["id"]) if operation in {"enable", "disable"}: return self._enabled(row, operation == "enable") @@ -158,9 +158,8 @@ def fire(self, row: dict, event_key: str, payload: Any = None) -> dict[str, Any] raise AgentTaskError("trigger event payload exceeds sixty-four kilobytes") digest = _digest(event_key) owner = AuthorizationContext(**row["owner"]) if row["owner"] else None - with DeferredAuthorization(owner).scope(): - with self.runtime._lock("trigger:" + row["id"]): - return self._fire_owned(row, digest, payload) + with DeferredAuthorization(owner).scope(), self.runtime._lock(_TRIGGER_PREFIX + row["id"]): + return self._fire_owned(row, digest, payload) def _fire_owned(self, row: dict, digest: str, payload: Any) -> dict: with self.runtime.store.connection() as connection: @@ -186,7 +185,7 @@ def _queue(self, row: dict, digest: str, payload: Any) -> dict: spec["inputs"] = {**spec.get("inputs", {}), "event": {"key": digest, "payload": payload}} spec["options"] = {**spec.get("options", {}), "trigger": {"id": row["id"], "event": digest}} validated = self.runtime._validated_spec(spec) - task = self.runtime._create_task(validated, "trigger:" + row["id"] + ":" + digest, + task = self.runtime._create_task(validated, _TRIGGER_PREFIX + row["id"] + ":" + digest, (row["id"], digest, _digest(payload))) if self.runtime.background is not None: self.runtime.background.start(task.task_id) diff --git a/je_auto_control/utils/agent_runtime/transfer_host.py b/je_auto_control/utils/agent_runtime/transfer_host.py index 742128e9e..5414e8763 100644 --- a/je_auto_control/utils/agent_runtime/transfer_host.py +++ b/je_auto_control/utils/agent_runtime/transfer_host.py @@ -99,6 +99,7 @@ def context(self, source_task: str, issuer: dict, *, connection: Any = None, with transaction as reading: row = self.managed(source_task, issuer, connection=reading) with _refresh(row['owner']).scope(): + # reason: entering the scope re-checks the owner's current rights; nothing runs inside it pass with _refresh(issuer).scope(): policy = self.policy(_with_origin_scope(row['spec'], origin_scope)) diff --git a/je_auto_control/utils/agent_runtime/transfer_import_ledger.py b/je_auto_control/utils/agent_runtime/transfer_import_ledger.py index 2ae1bfc55..300189326 100644 --- a/je_auto_control/utils/agent_runtime/transfer_import_ledger.py +++ b/je_auto_control/utils/agent_runtime/transfer_import_ledger.py @@ -10,6 +10,8 @@ QUEUE = 'agent_transfer_import' FINAL = frozenset({'published', 'requires_review', 'cancelled', 'expired'}) +_FAIL_OPEN_ITEM = ("UPDATE work_items SET status='failed',claim=claim+1,error=?,updated=? WHERE id=? " + "AND queue=? AND status IN ('new','in_progress')") def _delivery_data(data: Any, identifier: str, generation: int) -> bool: @@ -102,8 +104,7 @@ def recover(self, connection: Any) -> None: continue connection.execute("UPDATE agent_transfer_deliveries SET state='requires_review'," 'generation=generation+1,updated=? WHERE id=?', (time.time(), row['id'])) - connection.execute("UPDATE work_items SET status='failed',claim=claim+1,error=?,updated=? WHERE id=? " - "AND queue=? AND status IN ('new','in_progress')", + connection.execute(_FAIL_OPEN_ITEM, ('runtime restarted; import review required', time.time(), row['queue_id'], QUEUE)) def claim(self, item: Any) -> None: @@ -160,8 +161,7 @@ def cancel(self, connection: Any, identifier: str) -> None: state = 'requires_review' if row['publication'] else 'cancelled' connection.execute('UPDATE agent_transfer_deliveries SET state=?,generation=generation+1,updated=? ' 'WHERE id=?', (state, time.time(), identifier)) - connection.execute("UPDATE work_items SET status='failed',claim=claim+1,error=?,updated=? WHERE id=? " - "AND queue=? AND status IN ('new','in_progress')", + connection.execute(_FAIL_OPEN_ITEM, ('import cancelled', time.time(), row['queue_id'], QUEUE)) def expire(self, connection: Any, identifier: str) -> None: @@ -171,8 +171,7 @@ def expire(self, connection: Any, identifier: str) -> None: return connection.execute("UPDATE agent_transfer_deliveries SET state='expired',generation=generation+1,updated=? " 'WHERE id=?', (time.time(), identifier)) - connection.execute("UPDATE work_items SET status='failed',claim=claim+1,error=?,updated=? WHERE id=? " - "AND queue=? AND status IN ('new','in_progress')", + connection.execute(_FAIL_OPEN_ITEM, ('import receipt expired', time.time(), row['queue_id'], QUEUE)) def prunable(self, connection: Any, cutoff: float) -> list[str]: diff --git a/je_auto_control/utils/agent_runtime/transfer_lineage.py b/je_auto_control/utils/agent_runtime/transfer_lineage.py index 90b0e9914..aebabf67d 100644 --- a/je_auto_control/utils/agent_runtime/transfer_lineage.py +++ b/je_auto_control/utils/agent_runtime/transfer_lineage.py @@ -70,6 +70,7 @@ def _chain(self, connection: Any, leaf: dict, issuer: dict) -> tuple[list, list] seen.add(row['id']) self.manager(issuer, row['owner']) with DeferredAuthorization(AuthorizationContext(**row['owner']) if row['owner'] else None).scope(): + # reason: entering the scope re-checks the owner's current rights; nothing runs inside it pass if connection.execute('SELECT 1 FROM agent_control WHERE owner=?', (row['id'],)).fetchone(): raise AgentTaskError('source recovery ancestry still owns input control') diff --git a/je_auto_control/utils/agent_runtime/transfer_protocol.py b/je_auto_control/utils/agent_runtime/transfer_protocol.py index 0f397e96e..ba4b99dd4 100644 --- a/je_auto_control/utils/agent_runtime/transfer_protocol.py +++ b/je_auto_control/utils/agent_runtime/transfer_protocol.py @@ -18,7 +18,6 @@ IMPORT_FIELDS, TransferImporter, import_maintenance_pending, ) from je_auto_control.utils.config_sync.contract import ConfigStoreError, ConfigSyncError -from je_auto_control.utils.rbac.authorization import AuthorizationError from je_auto_control.utils.exception.cancellation import cancellation_checkpoint _OPERATION_SECONDS = 30 # the most a peer accepts as one request's timeout @@ -110,7 +109,8 @@ def _invoke(self, operation: str, request: Mapping[str, Any], *, peer: bool) -> result = handler(operation, request) self._checkpoint() return result - except (AuthorizationError, ConfigStoreError, ConfigSyncError, OSError) as error: + # OSError covers AuthorizationError, which is a PermissionError. + except (ConfigStoreError, ConfigSyncError, OSError) as error: raise AgentTaskError('transfer authority or private asset operation is unavailable') from error finally: with self._state: diff --git a/je_auto_control/utils/agent_runtime/transfer_records.py b/je_auto_control/utils/agent_runtime/transfer_records.py index 5c7ea4959..c344ab90b 100644 --- a/je_auto_control/utils/agent_runtime/transfer_records.py +++ b/je_auto_control/utils/agent_runtime/transfer_records.py @@ -11,6 +11,7 @@ MAX_MANIFEST_BYTES, MAX_TRANSFER_BYTES, _canonical, _deadline, _hex, ) +_UNSUPPORTED_FAMILY = 'unsupported private transfer family' _FAMILIES = {'export': 'agent_transfer_grants', 'evidence': 'agent_transfer_grants', 'import': 'agent_transfer_imports', 'route': 'agent_transfer_routed_reviews'} _TABLES = tuple(dict.fromkeys(_FAMILIES.values())) @@ -67,7 +68,7 @@ def _payload(record: dict[str, Any]) -> tuple[str, str]: _hex(record['actor'], 64) kind = record['kind'] if not isinstance(kind, str) or kind not in _FAMILIES: - raise AgentTaskError('unsupported private transfer family') + raise AgentTaskError(_UNSUPPORTED_FAMILY) _capacity_fields(record) _record_times(record) if not isinstance(record['scope'], dict): @@ -77,7 +78,7 @@ def _payload(record: dict[str, Any]) -> tuple[str, str]: values['key_hash'] = key_hash try: encoded = _canonical(values) - except (TypeError, ValueError, RecursionError, UnicodeError) as error: + except (TypeError, ValueError, RecursionError) as error: # ValueError covers UnicodeError raise AgentTaskError('transfer scope must contain finite JSON data') from error if len(encoded) > MAX_MANIFEST_BYTES: raise AgentTaskError('private transfer record exceeds one mebibyte') @@ -241,7 +242,7 @@ def review(self, connection: Any, review: str) -> dict[str, Any] | None: def entries(self, connection: Any, kind: str) -> list[dict[str, Any]]: """Return a bounded private family view for host-authorized management filtering.""" if kind not in _FAMILIES: - raise AgentTaskError('unsupported private transfer family') + raise AgentTaskError(_UNSUPPORTED_FAMILY) return [_view(row) for row in connection.execute(_sql(_FAMILIES[kind], 'entries'), (kind,))] def revoke(self, connection: Any, identifier: str) -> None: @@ -276,7 +277,7 @@ def prune_kind(self, connection: Any, kind: str, cutoff: float) -> int: """Prune one cleaned expired family under its owner's writer, preserving all consent tombstones.""" _writer(connection) if kind not in _FAMILIES: - raise AgentTaskError('unsupported private transfer family') + raise AgentTaskError(_UNSUPPORTED_FAMILY) _deadline(cutoff) cursor = connection.execute(_sql(_FAMILIES[kind], 'prune_kind'), (kind, min(cutoff, time.time()))) return cursor.rowcount diff --git a/je_auto_control/utils/agent_runtime/transfer_selection.py b/je_auto_control/utils/agent_runtime/transfer_selection.py index e93f1a36e..46d7ae4b6 100644 --- a/je_auto_control/utils/agent_runtime/transfer_selection.py +++ b/je_auto_control/utils/agent_runtime/transfer_selection.py @@ -107,7 +107,7 @@ def _manifest_selection(policy: Any, clause: dict, selection: TransferSelection, source = snapshot_file(policy, clause['path'], checkpoint, max_bytes=1_000_000, inspect=pieces.append) try: document = json.loads(b''.join(pieces).decode('utf-8')) - except (ValueError, UnicodeError) as error: + except ValueError as error: raise AgentTaskError('source file manifest is malformed') from error if (not isinstance(document, dict) or set(document) != {'files'} or not isinstance(document['files'], list) or len(document['files']) != len(clause['names'])): diff --git a/je_auto_control/utils/agent_runtime/transfer_transport.py b/je_auto_control/utils/agent_runtime/transfer_transport.py index 5154e8e1c..210a19acb 100644 --- a/je_auto_control/utils/agent_runtime/transfer_transport.py +++ b/je_auto_control/utils/agent_runtime/transfer_transport.py @@ -17,7 +17,6 @@ ) from je_auto_control.utils.config_sync.blob_models import CHUNK_BYTES from je_auto_control.utils.exception.cancellation import cancellation_checkpoint -from je_auto_control.utils.rbac.authorization import AuthorizationError _PEER_SECONDS = 30 # the longest timeout one peer request may carry @@ -79,7 +78,8 @@ def _exchange(self, host: str, binding: str, path: str, body: dict, deadline: fl return result except AgentTaskError: raise - except (AuthorizationError, OSError, ValueError, TypeError, KeyError, RecursionError) as error: + # OSError covers AuthorizationError, which is a PermissionError. + except (OSError, ValueError, TypeError, KeyError, RecursionError) as error: raise AgentTaskError('peer transfer is unavailable or its reply is malformed') from error @staticmethod diff --git a/je_auto_control/utils/agent_runtime/transport.py b/je_auto_control/utils/agent_runtime/transport.py index 729cfae5e..9f58472f2 100644 --- a/je_auto_control/utils/agent_runtime/transport.py +++ b/je_auto_control/utils/agent_runtime/transport.py @@ -31,23 +31,34 @@ def request_json(url: str, *, body: Any = None, token: str | None = None, def fetch() -> None: try: - headers = _headers(url, token, delegate_local, control=control) - from je_auto_control.utils.egress.egress_policy import get_egress_policy - get_egress_policy().check(url) - call = build_call(url, method="POST" if body is not None else "GET", - headers=headers, json_body=body, timeout=timeout) - call["follow_redirects"] = False - if urlsplit(url).path.endswith(('/agent/transfer', '/agent/transfer/peer')): - call['max_response_bytes'] = 1024 * 1024 - if len(call.get('body') or b'') > 1024 * 1024: - raise AgentTaskError('transfer request exceeds 1MiB') - replies.put(urllib_transport(call)) + replies.put(_fetch(url, body, token, timeout, control=control, delegate_local=delegate_local)) except BaseException as error: # reason: hand controls back to the owning caller; no failure is swallowed replies.put(error) finally: slots.release() threading.Thread(target=lambda: context.run(fetch), daemon=True, name="AgentHTTP").start() + return _await_reply(replies, timeout, check) + + +def _fetch(url: str, body: Any, token: str | None, timeout: float, *, + control: bool, delegate_local: bool) -> Any: + """Perform the bounded request on the worker thread under the caller's copied context.""" + headers = _headers(url, token, delegate_local, control=control) + from je_auto_control.utils.egress.egress_policy import get_egress_policy + get_egress_policy().check(url) + call = build_call(url, method="POST" if body is not None else "GET", + headers=headers, json_body=body, timeout=timeout) + call["follow_redirects"] = False + if urlsplit(url).path.endswith(('/agent/transfer', '/agent/transfer/peer')): + call['max_response_bytes'] = 1024 * 1024 + if len(call.get('body') or b'') > 1024 * 1024: + raise AgentTaskError('transfer request exceeds 1MiB') + return urllib_transport(call) + + +def _await_reply(replies: queue.Queue[Any], timeout: float, check: Callable[[], None]) -> dict[str, Any]: + """Poll for the worker's reply, checking cancellation, until the request deadline.""" deadline = time.monotonic() + timeout while time.monotonic() < deadline: check() diff --git a/je_auto_control/utils/agent_runtime/verification.py b/je_auto_control/utils/agent_runtime/verification.py index ded9b7a46..de22b791d 100644 --- a/je_auto_control/utils/agent_runtime/verification.py +++ b/je_auto_control/utils/agent_runtime/verification.py @@ -54,11 +54,14 @@ def verify_clauses(clauses: Sequence[Mapping[str, Any]], policy: TaskPolicy, rec for clause in clauses: try: passed = _evaluate(clause, policy, receipts, observation, browser_read) - status = "unknown" if passed is None else ("passed" if passed else "failed") + if passed is None: + status = "unknown" + else: + status = "passed" if passed else "failed" reports.append({"kind": clause["kind"], "status": status}) except (CancelledError, AuthorizationError, AgentHumanReview): raise - except (OSError, UnicodeError, TypeError, ValueError, LookupError, AgentTaskError) as error: + except (OSError, TypeError, ValueError, LookupError) as error: reports.append({"kind": clause["kind"], "status": "unknown", "reason": type(error).__name__}) return reports diff --git a/je_auto_control/utils/agent_runtime/workflow_specs.py b/je_auto_control/utils/agent_runtime/workflow_specs.py index e8d9e33b5..172b383ca 100644 --- a/je_auto_control/utils/agent_runtime/workflow_specs.py +++ b/je_auto_control/utils/agent_runtime/workflow_specs.py @@ -171,12 +171,14 @@ def _commit_actions(actions: list[dict], config: dict, row: str) -> None: if "confirmation_value" not in config: raise AgentTaskError("submission requires a deterministic confirmation_value") confirmation = str(config["confirmation_value"]) - actions.append({**_action("AC_workflow_control", {"operation": "get", - "selector": config["confirmation_selector"]}), "expect_not_result": confirmation, "row": row}) - actions.append({**_action("AC_workflow_control", {"operation": "invoke", - "selector": config["submit_selector"]}, True), "row": row, "side_effect": "submit"}) - actions.append({**_action("AC_workflow_wait", {"selector": config["confirmation_selector"], - "expected": confirmation}, confirmation), "row": row}) + actions.extend([ + {**_action("AC_workflow_control", {"operation": "get", "selector": config["confirmation_selector"]}), + "expect_not_result": confirmation, "row": row}, + {**_action("AC_workflow_control", {"operation": "invoke", "selector": config["submit_selector"]}, True), + "row": row, "side_effect": "submit"}, + {**_action("AC_workflow_wait", {"selector": config["confirmation_selector"], "expected": confirmation}, + confirmation), "row": row}, + ]) def _transfer(config: dict, _policy: TaskPolicy) -> dict: diff --git a/je_auto_control/utils/agent_runtime/workflow_values.py b/je_auto_control/utils/agent_runtime/workflow_values.py index 247cd8e4b..d02a3aa8d 100644 --- a/je_auto_control/utils/agent_runtime/workflow_values.py +++ b/je_auto_control/utils/agent_runtime/workflow_values.py @@ -6,6 +6,8 @@ from je_auto_control.utils.agent_runtime.errors import AgentTaskError from je_auto_control.utils.agent_runtime.models import AgentTaskSpec, thaw +_CAPTURE = "$capture" + def transformed(value: Any, operation: str = "identity") -> str: """Apply one allowlisted text transformation without evaluation or code execution.""" @@ -30,10 +32,10 @@ def captures(spec: AgentTaskSpec, receipts: Sequence[Mapping[str, Any]]) -> dict def resolve(value: Any, values: Mapping[str, Any]) -> Any: """Resolve exact capture objects recursively; external data cannot become command names.""" if isinstance(value, Mapping): - if "$capture" in value: - if set(value) - {"$capture", "transform"} or value["$capture"] not in values: + if _CAPTURE in value: + if set(value) - {_CAPTURE, "transform"} or value[_CAPTURE] not in values: raise AgentTaskError("workflow capture is missing or invalid") - return transformed(values[value["$capture"]], value.get("transform", "identity")) + return transformed(values[value[_CAPTURE]], value.get("transform", "identity")) return {key: resolve(item, values) for key, item in value.items()} if isinstance(value, (list, tuple)): return [resolve(item, values) for item in value] diff --git a/je_auto_control/utils/codegen/journal_source.py b/je_auto_control/utils/codegen/journal_source.py index 7cd8ffe58..6d97a3a8e 100644 --- a/je_auto_control/utils/codegen/journal_source.py +++ b/je_auto_control/utils/codegen/journal_source.py @@ -107,5 +107,5 @@ def _decode(raw: bytes, line: int) -> ActionEvent: try: payload = json.loads(raw, object_pairs_hook=unique_object) return ActionEvent.from_dict(payload) - except (ValueError, TypeError, UnicodeError, RecursionError): + except (ValueError, TypeError, RecursionError): raise CandidateGenerationError(f"invalid journal record at line {line}") from None diff --git a/je_auto_control/utils/config_sync/adapter_state.py b/je_auto_control/utils/config_sync/adapter_state.py index bb3786eb9..14e021cd3 100644 --- a/je_auto_control/utils/config_sync/adapter_state.py +++ b/je_auto_control/utils/config_sync/adapter_state.py @@ -23,6 +23,7 @@ from sqlite3 import Connection _APPLICATION = 0x41436164 +_BEGIN_IMMEDIATE = 'BEGIN IMMEDIATE' _TABLES = ( 'CREATE TABLE adapter_heads (scope TEXT NOT NULL,entry_id TEXT NOT NULL,change_id TEXT NOT NULL,' 'wire TEXT NOT NULL,digest TEXT NOT NULL,PRIMARY KEY(scope,change_id))', @@ -40,7 +41,7 @@ def _initialize(connection: Connection) -> None: version = _version(connection) if version == (_APPLICATION, 2): return - connection.execute('BEGIN IMMEDIATE') + connection.execute(_BEGIN_IMMEDIATE) version = _version(connection) if version == (_APPLICATION, 2): connection.execute('COMMIT') @@ -151,7 +152,7 @@ def update(self, kind: AdapterKind, sources: Optional[Mapping[str, dict[str, obj _validate_compacted(incoming, compacted) stored: set[str] = set() with self._database.connection() as connection: - connection.execute('BEGIN IMMEDIATE') + connection.execute(_BEGIN_IMMEDIATE) _profile_heads(connection, scope, kind) if sources is not None: capture_sources(connection, scope, kind, self.identity.origin, sources, checkpoint=self.checkpoint) @@ -174,7 +175,7 @@ def adopt(self, kind: AdapterKind, incoming: Mapping[str, SyncEntry], baseline: _validate_baseline(kind, baseline) scope = self._scope(kind) with self._database.connection() as connection: - connection.execute('BEGIN IMMEDIATE') + connection.execute(_BEGIN_IMMEDIATE) row = connection.execute('SELECT operation_id FROM adapter_adoptions WHERE scope=?', (scope,)).fetchone() if row is not None: if row[0] != operation_id: @@ -209,7 +210,7 @@ def resolve(self, kind: AdapterKind, entry_id: str, value: dict[str, object], *, validate_portable(value, root_key=entry_id if kind == 'settings' else '') expected = resolution_ids(observed) with self._database.connection() as connection: - connection.execute('BEGIN IMMEDIATE') + connection.execute(_BEGIN_IMMEDIATE) current = [item for item in _profile_heads(connection, scope, kind).values() if item.entry_id == entry_id] if sorted(expected) != sorted(item.change_id for item in current): raise ConfigSyncError('adapter conflict heads changed after review') @@ -232,7 +233,7 @@ def resolution_ids(observed: Iterable[str]) -> tuple[str, ...]: def _validate_compacted(incoming: Mapping[str, SyncEntry], compacted: frozenset[str]) -> None: - if (not isinstance(compacted, frozenset) or not compacted <= incoming.keys() + if (not isinstance(compacted, frozenset) or not compacted.issubset(incoming.keys()) or any(not incoming[key].is_deleted or incoming[key].value for key in compacted)): raise ConfigSyncError('compacted adapter evidence requires empty sealed deletion entries') diff --git a/je_auto_control/utils/config_sync/assets.py b/je_auto_control/utils/config_sync/assets.py index 62691c93f..135b52ba6 100644 --- a/je_auto_control/utils/config_sync/assets.py +++ b/je_auto_control/utils/config_sync/assets.py @@ -19,6 +19,7 @@ from je_auto_control.utils.exception.control_signals import raise_caused_run_interruption _CHUNK_BYTES = 64 * 1024 +_MANIFEST = 'manifest.json' _Cancelled = Optional[Callable[[], bool]] @@ -115,7 +116,7 @@ def _sync_assets(manifest: AssetManifest, transport: AssetTransport, *, path = _asset_path(files, asset) path.parent.mkdir(parents=True, exist_ok=True) _copy_asset(transport, asset, path, cancelled, staging) - with _output_file(stage / 'manifest.json', staging) as output: + with _output_file(stage / _MANIFEST, staging) as output: output.write(_encoded(manifest.to_dict()).encode('ascii')) output.flush() os.fsync(output.fileno()) @@ -201,7 +202,7 @@ def _generation_files(directory: Path, manifest: AssetManifest) -> Path: validate_path(directory, allowed_roots=[directory.parent]) if directory.is_symlink() or not directory.is_dir(): raise ConfigSyncError('asset generation is not an ordinary directory') - stamp = directory / 'manifest.json' + stamp = directory / _MANIFEST if stamp.is_symlink() or not stamp.is_file() or stamp.stat().st_size > 1024 * 1024: raise ConfigSyncError('invalid cached asset manifest') if stamp.read_bytes() != _encoded(manifest.to_dict()).encode('ascii'): @@ -246,7 +247,7 @@ def _source_file(root: Path, asset: AssetFile) -> Path: def _verify_layout(directory: Path, manifest: AssetManifest) -> None: for item in directory.iterdir(): - if item.name not in {'files', 'manifest.json'}: + if item.name not in {'files', _MANIFEST}: raise ConfigSyncError('asset generation contains unmanifested metadata') _verify_file_tree(directory / 'files', {item.path for item in manifest.files}) diff --git a/je_auto_control/utils/config_sync/blob_binding.py b/je_auto_control/utils/config_sync/blob_binding.py index ef2ef3696..9e0119ab2 100644 --- a/je_auto_control/utils/config_sync/blob_binding.py +++ b/je_auto_control/utils/config_sync/blob_binding.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from hashlib import sha256 from pathlib import Path -from typing import BinaryIO, Callable, Mapping, Optional, cast +from typing import BinaryIO, Callable, Iterable, Mapping, Optional, cast from je_auto_control.utils.config_sync.asset_models import AssetFile, MAX_FILE_BYTES from je_auto_control.utils.config_sync.assets import DirectoryAssetTransport, _source_file @@ -28,12 +28,21 @@ def asset_references(value: object, depth: int = 0) -> tuple[SyncAssetReference, raise ConfigSyncError('asset reference traversal exceeds the nesting limit') reference = asset_reference(value) if reference is not None: - return (reference,) - values = value.values() if isinstance(value, dict) else value if isinstance(value, list) else () - result = {item for child in values for item in asset_references(child, depth + 1)} + result = {reference} + else: + result = {item for child in _children(value) for item in asset_references(child, depth + 1)} return tuple(sorted(result, key=lambda item: (item.path, item.blob.sha256))) +def _children(value: object) -> Iterable[object]: + """Return the values a reference search descends into; scalars have none.""" + if isinstance(value, dict): + return value.values() + if isinstance(value, list): + return value + return () + + def convert_asset_bindings(value: object, aliases: Mapping[str, str], resolver: Optional[Callable[[str], SyncAssetReference]], depth: int = 0) -> object: """Replace only explicitly designated local aliases with frozen content references after private source export.""" diff --git a/je_auto_control/utils/config_sync/blob_codec.py b/je_auto_control/utils/config_sync/blob_codec.py index efe0dc33e..f9d5bbe8e 100644 --- a/je_auto_control/utils/config_sync/blob_codec.py +++ b/je_auto_control/utils/config_sync/blob_codec.py @@ -1,6 +1,5 @@ """Canonical bounded base64 chunks keep binary asset traffic inside the existing authenticated JSON body limit.""" import base64 -import binascii from je_auto_control.utils.config_sync.blob_models import CHUNK_BYTES from je_auto_control.utils.config_sync.contract import ConfigSyncError @@ -19,7 +18,7 @@ def decode_chunk(value: object) -> bytes: raise ConfigSyncError('asset chunk exceeds its encoded size limit') try: body = base64.b64decode(value, validate=True) - except (ValueError, binascii.Error) as error: + except ValueError as error: # includes binascii.Error raise ConfigSyncError('invalid asset chunk encoding') from error if encode_chunk(body) != value: raise ConfigSyncError('noncanonical asset chunk encoding') diff --git a/je_auto_control/utils/config_sync/blob_store.py b/je_auto_control/utils/config_sync/blob_store.py index 572b52cc1..fddde1a9c 100644 --- a/je_auto_control/utils/config_sync/blob_store.py +++ b/je_auto_control/utils/config_sync/blob_store.py @@ -14,6 +14,10 @@ from je_auto_control.utils.config_sync.contract import _identifier from je_auto_control.utils.config_sync.database import SyncDatabase +_USER_ID = 'asset user_id' +_BEGIN_IMMEDIATE = 'BEGIN IMMEDIATE' +_COUNT_BLOBS = 'SELECT count(*) FROM sync_asset_blobs WHERE user_id=?' + if TYPE_CHECKING: from sqlite3 import Connection @@ -35,11 +39,11 @@ def __init__(self, path: Optional[str | Path] = None, *, max_users: int = 1024, def put_chunk(self, user_id: str, digest: str, body: bytes) -> bool: """Store one verified nonempty bounded chunk once; repeat content consumes no additional quota.""" - _identifier(user_id, 'asset user_id') + _identifier(user_id, _USER_ID) validate_digest(digest) _validate_chunk(digest, body, received=True) with self._database.connection() as connection: - connection.execute('BEGIN IMMEDIATE') + connection.execute(_BEGIN_IMMEDIATE) existing = _chunk(connection, user_id, digest, missing_ok=True) if existing is not None: connection.execute('COMMIT') @@ -56,7 +60,7 @@ def put_chunk(self, user_id: str, digest: str, body: bytes) -> bool: def read_chunk(self, user_id: str, digest: str) -> bytes: """Return only a bounded chunk whose stored bytes still match their content address.""" - _identifier(user_id, 'asset user_id') + _identifier(user_id, _USER_ID) validate_digest(digest) with self._database.connection() as connection: body = _chunk(connection, user_id, digest) @@ -74,7 +78,7 @@ def commit(self, user_id: str, blob: SyncAssetBlob) -> bool: _validate_blob(user_id, blob) wire = _encoded(blob.to_dict()) with self._database.connection() as connection: - connection.execute('BEGIN IMMEDIATE') + connection.execute(_BEGIN_IMMEDIATE) if _receipt(connection, user_id, blob): connection.execute('COMMIT') return False @@ -110,25 +114,23 @@ def open(self, user_id: str, blob: SyncAssetBlob) -> BinaryIO: def status(self, user_id: str) -> dict[str, int]: """Read bounded account counts without exposing chunk data or other accounts.""" - _identifier(user_id, 'asset user_id') + _identifier(user_id, _USER_ID) with self._database.connection() as connection: row = connection.execute('SELECT bytes FROM sync_asset_users WHERE user_id=?', (user_id,)).fetchone() chunks = connection.execute('SELECT count(*) FROM sync_asset_chunks WHERE user_id=?', (user_id,)).fetchone()[0] - blobs = connection.execute('SELECT count(*) FROM sync_asset_blobs WHERE user_id=?', - (user_id,)).fetchone()[0] + blobs = connection.execute(_COUNT_BLOBS, (user_id,)).fetchone()[0] return {'bytes': row[0] if row else 0, 'chunks': chunks, 'blobs': blobs} def delete_account(self, user_id: str) -> dict[str, int]: """Atomically remove one host-selected account, including incomplete chunks and its quota slot.""" - _identifier(user_id, 'asset user_id') + _identifier(user_id, _USER_ID) with self._database.connection() as connection: - connection.execute('BEGIN IMMEDIATE') + connection.execute(_BEGIN_IMMEDIATE) size, chunks = connection.execute( 'SELECT COALESCE(SUM(length(body)),0),count(*) FROM sync_asset_chunks WHERE user_id=?', (user_id,)).fetchone() - blobs = connection.execute('SELECT count(*) FROM sync_asset_blobs WHERE user_id=?', - (user_id,)).fetchone()[0] + blobs = connection.execute(_COUNT_BLOBS, (user_id,)).fetchone()[0] connection.execute('DELETE FROM sync_asset_blobs WHERE user_id=?', (user_id,)) connection.execute('DELETE FROM sync_asset_chunks WHERE user_id=?', (user_id,)) connection.execute('DELETE FROM sync_asset_users WHERE user_id=?', (user_id,)) @@ -142,7 +144,7 @@ def _limit(value: int) -> None: def _validate_blob(user_id: str, blob: SyncAssetBlob) -> None: - _identifier(user_id, 'asset user_id') + _identifier(user_id, _USER_ID) if not isinstance(blob, SyncAssetBlob): raise ConfigSyncError('asset operations require a typed blob descriptor') @@ -178,7 +180,7 @@ def _object_limit(connection: Connection, user_id: str, table: str, maximum: int if table not in {'sync_asset_chunks', 'sync_asset_blobs'}: raise ConfigStoreError('invalid owned asset quota table') query = ('SELECT count(*) FROM sync_asset_chunks WHERE user_id=?' if table == 'sync_asset_chunks' - else 'SELECT count(*) FROM sync_asset_blobs WHERE user_id=?') + else _COUNT_BLOBS) count = connection.execute(query, (user_id,)).fetchone()[0] if count >= maximum: raise ConfigStoreFull('asset account object quota exceeded') diff --git a/je_auto_control/utils/config_sync/client.py b/je_auto_control/utils/config_sync/client.py index dfed5af88..fbb015e8e 100644 --- a/je_auto_control/utils/config_sync/client.py +++ b/je_auto_control/utils/config_sync/client.py @@ -155,32 +155,38 @@ def merge_buckets(local: ConfigBucket, conflicts: List[ConflictRecord] = [] sections = set(local.sections) | set(remote.sections) for name in sections: - merged_section: Dict[str, Dict[str, Any]] = {} - local_sec = local.sections.get(name, {}) - remote_sec = remote.sections.get(name, {}) - ids = set(local_sec) | set(remote_sec) - for entry_id in ids: - local_entry = local_sec.get(entry_id) - remote_entry = remote_sec.get(entry_id) - if local_entry is None: - if remote_entry is not None: - merged_section[entry_id] = remote_entry - continue - if remote_entry is None: - merged_section[entry_id] = local_entry - continue - kept, dropped = _winner(local_entry, remote_entry) - merged_section[entry_id] = kept - if dropped is not None: - conflicts.append(ConflictRecord( - section=name, entry_id=entry_id, dropped=dropped, kept=kept, - )) + merged_section = _merge_section(name, local.sections.get(name, {}), + remote.sections.get(name, {}), conflicts) merged.sections[name] = _without_expired( merged_section, (time.time() if now is None else now) - tombstone_retention_s) merged.revision = max(local.revision, remote.revision) + 1 return merged, conflicts +def _merge_section(name: str, local_sec: Dict[str, Dict[str, Any]], remote_sec: Dict[str, Dict[str, Any]], + conflicts: List[ConflictRecord]) -> Dict[str, Dict[str, Any]]: + """Merge one section entry by entry, appending every overruled entry to ``conflicts``.""" + merged_section: Dict[str, Dict[str, Any]] = {} + ids = set(local_sec) | set(remote_sec) + for entry_id in ids: + local_entry = local_sec.get(entry_id) + remote_entry = remote_sec.get(entry_id) + if local_entry is None: + if remote_entry is not None: + merged_section[entry_id] = remote_entry + continue + if remote_entry is None: + merged_section[entry_id] = local_entry + continue + kept, dropped = _winner(local_entry, remote_entry) + merged_section[entry_id] = kept + if dropped is not None: + conflicts.append(ConflictRecord( + section=name, entry_id=entry_id, dropped=dropped, kept=kept, + )) + return merged_section + + def _winner(local_entry: Dict[str, Any], remote_entry: Dict[str, Any], ) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]]]: """The entry that wins and the one it beat (``None`` when they are equal). diff --git a/je_auto_control/utils/config_sync/control_store.py b/je_auto_control/utils/config_sync/control_store.py index f8dadb748..8edf23b58 100644 --- a/je_auto_control/utils/config_sync/control_store.py +++ b/je_auto_control/utils/config_sync/control_store.py @@ -23,6 +23,7 @@ from sqlite3 import Connection _SELECT = "SELECT wire,digest,prepared FROM sync_controls WHERE endpoint=? AND user_id=?" +_BEGIN_IMMEDIATE = "BEGIN IMMEDIATE" _Scope = tuple[str, str] @@ -60,7 +61,7 @@ def enqueue(self, identity: SyncLocalIdentity, kind: ControlKind, *, peer_id: Op identity, kind, peer_id, discard_pending) scope = identity.endpoint, identity.user_id with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) historical = _historical(connection, candidate) if historical is not None: connection.execute("COMMIT") @@ -103,7 +104,7 @@ def claim(self, scope: _Scope, owner: str, policy: SyncRetryPolicy) -> Optional[ """Claim due metadata work without holding a database lock around HTTP callbacks.""" now = self._clock() with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) row = connection.execute("SELECT status,next_attempt,last_attempt,lease_owner,lease_until " "FROM sync_controls WHERE endpoint=? AND user_id=?", scope).fetchone() if row is None or not due(row, now): @@ -122,7 +123,7 @@ def prepare(self, operation: SyncControlOperation, owner: str, bucket: dict[str, prepared = {"base_revision": bucket.get("revision"), "bucket": bucket} candidate = replace(operation, prepared=prepared) with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) current = _owned(connection, operation, owner) if current.prepared is not None and _encoded(current.prepared) != _encoded(prepared): raise ConfigSyncError("uncertain peer control cannot be replaced") @@ -134,7 +135,7 @@ def prepare(self, operation: SyncControlOperation, owner: str, bucket: dict[str, def reject(self, operation: SyncControlOperation, owner: str) -> SyncControlOperation: """Clear only the current owner's definitely rejected frozen request before bounded rebasing.""" with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) current = _owned(connection, operation, owner) if (current.prepared is None or operation.prepared is None or _encoded(current.prepared) != _encoded(operation.prepared)): @@ -147,7 +148,7 @@ def reject(self, operation: SyncControlOperation, owner: str) -> SyncControlOper def complete(self, operation: SyncControlOperation, owner: str, receipt: CausalReceipt) -> bool: """Confirm the original receipt, authoritative snapshot and any generation adoption in one transaction.""" with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) scope = operation.identity.endpoint, operation.identity.user_id row = connection.execute("SELECT operation_id,lease_owner FROM sync_controls " "WHERE endpoint=? AND user_id=?", scope).fetchone() @@ -167,11 +168,15 @@ def complete(self, operation: SyncControlOperation, owner: str, receipt: CausalR def failed(self, operation: SyncControlOperation, owner: str, error: Exception, policy: SyncRetryPolicy) -> None: """Persist bounded retry/error state; definite generation failures block until explicit recovery.""" - state = ("blocked" if isinstance(error, FullResyncRequired) else - "conflict" if isinstance(error, ConfigRevisionConflict) else "offline") + if isinstance(error, FullResyncRequired): + state = "blocked" + elif isinstance(error, ConfigRevisionConflict): + state = "conflict" + else: + state = "offline" scope = operation.identity.endpoint, operation.identity.user_id with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) row = connection.execute("SELECT attempts FROM sync_controls WHERE endpoint=? AND user_id=? " "AND operation_id=? AND lease_owner=?", (*scope, operation.operation_id, owner)).fetchone() diff --git a/je_auto_control/utils/config_sync/http_commands.py b/je_auto_control/utils/config_sync/http_commands.py index 3f363084e..f2bd36868 100644 --- a/je_auto_control/utils/config_sync/http_commands.py +++ b/je_auto_control/utils/config_sync/http_commands.py @@ -30,7 +30,7 @@ def _client(endpoint: str, user_id: str, options: dict[str, object]) -> CausalSy environment = options.get("secret_env") secret = None if environment is not None: - if not isinstance(environment, str) or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]{0,127}", environment): + if not isinstance(environment, str) or not re.fullmatch(r"[A-Za-z_]\w{0,127}", environment, re.ASCII): raise ConfigSyncError("secret_env must name one environment variable") secret = os.environ.get(environment) if not secret: diff --git a/je_auto_control/utils/config_sync/outbox.py b/je_auto_control/utils/config_sync/outbox.py index 856c8aad3..f1ba2e27d 100644 --- a/je_auto_control/utils/config_sync/outbox.py +++ b/je_auto_control/utils/config_sync/outbox.py @@ -36,6 +36,7 @@ _Clock = Callable[[], float] _Sender = Callable[[SyncOperation], int | CausalReceipt] _Cancelled = Optional[Callable[[], bool]] +_BEGIN_IMMEDIATE = "BEGIN IMMEDIATE" _SELECT = "SELECT wire, digest, prepared FROM sync_operations WHERE endpoint = ? AND user_id = ? AND operation_id = ?" @@ -86,7 +87,7 @@ def identity(self, endpoint: str, user_id: str, *, origin: Optional[str] = None, if epoch is not None: _revision(epoch) with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) connection.execute("INSERT OR IGNORE INTO sync_namespaces (endpoint,user_id,origin,epoch) VALUES (?,?,?,?)", (*scope, origin or uuid4().hex, epoch or 0)) row = connection.execute("SELECT origin,epoch,confirmed_revision FROM sync_namespaces " @@ -161,7 +162,7 @@ def prepare(self, operation: SyncOperation, *, base_revision: int, bucket: dict[ encoded = _encoded(prepared) key = operation.endpoint, operation.user_id, operation.operation_id with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) row = connection.execute(_SELECT, key).fetchone() if row is None or _logical(_operation(row))[1] != _logical(operation)[1]: raise ConfigSyncError("cannot prepare an unknown or changed operation") @@ -177,7 +178,7 @@ def reject_prepared(self, operation: SyncOperation, *, rejected_base: int) -> No _revision(rejected_base) key = operation.endpoint, operation.user_id, operation.operation_id with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) row = connection.execute(_SELECT, key).fetchone() if row is None or _logical(_operation(row))[1] != _logical(operation)[1]: raise ConfigSyncError("unknown or changed queued operation") @@ -287,7 +288,7 @@ def _attempt(self, operation: SyncOperation, sender: _Sender, policy: SyncRetryP def _claim(self, key: _Key, owner: str, policy: SyncRetryPolicy) -> Optional[SyncOperation]: now = self._now() with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) control = connection.execute("SELECT 1 FROM sync_controls WHERE endpoint=? AND user_id=? " "AND status!='delivered'", key[:2]).fetchone() if control is not None: @@ -310,7 +311,7 @@ def _delivered(self, key: _Key, owner: str, receipt: int | CausalReceipt) -> boo revision = receipt.revision if isinstance(receipt, CausalReceipt) else receipt snapshot = _receipt_snapshot(receipt, key[1]) with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) changed = connection.execute("UPDATE sync_operations SET status='delivered',delivered_revision=?, " "error='',lease_owner='',lease_until=0 WHERE endpoint=? AND user_id=? " "AND operation_id=? AND lease_owner=?", (revision, *key, owner)).rowcount @@ -336,10 +337,9 @@ def _save_snapshot(connection: Connection, scope: _Scope, snapshot: Optional[Cau (_encoded(snapshot.to_dict()), *scope)) def _failed(self, key: _Key, owner: str, error: Exception, policy: SyncRetryPolicy) -> None: - status = ("blocked" if isinstance(error, FullResyncRequired) else - "conflict" if isinstance(error, ConfigRevisionConflict) else "offline") + status = _failure_status(error) with self._database.connection() as connection: - connection.execute("BEGIN IMMEDIATE") + connection.execute(_BEGIN_IMMEDIATE) row = connection.execute("SELECT attempts FROM sync_operations WHERE endpoint=? AND user_id=? " "AND operation_id=? AND lease_owner=?", (*key, owner)).fetchone() if row is not None: @@ -356,6 +356,14 @@ def _release(self, key: _Key, owner: str) -> None: "AND operation_id=? AND lease_owner=?", (*key, owner)) +def _failure_status(error: Exception) -> str: + if isinstance(error, FullResyncRequired): + return "blocked" + if isinstance(error, ConfigRevisionConflict): + return "conflict" + return "offline" + + def _receipt_snapshot(receipt: int | CausalReceipt, user_id: str) -> Optional[CausalBucket]: if not isinstance(receipt, CausalReceipt): return None diff --git a/je_auto_control/utils/config_sync/outbox_models.py b/je_auto_control/utils/config_sync/outbox_models.py index 2aaaea265..e2f6ac98a 100644 --- a/je_auto_control/utils/config_sync/outbox_models.py +++ b/je_auto_control/utils/config_sync/outbox_models.py @@ -18,7 +18,7 @@ def normalize_sync_endpoint(value: str) -> str: try: parts = _endpoint_parts(value) return urlunsplit((parts.scheme.lower(), _authority(parts), parts.path.rstrip("/"), "", "")) - except (ValueError, UnicodeError, AttributeError): + except (ValueError, AttributeError): # ValueError covers UnicodeError raise ConfigSyncError("sync endpoint must be an HTTP URL without credentials, query or fragment") from None diff --git a/je_auto_control/utils/config_sync/profile_bindings.py b/je_auto_control/utils/config_sync/profile_bindings.py index fdd83d42a..70ec4bd72 100644 --- a/je_auto_control/utils/config_sync/profile_bindings.py +++ b/je_auto_control/utils/config_sync/profile_bindings.py @@ -1,7 +1,7 @@ """Match native generation values to exact portable heads and rebind verified asset references to a working root.""" from copy import deepcopy from pathlib import Path -from typing import Optional, cast +from typing import Iterable, Optional, cast from je_auto_control.utils.config_sync.adapter_references import _reference from je_auto_control.utils.config_sync.asset_commands import _root @@ -87,5 +87,10 @@ def _unresolved(value: object, depth: int = 0) -> bool: raise ConfigSyncError('native local binding exceeds the nesting limit') if _reference(value) is not None or asset_reference(value) is not None: return True - items = value.values() if isinstance(value, dict) else value if isinstance(value, list) else () + if isinstance(value, dict): + items: Iterable[object] = value.values() + elif isinstance(value, list): + items = value + else: + return False return any(_unresolved(item, depth + 1) for item in items) diff --git a/je_auto_control/utils/config_sync/routes.py b/je_auto_control/utils/config_sync/routes.py index ec3e07b0b..9017335c5 100644 --- a/je_auto_control/utils/config_sync/routes.py +++ b/je_auto_control/utils/config_sync/routes.py @@ -28,37 +28,47 @@ def register_config_routes(app: FastAPI, store: ConfigStore, secret_dep: Callabl @app.get("/config/{user_id}", responses=_RESPONSES, dependencies=auth) def get_config(user_id: str, response: Response) -> dict[str, object]: - try: - body = store.get_document(user_id) - _require_plain(body) - except ConfigSyncError as error: - raise HTTPException(status_code=400, detail=str(error)) from error - except ConfigStoreError as error: - raise HTTPException(status_code=503, detail="config storage unavailable") from error - except ConfigRevisionConflict as error: - raise HTTPException(status_code=409, detail={"message": str(error), - "revision": error.current_revision}) from error - if body is None: - raise HTTPException(status_code=404, detail="no bucket") - response.headers["X-Autocontrol-Config-Protocol"] = "2" - response.headers["X-Autocontrol-Config-CAS"] = "legacy-enabled" if allow_legacy_writes else "required" - return body + return _read_config(store, user_id, response, allow_legacy_writes) @app.put("/config/{user_id}", responses=_RESPONSES, dependencies=auth) @app.put("/config/{user_id}/commit", responses=_RESPONSES, dependencies=auth) def put_config(user_id: str, body: dict[str, object], request: Request) -> dict[str, object]: - try: - _identifier(user_id, "user_id") - legacy = allow_legacy_writes and request.scope["path"] == f"/config/{user_id}" - revision = _commit(store, user_id, body, legacy) - except ConfigSyncError as error: - raise HTTPException(status_code=400, detail=str(error)) from error - except ConfigRevisionConflict as error: - raise HTTPException(status_code=409, detail={"message": str(error), - "revision": error.current_revision}) from error - except (ConfigStoreFull, ConfigStoreError) as error: - raise HTTPException(status_code=503, detail="config storage unavailable or full") from error - return {"ok": True, "version": 2, "revision": revision} + return _write_config(store, user_id, body, request, allow_legacy_writes) + + +def _read_config(store: ConfigStore, user_id: str, response: Response, + allow_legacy_writes: bool) -> dict[str, object]: + try: + body = store.get_document(user_id) + _require_plain(body) + except ConfigSyncError as error: + raise HTTPException(status_code=400, detail=str(error)) from error + except ConfigStoreError as error: + raise HTTPException(status_code=503, detail="config storage unavailable") from error + except ConfigRevisionConflict as error: + raise HTTPException(status_code=409, detail={"message": str(error), + "revision": error.current_revision}) from error + if body is None: + raise HTTPException(status_code=404, detail="no bucket") + response.headers["X-Autocontrol-Config-Protocol"] = "2" + response.headers["X-Autocontrol-Config-CAS"] = "legacy-enabled" if allow_legacy_writes else "required" + return body + + +def _write_config(store: ConfigStore, user_id: str, body: dict[str, object], request: Request, + allow_legacy_writes: bool) -> dict[str, object]: + try: + _identifier(user_id, "user_id") + legacy = allow_legacy_writes and request.scope["path"] == f"/config/{user_id}" + revision = _commit(store, user_id, body, legacy) + except ConfigSyncError as error: + raise HTTPException(status_code=400, detail=str(error)) from error + except ConfigRevisionConflict as error: + raise HTTPException(status_code=409, detail={"message": str(error), + "revision": error.current_revision}) from error + except (ConfigStoreFull, ConfigStoreError) as error: + raise HTTPException(status_code=503, detail="config storage unavailable or full") from error + return {"ok": True, "version": 2, "revision": revision} def _commit(store: ConfigStore, user_id: str, body: dict[str, object], legacy: bool) -> int: diff --git a/je_auto_control/utils/config_sync/workspace_options.py b/je_auto_control/utils/config_sync/workspace_options.py index 0afdae31a..e59c82c25 100644 --- a/je_auto_control/utils/config_sync/workspace_options.py +++ b/je_auto_control/utils/config_sync/workspace_options.py @@ -25,7 +25,7 @@ def _timeout(timeout: object) -> None: def _credential(environment: object) -> None: if environment is not None and (not isinstance(environment, str) - or re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]{0,127}', environment) is None): + or re.fullmatch(r'[A-Za-z_]\w{0,127}', environment, re.ASCII) is None): raise ConfigSyncError('secret_env must name one environment variable') def _rebases(rebases: object) -> None: diff --git a/je_auto_control/utils/config_sync/workspace_store.py b/je_auto_control/utils/config_sync/workspace_store.py index ac7d3e6bf..c636c426b 100644 --- a/je_auto_control/utils/config_sync/workspace_store.py +++ b/je_auto_control/utils/config_sync/workspace_store.py @@ -175,7 +175,7 @@ def _bind(connection: Connection, scope: tuple[str, str], bindings: Mapping[str, def _load(row: tuple[object, ...], scope: tuple[str, str]) -> SyncControlOperation: try: operation = _operation(row) - except (UnicodeError, ValueError, TypeError, RecursionError, ConfigSyncError) as error: + except (ValueError, TypeError, RecursionError, ConfigSyncError) as error: raise ConfigStoreError('invalid workspace control evidence') from error if (operation.identity.endpoint, operation.identity.user_id) != scope: raise ConfigStoreError('workspace control evidence belongs to another namespace') diff --git a/je_auto_control/utils/dag/batch_execution.py b/je_auto_control/utils/dag/batch_execution.py index 5045b1bef..a9f67c861 100644 --- a/je_auto_control/utils/dag/batch_execution.py +++ b/je_auto_control/utils/dag/batch_execution.py @@ -48,7 +48,7 @@ def execute_started(client: Any, started: dict, stop_event: threading.Event | No with cancellation_scope(None): try: runner.control("finish", {}) - except (AgentTaskError, OSError, RuntimeError, ValueError, TypeError): + except (OSError, RuntimeError, ValueError, TypeError): # ValueError covers AgentTaskError # The persisted running/claimed states require review; no completion is inferred from a lost reply. pass @@ -79,7 +79,7 @@ def run_node(self, node: Any, _definition: Any) -> dict: with cancellation_scope(None): try: self.control("record", {"node_id": node.id, "receipt": {"outcome": "unknown", "task_id": task_id}}) - except (AgentTaskError, OSError, RuntimeError, ValueError, TypeError): + except (OSError, RuntimeError, ValueError, TypeError): # ValueError covers AgentTaskError pass # the durable pre-delivery claim remains an unsettled, non-replayable witness raise diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index 5a975b686..5146dd515 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -1,6 +1,6 @@ import threading import types -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple from je_auto_control.utils.diagnostics.capability_commands import probe_current_capabilities as _probe_capabilities from je_auto_control.utils.exception.exception_tags import ( @@ -8369,7 +8369,7 @@ def execute_one(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> from je_auto_control.utils.executor.dispatch import execute_one return execute_one(self, name, arguments) - def execute_action(self, action_list: Union[list, dict], + def execute_action(self, action_list: list | dict, raise_on_error: bool = False, _validated: bool = False, dry_run: bool = False, diff --git a/je_auto_control/utils/logging/secret_scope.py b/je_auto_control/utils/logging/secret_scope.py index 59fdc03df..c3d5ceff7 100644 --- a/je_auto_control/utils/logging/secret_scope.py +++ b/je_auto_control/utils/logging/secret_scope.py @@ -162,22 +162,26 @@ class SecretLogFilter(logging.Filter): """Scrub before any handler, including propagated and queued handlers.""" def filter(self, record: logging.LogRecord) -> bool: - """Mask formatted messages, cached exception text and stack information.""" + """Mask formatted messages, cached exception text and stack information; never drop a record.""" scope = current_secret_scope() - if scope is None or not scope.has_values: - return True - try: - message = record.getMessage() - # pylint: disable-next=broad-exception-caught # reason: match logging handler containment - except Exception: # noqa: BLE001 # reason: user-defined formatting must not abort a command - message = "" - record.msg = scope.scrub(message) - record.args = () - if record.exc_info: - record.exc_text = logging.Formatter().formatException(record.exc_info) - record.exc_info = None - if record.exc_text: - record.exc_text = scope.scrub(record.exc_text) - if record.stack_info: - record.stack_info = scope.scrub(record.stack_info) + if scope is not None and scope.has_values: + _scrub_record(record, scope) return True + + +def _scrub_record(record: logging.LogRecord, scope: SecretLogScope) -> None: + """Replace the record's message, exception text and stack information with their masked forms.""" + try: + message = record.getMessage() + # pylint: disable-next=broad-exception-caught # reason: match logging handler containment + except Exception: # noqa: BLE001 # reason: user-defined formatting must not abort a command + message = "" + record.msg = scope.scrub(message) + record.args = () + if record.exc_info: + record.exc_text = logging.Formatter().formatException(record.exc_info) + record.exc_info = None + if record.exc_text: + record.exc_text = scope.scrub(record.exc_text) + if record.stack_info: + record.stack_info = scope.scrub(record.stack_info) diff --git a/je_auto_control/utils/mcp_server/_client_requests.py b/je_auto_control/utils/mcp_server/_client_requests.py index 0664651d3..f1b856d03 100644 --- a/je_auto_control/utils/mcp_server/_client_requests.py +++ b/je_auto_control/utils/mcp_server/_client_requests.py @@ -124,7 +124,7 @@ def _maybe_request_roots_async(self) -> None: def _refresh_roots_safely(self) -> None: try: self.refresh_roots(timeout=5.0) - except (AutoControlException, OSError, RuntimeError, TimeoutError) as error: + except (AutoControlException, OSError, RuntimeError) as error: # TimeoutError is an OSError autocontrol_logger.info("MCP roots refresh skipped: %r", error) def refresh_roots(self, timeout: float = 10.0) -> List[Dict[str, Any]]: diff --git a/je_auto_control/utils/mcp_server/disclosure.py b/je_auto_control/utils/mcp_server/disclosure.py index 4336743e5..75122adbb 100644 --- a/je_auto_control/utils/mcp_server/disclosure.py +++ b/je_auto_control/utils/mcp_server/disclosure.py @@ -16,6 +16,7 @@ CORE_TOOLS = frozenset({"ac_mcp_tool_search", "ac_mcp_tool_schema", "ac_mcp_tools_state", "ac_mcp_tools_enable", "ac_mcp_tools_disable"}) MODES = frozenset({"full", "progressive", "static"}) +_CLOSED = "tool view is closed" class ToolPage(TypedDict, total=False): @@ -68,14 +69,14 @@ def __init__(self, provider: Callable[[], ToolIndex], *, mode: str = "progressiv def _index(self) -> ToolIndex: if self._provider is None: - raise ToolDiscoveryError("tool view is closed") + raise ToolDiscoveryError(_CLOSED) return self._provider() def check_open(self) -> None: """Refuse a retired accepted call without querying or recreating any session metadata.""" with self._lock: if self._provider is None: - raise ToolDiscoveryError("tool view is closed") + raise ToolDiscoveryError(_CLOSED) def _visible(self, index: ToolIndex) -> tuple[str, ...]: names = index.names() @@ -130,7 +131,7 @@ def disable(self, names: Iterable[str]) -> dict[str, Any]: def _mutable(self) -> None: if self._provider is None: - raise ToolDiscoveryError("tool view is closed") + raise ToolDiscoveryError(_CLOSED) if self._mode != "progressive": raise ToolDiscoveryError("this disclosure profile is fixed") diff --git a/je_auto_control/utils/mcp_server/tools/_agent_tasks.py b/je_auto_control/utils/mcp_server/tools/_agent_tasks.py index 7c8ebe840..9e1515609 100644 --- a/je_auto_control/utils/mcp_server/tools/_agent_tasks.py +++ b/je_auto_control/utils/mcp_server/tools/_agent_tasks.py @@ -21,24 +21,35 @@ def handle(*, ctx: ToolCallContext | None = None, **arguments: Any) -> Any: ctx.check_cancelled() _paths(ctx, arguments) token = CancellationToken(0, event=ctx.cancelled_event) if ctx is not None else None - identity = current_authorization() - if host_management and ctx is not None and identity is None: - raise AgentTaskError("managed host tools require an authenticated host administrator") - if identity is None and not host_management: - if ctx is None or not ctx.session_id: - raise AgentTaskError("managed MCP tasks require an authenticated caller or a live session") - identity = AuthorizationContext("mcp:" + ctx.session_id, "operator") + identity = _caller_identity(ctx, host_management) origin = ctx.path_policy.managed_origin() if ctx is not None and ctx.path_policy is not None else None with authorization_scope(identity), cancellation_scope(token), origin_scope(origin): - try: - return function(**arguments) - except CancelledError: - if ctx is not None: - ctx.check_cancelled() - raise + return _invoke(function, arguments, ctx) return handle +def _caller_identity(ctx: ToolCallContext | None, host_management: bool) -> AuthorizationContext | None: + """Keep the caller's own authority, or bind a live MCP session as an operator for task tools.""" + identity = current_authorization() + if host_management and ctx is not None and identity is None: + raise AgentTaskError("managed host tools require an authenticated host administrator") + if identity is None and not host_management: + if ctx is None or not ctx.session_id: + raise AgentTaskError("managed MCP tasks require an authenticated caller or a live session") + identity = AuthorizationContext("mcp:" + ctx.session_id, "operator") + return identity + + +def _invoke(function: Any, arguments: dict[str, Any], ctx: ToolCallContext | None) -> Any: + """Run the task command, reporting a cancelled session call as the session's own cancellation.""" + try: + return function(**arguments) + except CancelledError: + if ctx is not None: + ctx.check_cancelled() + raise + + def _paths(context: ToolCallContext, arguments: Any) -> None: policy = context.path_policy if policy is None or not policy.enabled: diff --git a/je_auto_control/utils/path_guard/policy.py b/je_auto_control/utils/path_guard/policy.py index 428b3693c..275bb5a4b 100644 --- a/je_auto_control/utils/path_guard/policy.py +++ b/je_auto_control/utils/path_guard/policy.py @@ -30,6 +30,7 @@ MCP_CLIENT_ROOTS_ENV = "JE_AUTOCONTROL_MCP_PATH_ROOTS_FROM_CLIENT" _TRUTHY = frozenset({"1", "true", "yes", "on"}) +_POLICY_RETIRED = "origin path policy has retired" class PathPolicy: @@ -80,7 +81,7 @@ def set_client_roots(self, roots: Iterable[os.PathLike[str] | str]) -> None: resolved = tuple(_canonical(root) for root in roots) with self._lock: if self._origin_closed: - raise PathNotAllowedError("origin path policy has retired") + raise PathNotAllowedError(_POLICY_RETIRED) if self._origin_witness is not None: self._origin_witness.update(self._static + resolved) self._client = resolved @@ -95,7 +96,7 @@ def managed_origin(self) -> str | None: return None with self._lock: if self._origin_closed: - raise PathNotAllowedError("origin path policy has retired") + raise PathNotAllowedError(_POLICY_RETIRED) if self._origin_witness is None: from je_auto_control.utils.agent_runtime.origin_policy import OriginWitness self._origin_witness = OriginWitness(self._static + self._client) @@ -142,7 +143,7 @@ def validate(self, path: os.PathLike[str] | str, *, operation: str) -> Path: def _check_origin_active(self) -> None: with self._lock: if self._origin_closed: - raise PathNotAllowedError("origin path policy has retired") + raise PathNotAllowedError(_POLICY_RETIRED) __all__ = ["MCP_CLIENT_ROOTS_ENV", "MCP_PATH_ROOTS_ENV", "PathPolicy"] diff --git a/je_auto_control/utils/remote_desktop/_export_bindings.py b/je_auto_control/utils/remote_desktop/_export_bindings.py index 16ddacf14..df3f63bee 100644 --- a/je_auto_control/utils/remote_desktop/_export_bindings.py +++ b/je_auto_control/utils/remote_desktop/_export_bindings.py @@ -4,99 +4,98 @@ PUBLIC_ORDER_SHA256 = 'ac0179e53e00c2ae7a2987a049919a50d6671cd27a66f95ef29046b822b11819' +_UTILS_REMOTE_DESKTOP_ADDRESS_BOOK = 'je_auto_control.utils.remote_desktop.address_book' +_UTILS_REMOTE_DESKTOP_AUDIO = 'je_auto_control.utils.remote_desktop.audio' +_UTILS_REMOTE_DESKTOP_FILE_TRANSFER = 'je_auto_control.utils.remote_desktop.file_transfer' +_UTILS_REMOTE_DESKTOP_FINGERPRINT = 'je_auto_control.utils.remote_desktop.fingerprint' +_UTILS_REMOTE_DESKTOP_HOST_ACCESS = 'je_auto_control.utils.remote_desktop.host_access' +_UTILS_REMOTE_DESKTOP_HOST_ID = 'je_auto_control.utils.remote_desktop.host_id' +_UTILS_REMOTE_DESKTOP_INPUT_DISPATCH = 'je_auto_control.utils.remote_desktop.input_dispatch' +_UTILS_REMOTE_DESKTOP_PRESENCE = 'je_auto_control.utils.remote_desktop.presence' +_UTILS_REMOTE_DESKTOP_PROTOCOL = 'je_auto_control.utils.remote_desktop.protocol' +_UTILS_REMOTE_DESKTOP_TRUST_LIST = 'je_auto_control.utils.remote_desktop.trust_list' +_UTILS_REMOTE_DESKTOP_VIEWER_ID = 'je_auto_control.utils.remote_desktop.viewer_id' +_UTILS_REMOTE_DESKTOP_WAKE_ON_LAN = 'je_auto_control.utils.remote_desktop.wake_on_lan' +_UTILS_REMOTE_DESKTOP__OPTIONAL_FILE_TRANSFER = 'je_auto_control.utils.remote_desktop._optional.file_transfer' +_UTILS_REMOTE_DESKTOP__OPTIONAL_HW_CODEC = 'je_auto_control.utils.remote_desktop._optional.hw_codec' +_UTILS_REMOTE_DESKTOP__OPTIONAL_MIC_UPLINK = 'je_auto_control.utils.remote_desktop._optional.mic_uplink' +_UTILS_REMOTE_DESKTOP__OPTIONAL_WEBRTC = 'je_auto_control.utils.remote_desktop._optional.webrtc' + EXPORTS: dict[str, ExportBinding] = { 'RemoteDesktopHost': AttributeBinding('je_auto_control.utils.remote_desktop.host', 'RemoteDesktopHost'), 'RemoteDesktopViewer': AttributeBinding('je_auto_control.utils.remote_desktop.viewer', 'RemoteDesktopViewer'), - 'PendingViewer': AttributeBinding('je_auto_control.utils.remote_desktop.host_access', 'PendingViewer'), - 'PendingViewerCallback': - AttributeBinding('je_auto_control.utils.remote_desktop.host_access', 'PendingViewerCallback'), + 'PendingViewer': AttributeBinding(_UTILS_REMOTE_DESKTOP_HOST_ACCESS, 'PendingViewer'), + 'PendingViewerCallback': AttributeBinding(_UTILS_REMOTE_DESKTOP_HOST_ACCESS, 'PendingViewerCallback'), 'list_host_monitors': AttributeBinding('je_auto_control.utils.remote_desktop.host_capture', 'list_host_monitors'), 'WebSocketDesktopHost': AttributeBinding('je_auto_control.utils.remote_desktop.ws_host', 'WebSocketDesktopHost'), 'WebSocketDesktopViewer': AttributeBinding('je_auto_control.utils.remote_desktop.ws_viewer', 'WebSocketDesktopViewer'), - 'WebRTCDesktopHost': AttributeBinding('je_auto_control.utils.remote_desktop._optional.webrtc', 'WebRTCDesktopHost'), - 'WebRTCDesktopViewer': - AttributeBinding('je_auto_control.utils.remote_desktop._optional.webrtc', 'WebRTCDesktopViewer'), - 'WebRTCConfig': AttributeBinding('je_auto_control.utils.remote_desktop._optional.webrtc', 'WebRTCConfig'), + 'WebRTCDesktopHost': AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_WEBRTC, 'WebRTCDesktopHost'), + 'WebRTCDesktopViewer': AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_WEBRTC, 'WebRTCDesktopViewer'), + 'WebRTCConfig': AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_WEBRTC, 'WebRTCConfig'), 'signaling_client': ModuleBinding('je_auto_control.utils.remote_desktop.signaling_client'), - 'TrustList': AttributeBinding('je_auto_control.utils.remote_desktop.trust_list', 'TrustList'), - 'default_trust_list': AttributeBinding('je_auto_control.utils.remote_desktop.trust_list', 'default_trust_list'), - 'default_trust_list_path': - AttributeBinding('je_auto_control.utils.remote_desktop.trust_list', 'default_trust_list_path'), - 'AddressBook': AttributeBinding('je_auto_control.utils.remote_desktop.address_book', 'AddressBook'), - 'default_address_book': - AttributeBinding('je_auto_control.utils.remote_desktop.address_book', 'default_address_book'), - 'default_address_book_path': - AttributeBinding('je_auto_control.utils.remote_desktop.address_book', 'default_address_book_path'), - 'ViewerIdError': AttributeBinding('je_auto_control.utils.remote_desktop.viewer_id', 'ViewerIdError'), - 'generate_viewer_id': AttributeBinding('je_auto_control.utils.remote_desktop.viewer_id', 'generate_viewer_id'), - 'load_or_create_viewer_id': - AttributeBinding('je_auto_control.utils.remote_desktop.viewer_id', 'load_or_create_viewer_id'), - 'validate_viewer_id': AttributeBinding('je_auto_control.utils.remote_desktop.viewer_id', 'validate_viewer_id'), - 'build_magic_packet': AttributeBinding('je_auto_control.utils.remote_desktop.wake_on_lan', 'build_magic_packet'), - 'send_magic_packet': AttributeBinding('je_auto_control.utils.remote_desktop.wake_on_lan', 'send_magic_packet'), + 'TrustList': AttributeBinding(_UTILS_REMOTE_DESKTOP_TRUST_LIST, 'TrustList'), + 'default_trust_list': AttributeBinding(_UTILS_REMOTE_DESKTOP_TRUST_LIST, 'default_trust_list'), + 'default_trust_list_path': AttributeBinding(_UTILS_REMOTE_DESKTOP_TRUST_LIST, 'default_trust_list_path'), + 'AddressBook': AttributeBinding(_UTILS_REMOTE_DESKTOP_ADDRESS_BOOK, 'AddressBook'), + 'default_address_book': AttributeBinding(_UTILS_REMOTE_DESKTOP_ADDRESS_BOOK, 'default_address_book'), + 'default_address_book_path': AttributeBinding(_UTILS_REMOTE_DESKTOP_ADDRESS_BOOK, 'default_address_book_path'), + 'ViewerIdError': AttributeBinding(_UTILS_REMOTE_DESKTOP_VIEWER_ID, 'ViewerIdError'), + 'generate_viewer_id': AttributeBinding(_UTILS_REMOTE_DESKTOP_VIEWER_ID, 'generate_viewer_id'), + 'load_or_create_viewer_id': AttributeBinding(_UTILS_REMOTE_DESKTOP_VIEWER_ID, 'load_or_create_viewer_id'), + 'validate_viewer_id': AttributeBinding(_UTILS_REMOTE_DESKTOP_VIEWER_ID, 'validate_viewer_id'), + 'build_magic_packet': AttributeBinding(_UTILS_REMOTE_DESKTOP_WAKE_ON_LAN, 'build_magic_packet'), + 'send_magic_packet': AttributeBinding(_UTILS_REMOTE_DESKTOP_WAKE_ON_LAN, 'send_magic_packet'), 'SessionRecorder': AttributeBinding('je_auto_control.utils.remote_desktop._optional.session_recorder', 'SessionRecorder'), 'MultiViewerHost': AttributeBinding('je_auto_control.utils.remote_desktop._optional.multi_viewer', 'MultiViewerHost'), 'available_hardware_codecs': - AttributeBinding('je_auto_control.utils.remote_desktop._optional.hw_codec', 'available_hardware_codecs'), - 'active_hardware_codec': - AttributeBinding('je_auto_control.utils.remote_desktop._optional.hw_codec', 'active_hardware_codec'), - 'install_hardware_codec': - AttributeBinding('je_auto_control.utils.remote_desktop._optional.hw_codec', 'install_hardware_codec'), - 'uninstall_hardware_codec': - AttributeBinding('je_auto_control.utils.remote_desktop._optional.hw_codec', 'uninstall_hardware_codec'), + AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_HW_CODEC, 'available_hardware_codecs'), + 'active_hardware_codec': AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_HW_CODEC, 'active_hardware_codec'), + 'install_hardware_codec': AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_HW_CODEC, 'install_hardware_codec'), + 'uninstall_hardware_codec': AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_HW_CODEC, 'uninstall_hardware_codec'), 'SessionPermissions': AttributeBinding('je_auto_control.utils.remote_desktop.permissions', 'SessionPermissions'), - 'KnownHosts': AttributeBinding('je_auto_control.utils.remote_desktop.fingerprint', 'KnownHosts'), - 'default_known_hosts': AttributeBinding('je_auto_control.utils.remote_desktop.fingerprint', 'default_known_hosts'), - 'fingerprint_for_display': - AttributeBinding('je_auto_control.utils.remote_desktop.fingerprint', 'fingerprint_for_display'), + 'KnownHosts': AttributeBinding(_UTILS_REMOTE_DESKTOP_FINGERPRINT, 'KnownHosts'), + 'default_known_hosts': AttributeBinding(_UTILS_REMOTE_DESKTOP_FINGERPRINT, 'default_known_hosts'), + 'fingerprint_for_display': AttributeBinding(_UTILS_REMOTE_DESKTOP_FINGERPRINT, 'fingerprint_for_display'), 'load_or_create_host_fingerprint': - AttributeBinding('je_auto_control.utils.remote_desktop.fingerprint', 'load_or_create_host_fingerprint'), - 'MicUplinkSender': AttributeBinding('je_auto_control.utils.remote_desktop._optional.mic_uplink', 'MicUplinkSender'), - 'MicUplinkReceiver': - AttributeBinding('je_auto_control.utils.remote_desktop._optional.mic_uplink', 'MicUplinkReceiver'), - 'FileTransferSender': - AttributeBinding('je_auto_control.utils.remote_desktop._optional.file_transfer', 'FileTransferSender'), - 'FileTransferReceiver': - AttributeBinding('je_auto_control.utils.remote_desktop._optional.file_transfer', 'FileTransferReceiver'), + AttributeBinding(_UTILS_REMOTE_DESKTOP_FINGERPRINT, 'load_or_create_host_fingerprint'), + 'MicUplinkSender': AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_MIC_UPLINK, 'MicUplinkSender'), + 'MicUplinkReceiver': AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_MIC_UPLINK, 'MicUplinkReceiver'), + 'FileTransferSender': AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_FILE_TRANSFER, 'FileTransferSender'), + 'FileTransferReceiver': AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_FILE_TRANSFER, 'FileTransferReceiver'), 'FileTransferWebRTCError': - AttributeBinding('je_auto_control.utils.remote_desktop._optional.file_transfer', 'FileTransferWebRTCError'), - 'InputDispatchError': AttributeBinding('je_auto_control.utils.remote_desktop.input_dispatch', 'InputDispatchError'), - 'AuthenticationError': AttributeBinding('je_auto_control.utils.remote_desktop.protocol', 'AuthenticationError'), - 'ProtocolError': AttributeBinding('je_auto_control.utils.remote_desktop.protocol', 'ProtocolError'), - 'MessageType': AttributeBinding('je_auto_control.utils.remote_desktop.protocol', 'MessageType'), - 'encode_frame': AttributeBinding('je_auto_control.utils.remote_desktop.protocol', 'encode_frame'), - 'decode_frame_header': AttributeBinding('je_auto_control.utils.remote_desktop.protocol', 'decode_frame_header'), - 'dispatch_input': AttributeBinding('je_auto_control.utils.remote_desktop.input_dispatch', 'dispatch_input'), - 'HostIdError': AttributeBinding('je_auto_control.utils.remote_desktop.host_id', 'HostIdError'), - 'format_host_id': AttributeBinding('je_auto_control.utils.remote_desktop.host_id', 'format_host_id'), - 'generate_host_id': AttributeBinding('je_auto_control.utils.remote_desktop.host_id', 'generate_host_id'), - 'load_or_create_host_id': - AttributeBinding('je_auto_control.utils.remote_desktop.host_id', 'load_or_create_host_id'), - 'parse_host_id': AttributeBinding('je_auto_control.utils.remote_desktop.host_id', 'parse_host_id'), - 'validate_host_id': AttributeBinding('je_auto_control.utils.remote_desktop.host_id', 'validate_host_id'), - 'AudioBackendError': AttributeBinding('je_auto_control.utils.remote_desktop.audio', 'AudioBackendError'), - 'AudioCapture': AttributeBinding('je_auto_control.utils.remote_desktop.audio', 'AudioCapture'), - 'AudioPlayer': AttributeBinding('je_auto_control.utils.remote_desktop.audio', 'AudioPlayer'), - 'is_audio_backend_available': - AttributeBinding('je_auto_control.utils.remote_desktop.audio', 'is_audio_backend_available'), + AttributeBinding(_UTILS_REMOTE_DESKTOP__OPTIONAL_FILE_TRANSFER, 'FileTransferWebRTCError'), + 'InputDispatchError': AttributeBinding(_UTILS_REMOTE_DESKTOP_INPUT_DISPATCH, 'InputDispatchError'), + 'AuthenticationError': AttributeBinding(_UTILS_REMOTE_DESKTOP_PROTOCOL, 'AuthenticationError'), + 'ProtocolError': AttributeBinding(_UTILS_REMOTE_DESKTOP_PROTOCOL, 'ProtocolError'), + 'MessageType': AttributeBinding(_UTILS_REMOTE_DESKTOP_PROTOCOL, 'MessageType'), + 'encode_frame': AttributeBinding(_UTILS_REMOTE_DESKTOP_PROTOCOL, 'encode_frame'), + 'decode_frame_header': AttributeBinding(_UTILS_REMOTE_DESKTOP_PROTOCOL, 'decode_frame_header'), + 'dispatch_input': AttributeBinding(_UTILS_REMOTE_DESKTOP_INPUT_DISPATCH, 'dispatch_input'), + 'HostIdError': AttributeBinding(_UTILS_REMOTE_DESKTOP_HOST_ID, 'HostIdError'), + 'format_host_id': AttributeBinding(_UTILS_REMOTE_DESKTOP_HOST_ID, 'format_host_id'), + 'generate_host_id': AttributeBinding(_UTILS_REMOTE_DESKTOP_HOST_ID, 'generate_host_id'), + 'load_or_create_host_id': AttributeBinding(_UTILS_REMOTE_DESKTOP_HOST_ID, 'load_or_create_host_id'), + 'parse_host_id': AttributeBinding(_UTILS_REMOTE_DESKTOP_HOST_ID, 'parse_host_id'), + 'validate_host_id': AttributeBinding(_UTILS_REMOTE_DESKTOP_HOST_ID, 'validate_host_id'), + 'AudioBackendError': AttributeBinding(_UTILS_REMOTE_DESKTOP_AUDIO, 'AudioBackendError'), + 'AudioCapture': AttributeBinding(_UTILS_REMOTE_DESKTOP_AUDIO, 'AudioCapture'), + 'AudioPlayer': AttributeBinding(_UTILS_REMOTE_DESKTOP_AUDIO, 'AudioPlayer'), + 'is_audio_backend_available': AttributeBinding(_UTILS_REMOTE_DESKTOP_AUDIO, 'is_audio_backend_available'), 'ClipboardSyncError': AttributeBinding('je_auto_control.utils.remote_desktop.clipboard_sync', 'ClipboardSyncError'), - 'FileReceiver': AttributeBinding('je_auto_control.utils.remote_desktop.file_transfer', 'FileReceiver'), - 'FileSendResult': AttributeBinding('je_auto_control.utils.remote_desktop.file_transfer', 'FileSendResult'), - 'FileTransferError': AttributeBinding('je_auto_control.utils.remote_desktop.file_transfer', 'FileTransferError'), - 'confine_destination': - AttributeBinding('je_auto_control.utils.remote_desktop.file_transfer', 'confine_destination'), - 'default_download_dir': - AttributeBinding('je_auto_control.utils.remote_desktop.file_transfer', 'default_download_dir'), - 'send_file': AttributeBinding('je_auto_control.utils.remote_desktop.file_transfer', 'send_file'), - 'PresenceError': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'PresenceError'), - 'PresenceListener': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'PresenceListener'), - 'PresenceRegistry': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'PresenceRegistry'), - 'ROLE_CONTROLLER': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'ROLE_CONTROLLER'), - 'ROLE_OBSERVER': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'ROLE_OBSERVER'), - 'ViewerPresence': AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'ViewerPresence'), - 'default_presence_registry': - AttributeBinding('je_auto_control.utils.remote_desktop.presence', 'default_presence_registry'), + 'FileReceiver': AttributeBinding(_UTILS_REMOTE_DESKTOP_FILE_TRANSFER, 'FileReceiver'), + 'FileSendResult': AttributeBinding(_UTILS_REMOTE_DESKTOP_FILE_TRANSFER, 'FileSendResult'), + 'FileTransferError': AttributeBinding(_UTILS_REMOTE_DESKTOP_FILE_TRANSFER, 'FileTransferError'), + 'confine_destination': AttributeBinding(_UTILS_REMOTE_DESKTOP_FILE_TRANSFER, 'confine_destination'), + 'default_download_dir': AttributeBinding(_UTILS_REMOTE_DESKTOP_FILE_TRANSFER, 'default_download_dir'), + 'send_file': AttributeBinding(_UTILS_REMOTE_DESKTOP_FILE_TRANSFER, 'send_file'), + 'PresenceError': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'PresenceError'), + 'PresenceListener': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'PresenceListener'), + 'PresenceRegistry': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'PresenceRegistry'), + 'ROLE_CONTROLLER': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'ROLE_CONTROLLER'), + 'ROLE_OBSERVER': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'ROLE_OBSERVER'), + 'ViewerPresence': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'ViewerPresence'), + 'default_presence_registry': AttributeBinding(_UTILS_REMOTE_DESKTOP_PRESENCE, 'default_presence_registry'), } diff --git a/je_auto_control/utils/remote_desktop/signaling_server.py b/je_auto_control/utils/remote_desktop/signaling_server.py index 8de2c915d..862352ae2 100644 --- a/je_auto_control/utils/remote_desktop/signaling_server.py +++ b/je_auto_control/utils/remote_desktop/signaling_server.py @@ -143,7 +143,8 @@ class _AnswerIn(BaseModel): sdp: str -_AUTH_RESPONSES = {401: {"description": "bad shared secret"}} +_AUTH_FAILURE = "bad shared secret" +_AUTH_RESPONSES = {401: {"description": _AUTH_FAILURE}} _VALIDATION_RESPONSES = { 400: {"description": "invalid host_id or sdp"}, **_AUTH_RESPONSES, @@ -166,7 +167,7 @@ def _check( ] = None, ) -> None: if not _secret_matches(x_signaling_secret, shared_secret): - raise HTTPException(status_code=401, detail="bad shared secret") + raise HTTPException(status_code=401, detail=_AUTH_FAILURE) return _check @@ -287,7 +288,7 @@ def _guard_refusal(request: Request, shared_secret: Optional[str]) -> Optional[J if not path.startswith(("/sessions", "/config")) or request.method == "OPTIONS": return None if not _secret_matches(request.headers.get("X-Signaling-Secret"), shared_secret): - return JSONResponse({"detail": "bad shared secret"}, status_code=401) + return JSONResponse({"detail": _AUTH_FAILURE}, status_code=401) if request.method not in ("POST", "PUT"): return None length = request.headers.get("Content-Length") diff --git a/je_auto_control/utils/remote_desktop/webrtc_file_commit.py b/je_auto_control/utils/remote_desktop/webrtc_file_commit.py index f0b92db5f..9ace98d1e 100644 --- a/je_auto_control/utils/remote_desktop/webrtc_file_commit.py +++ b/je_auto_control/utils/remote_desktop/webrtc_file_commit.py @@ -25,6 +25,7 @@ _PROBE_TIMEOUT_S = 2.0 _COMMIT_TIMEOUT_S = 5.0 _MAX_ENVELOPE = 4096 +_OWNERSHIP_ENDED = 'file connection ownership ended' class FileCommitError(AutoControlException, RuntimeError): @@ -265,7 +266,7 @@ def guard(self) -> bool: _checkpoint(self.cancelled) self.bridge.validate() if not self.binding.usable(self.bridge.identity, self._generation): - raise FileCommitError('file connection ownership ended') + raise FileCommitError(_OWNERSHIP_ENDED) return False def probe(self) -> None: @@ -276,7 +277,7 @@ def probe(self) -> None: probe = _Probe() with self.binding.state.lock: if self.binding.state.closed or self.binding.state.generation != self._generation: - raise FileCommitError('file connection ownership ended') + raise FileCommitError(_OWNERSHIP_ENDED) if self.binding.state.probe is not None: raise FileCommitError('file capability probe already pending') self.binding.state.probe = probe @@ -301,7 +302,7 @@ def request(self, transfer_id: str, size: int) -> Iterator[CommitWait]: pending = _Commit(transfer_id, size) with self.binding.state.lock: if self.binding.state.closed or self.binding.state.generation != self._generation: - raise FileCommitError('file connection ownership ended') + raise FileCommitError(_OWNERSHIP_ENDED) if self.binding.state.commit is not None: raise FileCommitError('file publication confirmation already pending') self.binding.state.commit = pending diff --git a/je_auto_control/utils/remote_desktop/webrtc_file_receive.py b/je_auto_control/utils/remote_desktop/webrtc_file_receive.py index a0a646c4d..f3212dfcf 100644 --- a/je_auto_control/utils/remote_desktop/webrtc_file_receive.py +++ b/je_auto_control/utils/remote_desktop/webrtc_file_receive.py @@ -2,12 +2,12 @@ from __future__ import annotations from pathlib import Path -from typing import Callable, Optional, Protocol +from typing import Any, Callable, Optional, Protocol from je_auto_control.utils.remote_desktop.file_receipts import FileReceiveReceipt from je_auto_control.utils.logging.logging_instance import autocontrol_logger from je_auto_control.utils.remote_desktop.webrtc_file_commit import ( - FileCommitError, bind_commit_channel, current_epoch, parse_envelope, retire_commit_channel, + FileCommitBinding, FileCommitError, bind_commit_channel, current_epoch, parse_envelope, retire_commit_channel, ) @@ -45,6 +45,34 @@ def abort_file_receiver(receiver: Optional[_Receiver], channel: object = None) - receiver.abort() +def _admit_frame(binding: FileCommitBinding, receiver: _Receiver, message: object, + accept_begin: Callable[[], bool]) -> tuple[bool, Any]: + """Return whether the commit protocol consumed the frame, and the confirmation for a data frame.""" + try: + envelope = parse_envelope(message) + if envelope is not None and binding.control(envelope): + return True, None + if envelope is not None and envelope.get('type') == 'file_begin' and not accept_begin(): + return True, None + return False, binding.observe(envelope) + except FileCommitError as error: + binding.invalidate(terminal=True) + receiver.abort() + autocontrol_logger.debug('file protocol refused frame: %s', error) + return True, None + + +def _receipt_callback(binding: FileCommitBinding, active: Callable[[], bool], confirmation: Any, + received: Optional[Callable[[FileReceiveReceipt], None]], + ) -> Callable[[FileReceiveReceipt], None]: + def receipt_done(receipt: FileReceiveReceipt) -> None: + if active(): + binding.published(receipt, confirmation) + if received is not None: + received(receipt) + return receipt_done + + def bind_file_receiver(channel: _FileChannel, receiver: _Receiver, active: Callable[[], bool], completed: Callable[[Path], None], accept_begin: Callable[[], bool] = lambda: True, *, @@ -62,26 +90,10 @@ def message_received(message: object) -> None: binding.invalidate() receiver.abort() return - try: - envelope = parse_envelope(message) - if envelope is not None and binding.control(envelope): - return - if envelope is not None and envelope.get('type') == 'file_begin' and not accept_begin(): - return - confirmation = binding.observe(envelope) - except FileCommitError as error: - binding.invalidate(terminal=True) - receiver.abort() - autocontrol_logger.debug('file protocol refused frame: %s', error) - return - - def receipt_done(receipt: FileReceiveReceipt) -> None: - if active(): - binding.published(receipt, confirmation) - if received is not None: - received(receipt) - - receiver.handle_message(message, on_done=done, on_receipt=receipt_done) + consumed, confirmation = _admit_frame(binding, receiver, message, accept_begin) + if not consumed: + receiver.handle_message(message, on_done=done, + on_receipt=_receipt_callback(binding, active, confirmation, received)) @channel.on('close') def channel_closed() -> None: diff --git a/je_auto_control/utils/rest_api/rest_server.py b/je_auto_control/utils/rest_api/rest_server.py index 85af0abc3..a65095ea9 100644 --- a/je_auto_control/utils/rest_api/rest_server.py +++ b/je_auto_control/utils/rest_api/rest_server.py @@ -105,7 +105,8 @@ _GET_ROUTES.update({"/agent/tasks": agent_route("list"), "/agent/task": agent_route("get"), "/agent/targets": agent_route("targets")}) _POST_ROUTES.update({f"/agent/{operation}": agent_route(operation) for operation in AGENT_OPERATIONS}) -_POST_ROUTES['/agent/transfer/peer'] = agent_route('transfer_peer') +_PATH_TRANSFER_PEER = '/agent/transfer/peer' +_POST_ROUTES[_PATH_TRANSFER_PEER] = agent_route('transfer_peer') # /health is intentionally unauthenticated so probes / load balancers # can liveness-check without holding the bearer token. @@ -329,7 +330,7 @@ def _authenticate(self, client_ip: str, *, control: bool = False) -> AuthResult: return result def _peer_authentication(self, result: AuthResult) -> AuthResult: - if urlparse(self.path).path != '/agent/transfer/peer': + if urlparse(self.path).path != _PATH_TRANSFER_PEER: return result if result.verdict == 'ok' and any(self.headers.get(name) is not None for name in ('X-AutoControl-Principal', 'X-AutoControl-Agent-Origin')): @@ -338,14 +339,13 @@ def _peer_authentication(self, result: AuthResult) -> AuthResult: def _forwarded_principal(self, result: AuthResult) -> AuthResult: from je_auto_control.utils.agent_runtime.delegation import HEADER, verify_principal - from je_auto_control.utils.agent_runtime.errors import AgentTaskError forwarded = self.headers.get(HEADER) if not forwarded: return result try: principal = verify_principal(forwarded, getattr(self.server, "agent_token"), urlparse(self.path).path) return AuthResult("ok", principal) - except (AgentTaskError, AutoControlException): + except AutoControlException: # AgentTaskError included return AuthResult("unauthorized") def _metrics(self) -> RestMetrics: @@ -411,7 +411,7 @@ def _drain_unread_body(self, body: Any) -> None: def _read_json_body(self) -> Any: length = parse_content_length(self.headers) - transfer = urlparse(self.path).path in {'/agent/transfer', '/agent/transfer/peer'} + transfer = urlparse(self.path).path in {'/agent/transfer', _PATH_TRANSFER_PEER} limit = 1024 * 1024 if transfer else _MAX_BODY_BYTES if transfer and length > limit: self._send_json({'error': 'transfer body exceeds 1MiB'}, status=413, headers={'Connection': 'close'}) @@ -557,8 +557,9 @@ def __init__(self, host: str = "127.0.0.1", port: int = 9939, self._server: Optional[ThreadingHTTPServer] = None self._thread: Optional[threading.Thread] = None self._token = token if token else generate_token() - self._users = user_store if user_store is not None else ( - user_store_from_env() if use_environment_users else None) + if user_store is None and use_environment_users: + user_store = user_store_from_env() + self._users = user_store self._auth = RestAuthGate(expected_token=self._token, user_store=self._users) self._audit_log = self._open_audit_log() if enable_audit else None self._metrics = RestMetrics() diff --git a/je_auto_control/utils/self_healing/evaluation_io.py b/je_auto_control/utils/self_healing/evaluation_io.py index 52327ddb4..cd15c282b 100644 --- a/je_auto_control/utils/self_healing/evaluation_io.py +++ b/je_auto_control/utils/self_healing/evaluation_io.py @@ -113,7 +113,7 @@ def _load_samples_manifest(path: str | Path, path_policy: Optional[PathPolicy], candidate = _bounded_path(path, path_policy) try: payload = json.loads(_read(candidate, 1024 * 1024), object_pairs_hook=_unique_object) - except (ValueError, UnicodeError) as error: + except ValueError as error: raise HealingEvaluationError("manifest must be UTF-8 JSON") from error required = {"schema_version", "samples", "versions"} if require_versions else {"schema_version", "samples"} body = _object(payload, {"schema_version", "samples", "versions"}, required) diff --git a/je_auto_control/utils/self_healing/evaluation_models.py b/je_auto_control/utils/self_healing/evaluation_models.py index 086025d9e..6a91e2ae3 100644 --- a/je_auto_control/utils/self_healing/evaluation_models.py +++ b/je_auto_control/utils/self_healing/evaluation_models.py @@ -48,14 +48,15 @@ def png_size(frame: bytes) -> tuple[int, int]: if not isinstance(frame, bytes) or not frame or len(frame) > 16 * 1024 * 1024: raise HealingEvaluationError("frame must be immutable PNG bytes, at most 16 MiB") # pylint: disable-next=import-outside-toplevel # reason: importing the facade does not initialize image decoders - from PIL import Image, UnidentifiedImageError + from PIL import Image try: with Image.open(io.BytesIO(frame)) as image: if image.format != "PNG" or image.width * image.height > 16_000_000: raise HealingEvaluationError("frame must be a PNG of at most 16 million pixels") image.verify() return image.size - except (OSError, UnidentifiedImageError, Image.DecompressionBombError, SyntaxError) as error: + # OSError covers PIL.UnidentifiedImageError. + except (OSError, Image.DecompressionBombError, SyntaxError) as error: raise HealingEvaluationError("cannot decode evaluation PNG") from error diff --git a/je_auto_control/utils/self_healing/frame_locators.py b/je_auto_control/utils/self_healing/frame_locators.py index e3b8fb357..252daa82b 100644 --- a/je_auto_control/utils/self_healing/frame_locators.py +++ b/je_auto_control/utils/self_healing/frame_locators.py @@ -43,9 +43,8 @@ def _view(sample: EvaluationSample) -> _FrameView: # pylint: disable-next=import-outside-toplevel # reason: initialize optional decoders only when supplied pixels are used from PIL import Image stream = io.BytesIO() - with Image.open(io.BytesIO(sample.frame)) as image: - with image.crop(bounds) as cropped: - cropped.save(stream, format="PNG") + with Image.open(io.BytesIO(sample.frame)) as image, image.crop(bounds) as cropped: + cropped.save(stream, format="PNG") origin = ox + bounds[0] / sample.scale, oy + bounds[1] / sample.scale return _FrameView(stream.getvalue(), origin, sample.scale, (bounds[2] - bounds[0], bounds[3] - bounds[1])) diff --git a/je_auto_control/utils/self_healing/template_bundle.py b/je_auto_control/utils/self_healing/template_bundle.py index 3093de9dc..ffde94adc 100644 --- a/je_auto_control/utils/self_healing/template_bundle.py +++ b/je_auto_control/utils/self_healing/template_bundle.py @@ -1,6 +1,5 @@ """Immutable bounded template revision bundles and content-derived active-file state.""" import base64 -import binascii import json import os from dataclasses import dataclass, field @@ -42,7 +41,12 @@ def state(self, policy: Optional[PathPolicy]) -> TemplateCandidateState: current = None original, candidate = digest(self.original), digest(self.candidate) state: Literal["original", "candidate", "conflict", "missing"] - state = "missing" if current is None else "original" if current == original else "candidate" + if current is None: + state = "missing" + elif current == original: + state = "original" + else: + state = "candidate" if current is not None and current not in (original, candidate): state = "conflict" fmt, size = image_metadata(self.original) @@ -112,7 +116,7 @@ def read_bundle(bundle_path: str | Path, policy: Optional[PathPolicy]) -> Templa data = _read(path, 48 * 1024 * 1024) try: body = json.loads(data, object_pairs_hook=_unique_object) - except (ValueError, UnicodeError) as error: + except ValueError as error: raise TemplateRevisionError("candidate bundle must be UTF-8 JSON") from error body = _schema(body) target = _target(body["target"], path, policy) @@ -156,7 +160,7 @@ def _image(value: object) -> bytes: raise TemplateRevisionError("invalid candidate bundle image") try: result = base64.b64decode(value, validate=True) - except (ValueError, binascii.Error) as error: + except ValueError as error: # includes binascii.Error raise TemplateRevisionError("invalid candidate bundle image encoding") from error if not result or len(result) > MAX_IMAGE_BYTES: raise TemplateRevisionError("candidate bundle image exceeds 16 MiB") diff --git a/je_auto_control/utils/self_healing/template_images.py b/je_auto_control/utils/self_healing/template_images.py index f9d6deca6..c29585833 100644 --- a/je_auto_control/utils/self_healing/template_images.py +++ b/je_auto_control/utils/self_healing/template_images.py @@ -46,9 +46,8 @@ def encode_candidate(original: bytes, candidate_png: bytes) -> bytes: return candidate_png # pylint: disable-next=import-outside-toplevel # reason: initialize codecs only for an explicit candidate image from PIL import Image - with Image.open(io.BytesIO(candidate_png)) as image: - with image.convert("RGB") as decoded: - result = _encode(decoded, fmt) + with Image.open(io.BytesIO(candidate_png)) as image, image.convert("RGB") as decoded: + result = _encode(decoded, fmt) normalized_png(result) return result diff --git a/je_auto_control/wrapper/capabilities.py b/je_auto_control/wrapper/capabilities.py index ba60d81e2..caed4fc67 100644 --- a/je_auto_control/wrapper/capabilities.py +++ b/je_auto_control/wrapper/capabilities.py @@ -181,13 +181,18 @@ def probe_capabilities(context: BackendContext) -> CapabilitySnapshot: if not context.platform.startswith(('linux', 'freebsd', 'openbsd', 'netbsd')): return _unsupported(context) wayland = _is_wayland(context.environ) - override = context.environ.get('JE_AUTOCONTROL_LINUX_DISPLAY_SERVER', '').strip().lower() - selected = override if override in ('x11', 'wayland') else ('wayland' if wayland else 'x11') - if not context.platform.startswith('linux') or selected == 'x11': + if not context.platform.startswith('linux') or _display_server(context, wayland) == 'x11': return _x11(context, wayland) return CapabilitySnapshot(context.platform, 'wayland', _wayland_input(context), _wayland_capture(context)) +def _display_server(context: BackendContext, wayland: bool) -> str: + override = context.environ.get('JE_AUTOCONTROL_LINUX_DISPLAY_SERVER', '').strip().lower() + if override in ('x11', 'wayland'): + return override + return 'wayland' if wayland else 'x11' + + def _is_wayland(env: Mapping[str, str]) -> bool: return env.get('XDG_SESSION_TYPE', '').strip().lower() == 'wayland' or bool(env.get('WAYLAND_DISPLAY', '').strip()) @@ -207,11 +212,18 @@ def _unsupported(context: BackendContext) -> CapabilitySnapshot: return CapabilitySnapshot(context.platform, 'none', *statuses) +def _x11_reason(wayland: bool, present: bool) -> str: + if wayland: + return 'XWayland covers X11 clients only; native Wayland windows are outside its scope.' + if present: + return 'DISPLAY is configured; no X11 connection was attempted.' + return 'DISPLAY is not configured.' + + def _x11(context: BackendContext, wayland: bool) -> CapabilitySnapshot: present = bool(context.environ.get('DISPLAY', '').strip()) state = 'unverified' if present else 'needs_dependency' - reason = 'XWayland covers X11 clients only; native Wayland windows are outside its scope.' if wayland else ( - 'DISPLAY is configured; no X11 connection was attempted.' if present else 'DISPLAY is not configured.') + reason = _x11_reason(wayland, present) statuses = [_observation(context, name, 'x11') or CapabilityStatus( name, state, 'x11', reason, 'Provide a reachable X11 display and explicitly verify the intended operation.', not wayland) for name in ('input', 'capture')] diff --git a/pyproject.toml b/pyproject.toml index a5911f653..ac2ee761c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,12 @@ build-backend = "setuptools.build_meta" [project] name = "je_auto_control" -version = "1.1.0" +version = "1.2.0" authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ] description = "GUI Automation Framework" -requires-python = ">=3.10" +requires-python = ">=3.11" license-files = ["LICENSE"] dependencies = [ # Three requirements below carry the same marker, and it excludes exactly @@ -45,10 +45,8 @@ dependencies = [ # OpenCV release cannot silently change cv2 return shapes under us. "opencv-python>=4.8,<6; sys_platform != 'win32' or platform_machine != 'ARM64'", # Pillow 13.0.0 fixes the 12.x advisories (critical: GHSA-v9pv-5gc8-w2m6, - # GHSA-5x8w-8jj5-q52h) but needs Python >=3.11; 3.10 keeps 12.3.0, the - # newest release it can install. - "pillow==12.3.0; python_version < '3.11'", - "pillow==13.0.0; python_version >= '3.11'", + # GHSA-5x8w-8jj5-q52h). + "pillow==13.0.0", "pyobjc-core==12.2.2;platform_system=='Darwin'", "pyobjc==12.2.2;platform_system=='Darwin'", # The BSDs run the same X11 stack, and platform_system is the kernel @@ -60,7 +58,6 @@ dependencies = [ "cryptography>=50.0.0; sys_platform != 'win32' or platform_machine != 'ARM64'" ] classifiers = [ - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -129,10 +126,9 @@ locale = ["babel>=2.12"] audio = ["pycaw>=20240210"] # Lock-only security floor for a transitive dependency (uv.lock; the wheel -# metadata is unchanged). dnspython 2.9.0 fixes the advisory in 2.8.0 but -# needs Python >=3.11, so 3.10 keeps 2.8.0. +# metadata is unchanged). dnspython 2.9.0 fixes the advisory in 2.8.0. [tool.uv] -constraint-dependencies = ["dnspython>=2.9.0; python_version >= '3.11'"] +constraint-dependencies = ["dnspython>=2.9.0"] # CLAUDE.md's 120-character line limit. Without this table CI's `ruff check` # ran the default rule set, which has no E501, so the documented limit was @@ -246,7 +242,7 @@ precision = 2 fail_under = 81 [tool.mypy] -python_version = "3.10" +python_version = "3.11" warn_redundant_casts = true check_untyped_defs = true no_implicit_optional = true @@ -334,7 +330,7 @@ ignore_missing_imports = true [[tool.mypy.overrides]] # numpy's stubs use `type` statements, which mypy rejects while parsing under -# python_version 3.10 — skip them; the stable API does not expose numpy types. +# python_version below 3.12 — skip them; the stable API does not expose numpy types. # The top-level `numpy` module must be listed explicitly: `numpy.*` only matches # submodules, so numpy>=2.5's PEP 695 statements in numpy/__init__.pyi would leak. # `follow_imports_for_stubs` is required — otherwise `follow_imports = skip` is diff --git a/scripts/ci_native_diagnostics.py b/scripts/ci_native_diagnostics.py index 204f76f4f..570fcad15 100644 --- a/scripts/ci_native_diagnostics.py +++ b/scripts/ci_native_diagnostics.py @@ -68,19 +68,22 @@ def run(command: Sequence[str], directory: Path, *, native: bool = False) -> int report.write_text(json.dumps(record, indent=2), encoding='utf-8') environment = dict(os.environ, PYTHONFAULTHANDLER='1', PYTHONUNBUFFERED='1') started = time.monotonic() - with _debugger_argv(command, debugger) as actual, (directory / 'process.log').open('w', encoding='utf-8') as output: - with subprocess.Popen( # nosec B603 # reason: explicit CI child argv; shell remains disabled + with ( + _debugger_argv(command, debugger) as actual, + (directory / 'process.log').open('w', encoding='utf-8') as output, + subprocess.Popen( # nosec B603 # reason: explicit CI child argv; shell remains disabled actual, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=environment, text=True, encoding='utf-8', errors='replace', - ) as child: - if child.stdout is None: - raise RuntimeError('child output pipe was not created') - for line in child.stdout: - output.write(line) - output.flush() - sys.stdout.write(line) - sys.stdout.flush() - result = child.wait() + ) as child, + ): + if child.stdout is None: + raise RuntimeError('child output pipe was not created') + for line in child.stdout: + output.write(line) + output.flush() + sys.stdout.write(line) + sys.stdout.flush() + result = child.wait() record.update(returncode=result, elapsed_seconds=round(time.monotonic() - started, 3)) report.write_text(json.dumps(record, indent=2), encoding='utf-8') return result if result >= 0 else 128 - result diff --git a/scripts/generate_facade_exports.py b/scripts/generate_facade_exports.py index e1465472e..1cab625f8 100644 --- a/scripts/generate_facade_exports.py +++ b/scripts/generate_facade_exports.py @@ -11,6 +11,7 @@ from hashlib import sha256 import json from pathlib import Path +import re from typing import Sequence @@ -21,6 +22,8 @@ PACKAGES[0]: {"keyboard_keys_table", "start_autocontrol_gui"}, PACKAGES[1]: {"registry", "is_webrtc_available"}, } +_PACKAGE_INIT = "__init__.py" +_SCOPES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef) @dataclass(frozen=True) @@ -31,17 +34,42 @@ class Binding: attribute: str module: bool = False - def render(self) -> str: - """Return the generated binding constructor.""" + def render(self, owners: dict[str, str]) -> str: + """Return the generated binding constructor, naming shared owners by constant.""" if self.module: return f"ModuleBinding({self.owner + '.' + self.attribute!r})" - return f"AttributeBinding({self.owner!r}, {self.attribute!r})" + return f"AttributeBinding({owners.get(self.owner, repr(self.owner))}, {self.attribute!r})" + + +Inspection = tuple[list[str], dict[str, Binding], dict[str, Binding]] + + +def owner_constants(bindings: dict[str, Binding]) -> dict[str, str]: + """Name every owner module that more than one attribute binding repeats.""" + uses: dict[str, int] = {} + for binding in bindings.values(): + if not binding.module: + uses[binding.owner] = uses.get(binding.owner, 0) + 1 + return {owner: "_" + owner.removeprefix(PACKAGES[0] + ".").replace(".", "_").upper() + for owner, count in uses.items() if count > 1} + + +# bandit's B105 test flags any string bound to a name with one of these words in it. +_CREDENTIAL_NAME = re.compile(r"(^|_)(pas+wo?r?d|pass(phrase)?|pwd|token|secrete?)(_|$)", re.IGNORECASE) + + +def constant_line(constant: str, owner: str) -> str: + """Render one owner constant; a module named like a credential is still a module path.""" + line = f"{constant} = {owner!r}" + if _CREDENTIAL_NAME.search(constant): + line += " # nosec B105 # reason: an owner module path, not a credential" + return line def source_path(module: str) -> Path: """Require an actual package or module source file.""" path = ROOT.joinpath(*module.split(".")) - for candidate in (path.with_suffix(".py"), path / "__init__.py"): + for candidate in (path.with_suffix(".py"), path / _PACKAGE_INIT): if candidate.is_file(): return candidate raise ValueError(f"missing owner source: {module}") @@ -53,39 +81,58 @@ def read_tree(module: str) -> ast.Module: return ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) +def _import_binds(node: ast.Import | ast.ImportFrom, module: str, name: str) -> bool: + """Whether an import binds ``name``; a package importing its own child file does not count.""" + for alias in node.names: + if (alias.asname or alias.name.split(".")[0]) != name: + continue + if isinstance(node, ast.ImportFrom) and node.module == module and alias.name == name: + continue + return True + return False + + +def _assigns(node: ast.stmt, name: str) -> bool: + """Whether a plain assignment, or an annotated one with a value, binds ``name``.""" + if isinstance(node, ast.Assign): + return any(name in target_names(target) for target in node.targets) + if isinstance(node, ast.AnnAssign): + return node.value is not None and name in target_names(node.target) + return False + + +def _nested_statements(node: ast.stmt) -> list[ast.stmt]: + """Statements of every block nested in ``node`` that still runs in the module namespace.""" + statements = [statement for field in ("body", "orelse", "finalbody") for statement in getattr(node, field, [])] + if isinstance(node, ast.Try): + statements.extend(statement for handler in node.handlers for statement in handler.body) + return statements + + def defines_attribute(module: str, name: str) -> bool: """A package's re-export takes precedence over a same-named child file.""" pending = list(read_tree(module).body) while pending: node = pending.pop() - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + if isinstance(node, _SCOPES): if node.name == name: return True continue - if isinstance(node, (ast.ImportFrom, ast.Import)): - for alias in node.names: - if (alias.asname or alias.name.split(".")[0]) == name: - if isinstance(node, ast.ImportFrom) and node.module == module and alias.name == name: - continue - return True - if isinstance(node, ast.Assign) and any(name in target_names(target) for target in node.targets): + if isinstance(node, (ast.ImportFrom, ast.Import)) and _import_binds(node, module, name): return True - if isinstance(node, ast.AnnAssign) and node.value is not None and name in target_names(node.target): + if _assigns(node, name): return True - for field in ("body", "orelse", "finalbody"): - pending.extend(getattr(node, field, [])) - if isinstance(node, ast.Try): - pending.extend(statement for handler in node.handlers for statement in handler.body) + pending.extend(_nested_statements(node)) return False def child_module(module: str, name: str) -> bool: """Use exact source spelling even on case-insensitive filesystems.""" parent = source_path(module) - if parent.name != "__init__.py": + if parent.name != _PACKAGE_INIT: return False return any((path.name == name + ".py" and path.is_file()) - or (path.name == name and (path / "__init__.py").is_file()) + or (path.name == name and (path / _PACKAGE_INIT).is_file()) for path in parent.parent.iterdir()) @@ -112,25 +159,36 @@ def public_names(tree: ast.Module) -> list[str]: return names +def _source_module(node: ast.ImportFrom) -> str: + """Require an absolute import from a module that has a source file.""" + if node.level or node.module is None: + raise ValueError("export declarations must use absolute source modules") + source_path(node.module) + return node.module + + +def _alias_binding(module: str, attribute: str) -> Binding: + """Resolve an imported name to the owner's attribute, or to its child module when it defines none.""" + is_module = not defines_attribute(module, attribute) and child_module(module, attribute) + if not is_module and not defines_attribute(module, attribute): + raise ValueError(f"missing source attribute: {module}.{attribute}") + return Binding(module, attribute, is_module) + + def imported_bindings(nodes: Sequence[ast.stmt]) -> dict[str, Binding]: """Preserve the exact imported attribute behind every explicit alias.""" result: dict[str, Binding] = {} for node in nodes: if not isinstance(node, ast.ImportFrom): continue - if node.level or node.module is None: - raise ValueError("export declarations must use absolute source modules") - source_path(node.module) + module = _source_module(node) for alias in node.names: if alias.name == "*": raise ValueError("wildcard export declaration") name = alias.asname or alias.name if name in result: raise ValueError(f"duplicate export declaration: {name}") - is_module = not defines_attribute(node.module, alias.name) and child_module(node.module, alias.name) - if not is_module and not defines_attribute(node.module, alias.name): - raise ValueError(f"missing source attribute: {node.module}.{alias.name}") - result[name] = Binding(node.module, alias.name, is_module) + result[name] = _alias_binding(module, alias.name) return result @@ -153,9 +211,8 @@ def return_names(value: ast.expr | None) -> list[str]: return names -def optional_owners(module: str) -> dict[str, Binding]: - """Verify each all-or-None outcome and trace it to the original owner.""" - tree = read_tree(module) +def _optional_loader(module: str, tree: ast.Module) -> ast.FunctionDef: + """Require exactly one concretely typed ``_load`` function.""" loaders = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "_load"] if len(loaders) != 1: raise ValueError(f"{module}: expected one _load function") @@ -163,13 +220,22 @@ def optional_owners(module: str) -> dict[str, Binding]: if loader.returns is None or any(isinstance(node, ast.Name) and node.id == "Any" for node in ast.walk(loader.returns)): raise ValueError(f"{module}: optional result requires concrete types") + return loader + + +def _loader_statements(module: str, loader: ast.FunctionDef) -> tuple[ast.Try, ast.Return]: + """Require one import ``try`` followed by one ``return``, after an optional docstring.""" body = loader.body if (body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) and isinstance(body[0].value.value, str)): body = body[1:] if len(body) != 2 or not isinstance(body[0], ast.Try) or not isinstance(body[1], ast.Return): raise ValueError(f"{module}: optional loader must be one import try and an object return") - attempt = body[0] + return body[0], body[1] + + +def _failure_handler(module: str, attempt: ast.Try) -> ast.ExceptHandler: + """Require that only ImportError, through one plain return, ends the direct-import attempt.""" if attempt.orelse or attempt.finalbody or len(attempt.handlers) != 1: raise ValueError(f"{module}: unsupported optional failure policy") handler = attempt.handlers[0] @@ -179,89 +245,141 @@ def optional_owners(module: str) -> dict[str, Binding]: raise ValueError(f"{module}: failure must return the complete None outcome") if not all(isinstance(node, ast.ImportFrom) for node in attempt.body): raise ValueError(f"{module}: optional loader must import original owners directly") - imports = imported_bindings(attempt.body) - returned = return_names(body[1].value) - failed = ast.literal_eval(handler.body[0].value) - if failed != (None if len(returned) == 1 else (None,) * len(returned)): - raise ValueError(f"{module}: failure changes the all-None group") + return handler + + +def _outcome_targets(module: str, tree: ast.Module) -> list[str]: + """Require one module-level assignment that unpacks the whole ``_load()`` outcome at once.""" assignments = [node for node in tree.body if isinstance(node, ast.Assign) and isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) and node.value.func.id == "_load"] if len(assignments) != 1 or len(assignments[0].targets) != 1: raise ValueError(f"{module}: expected one atomic outcome assignment") - exported = target_names(assignments[0].targets[0]) + return target_names(assignments[0].targets[0]) + + +def optional_owners(module: str) -> dict[str, Binding]: + """Verify each all-or-None outcome and trace it to the original owner.""" + tree = read_tree(module) + attempt, result = _loader_statements(module, _optional_loader(module, tree)) + handler = _failure_handler(module, attempt) + imports = imported_bindings(attempt.body) + returned = return_names(result.value) + failed = ast.literal_eval(handler.body[0].value) + if failed != (None if len(returned) == 1 else (None,) * len(returned)): + raise ValueError(f"{module}: failure changes the all-None group") + exported = _outcome_targets(module, tree) if len(exported) != len(returned) or set(returned) != set(imports): raise ValueError(f"{module}: incomplete optional owner/result mapping") return dict(zip(exported, (imports[name] for name in returned))) -def inspect_package(package: str) -> tuple[list[str], dict[str, Binding], dict[str, Binding]]: - """Classify every public export and every immediate child-name collision.""" - tree = read_tree(package) - names = public_names(tree) - bindings = declarations(tree) - if set(bindings) | SPECIAL[package] != set(names) or set(bindings) & SPECIAL[package]: - raise ValueError(f"{package}: public declarations differ from __all__ or special exports") +def _resolved_owners(bindings: dict[str, Binding]) -> dict[str, Binding]: + """Replace each optional-group declaration by the original owner its loader imports.""" owners = dict(bindings) for name, binding in bindings.items(): - if binding.owner.startswith(OPTIONAL): - group = optional_owners(binding.owner) - if binding.attribute not in group: - raise ValueError(f"{name}: missing optional outcome") - owners[name] = group[binding.attribute] - declared_group = {item.attribute for item in bindings.values() if item.owner == binding.owner} - if declared_group != set(group): - raise ValueError(f"{binding.owner}: facade does not declare the complete optional group") + if not binding.owner.startswith(OPTIONAL): + continue + group = optional_owners(binding.owner) + if binding.attribute not in group: + raise ValueError(f"{name}: missing optional outcome") + owners[name] = group[binding.attribute] + declared_group = {item.attribute for item in bindings.values() if item.owner == binding.owner} + if declared_group != set(group): + raise ValueError(f"{binding.owner}: facade does not declare the complete optional group") + return owners + + +def _check_child_collisions(package: str, names: list[str]) -> None: + """Every public name that is also a child module's name must be a known, classified one.""" directory = source_path(package).parent children = {path.stem for path in directory.glob("*.py") if path.stem != "__init__"} - children |= {path.name for path in directory.iterdir() if (path / "__init__.py").is_file()} + children |= {path.name for path in directory.iterdir() if (path / _PACKAGE_INIT).is_file()} collisions = set(names) & children expected = {"registry", "signaling_client"} if package == PACKAGES[1] else set() if collisions != expected: raise ValueError(f"{package}: unclassified child collisions: {collisions ^ expected}") + + +def _remote_eager_bindings(package: str, tree: ast.Module, bindings: dict[str, Binding]) -> dict[str, Binding]: + """The remote desktop package keeps its eager registry and its actual signaling_client module.""" + eager = imported_bindings([node for node in tree.body if isinstance(node, ast.ImportFrom) + and node.module == package + ".registry"]) + if eager != {"registry": Binding(package + ".registry", "registry")}: + raise ValueError("remote registry must retain its eager singleton binding") + if bindings["signaling_client"] != Binding(package, "signaling_client", True): + raise ValueError("signaling_client must remain the actual child module") + return eager + + +def inspect_package(package: str) -> Inspection: + """Classify every public export and every immediate child-name collision.""" + tree = read_tree(package) + names = public_names(tree) + bindings = declarations(tree) + if set(bindings) | SPECIAL[package] != set(names) or set(bindings) & SPECIAL[package]: + raise ValueError(f"{package}: public declarations differ from __all__ or special exports") + owners = _resolved_owners(bindings) + _check_child_collisions(package, names) if package == PACKAGES[1]: - eager = imported_bindings([node for node in tree.body if isinstance(node, ast.ImportFrom) - and node.module == package + ".registry"]) - if eager != {"registry": Binding(package + ".registry", "registry")}: - raise ValueError("remote registry must retain its eager singleton binding") - owners.update(eager) - if bindings["signaling_client"] != Binding(package, "signaling_client", True): - raise ValueError("signaling_client must remain the actual child module") + owners.update(_remote_eager_bindings(package, tree, bindings)) return names, bindings, owners -def compare_baseline(path: Path, assignments_path: Path, - inspected: dict[str, tuple[list[str], dict[str, Binding], dict[str, Binding]]]) -> None: +def _original_loader_groups(package: str, original: dict, assignments: dict, + owners: dict[str, Binding]) -> set[frozenset[Binding]]: + """Check every original optional alias against the current owners; return the original groups.""" + previous = set() + for loader_name, loader in original["optional_loaders"].items(): + loader_tree = ast.parse(loader["source"]) + imported = imported_bindings([node for node in ast.walk(loader_tree) + if isinstance(node, ast.ImportFrom)]) + previous.add(frozenset(imported.values())) + returned = return_names(loader_tree.body[0].body[-1].value) + targets = assignments[loader_name] + if len(targets) != len(returned): + raise ValueError(f"{loader_name}: incomplete original assignment contract") + for public, local in zip(targets, returned): + if owners.get(public) != imported[local]: + raise ValueError(f"{package}.{public}: original optional owner or alias changed") + return previous + + +def _compare_package(package: str, inspection: Inspection, original: dict, assignments: dict) -> None: + """Compare one package's public order, required owners and optional groups with its baseline.""" + names, bindings, owners = inspection + if names != original["ordered_exports"]: + raise ValueError(f"{package}: ordered public contract changed") + for name, expected in original["required_bindings"].items(): + actual = owners.get(name) + if actual is None or (actual.owner, actual.attribute) != (expected["owner"], expected["attribute"]): + raise ValueError(f"{package}.{name}: original owner or alias changed") + current_groups = {binding.owner for binding in bindings.values() if binding.owner.startswith(OPTIONAL)} + current = {frozenset(optional_owners(group).values()) for group in current_groups} + if current != _original_loader_groups(package, original, assignments, owners): + raise ValueError(f"{package}: original optional ownership or grouping changed") + + +def compare_baseline(path: Path, assignments_path: Path, inspected: dict[str, Inspection]) -> None: """Compare all original aliases and loader group membership, not just counts.""" # Both paths are this repository's own baseline files, named on the command line by the maintainer. baseline = json.loads(path.read_text(encoding="utf-8"))["packages"] # NOSONAR pythonsecurity:S8707 assignments_text = assignments_path.read_text(encoding="utf-8") # NOSONAR pythonsecurity:S8707 assignments = json.loads(assignments_text)["optional_assignments"] - for package, (names, _bindings, owners) in inspected.items(): - original = baseline[package.replace(".", "/")] - if names != original["ordered_exports"]: - raise ValueError(f"{package}: ordered public contract changed") - for name, expected in original["required_bindings"].items(): - actual = owners.get(name) - if actual is None or (actual.owner, actual.attribute) != (expected["owner"], expected["attribute"]): - raise ValueError(f"{package}.{name}: original owner or alias changed") - current_groups = {binding.owner for binding in _bindings.values() if binding.owner.startswith(OPTIONAL)} - current = {frozenset(optional_owners(group).values()) for group in current_groups} - previous = set() - for loader_name, loader in original["optional_loaders"].items(): - loader_tree = ast.parse(loader["source"]) - imported = imported_bindings([node for node in ast.walk(loader_tree) - if isinstance(node, ast.ImportFrom)]) - previous.add(frozenset(imported.values())) - returned = return_names(loader_tree.body[0].body[-1].value) - targets = assignments[loader_name] - if len(targets) != len(returned): - raise ValueError(f"{loader_name}: incomplete original assignment contract") - for public, local in zip(targets, returned): - if owners.get(public) != imported[local]: - raise ValueError(f"{package}.{public}: original optional owner or alias changed") - if current != previous: - raise ValueError(f"{package}: original optional ownership or grouping changed") + for package, inspection in inspected.items(): + _compare_package(package, inspection, baseline[package.replace(".", "/")], assignments) + + +def binding_lines(name: str, binding: Binding, owners: dict[str, str]) -> list[str]: + """Render one mapping entry, wrapping it to stay within 120 columns.""" + value = binding.render(owners) + line = f" {name!r}: {value}," + if len(line) <= 120: + return [line] + if len(value) <= 111: + return [f" {name!r}:", f" {value},"] + owner = owners.get(binding.owner, repr(binding.owner)) + return [f" {name!r}: AttributeBinding(", f" {owner}, {binding.attribute!r},", " ),"] def generated_source(names: list[str], bindings: dict[str, Binding]) -> str: @@ -270,21 +388,15 @@ def generated_source(names: list[str], bindings: dict[str, Binding]) -> str: if any(binding.module for binding in bindings.values()): constructors += ", ModuleBinding" order_digest = sha256(json.dumps(names).encode("utf-8")).hexdigest() + owners = owner_constants(bindings) lines = ['"""Generated by scripts/generate_facade_exports.py; edit concrete facade declarations."""', f"from je_auto_control._lazy_exports import {constructors}", "", "", - f"PUBLIC_ORDER_SHA256 = {order_digest!r}", "", "EXPORTS: dict[str, ExportBinding] = {"] + f"PUBLIC_ORDER_SHA256 = {order_digest!r}", ""] + lines += [constant_line(constant, owner) for owner, constant in sorted(owners.items(), key=lambda item: item[1])] + lines += ["", "EXPORTS: dict[str, ExportBinding] = {"] for name in names: if name in bindings: - value = bindings[name].render() - line = f" {name!r}: {value}," - if len(line) <= 120: - lines.append(line) - elif len(value) <= 111: - lines.extend([f" {name!r}:", f" {value},"]) - else: - binding = bindings[name] - lines.extend([f" {name!r}: AttributeBinding(", - f" {binding.owner!r}, {binding.attribute!r},", " ),"]) + lines.extend(binding_lines(name, bindings[name], owners)) lines += ["}", ""] return "\n".join(lines) diff --git a/test/unit_test/headless/test_action_journal.py b/test/unit_test/headless/test_action_journal.py index e4b56e509..4ae00207b 100644 --- a/test/unit_test/headless/test_action_journal.py +++ b/test/unit_test/headless/test_action_journal.py @@ -22,8 +22,10 @@ def test_event_is_frozen_and_unfinished_is_not_success(tmp_path): journal = ActionJournal(tmp_path / "events.jsonl") journal.append(event) loaded = read_events(journal.path) - assert loaded[0].status == "incomplete" and loaded[0].finished_at is None - assert loaded[0].schema_version == 1 and loaded[0].sequence == 1 + assert loaded[0].status == "incomplete" + assert loaded[0].finished_at is None + assert loaded[0].schema_version == 1 + assert loaded[0].sequence == 1 def test_redaction_precedes_append_and_keeps_secret_references(tmp_path, monkeypatch): @@ -33,7 +35,8 @@ def test_redaction_precedes_append_and_keeps_secret_references(tmp_path, monkeyp def append(path, line): seen.append(line) - assert "literal-password" not in line and "literal-token" not in line + assert "literal-password" not in line + assert "literal-token" not in line original(path, line) monkeypatch.setattr(store, "append_json_line", append) @@ -41,7 +44,8 @@ def append(path, line): event = _event(parameters={"password": "literal-password", "headers": {"Authorization": "literal-token"}, "text": "${secrets.LOGIN}"}) journal.append(event) - assert seen and "${secrets.LOGIN}" in seen[0] + assert seen + assert "${secrets.LOGIN}" in seen[0] assert event.parameters["password"] == "literal-password" assert read_events(journal.path)[0].parameters["password"] == "***" @@ -53,7 +57,8 @@ def test_secret_command_outcome_and_error_are_not_persisted(tmp_path): status="error", finished_at=2.0)) assert "hidden" not in journal.path.read_text(encoding="utf-8") event = read_events(journal.path)[0] - assert event.outcome == "***" and event.error == "***" + assert event.outcome == "***" + assert event.error == "***" assert event.non_replayable @@ -70,9 +75,11 @@ def test_opaque_outcome_keys_cannot_leak_credentials(tmp_path): journal.append(_event(outcome={"private-result": {"another-credential": "anything"}}, status="ok", finished_at=2.0)) text = journal.path.read_text(encoding="utf-8") - assert "private-result" not in text and "another-credential" not in text + assert "private-result" not in text + assert "another-credential" not in text event = read_events(journal.path)[0] - assert "redacted_outcome" in event.non_replayable and event.outcome_type == "object" + assert "redacted_outcome" in event.non_replayable + assert event.outcome_type == "object" def test_sensitive_fields_can_retain_explicit_secret_references(tmp_path): @@ -103,7 +110,8 @@ def __repr__(self): journal = ActionJournal(tmp_path / "events.jsonl") journal.append(_event(parameters={"payload": Payload()})) event = read_events(journal.path)[0] - assert event.non_replayable and "unsupported" in str(event.parameters) + assert event.non_replayable + assert "unsupported" in str(event.parameters) def test_run_filter_and_parent_links_survive_round_trip(tmp_path): @@ -136,9 +144,11 @@ def write(index): thread.start() for thread in threads: thread.join(timeout=15) - assert not any(thread.is_alive() for thread in threads) and failures == [] + assert not any(thread.is_alive() for thread in threads) + assert failures == [] events = read_events(path) - assert len(events) == 32 and [event.sequence for event in events] == list(range(1, 33)) + assert len(events) == 32 + assert [event.sequence for event in events] == list(range(1, 33)) assert all(event.run_id == "run" and event.parent_id == "parent" for event in events) @@ -163,8 +173,9 @@ def test_partial_tail_does_not_complete_a_step_and_append_recovers_it(tmp_path): ]) def test_invalid_events_fail_before_any_write(tmp_path, change): journal = ActionJournal(tmp_path / "events.jsonl") + event = _event(**change) with pytest.raises(ActionJournalError): - journal.append(_event(**change)) + journal.append(event) assert not journal.path.exists() @@ -210,8 +221,9 @@ def test_reader_checks_sequence_even_for_runs_it_filters_out(tmp_path): @pytest.mark.parametrize("value", ["\ud800", 10 ** 5000], ids=["invalid-unicode", "oversized-integer"]) def test_invalid_json_encodings_are_framework_errors_before_write(tmp_path, value): journal = ActionJournal(tmp_path / "events.jsonl") + event = _event(parameters={"value": value}) with pytest.raises(ActionJournalError, match="encoding"): - journal.append(_event(parameters={"value": value})) + journal.append(event) assert not journal.path.exists() @@ -221,8 +233,9 @@ def test_sequence_exhaustion_does_not_append_an_unreadable_record(tmp_path): row = json.loads(journal.path.read_text(encoding="utf-8")) row["sequence"] = 2 ** 63 - 1 journal.path.write_text(json.dumps(row) + "\n", encoding="utf-8") + event = _event() with pytest.raises(ActionJournalError): - journal.append(_event()) + journal.append(event) assert len(read_events(journal.path)) == 1 @@ -271,8 +284,9 @@ def test_processes_share_a_monotonic_append_order(tmp_path): def test_record_budget_uses_encoded_bytes_and_fails_before_write(tmp_path): journal = ActionJournal(tmp_path / "events.jsonl") + event = _event(parameters={"text": "界" * 3_000_000}) with pytest.raises(ActionJournalError, match="8 MiB"): - journal.append(_event(parameters={"text": "界" * 3_000_000})) + journal.append(event) assert not journal.path.exists() @@ -280,11 +294,14 @@ def test_record_budget_uses_encoded_bytes_and_fails_before_write(tmp_path): def test_storage_errors_stay_in_the_framework_exception_family(tmp_path, operation): destination = tmp_path / "directory" destination.mkdir() - with pytest.raises(ActionJournalError): - if operation == "read": + if operation == "read": + with pytest.raises(ActionJournalError): read_events(destination) - else: - ActionJournal(destination).append(_event()) + else: + journal = ActionJournal(destination) + event = _event() + with pytest.raises(ActionJournalError): + journal.append(event) @pytest.mark.parametrize("existing", [ @@ -297,8 +314,9 @@ def test_storage_errors_stay_in_the_framework_exception_family(tmp_path, operati def test_append_never_truncates_an_existing_nonjournal_destination(tmp_path, existing): journal = ActionJournal(tmp_path / "existing.jsonl") journal.path.write_bytes(existing) + event = _event() with pytest.raises(ActionJournalError): - journal.append(_event()) + journal.append(event) assert journal.path.read_bytes() == existing with pytest.raises(ActionJournalError): read_events(journal.path) @@ -310,13 +328,15 @@ def test_corrupt_complete_record_prevents_torn_tail_repair(tmp_path): with journal.path.open("ab") as handle: handle.write(b'{not-json}\npartial') original = journal.path.read_bytes() + event = _event() with pytest.raises(ActionJournalError): - journal.append(_event()) + journal.append(event) assert journal.path.read_bytes() == original def test_foreign_destination_reports_the_format_refusal(tmp_path): journal = ActionJournal(tmp_path / "existing.jsonl") journal.path.write_bytes(b"unrelated user content") + event = _event() with pytest.raises(ActionJournalError, match="destination has no committed journal"): - journal.append(_event()) + journal.append(event) diff --git a/test/unit_test/headless/test_action_journal_capture.py b/test/unit_test/headless/test_action_journal_capture.py index 6797a25ce..7525e6b32 100644 --- a/test/unit_test/headless/test_action_journal_capture.py +++ b/test/unit_test/headless/test_action_journal_capture.py @@ -30,7 +30,8 @@ def test_scoped_capture_preserves_source_refs_and_never_writes_resolved_secrets( monkeypatch.setattr(default_secret_manager, "get", lambda name: "resolved-private-value") path = tmp_path / "events.jsonl" result = execute_action_with_journal([[PROBE, {"value": "${secrets.LOGIN}"}]], path, run_id="run") - assert result["run_id"] == "run" and result["failed_actions"] == 0 + assert result["run_id"] == "run" + assert result["failed_actions"] == 0 assert seen == ["resolved-private-value"] assert "resolved-private-value" not in path.read_text(encoding="utf-8") events = read_events(path) @@ -47,9 +48,12 @@ def test_loop_children_share_run_and_point_to_the_observed_parent(tmp_path, seen completed = _completed(path) parent = next(event for event in completed if event.command == "AC_loop") children = [event for event in completed if event.command == PROBE] - assert len(children) == 2 and len({event.step_id for event in children}) == 2 + assert len(children) == 2 + assert len({event.step_id for event in children}) == 2 assert all(event.run_id == "loop" and event.parent_id == parent.step_id for event in children) - assert parent.parent_id is None and parent.status == "ok" and seen == ["child", "child"] + assert parent.parent_id is None + assert parent.status == "ok" + assert seen == ["child", "child"] assert all(event.sequence < parent.sequence for event in children) @@ -68,7 +72,8 @@ def test_worker_actions_retain_parent_and_identity(tmp_path, seen, block): completed = _completed(path) parent = next(event for event in completed if event.command == parent_command) children = [event for event in completed if event.command == PROBE] - assert sorted(seen) == ["left", "right"] and len(children) == 2 + assert sorted(seen) == ["left", "right"] + assert len(children) == 2 assert all(event.parent_id == parent.step_id for event in children) assert all((event.run_id, event.device_id, event.session_id) == ("workers", "device", "session") for event in read_events(path)) @@ -118,7 +123,8 @@ def test_dry_run_is_labelled_and_invokes_no_handler(tmp_path, seen): path = tmp_path / "events.jsonl" execute_action_with_journal([[PROBE, {"value": "never"}]], path, dry_run=True) event = _completed(path)[0] - assert event.status == "dry_run" and "not_executed" in event.non_replayable + assert event.status == "dry_run" + assert "not_executed" in event.non_replayable assert seen == [] @@ -130,15 +136,18 @@ def test_nested_scopes_restore_parent_and_isolate_seeded_variables(tmp_path, see variables={"owner": "inner"}, run_id="inner") assert current_journal() is binding executor.execute_action([[PROBE, {"value": "${owner}"}]]) - assert seen == ["inner", "outer"] and current_journal() is None + assert seen == ["inner", "outer"] + assert current_journal() is None assert {event.run_id for event in read_events(outer)} == {"outer"} assert {event.run_id for event in read_events(inner)} == {"inner"} def test_plain_execution_keeps_its_existing_result_contract(tmp_path, seen): record = executor.execute_action([[PROBE, {"value": "plain"}]]) - assert list(record.values()) == ["plain"] and seen == ["plain"] - assert list(tmp_path.iterdir()) == [] and current_journal() is None + assert list(record.values()) == ["plain"] + assert seen == ["plain"] + assert list(tmp_path.iterdir()) == [] + assert current_journal() is None @pytest.mark.parametrize("strict", [False, True]) @@ -158,8 +167,9 @@ def interrupted(event): append(event) monkeypatch.setattr(journal, "append", interrupted) + scope = journal_scope(journal) with pytest.raises(AutoControlActionException if strict else ActionJournalError): - with journal_scope(journal): + with scope: executor.execute_action([["AC_journal_fail"]], raise_on_error=strict) assert [event.status for event in read_events(journal.path)] == ["incomplete"] assert current_journal() is None @@ -171,10 +181,13 @@ def test_pre_dispatch_callback_cancellation_does_not_invent_an_attempt(tmp_path, def stopped(_action): raise KeyboardInterrupt() + scope = journal_scope(ActionJournal(path)) with pytest.raises(KeyboardInterrupt): - with journal_scope(ActionJournal(path)): + with scope: executor.execute_action([[PROBE, {"value": "never"}]], step_callback=stopped) - assert not path.exists() and seen == [] and current_journal() is None + assert not path.exists() + assert seen == [] + assert current_journal() is None def test_process_exit_during_a_command_preserves_only_its_incomplete_start(tmp_path): @@ -193,4 +206,6 @@ def test_process_exit_during_a_command_preserves_only_its_incomplete_start(tmp_p ) assert done.returncode == 23, done.stderr events = read_events(path) - assert len(events) == 1 and events[0].run_id == "crashed" and events[0].status == "incomplete" + assert len(events) == 1 + assert events[0].run_id == "crashed" + assert events[0].status == "incomplete" diff --git a/test/unit_test/headless/test_agent_android_action_result.py b/test/unit_test/headless/test_agent_android_action_result.py index 0e989c00a..21dbb0064 100644 --- a/test/unit_test/headless/test_agent_android_action_result.py +++ b/test/unit_test/headless/test_agent_android_action_result.py @@ -46,15 +46,17 @@ def test_returned_input_and_partial_failure_keep_possible_effects(stdout, code, ]) @pytest.mark.parametrize("code", [0, 91, 93, 94, 95, 96]) def test_partial_forged_extra_or_legacy_output_remains_unknown(stdout, code): + result = AdbShellResult(stdout, b"", code) with pytest.raises(AgentTaskError, match="unknown") as error: - parse_android_action_result(AdbShellResult(stdout, b"", code)) + parse_android_action_result(result) assert "private-device-diagnostic" not in str(error.value) @pytest.mark.parametrize("code", [0, 91, 92, 94, 95, True, False, 93.0, "93"]) def test_pre_refusal_with_wrong_exit_type_or_value_is_unknown(code): + result = AdbShellResult(ACTION_HEADER + b"refused:93\n", b"", code) with pytest.raises(AgentTaskError, match="unknown"): - parse_android_action_result(AdbShellResult(ACTION_HEADER + b"refused:93\n", b"", code)) + parse_android_action_result(result) @pytest.mark.parametrize("result", [ diff --git a/test/unit_test/headless/test_agent_android_adapter.py b/test/unit_test/headless/test_agent_android_adapter.py index 4f14e883c..6478a5368 100644 --- a/test/unit_test/headless/test_agent_android_adapter.py +++ b/test/unit_test/headless/test_agent_android_adapter.py @@ -110,12 +110,14 @@ def test_actual_worker_and_sdk_only_invoke_finite_input_after_durable_begin_ack( assert adapter.ready(spec) == (True, '') observed = adapter.observe() assert observed['identity']['physical_id'] == DEVICE - assert observed['elements'] == [] and observed['text'] == '' + assert observed['elements'] == [] + assert observed['text'] == '' assert not any('/system/bin/input -d 0 ' in command for command in sdk.services) assert adapter.perform('AC_android_tap', {'x': 10, 'y': 20}) == { 'android_phase': 'input_returned', 'may_have_effect': True} assert (sdk.directory / 'actions').read_text().splitlines() == ['-d', '0', 'tap', '10', '20'] - assert not adapter._journal.pending and adapter._journal.sequence == 2 + assert not adapter._journal.pending + assert adapter._journal.sequence == 2 assert json.loads((sdk.directory / 'fence.json').read_text())['active'] is False assert not sdk.errors finally: @@ -146,7 +148,8 @@ def test_bad_independent_settlement_keeps_effect_quarantined(android_sdk, flag): with pytest.raises(AgentTaskError): adapter.perform('AC_android_tap', {'x': 10, 'y': 20}) assert (sdk.directory / 'actions').exists() - assert adapter._journal.pending and adapter._channel.retired + assert adapter._journal.pending + assert adapter._channel.retired finally: release_fixture(adapter) @@ -161,7 +164,8 @@ def test_lost_shell_exit_stays_unknown_even_after_fixture_writer_settles(android with pytest.raises(AgentTaskError): adapter.perform('AC_android_tap', {'x': 10, 'y': 20}) assert json.loads((sdk.directory / 'settlement.json').read_text())['queue_empty'] is True - assert adapter._journal.pending and adapter._channel.retired + assert adapter._journal.pending + assert adapter._channel.retired finally: release_fixture(adapter) @@ -180,7 +184,8 @@ def test_stale_scope_or_unmatched_lease_cannot_begin_input(android_sdk, change): (sdk.directory / 'wrong_lease').touch() with pytest.raises(AgentTaskError): adapter.perform('AC_android_tap', {'x': 10, 'y': 20}) - assert not adapter._journal.pending and not (sdk.directory / 'actions').exists() + assert not adapter._journal.pending + assert not (sdk.directory / 'actions').exists() assert not any('/system/bin/input -d 0 ' in command for command in sdk.services) finally: release_fixture(adapter) @@ -275,6 +280,7 @@ def revoke(journal, event): adapter.perform('AC_android_tap', {'x': 10, 'y': 20}) assert not (sdk.directory / 'actions').exists() assert not any('/system/bin/input -d 0 ' in command for command in sdk.services) - assert adapter._journal.pending and adapter._channel.retired + assert adapter._journal.pending + assert adapter._channel.retired finally: release_fixture(adapter) diff --git a/test/unit_test/headless/test_agent_android_fixture.py b/test/unit_test/headless/test_agent_android_fixture.py index 1c5701cee..b32a9258a 100644 --- a/test/unit_test/headless/test_agent_android_fixture.py +++ b/test/unit_test/headless/test_agent_android_fixture.py @@ -15,7 +15,8 @@ def test_focus_guard_accepts_all_field_orders_without_normalizing_values(run_gua lines = WM.splitlines(keepends=True) (tmp_path / "wm").write_text("".join(lines[index] for index in order), encoding="utf-8", newline="") result = run_guard() - assert result.returncode == 0 and (tmp_path / "actions").read_text().splitlines().count("tap") == 1 + assert result.returncode == 0 + assert (tmp_path / "actions").read_text().splitlines().count("tap") == 1 @pytest.mark.parametrize("data", [b"", b"line\n\n", b"no-final-newline", b"a\x00b\x00\x00", b"\xff\xfe\n\\ \r\n"]) @@ -25,7 +26,8 @@ def test_fast_fixture_reader_preserves_exact_bytes_including_nulls_and_newlines( (tmp_path / "device").write_bytes(data) prefix = "unset BASH_VERSION; " if fallback else "" result = fixture.run(prefix + "/system/bin/getprop ro.serialno") - assert result.returncode == 0 and result.stdout == data + assert result.returncode == 0 + assert result.stdout == data def test_fixture_timeout_diagnostic_has_phase_and_duration_but_no_command_or_output(tmp_path, snapshot, monkeypatch): @@ -37,9 +39,11 @@ def test_fixture_timeout_diagnostic_has_phase_and_duration_but_no_command_or_out fixture.run("/system/bin/input -d 0 text " + secret) assert str(error.value) == "Android fixture action shell exceeded its five-second deadline" diagnostics = fixture.diagnostics() - assert diagnostics[-1]["phase"] == "action" and diagnostics[-1]["outcome"] == "timeout" + assert diagnostics[-1]["phase"] == "action" + assert diagnostics[-1]["outcome"] == "timeout" assert isinstance(diagnostics[-1]["seconds"], float) - assert secret not in repr(diagnostics) and "private" not in repr(diagnostics) + assert secret not in repr(diagnostics) + assert "private" not in repr(diagnostics) assert run.call_args.kwargs["timeout"] == 5 @@ -61,7 +65,8 @@ def test_duplicate_focus_fields_including_empty_values_fail_on_the_correct_side_ def test_raw_labels_cannot_replace_a_missing_native_focus_field(run_guard, tmp_path): missing = WM.replace("mTopFocusedDisplayId=0", "display=0") (tmp_path / "wm").write_text(missing, encoding="utf-8", newline="") - assert run_guard().returncode == 93 and not (tmp_path / "actions").exists() + assert run_guard().returncode == 93 + assert not (tmp_path / "actions").exists() def test_shell_metacharacters_and_globs_in_current_focus_cannot_match_or_execute(tmp_path, snapshot): @@ -69,5 +74,6 @@ def test_shell_metacharacters_and_globs_in_current_focus_cannot_match_or_execute hostile = WM.replace(snapshot.window, "Window{[a]* $(touch injected) 'quoted'}") (tmp_path / "wm").write_text(hostile, encoding="utf-8", newline="") result = fixture.run(compile_android_action(snapshot, "AC_android_tap", {"x": 1, "y": 2})) - assert result.returncode == 93 and not (tmp_path / "injected").exists() + assert result.returncode == 93 + assert not (tmp_path / "injected").exists() assert not (tmp_path / "actions").exists() diff --git a/test/unit_test/headless/test_agent_android_hierarchy.py b/test/unit_test/headless/test_agent_android_hierarchy.py index 875a60298..0ac6e0ffe 100644 --- a/test/unit_test/headless/test_agent_android_hierarchy.py +++ b/test/unit_test/headless/test_agent_android_hierarchy.py @@ -65,12 +65,14 @@ def semantic_tree(): def test_no_plain_xml_text_is_authorized_and_review_cannot_reveal_password_or_editable_values(): raw = semantic_tree() hidden = parse(sample(raw)) - assert hidden.text == '' and all(item['name'] == '' for item in hidden.elements) + assert hidden.text == '' + assert all(item['name'] == '' for item in hidden.elements) reviewed = parse(sample(raw, grants=('0/0', '0/1', '0/2'))) result = reviewed.to_dict() assert result['text'] == 'Save' assert result['elements'][1]['bounds'] == [10, 10, 30, 30] - assert result['elements'][1]['role'] == 'button' and result['elements'][1]['actionable'] is True + assert result['elements'][1]['role'] == 'button' + assert result['elements'][1]['actionable'] is True assert all(item['sensitive'] for item in result['elements'][2:]) assert not any(item['actionable'] for item in result['elements'][2:]) assert 'private-' not in json.dumps(result) @@ -81,16 +83,19 @@ def test_foreign_packages_and_descendants_never_escape_the_owned_app_scope(): foreign = node({'index': '1', 'package': 'org.foreign.app', 'text': 'foreign-secret'}, [ node({'package': PACKAGE, 'text': 'smuggled-own-package-secret'})]) view = parse(sample(document(node(children=[node({'text': 'own label'}), foreign])), grants=('0/0',))) - assert len(view.elements) == 2 and view.text == 'own label' + assert len(view.elements) == 2 + assert view.text == 'own label' assert 'secret' not in json.dumps(view.to_dict()) + foreign_only = sample(document(node(children=[foreign])), grants=('0/1',)) with pytest.raises(AgentTaskError): - parse(sample(document(node(children=[foreign])), grants=('0/1',))) + parse(foreign_only) def test_sensitive_ancestor_overrides_descendant_text_grants(): raw = document(node({'password': 'true'}, [node({'text': 'protected-child', 'clickable': 'true'})])) view = parse(sample(raw, grants=('0/0',))) - assert view.text == '' and all(item['sensitive'] for item in view.elements) + assert view.text == '' + assert all(item['sensitive'] for item in view.elements) assert not any(item['actionable'] for item in view.elements) @@ -100,8 +105,9 @@ def test_sensitive_ancestor_overrides_descendant_text_grants(): {'resource-id': 'org.foreign.app:id/secret'}, {'text': 'x' * 513}, ]) def test_unknown_or_ambiguous_metadata_is_refused_without_echoing_source(attrs): + value = sample(document(node(attrs))) with pytest.raises(AgentTaskError): - parse(sample(document(node(attrs)))) + parse(value) @pytest.mark.parametrize('raw', [ @@ -114,15 +120,17 @@ def test_unknown_or_ambiguous_metadata_is_refused_without_echoing_source(attrs): b'', ], ids=lambda value: f'xml-bytes-{len(value)}') def test_malformed_xml_entities_dtd_instructions_and_encodings_are_never_accepted(raw): + value = sample(raw) with pytest.raises(AgentTaskError) as error: - parse(sample(raw)) + parse(value) assert 'private-' not in str(error.value) @pytest.mark.parametrize('grants', [('0/9',), ('0', '0'), ['0'], (True,), ('0/00',)]) def test_text_authority_requires_unique_existing_host_paths(grants): + value = sample(grants=grants) with pytest.raises(AgentTaskError): - parse(sample(grants=grants)) + parse(value) @pytest.mark.parametrize('change', ['nonce', 'window', 'size', 'size_bool', 'key']) @@ -161,8 +169,9 @@ def test_duplicate_siblings_and_excessive_depth_or_nodes_are_rejected(): deep = node(children=[deep]) wide = document(node(children=[node({'index': str(index)}) for index in range(1000)])) for raw in (duplicate, document(deep), wide): + value = sample(raw) with pytest.raises(AgentTaskError): - parse(sample(raw)) + parse(value) def test_cancellation_is_checked_during_decode_and_remains_the_primary_exception(): @@ -173,8 +182,9 @@ def check(): if count[0] == 3: raise CancelledError('cancel fixture') + value = sample(semantic_tree()) with pytest.raises(CancelledError): - parse(sample(semantic_tree()), check=check) + parse(value, check=check) def test_android_reference_requires_semantic_scope_and_an_actionable_element(): diff --git a/test/unit_test/headless/test_agent_android_scope.py b/test/unit_test/headless/test_agent_android_scope.py index 0361cac78..f52984dea 100644 --- a/test/unit_test/headless/test_agent_android_scope.py +++ b/test/unit_test/headless/test_agent_android_scope.py @@ -156,7 +156,8 @@ def test_identity_failure_after_input_never_masquerades_as_pre_input_refusal(run result = run_guard() assert result.returncode == code parsed = parse_android_action_result(AdbShellResult(result.stdout, result.stderr, result.returncode)) - assert parsed.phase == "post_input_refused" and parsed.may_have_effect + assert parsed.phase == "post_input_refused" + assert parsed.may_have_effect assert (tmp_path / "actions").read_text().splitlines().count("tap") == 1 @@ -224,8 +225,9 @@ def test_proposal_only_queries_identity_and_never_dispatches_input(): def test_selection_never_falls_back_to_first_device_or_ambiguous_transport(rows): server = Server() server.rows = rows + adapter = session(server) with pytest.raises(AgentTaskError): - session(server).inspect() + adapter.inspect() assert server.calls == ["devices"] @@ -251,16 +253,18 @@ def test_server_reused_transport_must_still_identify_the_reviewed_physical_devic def test_bad_tool_arguments_are_refused_without_even_querying_the_server(): server = Server() + adapter = session(server) with pytest.raises(AgentTaskError): - session(server).propose("AC_android_shell", {"command": "arbitrary"}) + adapter.propose("AC_android_shell", {"command": "arbitrary"}) assert not server.calls def test_remote_probe_diagnostics_cannot_escape_as_task_errors(): server = Server() server.result = AdbShellResult(evidence(), b"private-untrusted-diagnostic", 1) + adapter = session(server) with pytest.raises(AgentTaskError) as caught: - session(server).inspect() + adapter.inspect() assert "private-untrusted-diagnostic" not in str(caught.value) @@ -376,7 +380,8 @@ def test_concurrent_inspection_cannot_commit_two_different_boots_into_one_sessio class ConcurrentServer(Server): def shell(self, transport, command, **_kwargs): - assert transport == 42 and command == IDENTITY_PROBE + assert transport == 42 + assert command == IDENTITY_PROBE boot = BOOT if threading.current_thread().name == "old-boot" else BOOT.replace("1", "2") return AdbShellResult(evidence(boot=boot), b"", 0) @@ -481,8 +486,9 @@ def test_current_native_input_evidence_binds_owner_but_still_grants_no_input(sna (" Global Monitors: ", " unparsed-foreign-window"), ]) def test_unsettled_or_unknown_dispatcher_state_is_refused(snapshot, old, new): + raw, policy = input_dump().replace(old.encode(), new.encode()), policy_dump() with pytest.raises(AgentTaskError): - parse_android_input_evidence(input_dump().replace(old.encode(), new.encode()), policy_dump(), snapshot) + parse_android_input_evidence(raw, policy, snapshot) @pytest.mark.parametrize("old,new", [ @@ -495,8 +501,9 @@ def test_unsettled_or_unknown_dispatcher_state_is_refused(snapshot, old, new): (" showing=false", " showing=false\n showing=true"), ]) def test_either_locked_or_inconsistent_keyguard_view_refuses_input_evidence(snapshot, old, new): + raw, policy = input_dump(), policy_dump().replace(old.encode(), new.encode()) with pytest.raises(AgentTaskError): - parse_android_input_evidence(input_dump(), policy_dump().replace(old.encode(), new.encode()), snapshot) + parse_android_input_evidence(raw, policy, snapshot) @pytest.mark.parametrize("changes", [ @@ -505,8 +512,9 @@ def test_either_locked_or_inconsistent_keyguard_view_refuses_input_evidence(snap {"app": "another-app"}, {"token": ""}, {"app_token": ""}, ]) def test_native_target_must_have_one_matching_live_app_channel(snapshot, changes): + raw, policy = input_dump(input_record(**changes)), policy_dump() with pytest.raises(AgentTaskError): - parse_android_input_evidence(input_dump(input_record(**changes)), policy_dump(), snapshot) + parse_android_input_evidence(raw, policy, snapshot) def test_whole_swipe_must_stay_in_region_union_not_just_its_endpoints(snapshot): @@ -534,8 +542,9 @@ def test_background_window_does_not_occlude_but_background_spy_still_receives_in behind = input_record(1, name="background", id=101) parse_android_input_evidence(input_dump(input_record(), behind), policy_dump(), snapshot).require_path((5, 5), (6, 6)) spy = behind.replace("inputConfig=0x0", "inputConfig=SPY | TRUSTED_OVERLAY") + proof = parse_android_input_evidence(input_dump(input_record(), spy), policy_dump(), snapshot) with pytest.raises(AgentTaskError, match="foreign"): - parse_android_input_evidence(input_dump(input_record(), spy), policy_dump(), snapshot).require_path((5, 5), (6, 6)) + proof.require_path((5, 5), (6, 6)) def test_history_and_old_anr_cannot_replace_current_input_or_renew_its_fingerprint(snapshot): @@ -544,9 +553,11 @@ def test_history_and_old_anr_cannot_replace_current_input_or_renew_its_fingerpri changed = live.replace(b"RecentQueue: ", b"RecentQueue: length=1\n historical-only") changed += b"\nInput Dispatcher State at time of last ANR:\n DispatchEnabled: false\n" assert parse_android_input_evidence(changed, policy_dump(), snapshot).input_sha256 == proof.input_sha256 + disabled = (live.replace(b"DispatchEnabled: true", b"DispatchEnabled: false") + + b"\nInput Dispatcher State at time of last ANR:\n" + live) + policy = policy_dump() with pytest.raises(AgentTaskError): - parse_android_input_evidence(live.replace(b"DispatchEnabled: true", b"DispatchEnabled: false") - + b"\nInput Dispatcher State at time of last ANR:\n" + live, policy_dump(), snapshot) + parse_android_input_evidence(disabled, policy, snapshot) def test_half_open_geometry_detects_even_an_isolated_missing_swipe_point(): @@ -569,7 +580,8 @@ def test_same_command_connection_refuses_changed_native_input_before_any_write(r result = run_guard(input_evidence=proof) assert result.returncode == 95 parsed = parse_android_action_result(AdbShellResult(result.stdout, result.stderr, result.returncode)) - assert parsed.phase == "pre_input_refused" and not parsed.may_have_effect + assert parsed.phase == "pre_input_refused" + assert not parsed.may_have_effect assert not (tmp_path / "actions").exists() @@ -631,10 +643,11 @@ def test_global_monitors_remain_explicit_unapproved_metadata(snapshot): def test_native_connection_cannot_claim_a_window_with_a_different_channel_name(snapshot): + raw = input_dump().replace(b"channelName='abc123 org.example.app/.MainActivity (server)'", + b"channelName='forged-other-channel'") + policy = policy_dump() with pytest.raises(AgentTaskError): - parse_android_input_evidence(input_dump().replace(b"channelName='abc123 org.example.app/.MainActivity (server)'", - b"channelName='forged-other-channel'"), - policy_dump(), snapshot) + parse_android_input_evidence(raw, policy, snapshot) @pytest.mark.parametrize("bad", [ @@ -645,5 +658,6 @@ def test_native_connection_cannot_claim_a_window_with_a_different_channel_name(s lambda raw: raw.replace(b" transform (ROT_0) (IDENTITY)\n Global", b" Global"), ]) def test_unparsed_duplicate_or_wrong_display_evidence_never_certifies_input(snapshot, bad): + raw, policy = bad(input_dump()), policy_dump() with pytest.raises(AgentTaskError): - parse_android_input_evidence(bad(input_dump()), policy_dump(), snapshot) + parse_android_input_evidence(raw, policy, snapshot) diff --git a/test/unit_test/headless/test_agent_android_semantic.py b/test/unit_test/headless/test_agent_android_semantic.py index 5551d84d1..ed4669cdb 100644 --- a/test/unit_test/headless/test_agent_android_semantic.py +++ b/test/unit_test/headless/test_agent_android_semantic.py @@ -29,11 +29,13 @@ def test_semantic_observation_is_stable_masked_and_resolves_reviewed_tap(android adapter.ready(current) first, second = adapter.observe(), adapter.observe() assert first == second - assert first['text'] == 'Save' and 'private-' not in json.dumps(first) + assert first['text'] == 'Save' + assert 'private-' not in json.dumps(first) args, _ground = resolve_action('AC_android_tap', {}, {'element_index': 1}, first) adapter.perform('AC_android_tap', args) assert (sdk.directory / 'actions').read_text().splitlines() == ['-d', '0', 'tap', '25', '25'] - assert not adapter._journal.pending and not sdk.errors + assert not adapter._journal.pending + assert not sdk.errors finally: release_fixture(adapter) @@ -53,7 +55,8 @@ def test_changed_semantic_content_or_capture_identity_refuses_input_before_begin (sdk.directory / 'hierarchy.xml').write_bytes(semantic_tree().replace(old, new)) with pytest.raises(AgentTaskError): adapter.perform('AC_android_tap', {'x': 25, 'y': 25}) - assert not (sdk.directory / 'actions').exists() and not adapter._journal.pending + assert not (sdk.directory / 'actions').exists() + assert not adapter._journal.pending finally: release_fixture(adapter) @@ -76,7 +79,8 @@ def change(journal, event): with pytest.raises(AgentTaskError): adapter.perform('AC_android_tap', {'x': 25, 'y': 25}) assert not (sdk.directory / 'actions').exists() - assert adapter._journal.pending and adapter._channel.retired + assert adapter._journal.pending + assert adapter._channel.retired finally: release_fixture(adapter) diff --git a/test/unit_test/headless/test_agent_android_transport.py b/test/unit_test/headless/test_agent_android_transport.py index 6b202bc07..e7ba19442 100644 --- a/test/unit_test/headless/test_agent_android_transport.py +++ b/test/unit_test/headless/test_agent_android_transport.py @@ -129,8 +129,9 @@ def test_nonliteral_or_unreviewable_endpoints_fail_before_connection(host, port, ]) def test_invalid_shell_bounds_never_open_a_connection(transport, command, changes, monkeypatch): monkeypatch.setattr(socket, "create_connection", lambda *_args, **_kwargs: pytest.fail("must not connect")) + client = ManagedAdbServer("127.0.0.1", 5037) with pytest.raises(AgentTaskError): - ManagedAdbServer("127.0.0.1", 5037).shell(transport, command, **changes) + client.shell(transport, command, **changes) def test_server_failure_message_cannot_publish_typed_secret(): @@ -140,9 +141,10 @@ def dialogue(connection): assert request(connection) == b"host:transport-id:42" connection.sendall(b"FAIL" + f"{len(secret):04x}".encode("ascii") + secret) + command = "input text " + secret.decode() with server(dialogue) as (client, connections): with pytest.raises(AgentTaskError) as caught: - client.shell(42, "input text " + secret.decode()) + client.shell(42, command) assert secret.decode() not in "".join(traceback.format_exception(caught.value)) assert len(connections) == 1 @@ -211,8 +213,9 @@ def fail_connect(*_args, **_kwargs): raise OSError("untrusted diagnostic with private-value") monkeypatch.setattr(socket, "create_connection", fail_connect) + client = ManagedAdbServer("127.0.0.1", 5037) with pytest.raises(AgentTaskError, match="server state was not changed") as caught: - ManagedAdbServer("127.0.0.1", 5037).list_devices() + client.list_devices() assert calls == [True] assert "private-value" not in "".join(traceback.format_exception(caught.value)) diff --git a/test/unit_test/headless/test_agent_bound_client.py b/test/unit_test/headless/test_agent_bound_client.py index f0e7e89bb..dc3bbb7df 100644 --- a/test/unit_test/headless/test_agent_bound_client.py +++ b/test/unit_test/headless/test_agent_bound_client.py @@ -63,6 +63,7 @@ def test_legacy_ordinary_client_stays_adaptive_but_binding_requires_a_generation def test_malformed_local_metadata_is_a_typed_failure_without_transport(connection, value): path, _metadata, transport = connection path.write_text(json.dumps(value)) + client = AgentRuntimeClient(path) with pytest.raises(AgentTaskError, match="connection metadata"): - AgentRuntimeClient(path).call("list") + client.call("list") transport.assert_not_called() diff --git a/test/unit_test/headless/test_agent_browser_environment.py b/test/unit_test/headless/test_agent_browser_environment.py index 8520c7a1c..4b03ae508 100644 --- a/test/unit_test/headless/test_agent_browser_environment.py +++ b/test/unit_test/headless/test_agent_browser_environment.py @@ -25,7 +25,8 @@ def test_reviewed_helper_interpreter_selects_isolated_installed_modules(): assert target._argv[0] == configuration.settings["helper_python"] assert "-I" in target._argv assert target._argv[-1] == "je_auto_control.utils.agent_runtime.native_worker" - assert target._channel is None and target._resource is None + assert target._channel is None + assert target._resource is None def test_target_probe_uses_existing_host_management_and_does_not_enable_target(tmp_path, monkeypatch): @@ -81,8 +82,10 @@ def test_worker_probe_imports_without_starting_sdk_or_playwright(monkeypatch): worker = NativeWorker(io.BytesIO(), io.BytesIO()) report = worker._dispatch("probe_browser", {"transfer": True}) assert report["status"] == "python_dependencies_ready" - assert report["browser_launch"] == "not_probed" and report["native_evidence"] == "not_recorded" - assert worker._spec is None and worker._activity is None + assert report["browser_launch"] == "not_probed" + assert report["native_evidence"] == "not_recorded" + assert worker._spec is None + assert worker._activity is None sdk.open.assert_not_called() require_runtime.assert_called_once() runtime.assert_not_called() @@ -130,7 +133,8 @@ def test_dependency_reply_cannot_claim_native_success_or_include_unknown_metadat channel = SimpleNamespace(request=Mock(side_effect=[{"protocol": 1}, report]), close=Mock()) monkeypatch.setattr(helper_environment, "owned_channel", Mock(return_value=channel)) result = helper_environment.probe_browser_helper((sys.executable,), transfer=True) - assert result["status"] == "unknown" and result["native_evidence"] == "not_recorded" + assert result["status"] == "unknown" + assert result["native_evidence"] == "not_recorded" channel.close.assert_called_once() @@ -153,8 +157,10 @@ def test_default_environment_probe_does_not_acquire_target_input_or_start_a_brow target = build_target(profile()) result = target.probe() assert result["status"] in {"python_dependencies_ready", "dependency_missing", "incompatible"} - assert result["native_evidence"] == "not_recorded" and result["browser_launch"] == "not_probed" - assert target._channel is None and target._resource is None + assert result["native_evidence"] == "not_recorded" + assert result["browser_launch"] == "not_probed" + assert target._channel is None + assert target._resource is None def test_cli_and_command_schema_offer_the_same_host_probe_operation(): diff --git a/test/unit_test/headless/test_agent_browser_target.py b/test/unit_test/headless/test_agent_browser_target.py index b97d6ca50..bb006e495 100644 --- a/test/unit_test/headless/test_agent_browser_target.py +++ b/test/unit_test/headless/test_agent_browser_target.py @@ -87,8 +87,9 @@ def test_profile_is_inert_narrows_origins_and_refuses_unreviewed_settings(fake_s configuration = profile() configuration.check(spec()) assert configuration.settings["allowed_origins"] == ("https://allowed.test:443",) + outside = spec(options={"allowed_origins": ["https://outside.test"]}) with pytest.raises(AgentTaskError, match="network origins"): - configuration.check(spec(options={"allowed_origins": ["https://outside.test"]})) + configuration.check(outside) for settings in ({"allowed_origins": []}, {"allowed_origins": ["https://allowed.test/path"]}, {"allowed_origins": ["https://allowed.test"], "argv": ["external"]}): with pytest.raises(AgentTaskError): @@ -269,6 +270,8 @@ def test_browser_fill_rejects_incompatible_unicode_before_sdk_effect(fake_sdk, v def test_readiness_keeps_actionable_trusted_sdk_compatibility_failure(fake_sdk): session, opener = fake_sdk opener.side_effect = AgentTaskError("managed browser contract version 1 is required before startup") + target = BrowserTarget() + specification = spec() with pytest.raises(AgentTaskError, match="contract version 1"): - BrowserTarget().ready(spec()) + target.ready(specification) assert session.actions == [] diff --git a/test/unit_test/headless/test_agent_browser_transfers.py b/test/unit_test/headless/test_agent_browser_transfers.py index 15d04212a..64fb4c054 100644 --- a/test/unit_test/headless/test_agent_browser_transfers.py +++ b/test/unit_test/headless/test_agent_browser_transfers.py @@ -218,8 +218,9 @@ def test_transfer_marker_cannot_substitute_for_required_sdk_methods(monkeypatch, def test_file_grants_require_explicit_roots_in_profiles_and_task_policies(tmp_path): + specification = scoped(tmp_path, roots=[]) with pytest.raises(AgentTaskError, match="roots"): - TaskPolicy(scoped(tmp_path, roots=[])) + TaskPolicy(specification) with pytest.raises(AgentTaskError, match="roots"): AgentTargetProfile(name="files-browser", kind="browser", allowed_tools=("AC_browser_download",), settings={"allowed_origins": ["https://allowed.test"]}) diff --git a/test/unit_test/headless/test_agent_browser_verification.py b/test/unit_test/headless/test_agent_browser_verification.py index 6d5babdf8..4a615cd99 100644 --- a/test/unit_test/headless/test_agent_browser_verification.py +++ b/test/unit_test/headless/test_agent_browser_verification.py @@ -114,16 +114,18 @@ def changed(_selector, _frame=""): criterion("url", "https://allowed.test:bad/form"), ]) def test_invalid_or_ungranted_checks_are_rejected_before_sdk_start(fake_sdk, tmp_path, clause): + specification = task(clause) with closing(AgentRuntime(tmp_path / "runtime.sqlite", targets={"reviewed-browser": BrowserTarget()})) as runtime: with pytest.raises(AgentTaskError): - runtime.submit(task(clause)) + runtime.submit(specification) fake_sdk[1].assert_not_called() def test_read_permission_is_not_implicitly_added_by_a_completion_clause(fake_sdk, tmp_path): + specification = task(allowed_tools=["AC_browser_fill"]) with closing(AgentRuntime(tmp_path / "runtime.sqlite", targets={"reviewed-browser": BrowserTarget()})) as runtime: with pytest.raises(AgentTaskError, match="explicit browser-read"): - runtime.submit(task(allowed_tools=["AC_browser_fill"])) + runtime.submit(specification) fake_sdk[1].assert_not_called() @@ -160,8 +162,9 @@ def test_verifier_preserves_control_failures_instead_of_reporting_unknown(failur specification = task() def failed(_clause): raise failure + policy = TaskPolicy(specification) with pytest.raises(type(failure)): - verify(TaskPolicy(specification), [], {}, browser_read=failed) + verify(policy, [], {}, browser_read=failed) @pytest.mark.parametrize("value", ["passed", 1, {"passed": True}]) @@ -177,8 +180,9 @@ def test_helper_verifier_refuses_a_clause_not_pinned_in_its_task(fake_sdk): try: with operation_scope(TaskOperation(specification, lambda: None, None)): target.ready(specification) + different = criterion(value="different") with pytest.raises(AgentTaskError, match="outside the owned task"): - target.verify_browser(criterion(value="different")) + target.verify_browser(different) finally: target.cleanup() assert fake_sdk[0].actions == [] diff --git a/test/unit_test/headless/test_agent_browser_webrunner_contract.py b/test/unit_test/headless/test_agent_browser_webrunner_contract.py index 8221779bb..2390349f7 100644 --- a/test/unit_test/headless/test_agent_browser_webrunner_contract.py +++ b/test/unit_test/headless/test_agent_browser_webrunner_contract.py @@ -140,7 +140,8 @@ def test_hung_sdk_effect_reaps_only_owned_helper_and_preserves_uncertainty(tmp_p result = run_task(runtime, handle.task_id) assert result.state == "waiting_user" receipts = runtime.store.actions(handle.task_id) - assert len(receipts) == 1 and receipts[0]["status"] == "uncertain" + assert len(receipts) == 1 + assert receipts[0]["status"] == "uncertain" assert adapter._channel is None assert adapter._ledger.empty finally: @@ -279,8 +280,10 @@ def test_owned_browser_helper_refuses_cold_skill_prefix_before_relaunch(tmp_path assert adapter._channel is None runtime.resume(handle.task_id) outcome = run_task(runtime, handle.task_id) - assert outcome.state == "waiting_user" and "browser context" in outcome.reason - assert adapter._channel is None and adapter._resource is None + assert outcome.state == "waiting_user" + assert "browser context" in outcome.reason + assert adapter._channel is None + assert adapter._resource is None assert len(runtime.store.actions(handle.task_id)) == 2 finally: runtime.close() @@ -295,8 +298,10 @@ def test_dependency_probe_uses_real_sdk_loader_without_starting_its_factory(monk monkeypatch.setattr(playwright_wrapper, "_require_playwright", loader) report = inspect_browser_dependencies(True) assert report["status"] == "python_dependencies_ready" - assert report["readback_contract"] and report["transfer_contract"] - assert report["browser_launch"] == "not_probed" and report["native_evidence"] == "not_recorded" + assert report["readback_contract"] + assert report["transfer_contract"] + assert report["browser_launch"] == "not_probed" + assert report["native_evidence"] == "not_recorded" loader.assert_called_once() factory.assert_not_called() @@ -308,6 +313,8 @@ def test_dependency_probe_contains_the_real_sdk_missing_runtime_error(monkeypatc loader = Mock(side_effect=playwright_wrapper.PlaywrightBackendError("private fixture diagnostic")) monkeypatch.setattr(playwright_wrapper, "_require_playwright", loader) report = inspect_browser_dependencies(True) - assert report["status"] == "dependency_missing" and report["playwright"] is False - assert report["browser_launch"] == "not_probed" and report["native_evidence"] == "not_recorded" + assert report["status"] == "dependency_missing" + assert report["playwright"] is False + assert report["browser_launch"] == "not_probed" + assert report["native_evidence"] == "not_recorded" assert "private fixture diagnostic" not in str(report) diff --git a/test/unit_test/headless/test_agent_browser_workflows.py b/test/unit_test/headless/test_agent_browser_workflows.py index dcc833066..c86ad7669 100644 --- a/test/unit_test/headless/test_agent_browser_workflows.py +++ b/test/unit_test/headless/test_agent_browser_workflows.py @@ -110,8 +110,9 @@ def test_browser_transfer_captures_transforms_and_reads_back_before_save(browser {"confirmation_column": "missing"}, {"submit_selector": ""}, ]) def test_invalid_browser_drafts_have_no_session_or_action_effects(fake_sdk, tmp_path, changes): + config = form_config(tmp_path, **changes) with pytest.raises(AgentTaskError): - draft_agent_workflow("batch_form", form_config(tmp_path, **changes)) + draft_agent_workflow("batch_form", config) fake_sdk[1].assert_not_called() assert fake_sdk[0].actions == [] diff --git a/test/unit_test/headless/test_agent_budget_clock.py b/test/unit_test/headless/test_agent_budget_clock.py index 97c085051..66d5e7fe2 100644 --- a/test/unit_test/headless/test_agent_budget_clock.py +++ b/test/unit_test/headless/test_agent_budget_clock.py @@ -71,7 +71,8 @@ def read(): finally: release.set() worker.join(timeout=3) - assert not worker.is_alive() and not failures + assert not worker.is_alive() + assert not failures assert result == [280] finally: release.set() diff --git a/test/unit_test/headless/test_agent_completion_admission.py b/test/unit_test/headless/test_agent_completion_admission.py index 1589150c0..f3dfe02cd 100644 --- a/test/unit_test/headless/test_agent_completion_admission.py +++ b/test/unit_test/headless/test_agent_completion_admission.py @@ -83,8 +83,10 @@ def test_dynamic_confirmed_effects_can_be_completed_by_fresh_independent_criteri connection.execute('UPDATE agent_tasks SET spec=? WHERE id=?', (json.dumps(spec), completed.task_id)) identifier = child(completed) result = runtime.finish(identifier) - assert result.state == 'succeeded' and len(result.verification) == 1 - assert runtime.store.get(identifier)['spec']['mode'] == 'external' and runtime.store.actions(identifier) == [] + assert result.state == 'succeeded' + assert len(result.verification) == 1 + assert runtime.store.get(identifier)['spec']['mode'] == 'external' + assert runtime.store.actions(identifier) == [] def test_missing_source_capture_is_a_typed_review_refusal_not_an_unhandled_lookup(completed): diff --git a/test/unit_test/headless/test_agent_completion_authorization.py b/test/unit_test/headless/test_agent_completion_authorization.py index cfbfedf06..a3ef9c0ed 100644 --- a/test/unit_test/headless/test_agent_completion_authorization.py +++ b/test/unit_test/headless/test_agent_completion_authorization.py @@ -115,25 +115,30 @@ def test_explicit_renewal_finishes_real_files_without_reviving_the_original_orig with pytest.raises(AgentTaskError, match='managed origin'): runtime.recovery.control('preview_completion', {'task_id': source}) preview, request = reviewed(renewal) - assert preview['approval_required'] is True and preview['source_completion_inherited'] is False + assert preview['approval_required'] is True + assert preview['source_completion_inherited'] is False assert preview['verification_spec']['actions'] == [] assert preview['verification_spec']['roots'] == [str(renewal.reports)] assert preview['authorization']['replaced_origins'] == [renewal.old.identifier] child = runtime.recovery.control('create_authorized_completion', request)['task_id'] runtime.observe(child) result = runtime.finish(child) - assert result.state == 'succeeded' and result.metadata['input_released'] is True + assert result.state == 'succeeded' + assert result.metadata['input_released'] is True assert result.metadata['completion_contract'] == 1 assert [report['status'] for report in result.verification] == ['passed', 'passed', 'passed'] - assert runtime.get(source).state == 'paused' and runtime.store.actions(source) == historical - assert runtime.store.actions(child) == [] and renewal.manifest.read_bytes() == manifest + assert runtime.get(source).state == 'paused' + assert runtime.store.actions(source) == historical + assert runtime.store.actions(child) == [] + assert renewal.manifest.read_bytes() == manifest assert origin_policy._path(renewal.old.identifier).read_bytes() == revoked assert runtime.store.get(source)['spec']['roots'] == [str(renewal.allowed)] assert runtime.store.get(child)['spec']['roots'] == [str(renewal.reports)] assert runtime.recovery.control('create_authorized_completion', request)['task_id'] == child + upgrade = {'task_id': child, 'snapshot': 'irrelevant', + 'spec': runtime.store.get(child)['spec'], 'note': 'Cannot upgrade to executable recovery'} with pytest.raises(AgentTaskError, match='read-only'): - runtime.recovery.control('create', {'task_id': child, 'snapshot': 'irrelevant', - 'spec': runtime.store.get(child)['spec'], 'note': 'Cannot upgrade to executable recovery'}) + runtime.recovery.control('create', upgrade) def test_original_effect_change_inside_admission_sql_rolls_back_the_entire_reader(renewal): diff --git a/test/unit_test/headless/test_agent_completion_authorization_consumers.py b/test/unit_test/headless/test_agent_completion_authorization_consumers.py index acd5dd3a6..fb988ada9 100644 --- a/test/unit_test/headless/test_agent_completion_authorization_consumers.py +++ b/test/unit_test/headless/test_agent_completion_authorization_consumers.py @@ -18,8 +18,10 @@ def test_historical_get_cannot_attest_live_completion_after_issuing_origin_is_re result = Context().run(renewal.runtime.get, child) assert result.state == 'succeeded' assert result.metadata['completion_contract'] == 0 + recorded = result.to_dict() + spec = renewal.runtime.store.get(renewal.source)['spec'] with pytest.raises(AgentTaskError, match='retirement contract'): - validate_recorded_completion(result.to_dict(), renewal.runtime.store.get(renewal.source)['spec']) + validate_recorded_completion(recorded, spec) def test_recording_uses_reviewed_scope_and_still_requires_its_own_fresh_validation(renewal, monkeypatch): @@ -28,7 +30,8 @@ def test_recording_uses_reviewed_scope_and_still_requires_its_own_fresh_validati before = renewal.manifest.read_bytes() candidate = renewal.runtime.skills.control('record', {'task_id': renewal.source, 'completion_task_id': child, 'name': 'Explicitly renewed file workflow'}) - assert candidate['state'] == 'draft' and candidate['version'] == 0 + assert candidate['state'] == 'draft' + assert candidate['version'] == 0 assert candidate['spec']['roots'] == [str(renewal.reports)] assert candidate['spec']['actions'] == [renewal.action] assert renewal.old.identifier not in candidate['spec']['options']['origin_policies'] @@ -44,4 +47,5 @@ def test_recording_uses_reviewed_scope_and_still_requires_its_own_fresh_validati assert run_task(renewal.runtime, trial['task_id']).state == 'succeeded' accepted = renewal.runtime.skills.control('accept', {'candidate_id': candidate['id'], 'task_id': trial['task_id'], 'expected_version': 0, 'note': 'Accepted only the separately executed current-scope trial'}) - assert accepted['version'] == 1 and renewal.runtime.get(renewal.source).state == 'paused' + assert accepted['version'] == 1 + assert renewal.runtime.get(renewal.source).state == 'paused' diff --git a/test/unit_test/headless/test_agent_completion_authorization_dag.py b/test/unit_test/headless/test_agent_completion_authorization_dag.py index 0695ab2b5..cfb0fdbf9 100644 --- a/test/unit_test/headless/test_agent_completion_authorization_dag.py +++ b/test/unit_test/headless/test_agent_completion_authorization_dag.py @@ -30,7 +30,8 @@ def test_original_batch_adoption_is_invalidated_when_renewed_evidence_loses_auth assert connection.execute('SELECT phase FROM agent_dag_nodes WHERE batch_id=?', (renewal.batch_id,)).fetchone()[0] == 'unknown' assert renewal.runtime.get(renewal.source).state == 'paused' - assert len(renewal.runtime.store.actions(renewal.source)) == 1 and renewal.runtime.store.actions(child) == [] + assert len(renewal.runtime.store.actions(renewal.source)) == 1 + assert renewal.runtime.store.actions(child) == [] @pytest.mark.parametrize('renewal', ['named_dag'], indirect=True) diff --git a/test/unit_test/headless/test_agent_completion_authorization_gui.py b/test/unit_test/headless/test_agent_completion_authorization_gui.py index be07a2ea0..f0a4fbfff 100644 --- a/test/unit_test/headless/test_agent_completion_authorization_gui.py +++ b/test/unit_test/headless/test_agent_completion_authorization_gui.py @@ -27,7 +27,8 @@ def test_readonly_renewal_actions_use_the_shared_recovery_protocol(monkeypatch, actions = dict(tab.menu_actions()) assert action in actions actions[action]() - assert requests[0][0] == 'recovery' and requests[0][1]['operation'] == operation + assert requests[0][0] == 'recovery' + assert requests[0][1]['operation'] == operation expected = {'task_id': 'original-workflow'} if operation.startswith('review') else request assert requests[0][1]['request'] == expected finally: diff --git a/test/unit_test/headless/test_agent_completion_authorization_lifecycle.py b/test/unit_test/headless/test_agent_completion_authorization_lifecycle.py index f546b3476..7bc81f5f3 100644 --- a/test/unit_test/headless/test_agent_completion_authorization_lifecycle.py +++ b/test/unit_test/headless/test_agent_completion_authorization_lifecycle.py @@ -29,7 +29,8 @@ def test_private_renewal_survives_runtime_restart_with_the_same_readonly_limits( try: assert reopened.finish(child).state == 'succeeded' assert reopened.store.get(child)['spec']['roots'] == [str(renewal.reports)] - assert reopened.store.actions(child) == [] and reopened.get(renewal.source).state == 'paused' + assert reopened.store.actions(child) == [] + assert reopened.get(renewal.source).state == 'paused' finally: reopened.close() @@ -43,7 +44,8 @@ def test_sixteen_real_readonly_links_succeed_and_a_seventeenth_is_refused(renewa assert renewal.runtime.finish(parent).state == 'succeeded' with pytest.raises(AgentTaskError, match='sixteen'): reviewed(renewal, parent) - assert renewal.runtime.store.actions(parent) == [] and len(renewal.runtime.list()) == 17 + assert renewal.runtime.store.actions(parent) == [] + assert len(renewal.runtime.list()) == 17 def test_source_change_inside_final_completion_sql_cannot_publish_stale_success(renewal): diff --git a/test/unit_test/headless/test_agent_completion_authorization_roles.py b/test/unit_test/headless/test_agent_completion_authorization_roles.py index 8cea8ba19..522f3762f 100644 --- a/test/unit_test/headless/test_agent_completion_authorization_roles.py +++ b/test/unit_test/headless/test_agent_completion_authorization_roles.py @@ -38,7 +38,8 @@ def test_actual_renewal_reviewer_revocation_invalidates_current_completion_evide assert renewal.runtime.get(child).metadata['completion_contract'] == 1 renewal.users.remove_user('reviewer') result = renewal.runtime.get(child) - assert result.state == 'succeeded' and result.metadata['completion_contract'] == 0 + assert result.state == 'succeeded' + assert result.metadata['completion_contract'] == 0 @pytest.mark.parametrize('renewal', ['named'], indirect=True) diff --git a/test/unit_test/headless/test_agent_completion_authorization_surfaces.py b/test/unit_test/headless/test_agent_completion_authorization_surfaces.py index 1ceaec5ed..7f16c409c 100644 --- a/test/unit_test/headless/test_agent_completion_authorization_surfaces.py +++ b/test/unit_test/headless/test_agent_completion_authorization_surfaces.py @@ -32,6 +32,7 @@ def test_shared_surfaces_renew_only_the_reviewed_independent_completion(renewal, metadata = renewal.runtime.get(child).metadata assert metadata['completion_contract'] == 1 assert metadata['recovery']['reauthorization']['actor'] == 'owner' - assert renewal.runtime.store.actions(child) == [] and renewal.runtime.get(renewal.source).state == 'paused' + assert renewal.runtime.store.actions(child) == [] + assert renewal.runtime.get(renewal.source).state == 'paused' finally: server.stop() diff --git a/test/unit_test/headless/test_agent_completion_gui.py b/test/unit_test/headless/test_agent_completion_gui.py index 2ab8db658..ea3170fb9 100644 --- a/test/unit_test/headless/test_agent_completion_gui.py +++ b/test/unit_test/headless/test_agent_completion_gui.py @@ -30,8 +30,10 @@ def test_actions_preserve_exact_completion_review_without_creating_an_executable response['ownership'] = {'review': 'b' * 64} tab._present_result(response) approved = json.loads(tab._remainder_request.toPlainText()) - assert approved['task_id'] == 'source-task' and approved['snapshot'] == 'a' * 64 - assert 'spec' not in approved and approved['note'] == '' + assert approved['task_id'] == 'source-task' + assert approved['snapshot'] == 'a' * 64 + assert 'spec' not in approved + assert approved['note'] == '' approved['note'] = 'Reviewed independently' tab._remainder_request.setPlainText(json.dumps(approved)) actions['agent_completion_create']() diff --git a/test/unit_test/headless/test_agent_completion_handoff.py b/test/unit_test/headless/test_agent_completion_handoff.py index 9b4a079bc..ec11dfdcb 100644 --- a/test/unit_test/headless/test_agent_completion_handoff.py +++ b/test/unit_test/headless/test_agent_completion_handoff.py @@ -50,7 +50,9 @@ def forbidden(*_args, **_kwargs): assert authorization['approved_root_count'] == 1 assert completed.old.identifier in authorization['replaced_origins'] public = json.dumps([review, grant]) - assert 'store_path' not in public and 'arguments' not in public and str(completed.reports) not in public + assert 'store_path' not in public + assert 'arguments' not in public + assert str(completed.reports) not in public with authorization_scope(completed.reviewer): assert runtime.recovery.control('create_handoff', request)['handoff']['id'] == grant['id'] with pytest.raises(AgentTaskError, match='another caller'): @@ -68,8 +70,10 @@ def forbidden(*_args, **_kwargs): validation = {'batch_id': completed.batch_id, 'node_id': 'a', 'attempt': started['attempt']} assert runtime.batches.control('validate_node', validation)['outcome'] == 'completed' assert runtime.get(completed.source).state == 'paused' - assert runtime.store.actions(completed.source) == before and runtime.store.actions(completed.child) == [] - assert completed.manifest.read_bytes() == manifest and old_path.read_bytes() == old_bytes + assert runtime.store.actions(completed.source) == before + assert runtime.store.actions(completed.child) == [] + assert completed.manifest.read_bytes() == manifest + assert old_path.read_bytes() == old_bytes runtime.recovery.control('revoke_handoff', {'handoff_id': grant['id']}) with pytest.raises(AgentTaskError): runtime.batches.control('validate_node', validation) diff --git a/test/unit_test/headless/test_agent_completion_handoff_gui.py b/test/unit_test/headless/test_agent_completion_handoff_gui.py index d56bb93e5..89c05e0a7 100644 --- a/test/unit_test/headless/test_agent_completion_handoff_gui.py +++ b/test/unit_test/headless/test_agent_completion_handoff_gui.py @@ -28,7 +28,8 @@ def test_existing_handoff_action_and_result_keep_the_renewed_review_explicit(mon 'approved_root_count': 1, 'replaced_origins': ['expired-origin']}}} tab._completed(SimpleNamespace(task_id='review', status='ok', value=result)) assert json.loads(tab._output.toPlainText()) == result - assert json.loads(tab._handoff_request.toPlainText()) == request and len(requests) == 1 + assert json.loads(tab._handoff_request.toPlainText()) == request + assert len(requests) == 1 finally: tab.close() shiboken6.delete(tab) diff --git a/test/unit_test/headless/test_agent_completion_handoff_lifecycle.py b/test/unit_test/headless/test_agent_completion_handoff_lifecycle.py index b2cf34068..a9de02d52 100644 --- a/test/unit_test/headless/test_agent_completion_handoff_lifecycle.py +++ b/test/unit_test/headless/test_agent_completion_handoff_lifecycle.py @@ -23,7 +23,8 @@ def test_real_reader_chain_keeps_successful_intermediate_readers_and_sixteen_lin assert len(review['evidence']['segments']) == count reply, chain = completed.runtime.recovery.handoff_evidence(completed.batch_id, 'a', completed.source, completed.child, grant['id']) - assert reply['state'] == 'succeeded' and len(chain) == count + assert reply['state'] == 'succeeded' + assert len(chain) == count assert all(not completed.runtime.store.actions(item['task_id']) for item in chain) @@ -34,7 +35,8 @@ def test_existing_renewed_grant_survives_real_runtime_restart(completed): try: reply, _chain = reopened.recovery.handoff_evidence(completed.batch_id, 'a', completed.source, completed.child, grant['id']) - assert reply['state'] == 'succeeded' and reopened.get(completed.source).state == 'paused' + assert reply['state'] == 'succeeded' + assert reopened.get(completed.source).state == 'paused' with pytest.raises(AgentTaskError, match='another caller'): reopened.get(completed.child) finally: diff --git a/test/unit_test/headless/test_agent_completion_handoff_mixed.py b/test/unit_test/headless/test_agent_completion_handoff_mixed.py index 6fe6d2c41..465d96314 100644 --- a/test/unit_test/headless/test_agent_completion_handoff_mixed.py +++ b/test/unit_test/headless/test_agent_completion_handoff_mixed.py @@ -37,7 +37,8 @@ def test_mixed_executable_and_renewed_reader_chain_adopts_all_original_confirmed _review, _request, grant = issue(mixed) reply, chain = mixed.runtime.recovery.handoff_evidence(mixed.batch_id, 'a', mixed.source, mixed.child, grant['id']) - assert reply['state'] == 'succeeded' and len(chain) == 2 + assert reply['state'] == 'succeeded' + assert len(chain) == 2 assert len(mixed.runtime.store.actions(mixed.source)) == 1 assert len(mixed.runtime.store.actions(mixed.execution_child)) == 1 assert mixed.runtime.store.actions(mixed.child) == [] diff --git a/test/unit_test/headless/test_agent_completion_handoff_security.py b/test/unit_test/headless/test_agent_completion_handoff_security.py index 7b3883ea5..54f5025e0 100644 --- a/test/unit_test/headless/test_agent_completion_handoff_security.py +++ b/test/unit_test/headless/test_agent_completion_handoff_security.py @@ -17,8 +17,9 @@ @pytest.mark.parametrize('field', ['scope', 'completion_authorization', 'owner_policy', 'spec']) def test_public_requests_cannot_supply_private_completion_authority(completed, field): + request = {**request_for(completed), field: {}} with authorization_scope(completed.reviewer), pytest.raises(AgentTaskError, match='fields'): - completed.runtime.recovery.control('review_handoff', {**request_for(completed), field: {}}) + completed.runtime.recovery.control('review_handoff', request) @pytest.mark.parametrize('change', ['source', 'origin', 'reviewer']) @@ -34,9 +35,10 @@ def test_review_cannot_survive_changed_source_or_current_authority(completed, ch completed.current.revoke() else: completed.users.remove_user('reviewer') + request = {**request_for(completed), 'review': preview['handoff_review'], + 'note': 'Do not accept a stale completion grant'} with pytest.raises(AgentTaskError): - runtime.recovery.control('create_handoff', {**request_for(completed), - 'review': preview['handoff_review'], 'note': 'Do not accept a stale completion grant'}) + runtime.recovery.control('create_handoff', request) with runtime.store.connection() as connection: assert connection.execute('SELECT COUNT(*) FROM agent_evidence_handoffs').fetchone()[0] == 0 diff --git a/test/unit_test/headless/test_agent_completion_handoff_surfaces.py b/test/unit_test/headless/test_agent_completion_handoff_surfaces.py index 223dedc06..e748f74aa 100644 --- a/test/unit_test/headless/test_agent_completion_handoff_surfaces.py +++ b/test/unit_test/headless/test_agent_completion_handoff_surfaces.py @@ -32,7 +32,8 @@ def test_shared_surfaces_review_issue_inspect_and_revoke_the_exact_renewed_reade assert preview['evidence']['completion_authorization']['completion_task'] == completed.child grant = invoke(surface, connection, tmp_path, 'create_handoff', {**request_for(completed), 'review': preview['handoff_review'], 'note': 'Explicit current read-only evidence handoff'})['handoff'] - assert grant['evidence_only'] is True and grant['input_authorized'] is False + assert grant['evidence_only'] is True + assert grant['input_authorized'] is False listed = invoke(surface, connection, tmp_path, 'list_handoffs', {'batch_id': completed.batch_id, 'node_id': 'a'}) assert [item['id'] for item in listed['handoffs']] == [grant['id']] diff --git a/test/unit_test/headless/test_agent_completion_recovery.py b/test/unit_test/headless/test_agent_completion_recovery.py index 2a5eaa364..78b2c8324 100644 --- a/test/unit_test/headless/test_agent_completion_recovery.py +++ b/test/unit_test/headless/test_agent_completion_recovery.py @@ -43,7 +43,8 @@ def forbidden(*_args, **_kwargs): def approval(value, task_id=None): identifier = task_id or value.task_id review = value.runtime.recovery.control('preview_completion', {'task_id': identifier}) - assert review['approval_required'] is True and review['source_completion_inherited'] is False + assert review['approval_required'] is True + assert review['source_completion_inherited'] is False return {'task_id': identifier, 'snapshot': review['snapshot'], 'note': 'Reviewed confirmed effects and criteria'} @@ -55,14 +56,17 @@ def test_reviewed_completion_verifies_fresh_file_without_replaying_a_confirmed_a runtime, source = completed.runtime, completed.task_id historical = runtime.store.actions(source) ordinary = runtime.recovery.control('preview', {'task_id': source}) - assert ordinary['draft'] is None and 'no_remaining_actions' in ordinary['issues'] + assert ordinary['draft'] is None + assert 'no_remaining_actions' in ordinary['issues'] identifier = child(completed) result = runtime.finish(identifier) assert result.state == 'succeeded' - assert result.metadata['input_released'] is True and result.metadata['completion_contract'] == 1 + assert result.metadata['input_released'] is True + assert result.metadata['completion_contract'] == 1 assert [item['status'] for item in result.verification] == ['passed', 'passed'] assert runtime.store.actions(identifier) == [] - assert runtime.get(source).state == 'paused' and runtime.store.actions(source) == historical + assert runtime.get(source).state == 'paused' + assert runtime.store.actions(source) == historical def test_source_changes_during_reader_retirement_cannot_publish_stale_success(completed, monkeypatch): @@ -75,7 +79,8 @@ def changed(adapter): with pytest.raises(AgentTaskError, match='snapshot'): runtime.finish(identifier) result = runtime.get(identifier) - assert result.state != 'succeeded' and result.metadata['completion_contract'] == 0 + assert result.state != 'succeeded' + assert result.metadata['completion_contract'] == 0 def test_observe_then_finish_reuses_the_same_readonly_authority_without_reconfiguration(completed): @@ -84,7 +89,8 @@ def test_observe_then_finish_reuses_the_same_readonly_authority_without_reconfig assert frame.data['identity'] == 'local-files' assert runtime.get(identifier).metadata['input_released'] is False result = runtime.finish(identifier) - assert result.state == 'succeeded' and result.metadata['input_released'] is True + assert result.state == 'succeeded' + assert result.metadata['input_released'] is True assert runtime.store.actions(identifier) == [] diff --git a/test/unit_test/headless/test_agent_completion_security.py b/test/unit_test/headless/test_agent_completion_security.py index 9289bdd16..6b7966319 100644 --- a/test/unit_test/headless/test_agent_completion_security.py +++ b/test/unit_test/headless/test_agent_completion_security.py @@ -24,7 +24,8 @@ def changed(adapter): with pytest.raises(AgentTaskError, match='snapshot'): runtime.finish(identifier) result = runtime.get(identifier) - assert result.state == 'waiting_user' and result.metadata['input_released'] is True + assert result.state == 'waiting_user' + assert result.metadata['input_released'] is True assert result.metadata['completion_contract'] == 0 @@ -50,7 +51,8 @@ def test_fresh_criteria_fail_then_resume_reads_current_files_without_source_repl runtime, identifier = completed.runtime, child(completed) completed.output.write_text('missing marker', encoding='utf-8') result = runtime.finish(identifier) - assert result.state == 'waiting_user' and result.metadata['input_released'] is True + assert result.state == 'waiting_user' + assert result.metadata['input_released'] is True assert [item['status'] for item in result.verification] == ['passed', 'failed'] completed.output.write_text('complete', encoding='utf-8') runtime.resume(identifier) @@ -84,7 +86,8 @@ def cancelled(adapter): with pytest.raises(CancelledError): runtime.finish(identifier) result = runtime.get(identifier) - assert result.state == 'cancelled' and result.metadata['input_released'] is True + assert result.state == 'cancelled' + assert result.metadata['input_released'] is True assert result.metadata['completion_contract'] == 0 @@ -100,7 +103,8 @@ def test_restart_requires_explicit_resume_and_new_independent_evidence(completed restored.finish(identifier) restored.resume(identifier) result = restored.finish(identifier) - assert result.state == 'waiting_user' and result.metadata['completion_contract'] == 0 + assert result.state == 'waiting_user' + assert result.metadata['completion_contract'] == 0 assert result.metadata['input_released'] is True finally: restored.close() @@ -118,8 +122,9 @@ def test_duplicate_approvals_create_exactly_one_private_bound_successor(complete def test_stale_approval_and_unreviewed_spec_fields_never_create_a_successor(completed): runtime, request = completed.runtime, approval(completed) + unreviewed = {**request, 'spec': runtime.store.get(completed.task_id)['spec']} with pytest.raises(AgentTaskError, match='fields'): - runtime.recovery.control('create_completion', {**request, 'spec': runtime.store.get(completed.task_id)['spec']}) + runtime.recovery.control('create_completion', unreviewed) runtime.store.update(completed.task_id, 'paused', reason='changed after preview') with pytest.raises(AgentTaskError, match='snapshot'): runtime.recovery.control('create_completion', request) @@ -143,9 +148,11 @@ def test_cancelled_readonly_child_can_only_create_another_reviewed_readonly_succ runtime.cancel(identifier) retry = child(completed, identifier) result = runtime.finish(retry) - assert result.state == 'succeeded' and result.metadata['completion_review']['effect_task'] == completed.task_id + assert result.state == 'succeeded' + assert result.metadata['completion_review']['effect_task'] == completed.task_id assert result.metadata['recovery']['source_task'] == identifier - assert runtime.get(identifier).state == 'cancelled' and runtime.store.actions(retry) == [] + assert runtime.get(identifier).state == 'cancelled' + assert runtime.store.actions(retry) == [] @pytest.mark.parametrize('revoked', ['owner', 'reviewer']) @@ -171,5 +178,6 @@ def test_source_owned_completion_keeps_both_principals_live_during_maintenance(c users.set_role(revoked, 'viewer') runtime.maintain() result = runtime.get(identifier) - assert result.state == 'cancelled' and result.metadata['input_released'] is True + assert result.state == 'cancelled' + assert result.metadata['input_released'] is True assert result.metadata['completion_contract'] == 0 diff --git a/test/unit_test/headless/test_agent_completion_surfaces.py b/test/unit_test/headless/test_agent_completion_surfaces.py index 53944667e..ce0ba13a4 100644 --- a/test/unit_test/headless/test_agent_completion_surfaces.py +++ b/test/unit_test/headless/test_agent_completion_surfaces.py @@ -79,4 +79,5 @@ def test_fenced_dag_adopts_readonly_successor_without_replaying_the_last_action( approved['note'] = 'Reviewed independent completion after the fenced finite action' result = run_dag(definition, batch_id=batch['batch_id'], recovery=approved) assert result.succeeded, result.to_dict() - assert runtime.get(source).state == 'paused' and runtime.store.actions(successor) == [] + assert runtime.get(source).state == 'paused' + assert runtime.store.actions(successor) == [] diff --git a/test/unit_test/headless/test_agent_crash_recovery.py b/test/unit_test/headless/test_agent_crash_recovery.py index 684fb4baa..f0f663e2a 100644 --- a/test/unit_test/headless/test_agent_crash_recovery.py +++ b/test/unit_test/headless/test_agent_crash_recovery.py @@ -22,7 +22,8 @@ def test_unfinished_host_ownership_requires_explicit_recovery(tmp_path): replacement.acquire() probes = [] result = replacement.recover("Operator released held inputs and reviewed effects", lambda: probes.append(1), "host") - assert result["recovered"] and probes == [1] + assert result["recovered"] + assert probes == [1] assert replacement.read()["held"] == {} replacement.acquire() replacement.release() @@ -74,10 +75,12 @@ def test_durable_witness_is_written_before_native_ack(tmp_path): runtime.act(task.task_id, {"tool": "AC_increment", "arguments": {"hold": True}, "observation_id": frame.observation_id}) data = json.loads((tmp_path / "input.lock.journal").read_text(encoding="utf-8")) - assert data["owner"] and data["held"]["keys"] == [{"keycode": 65, "is_shift": False}] + assert data["owner"] + assert data["held"]["keys"] == [{"keycode": 65, "is_shift": False}] runtime.pause(task.task_id) data = json.loads((tmp_path / "input.lock.journal").read_text(encoding="utf-8")) - assert not data["owner"] and not data["held"] + assert not data["owner"] + assert not data["held"] finally: runtime.close() diff --git a/test/unit_test/headless/test_agent_dag_background.py b/test/unit_test/headless/test_agent_dag_background.py index 6cd186915..55c1f06de 100644 --- a/test/unit_test/headless/test_agent_dag_background.py +++ b/test/unit_test/headless/test_agent_dag_background.py @@ -50,8 +50,10 @@ def test_service_runs_without_any_default_client_connection_file(service, monkey accepted = submit(client, batch, definition) assert accepted["background"]["state"] in {"new", "in_progress", "success"} result = until(lambda: finished(instance, batch)) - assert result["state"] == "succeeded" and result["background"]["state"] == "success" - assert target.count == 2 and all(node["phase"] == "confirmed" for node in result["nodes"]) + assert result["state"] == "succeeded" + assert result["background"]["state"] == "success" + assert target.count == 2 + assert all(node["phase"] == "confirmed" for node in result["nodes"]) with instance.runtime.store.connection() as connection: data = json.loads(connection.execute("SELECT data FROM work_items WHERE queue='agent_dag'").fetchone()[0]) assert data["started"]["max_parallel"] == 2 @@ -67,7 +69,8 @@ def test_admin_dispatch_refreshes_actor_but_destination_tasks_belong_to_original with authorization_scope(AuthorizationContext("reviewer", "admin")): submit(client, batch, graph()) result = until(lambda: finished(instance, batch)) - assert result["state"] == "succeeded" and target.count == 2 + assert result["state"] == "succeeded" + assert target.count == 2 for node in result["nodes"]: assert instance.runtime.store.get(node["receipt"]["task_id"])["owner"]["user_id"] == "alice" @@ -78,7 +81,8 @@ def test_approved_continuation_runs_in_background_without_redispatching_complete approved = client.call("dag", {"operation": "approve_continuation", "request": request}) submit(client, batch, definition, approval_id=approved["approval_id"]) result = until(lambda: finished(instance, batch)) - assert result["state"] == "succeeded" and target.count == 2 + assert result["state"] == "succeeded" + assert target.count == 2 assert result["continuations"][0]["state"] == "used" with pytest.raises(AgentTaskError, match="consumed"): submit(client, batch, definition, approval_id=approved["approval_id"]) @@ -88,18 +92,21 @@ def test_failed_queue_acceptance_fences_attempt_without_input(service, monkeypat client, instance, target = service batch = client.call("dag", {"operation": "register", "request": {"definition": graph()}}) monkeypatch.setattr(instance._dag_jobs.queue, "add", Mock(side_effect=AgentTaskError("fixture queue failure"))) + definition = graph() with pytest.raises(AgentTaskError, match="fixture queue failure"): - submit(client, batch, graph()) - assert read(instance, batch)["state"] == "waiting_review" and target.count == 0 + submit(client, batch, definition) + assert read(instance, batch)["state"] == "waiting_review" + assert target.count == 0 def test_background_requires_an_explicit_service_and_never_runs_on_import(tmp_path): instance = AgentRuntime(tmp_path / "runtime.sqlite", targets={"fixture": object()}) try: batch = instance.batches.control("register", {"definition": graph()}) + definition = graph() with pytest.raises(AgentTaskError, match="explicitly started service"): instance.batches.control("run_background", {"batch_id": batch["batch_id"], - "definition": graph(), "max_parallel": 4}) + "definition": definition, "max_parallel": 4}) assert instance.batches.control("get", {"batch_id": batch["batch_id"]})["state"] == "registered" finally: instance.close() @@ -120,11 +127,14 @@ def test_shutdown_and_restart_do_not_replay_a_queued_delivery_or_reset_a_claim(s try: restarted.start() record = read(restarted, batch) - assert record["state"] == "waiting_review" and record["background"]["state"] == "failed" - assert record["nodes"][0]["phase"] == "claimed" and target.count == 0 + assert record["state"] == "waiting_review" + assert record["background"]["state"] == "failed" + assert record["nodes"][0]["phase"] == "claimed" + assert target.count == 0 inspected = restarted.runtime.batches.control("review_continuation", { "batch_id": batch["batch_id"], "definition": graph()}) - assert not inspected["can_approve"] and target.count == 0 + assert not inspected["can_approve"] + assert target.count == 0 finally: restarted.close() @@ -153,7 +163,8 @@ def delayed(worker): users.set_role(revoked, "viewer") proceed.set() result = until(lambda: finished(instance, batch)) - assert result["state"] == "waiting_review" and target.count == 0 + assert result["state"] == "waiting_review" + assert target.count == 0 assert all(node["phase"] == "pending" for node in result["nodes"]) finally: proceed.set() @@ -177,7 +188,8 @@ def delayed(worker): client.call("stop_all", {}) proceed.set() result = until(lambda: finished(instance, batch)) - assert result["state"] == "waiting_review" and target.count == 0 + assert result["state"] == "waiting_review" + assert target.count == 0 finally: proceed.set() @@ -197,10 +209,12 @@ def test_service_background_keeps_local_ownership_and_never_forwards_it_to_remot batch = client.call("dag", {"operation": "register", "request": {"definition": definition}}) submit(client, batch, definition) result = until(lambda: finished(local, batch)) - assert result["state"] == "succeeded" and target.count == remote_target.count == 1 + assert result["state"] == "succeeded" + assert target.count == remote_target.count == 1 local_row = local.runtime.store.get(result["nodes"][0]["receipt"]["task_id"]) remote_row = remote.runtime.store.get(result["nodes"][1]["receipt"]["task_id"]) - assert local_row["owner"]["user_id"] == "source-admin" and remote_row["owner"] == {} + assert local_row["owner"]["user_id"] == "source-admin" + assert remote_row["owner"] == {} assert "origin_policies" not in remote_row["spec"]["options"] finally: remote.close() diff --git a/test/unit_test/headless/test_agent_dag_background_crash.py b/test/unit_test/headless/test_agent_dag_background_crash.py index d004191ba..d2bf4705b 100644 --- a/test/unit_test/headless/test_agent_dag_background_crash.py +++ b/test/unit_test/headless/test_agent_dag_background_crash.py @@ -46,11 +46,14 @@ def test_process_death_retires_old_delivery_and_preserves_unknown_claim(tmp_path try: service.start() result = service.runtime.batches.control('get', {'batch_id': batch['batch_id']}) - assert result['state'] == 'waiting_review' and result['background']['state'] == 'failed' - assert result['nodes'][0]['phase'] == 'claimed' and service.runtime.list() == [] + assert result['state'] == 'waiting_review' + assert result['background']['state'] == 'failed' + assert result['nodes'][0]['phase'] == 'claimed' + assert service.runtime.list() == [] review = service.runtime.batches.control('review_continuation', { 'batch_id': batch['batch_id'], 'definition': graph()}) - assert not review['can_approve'] and review['decisions'][0]['action'] == 'wait' + assert not review['can_approve'] + assert review['decisions'][0]['action'] == 'wait' finally: service.close() finally: diff --git a/test/unit_test/headless/test_agent_dag_background_gui.py b/test/unit_test/headless/test_agent_dag_background_gui.py index e0a96ddac..36a869410 100644 --- a/test/unit_test/headless/test_agent_dag_background_gui.py +++ b/test/unit_test/headless/test_agent_dag_background_gui.py @@ -57,7 +57,8 @@ def accepted_and_view_worker_finished(): tab.close() proceed.set() result = until(lambda: finished(instance, batch)) - assert result["state"] == "succeeded" and target.count == 2 + assert result["state"] == "succeeded" + assert target.count == 2 finally: proceed.set() tab.close() diff --git a/test/unit_test/headless/test_agent_dag_batch_loopback.py b/test/unit_test/headless/test_agent_dag_batch_loopback.py index fa5182515..41da61028 100644 --- a/test/unit_test/headless/test_agent_dag_batch_loopback.py +++ b/test/unit_test/headless/test_agent_dag_batch_loopback.py @@ -52,7 +52,8 @@ def test_bound_graph_executes_once_and_independent_receipts_release_dependencies result = run_dag(definition, batch_id=registered["batch_id"]) assert result.succeeded, result.to_dict() batch = client.call("dag", {"operation": "get", "request": {"batch_id": registered["batch_id"]}}) - assert batch["state"] == "succeeded" and target.count == 2 + assert batch["state"] == "succeeded" + assert target.count == 2 assert all(node["phase"] == "confirmed" for node in batch["nodes"]) for node in batch["nodes"]: task = instance.runtime.store.get(node["receipt"]["task_id"]) @@ -82,12 +83,14 @@ def lost_reply(operation, body, timeout): monkeypatch.setattr(managed_tasks, "_connection", connection) result = run_dag(graph(), batch_id=registered["batch_id"]) - assert not result.succeeded and target.performed.wait(5) + assert not result.succeeded + assert target.performed.wait(5) batch = client.call("dag", {"operation": "get", "request": {"batch_id": registered["batch_id"]}}) assert batch["state"] == "waiting_review" assert [node["phase"] for node in batch["nodes"]] == ["unknown", "pending"] + retry = graph() with pytest.raises(AgentTaskError, match="explicit recovery"): - run_dag(graph(), batch_id=registered["batch_id"]) + run_dag(retry, batch_id=registered["batch_id"]) assert target.count == 1 @@ -120,7 +123,8 @@ def test_two_host_batch_tags_match_without_forwarding_local_principal_or_origins assert metadata["token"] not in str(saved) remote_row = remote.runtime.store.get(result.nodes["b"].result["task_id"]) local_row = local.runtime.store.get(result.nodes["a"].result["task_id"]) - assert remote_row["owner"] == {} and local_row["owner"]["user_id"] == "local-admin" + assert remote_row["owner"] == {} + assert local_row["owner"]["user_id"] == "local-admin" assert "origin_policies" not in remote_row["spec"]["options"] assert remote_row["spec"]["options"]["dag_batch"]["batch_id"] == registered["batch_id"] finally: diff --git a/test/unit_test/headless/test_agent_dag_batch_surfaces.py b/test/unit_test/headless/test_agent_dag_batch_surfaces.py index b8fa259db..50bfd6ef9 100644 --- a/test/unit_test/headless/test_agent_dag_batch_surfaces.py +++ b/test/unit_test/headless/test_agent_dag_batch_surfaces.py @@ -27,11 +27,13 @@ def test_versioned_api_and_catalog_expose_the_same_managed_batch_protocol(): from je_auto_control.utils.agent_runtime.commands import CATALOG assert root.manage_agent_dag is api.manage_agent_dag is core.manage_agent_dag function, fields, required, _read_only, _host_management = CATALOG["AC_agent_dag"] - assert function is root.manage_agent_dag and required == ("operation",) + assert function is root.manage_agent_dag + assert required == ("operation",) assert fields["operation"]["enum"] == list(OPERATIONS) for operation in OPERATIONS: parsed = build_parser().parse_args(["agent-task", "dag", operation, "request.json"]) - assert parsed.dag_operation == operation and parsed.request == "request.json" + assert parsed.dag_operation == operation + assert parsed.request == "request.json" def test_gui_registers_only_on_action_and_discards_graph_or_batch_selection_changes(monkeypatch): @@ -52,7 +54,8 @@ def test_gui_registers_only_on_action_and_discards_graph_or_batch_selection_chan tab._inspection_source = tab._editor.toPlainText() tab.setProperty("dag_batch_source", "") tab._on_batch_finished({"batch_id": "registered", "nodes": []}) - assert tab._batch_id.text() == "registered" and len(requests) == 1 + assert tab._batch_id.text() == "registered" + assert len(requests) == 1 tab._batch_id.setText("other-batch") tab._on_batch_finished({"batch_id": "late-old", "nodes": []}) assert tab._batch_id.text() == "other-batch" diff --git a/test/unit_test/headless/test_agent_dag_batches.py b/test/unit_test/headless/test_agent_dag_batches.py index 973f5d0e5..17ea2d650 100644 --- a/test/unit_test/headless/test_agent_dag_batches.py +++ b/test/unit_test/headless/test_agent_dag_batches.py @@ -27,7 +27,8 @@ def runtime(tmp_path): def test_registered_batch_is_durable_and_does_not_dispatch(runtime): value = runtime.batches.control("register", {"definition": graph()}) - assert value["state"] == "registered" and runtime.list() == [] + assert value["state"] == "registered" + assert runtime.list() == [] assert value["definition"]["nodes"][1]["depends_on"] == ["a"] assert value["resume_permitted"] is False assert runtime.batches.control("get", {"batch_id": value["batch_id"]}) == value @@ -121,8 +122,9 @@ def test_atomic_registration_rolls_back_graph_and_nodes(runtime): with runtime.store.connection(write=True) as connection: connection.execute("CREATE TRIGGER fail_node BEFORE INSERT ON agent_dag_nodes " "BEGIN SELECT RAISE(ABORT, 'fixture rollback'); END") + definition = graph() with pytest.raises(AgentTaskError, match="database operation failed"): - runtime.batches.control("register", {"definition": graph()}) + runtime.batches.control("register", {"definition": definition}) assert runtime.batches.control("list")["batches"] == [] @@ -135,13 +137,16 @@ def test_claim_survives_restart_and_never_becomes_proof_of_non_delivery(runtime) restored = AgentRuntime(runtime.store.path, targets={"fixture": object()}) try: current = restored.batches.control("get", {"batch_id": value["batch_id"]}) - assert current["state"] == "waiting_review" and current["nodes"][0]["phase"] == "claimed" + assert current["state"] == "waiting_review" + assert current["nodes"][0]["phase"] == "claimed" assert not current["resume_permitted"] with pytest.raises(AgentTaskError, match="inactive"): restored.batches.control("claim", {"batch_id": value["batch_id"], "attempt": started["attempt"], "node_id": "b"}) + definition = graph() with pytest.raises(AgentTaskError, match="explicit recovery"): - restored.batches.control("begin", {"batch_id": value["batch_id"], "definition": graph(), "max_parallel": 4}) + restored.batches.control("begin", {"batch_id": value["batch_id"], "definition": definition, + "max_parallel": 4}) finally: restored.close() @@ -174,8 +179,9 @@ def test_graph_tampering_and_closed_runtime_fail_closed(runtime): with pytest.raises(AgentTaskError, match="digest mismatch"): runtime.batches.control("get", {"batch_id": value["batch_id"]}) runtime.close() + definition = graph() with pytest.raises(AgentTaskError, match="closed"): - runtime.batches.control("register", {"definition": graph()}) + runtime.batches.control("register", {"definition": definition}) def test_changed_registered_host_is_refused_without_revealing_credentials(runtime, monkeypatch): @@ -200,9 +206,11 @@ def test_original_owner_revocation_blocks_admin_delivery_of_registered_work(runt with authorization_scope(AuthorizationContext("owner", "operator", store_path=str(users.path))): value = runtime.batches.control("register", {"definition": graph()}) users.set_role("owner", "viewer") + definition = graph() with authorization_scope(AuthorizationContext("admin", "admin")): with pytest.raises(AuthorizationError): - runtime.batches.control("begin", {"batch_id": value["batch_id"], "definition": graph(), "max_parallel": 4}) + runtime.batches.control("begin", {"batch_id": value["batch_id"], "definition": definition, + "max_parallel": 4}) def test_nested_managed_operation_is_refused_before_batch_client_contact(monkeypatch): @@ -210,6 +218,7 @@ def test_nested_managed_operation_is_refused_before_batch_client_contact(monkeyp client = Mock(side_effect=AssertionError("nested batch contacted the coordinator")) monkeypatch.setattr(batch_execution, "current_operation", lambda: object()) monkeypatch.setattr(batch_execution, "AgentRuntimeClient", client) + definition = graph() with pytest.raises(AgentTaskError, match="cannot coordinate"): - run_dag(graph(), batch_id="registered-batch") + run_dag(definition, batch_id="registered-batch") client.assert_not_called() diff --git a/test/unit_test/headless/test_agent_dag_connection_binding.py b/test/unit_test/headless/test_agent_dag_connection_binding.py index ae29149a2..55795d162 100644 --- a/test/unit_test/headless/test_agent_dag_connection_binding.py +++ b/test/unit_test/headless/test_agent_dag_connection_binding.py @@ -32,7 +32,9 @@ def after_first_record(self, operation, body=None, **kwargs): monkeypatch.setattr(AgentRuntimeClient, "call", after_first_record) result = run_dag(graph(), batch_id=batch["batch_id"]) assert result.succeeded, result.to_dict() - assert target.count == 2 and other_target.count == 0 and other.runtime.list() == [] + assert target.count == 2 + assert other_target.count == 0 + assert other.runtime.list() == [] assert source.runtime.batches.control("get", {"batch_id": batch["batch_id"]})["state"] == "succeeded" finally: other.close() @@ -61,13 +63,15 @@ def after_first_record(self, operation, body=None, **kwargs): batch = client.call("dag", {"operation": "register", "request": {"definition": graph()}}) monkeypatch.setattr(AgentRuntimeClient, "call", after_first_record) result = run_dag(graph(), batch_id=batch["batch_id"]) - assert not result.succeeded and target.count == 1 and other_target.count == 0 + assert not result.succeeded + assert target.count == 1 + assert other_target.count == 0 assert other.runtime.list() == [] saved = source.runtime.batches.control("get", {"batch_id": batch["batch_id"]}) assert [row["phase"] for row in saved["nodes"]] == ["confirmed", "pending"] + restart = {"batch_id": batch["batch_id"], "definition": graph(), "max_parallel": 4} with pytest.raises(AgentTaskError, match="already started"): - source.runtime.batches.control("begin", {"batch_id": batch["batch_id"], - "definition": graph(), "max_parallel": 4}) + source.runtime.batches.control("begin", restart) finally: path.write_text(json.dumps(metadata)) other.close() @@ -88,7 +92,8 @@ def test_admin_inspection_uses_original_owners_keys_on_the_owning_runtime(servic monkeypatch.setattr("je_auto_control.utils.agent_runtime.client.default_connection_path", lambda: other.directory / "agent_runtime.json") result = client.call("dag", {"operation": "inspect", "request": {"batch_id": batch["batch_id"]}}) - assert target.count == 2 and other.runtime.list() == [] + assert target.count == 2 + assert other.runtime.list() == [] assert all(node["recorded_completion"] is True for node in result["nodes"]) assert all(node["request_match"] == "matches" for node in result["nodes"]) finally: diff --git a/test/unit_test/headless/test_agent_dag_continuation.py b/test/unit_test/headless/test_agent_dag_continuation.py index b89c14740..185950eb6 100644 --- a/test/unit_test/headless/test_agent_dag_continuation.py +++ b/test/unit_test/headless/test_agent_dag_continuation.py @@ -32,7 +32,8 @@ def after_first_record(self, operation, body=None, **kwargs): monkeypatch.setattr(AgentRuntimeClient, "call", after_first_record) result = run_dag(definition, batch_id=batch["batch_id"], stop_event=stop) - assert not result.succeeded and target.count == 1 + assert not result.succeeded + assert target.count == 1 return client, instance, target, batch, definition @@ -49,8 +50,10 @@ def approval(value): def test_explicit_whole_graph_continuation_adopts_parent_and_dispatches_only_pending_child(stopped): client, _instance, target, batch, definition = stopped value = review(client, batch, definition) - assert value["can_approve"] and [item["action"] for item in value["decisions"]] == ["adopt", "dispatch"] - assert target.count == 1 and not value["resume_permitted"] + assert value["can_approve"] + assert [item["action"] for item in value["decisions"]] == ["adopt", "dispatch"] + assert target.count == 1 + assert not value["resume_permitted"] request = approval(value) recorded = client.call("dag", {"operation": "approve_continuation", "request": request}) assert target.count == 1 @@ -73,10 +76,12 @@ def test_lookup_absence_never_turns_a_claimed_node_back_into_dispatch(service): "batch_id": batch["batch_id"], "attempt": started["attempt"], "node_id": "a"}}) client.call("dag", {"operation": "pause_batch", "request": {"batch_id": batch["batch_id"]}}) value = review(client, batch, definition) - assert not value["can_approve"] and value["decisions"][0]["action"] == "wait" + assert not value["can_approve"] + assert value["decisions"][0]["action"] == "wait" value["decisions"][0] = {"node_id": "a", "action": "dispatch", "spec": definition["nodes"][0]["task"]["spec"]} + request = {"operation": "approve_continuation", "request": approval(value)} with pytest.raises(AgentTaskError, match="evidence or decisions changed"): - client.call("dag", {"operation": "approve_continuation", "request": approval(value)}) + client.call("dag", request) assert target.count == 0 @@ -158,14 +163,16 @@ def test_successor_adoption_uses_real_recovery_provenance_and_fresh_completion(s "max_parallel": 4}}) result = client.call("dag", {"operation": "validate_node", "request": { "batch_id": batch["batch_id"], "attempt": begun["attempt"], "node_id": "a"}}) - assert result["task_id"] == child and target.count == 2 + assert result["task_id"] == child + assert target.count == 2 unrelated = instance.runtime.submit(graph()["nodes"][1]["task"]["spec"]) assert run_task(instance.runtime, unrelated.task_id).state == "succeeded" client.call("dag", {"operation": "pause_batch", "request": {"batch_id": batch["batch_id"]}}) value = review(client, batch, definition) value["decisions"][0] = {"node_id": "a", "action": "successor", "task_id": unrelated.task_id} value = review(client, batch, definition, decisions=value["decisions"]) - assert not value["can_approve"] and target.count == 3 + assert not value["can_approve"] + assert target.count == 3 def test_expired_origin_can_be_replaced_only_for_never_dispatched_scoped_work(service, monkeypatch, tmp_path): @@ -200,4 +207,5 @@ def after_first(self, operation, body=None, **kwargs): result = run_dag(definition, batch_id=batch["batch_id"], recovery=approval(value)) assert result.succeeded, result.to_dict() child = instance.runtime.store.get(result.nodes["b"].result["task_id"]) - assert child["spec"]["roots"] == [str(narrow)] and target.count == 2 + assert child["spec"]["roots"] == [str(narrow)] + assert target.count == 2 diff --git a/test/unit_test/headless/test_agent_dag_continuation_guards.py b/test/unit_test/headless/test_agent_dag_continuation_guards.py index 0578d3291..1c6baa6fb 100644 --- a/test/unit_test/headless/test_agent_dag_continuation_guards.py +++ b/test/unit_test/headless/test_agent_dag_continuation_guards.py @@ -43,7 +43,8 @@ def test_lost_adoption_evidence_after_start_cannot_publish_false_batch_success(s "batch_id": batch["batch_id"], "attempt": started["attempt"], "node_id": "a"}}) finished = client.call("dag", {"operation": "finish", "request": { "batch_id": batch["batch_id"], "attempt": started["attempt"]}}) - assert finished["state"] == "waiting_review" and finished["nodes"][0]["phase"] == "unknown" + assert finished["state"] == "waiting_review" + assert finished["nodes"][0]["phase"] == "unknown" assert target.count == 1 @@ -61,7 +62,8 @@ def consume(_index): with ThreadPoolExecutor(max_workers=2) as workers: results = list(workers.map(consume, (0, 1))) - assert sum(value is not None for value in results) == 1 and target.count == 1 + assert sum(value is not None for value in results) == 1 + assert target.count == 1 def test_another_admin_cannot_reuse_an_actors_review_and_audit_keeps_the_real_actor(stopped): @@ -73,7 +75,8 @@ def test_another_admin_cannot_reuse_an_actors_review_and_audit_keeps_the_real_ac approve(client, batch, definition) current = client.call("dag", {"operation": "get", "request": {"batch_id": batch["batch_id"]}}) assert current["continuations"][0]["actor"] == "reviewer" - assert current["continuations"][0]["note"] and target.count == 1 + assert current["continuations"][0]["note"] + assert target.count == 1 @pytest.mark.parametrize("fault", ["missing", "duplicate", "unknown", "extra", "null"]) @@ -112,7 +115,8 @@ def test_tagged_submission_witness_is_atomic_and_legacy_retries_never_backfill(s connection.execute("DROP TRIGGER fail_admission") task = instance.runtime.submit(data, idempotency_key="tagged") metadata = instance.runtime.get(task.task_id).metadata - assert metadata["submission_contract"] == 1 and metadata["submitted_spec_digest"] == submitted_digest(data) + assert metadata["submission_contract"] == 1 + assert metadata["submitted_spec_digest"] == submitted_digest(data) legacy = instance.runtime._create_task(instance.runtime._validated_spec(data), "legacy-tagged") assert instance.runtime.submit(data, idempotency_key="legacy-tagged").task_id == legacy.task_id assert "submission_contract" not in instance.runtime.get(legacy.task_id).metadata @@ -127,4 +131,5 @@ def test_fencing_a_started_continuation_rejects_its_next_claim(stopped): client.call("dag", {"operation": "claim", "request": { "batch_id": batch["batch_id"], "attempt": started["attempt"], "node_id": "b"}}) current = client.call("dag", {"operation": "get", "request": {"batch_id": batch["batch_id"]}}) - assert current["nodes"][1]["phase"] == "pending" and target.count == 1 + assert current["nodes"][1]["phase"] == "pending" + assert target.count == 1 diff --git a/test/unit_test/headless/test_agent_dag_continuation_gui.py b/test/unit_test/headless/test_agent_dag_continuation_gui.py index d9999bb88..bbc7d7ef4 100644 --- a/test/unit_test/headless/test_agent_dag_continuation_gui.py +++ b/test/unit_test/headless/test_agent_dag_continuation_gui.py @@ -29,7 +29,9 @@ def test_review_and_run_actions_preserve_source_selection_and_do_not_auto_execut tab._on_batch_finished({"batch_id": "batch-a", "definition": graph(), "snapshot": "a" * 64, "decisions": [{"node_id": "a", "action": "wait"}], "nodes": []}) request = json.loads(tab._continuation_request.toPlainText()) - assert request["note"] == "" and tab._editor.toPlainText() == ordinary and dispatched == [] + assert request["note"] == "" + assert tab._editor.toPlainText() == ordinary + assert dispatched == [] actions["dag_continuation_run_btn"]() assert dispatched.pop()[1] == {"inspect_only": False, "batch_operation": "run_continuation"} tab._batch_id.setText("batch-b") diff --git a/test/unit_test/headless/test_agent_dag_inspection.py b/test/unit_test/headless/test_agent_dag_inspection.py index be1b2a1c8..a46b0e0c3 100644 --- a/test/unit_test/headless/test_agent_dag_inspection.py +++ b/test/unit_test/headless/test_agent_dag_inspection.py @@ -25,13 +25,17 @@ def call(operation, body, timeout): monkeypatch.setattr(managed_tasks, "_connection", connection) result = inspect_agent_dag(definition()) - assert result["inspection_only"] is True and result["resume_permitted"] is False + assert result["inspection_only"] is True + assert result["resume_permitted"] is False first, second = result["nodes"] - assert first["task_id"] == "task-one" and first["lookup"] == "found" - assert first["recorded_completion"] is True and first["request_match"] == "unverified" + assert first["task_id"] == "task-one" + assert first["lookup"] == "found" + assert first["recorded_completion"] is True + assert first["request_match"] == "unverified" assert second["lookup"] == "not_managed" assert "private-remote-path" not in str(result) - assert len(calls) == 1 and calls[0][0:2] == ("lookup", {"idempotency_key": "reviewed-one"}) + assert len(calls) == 1 + assert calls[0][0:2] == ("lookup", {"idempotency_key": "reviewed-one"}) assert 0 < calls[0][2] <= 5 @@ -63,8 +67,9 @@ def fail(*_args): @pytest.mark.parametrize("role,host", [("viewer", "local"), ("operator", "remote")]) def test_role_failure_prevents_opening_a_connection(role, host, monkeypatch): monkeypatch.setattr(managed_tasks, "_connection", lambda *_args: pytest.fail("opened forbidden connection")) - with authorization_scope(AuthorizationContext("caller", role)), pytest.raises(AuthorizationError): - inspect_agent_dag(definition(host=host)) + caller, graph = AuthorizationContext("caller", role), definition(host=host) + with authorization_scope(caller), pytest.raises(AuthorizationError): + inspect_agent_dag(graph) def test_owner_revocation_during_lookup_discards_the_reply(tmp_path, monkeypatch): @@ -77,8 +82,9 @@ def lookup(*_args): monkeypatch.setattr(managed_tasks, "_connection", lambda _host: lookup) owner = AuthorizationContext("caller", "operator", store_path=str(store.path)) + graph = definition() with authorization_scope(owner), pytest.raises(AuthorizationError): - inspect_agent_dag(definition()) + inspect_agent_dag(graph) def test_cancellation_and_explicit_stop_do_not_send_control_requests(monkeypatch): @@ -91,21 +97,24 @@ def lookup(operation, *_args): return {"found": True, "task": completed()} monkeypatch.setattr(managed_tasks, "_connection", lambda _host: lookup) + graph = definition() with cancellation_scope(token), pytest.raises(CancelledError): - inspect_agent_dag(definition()) + inspect_agent_dag(graph) assert calls == ["lookup"] stop = threading.Event() stop.set() + graph = definition() with managed_tasks.managed_stop_scope(stop), pytest.raises(AgentTaskError, match="stopped"): - inspect_agent_dag(definition()) + inspect_agent_dag(graph) assert calls == ["lookup"] @pytest.mark.parametrize("timeout", [False, 0, 301, float("inf")]) def test_invalid_budgets_fail_before_queries(timeout, monkeypatch): monkeypatch.setattr(managed_tasks, "_connection", lambda *_args: pytest.fail("invalid budget made a request")) + graph = definition() with pytest.raises(AgentTaskError): - inspect_agent_dag(definition(), timeout) + inspect_agent_dag(graph, timeout) def test_graph_bounds_and_cycles_are_checked_before_queries(monkeypatch): @@ -159,8 +168,9 @@ def test_managed_task_cannot_inspect_other_task_keys(monkeypatch): monkeypatch.setattr(managed_tasks, "_connection", lambda *_args: pytest.fail("nested lookup")) parent = TaskOperation(AgentTaskSpec.from_dict(task()["spec"]), lambda: None, None) + graph = definition() with operation_scope(parent), pytest.raises(AgentTaskError, match="cannot inspect"): - inspect_agent_dag(definition()) + inspect_agent_dag(graph) def test_real_registered_host_lookup_does_not_submit_even_when_reviewed_spec_differs(tmp_path, monkeypatch): @@ -195,12 +205,16 @@ def request(url, **kwargs): with authorization_scope(AuthorizationContext("local-admin", "admin")): result = inspect_agent_dag(graph) first = result["nodes"][0] - assert first["task_id"] == accepted.task_id and first["recorded_completion"] is True - assert first["request_match"] == "differs" and result["resume_permitted"] is False - assert len(calls) == 2 and all(call[0].endswith("/agent/lookup") for call in calls) + assert first["task_id"] == accepted.task_id + assert first["recorded_completion"] is True + assert first["request_match"] == "differs" + assert result["resume_permitted"] is False + assert len(calls) == 2 + assert all(call[0].endswith("/agent/lookup") for call in calls) assert calls[0][1]["body"] == {"idempotency_key": "reviewed-one"} assert set(calls[1][1]["body"]) == {"idempotency_key", "spec"} - assert target.calls == ["AC_increment"] and len(service.runtime.list()) == 1 + assert target.calls == ["AC_increment"] + assert len(service.runtime.list()) == 1 assert "history_path" not in str(result) finally: service.close() diff --git a/test/unit_test/headless/test_agent_dag_local_tasks.py b/test/unit_test/headless/test_agent_dag_local_tasks.py index 640eebea1..eedec3ead 100644 --- a/test/unit_test/headless/test_agent_dag_local_tasks.py +++ b/test/unit_test/headless/test_agent_dag_local_tasks.py @@ -64,8 +64,10 @@ def test_admin_caller_executes_all_nodes_under_original_owner_and_records_actual row = connection.execute("SELECT actor FROM agent_dag_local_tasks WHERE task_id=?", (task_id,)).fetchone() assert json.loads(row["actor"])["user_id"] == "reviewer" visible = instance.runtime.get(task_id).metadata["local_dag_admission"] - assert visible["actor"] == "reviewer" and visible["contract"] == 1 - assert "store_path" not in visible and "attempt" not in visible + assert visible["actor"] == "reviewer" + assert visible["contract"] == 1 + assert "store_path" not in visible + assert "attempt" not in visible def test_local_admission_is_idempotent_and_cannot_bind_an_unrelated_task(owned, monkeypatch): @@ -76,7 +78,8 @@ def test_local_admission_is_idempotent_and_cannot_bind_an_unrelated_task(owned, assert local(owned, started, "submit", submission(node)) == first with pytest.raises(AgentTaskError, match="identity or admitting actor"): local(owned, started, "get", {"task_id": "unrelated-task"}) - assert len(instance.runtime.list()) == 1 and target.count == 0 + assert len(instance.runtime.list()) == 1 + assert target.count == 0 @pytest.mark.parametrize("fault", ["goal", "key", "extra", "operation", "task_id"]) @@ -97,7 +100,8 @@ def test_local_transport_rejects_changed_contracts_and_unbounded_operations(owne operation, body = "get", {"task_id": []} with pytest.raises(AgentTaskError): local(owned, started, operation, body) - assert instance.runtime.list() == [] and target.count == 0 + assert instance.runtime.list() == [] + assert target.count == 0 def test_preexisting_request_without_admission_witness_is_never_backfilled(owned, monkeypatch): @@ -106,12 +110,14 @@ def test_preexisting_request_without_admission_witness_is_never_backfilled(owned monkeypatch.setattr(instance.runtime.background, "start", Mock()) with authorization_scope(owner): task = instance.runtime.submit(node["task"]["spec"], idempotency_key=node["task"]["idempotency_key"]) + request = submission(node) with pytest.raises(AgentTaskError, match="trusted admission evidence"): - local(owned, started, "submit", submission(node)) + local(owned, started, "submit", request) with instance.runtime.store.connection() as connection: assert connection.execute("SELECT 1 FROM agent_dag_local_tasks WHERE task_id=?", (task.task_id,)).fetchone() is None - assert target.count == 0 and len(instance.runtime.list()) == 1 + assert target.count == 0 + assert len(instance.runtime.list()) == 1 def test_admission_failure_rolls_back_task_key_and_both_witnesses(owned, monkeypatch): @@ -122,8 +128,9 @@ def test_admission_failure_rolls_back_task_key_and_both_witnesses(owned, monkeyp with instance.runtime.store.connection(write=True) as connection: connection.execute("CREATE TRIGGER fail_dag_admission BEFORE INSERT ON agent_dag_local_tasks " "BEGIN SELECT RAISE(ABORT,'fixture rollback'); END") + request = submission(node) with pytest.raises(AgentTaskError, match="database operation failed"): - local(owned, started, "submit", submission(node)) + local(owned, started, "submit", request) with instance.runtime.store.connection() as connection: for table in ("agent_tasks", "agent_request_keys", "agent_task_submissions", "agent_dag_local_tasks"): assert connection.execute("SELECT COUNT(*) FROM " + table).fetchone()[0] == 0 @@ -154,7 +161,9 @@ def test_fenced_attempt_can_retire_only_its_admitted_task_after_original_owner_l with authorization_scope(actor): client.call("dag", {"operation": "pause_batch", "request": {"batch_id": batch["batch_id"]}}) paused = local(owned, started, "pause", {"task_id": task["task_id"]}) - assert paused["state"] == "paused" and paused["metadata"]["input_released"] is True and target.count == 0 + assert paused["state"] == "paused" + assert paused["metadata"]["input_released"] is True + assert target.count == 0 def test_maintenance_retires_lease_when_admin_loses_cross_owner_authority(owned, monkeypatch): @@ -170,7 +179,9 @@ def test_maintenance_retires_lease_when_admin_loses_cross_owner_authority(owned, # cancelling and a later one settles it. until(lambda: runtime.maintain() or runtime.get(task["task_id"]).state != "cancelling") result = runtime.get(task["task_id"]) - assert result.state == "cancelled" and result.metadata["input_released"] is True and target.count == 0 + assert result.state == "cancelled" + assert result.metadata["input_released"] is True + assert target.count == 0 def test_revocation_during_grounding_is_refused_before_the_target_input_call(owned, monkeypatch): @@ -191,4 +202,5 @@ def revoke_during_grounding(): with pytest.raises(AuthorizationError): instance.runtime.act(task["task_id"], {"tool": "AC_increment", "observation_id": frame.observation_id}) receipts = instance.runtime.store.actions(task["task_id"]) - assert target.count == 0 and [item["status"] for item in receipts] == ["failed"] + assert target.count == 0 + assert [item["status"] for item in receipts] == ["failed"] diff --git a/test/unit_test/headless/test_agent_dag_matching.py b/test/unit_test/headless/test_agent_dag_matching.py index 2153ffa7e..b90dbb334 100644 --- a/test/unit_test/headless/test_agent_dag_matching.py +++ b/test/unit_test/headless/test_agent_dag_matching.py @@ -62,7 +62,8 @@ def call(operation, body, _timeout): monkeypatch.setattr(managed_tasks, "_connection", lambda _host: call) result = inspect_agent_dag(definition()) assert result["nodes"][0]["request_match"] == "unverified" - assert len(calls) == 1 and set(calls[0][1]) == {"idempotency_key"} + assert len(calls) == 1 + assert set(calls[0][1]) == {"idempotency_key"} @pytest.mark.parametrize("changed", ["removed", "replaced"]) @@ -80,9 +81,12 @@ def call(operation, body, _timeout): monkeypatch.setattr(managed_tasks, "_connection", lambda _host: call) result = inspect_agent_dag(definition()) node = result["nodes"][0] - assert node["lookup"] == "unavailable" and node["request_match"] == "unverifiable" - assert node["recorded_completion"] is None and node["input_released"] is None - assert result["resume_permitted"] is False and calls == ["lookup", "lookup"] + assert node["lookup"] == "unavailable" + assert node["request_match"] == "unverifiable" + assert node["recorded_completion"] is None + assert node["input_released"] is None + assert result["resume_permitted"] is False + assert calls == ["lookup", "lookup"] def test_remote_matching_never_sends_local_origin_witnesses(monkeypatch): @@ -114,8 +118,9 @@ def call(operation, body, _timeout): monkeypatch.setattr(managed_tasks, "_connection", lambda _host: call) owner = AuthorizationContext("caller", "operator", store_path=str(store.path)) + graph = definition() with authorization_scope(owner), pytest.raises(AuthorizationError): - inspect_agent_dag(definition()) + inspect_agent_dag(graph) assert calls == ["lookup", "lookup"] @@ -143,10 +148,12 @@ def test_real_destination_matches_the_reviewed_skill_without_repeating_effects(t changed["nodes"][0]["task"]["spec"]["goal"] = "changed reviewed goal" different = inspect_agent_dag(changed) first = result["nodes"][0] - assert first["request_match"] == "matches" and first["recorded_completion"] is True + assert first["request_match"] == "matches" + assert first["recorded_completion"] is True assert different["nodes"][0]["request_match"] == "differs" assert result["resume_permitted"] is False - assert target.calls == ["AC_increment"] and len(service.runtime.list()) == 1 + assert target.calls == ["AC_increment"] + assert len(service.runtime.list()) == 1 assert not service.runtime._leases finally: service.close() diff --git a/test/unit_test/headless/test_agent_dag_retention.py b/test/unit_test/headless/test_agent_dag_retention.py index 3169a54bb..2cf0e83cc 100644 --- a/test/unit_test/headless/test_agent_dag_retention.py +++ b/test/unit_test/headless/test_agent_dag_retention.py @@ -90,7 +90,8 @@ def test_completed_batch_expires_with_owned_evidence_and_completed_delivery(serv age(host.runtime, batch["batch_id"]) result = client.call("prune", {"keep_days": 30}) assert result["deleted_batches"] == [batch["batch_id"]] - assert set(result["deleted"]) == set(task_ids) and result["deleted_dag_deliveries"] == 1 + assert set(result["deleted"]) == set(task_ids) + assert result["deleted_dag_deliveries"] == 1 assert target.count == 2 with host.runtime.store.connection() as connection: for table in ("agent_dag_batches", "agent_dag_nodes", "agent_dag_inputs", "agent_dag_local_tasks"): @@ -153,7 +154,8 @@ def test_completed_batch_keeps_its_gate_when_related_work_is_unsettled(service, connection.execute("UPDATE agent_dag_nodes SET phase='unknown' WHERE batch_id=?", (batch["batch_id"],)) try: result = instance.prune() - assert result["deleted_batches"] == [] and result["deleted"] == [] + assert result["deleted_batches"] == [] + assert result["deleted"] == [] with instance.store.connection() as connection: assert connection.execute("SELECT 1 FROM agent_dag_local_tasks WHERE task_id=?", (task_id,)).fetchone() finally: @@ -198,5 +200,6 @@ def notice_lock(identifier): assert selected.wait(5) runtime.batches.control("begin", {"batch_id": batch["batch_id"], "definition": graph(), "max_parallel": 4}) worker.join(5) - assert not worker.is_alive() and results[0]["deleted_batches"] == [] + assert not worker.is_alive() + assert results[0]["deleted_batches"] == [] assert runtime.batches.control("get", {"batch_id": batch["batch_id"]})["state"] == "running" diff --git a/test/unit_test/headless/test_agent_desktop_continuity.py b/test/unit_test/headless/test_agent_desktop_continuity.py index d3d7d8e86..2568c7e9e 100644 --- a/test/unit_test/headless/test_agent_desktop_continuity.py +++ b/test/unit_test/headless/test_agent_desktop_continuity.py @@ -45,7 +45,8 @@ def test_retired_desktop_context_cannot_submit_an_edited_form_after_pause(tmp_pa target.values["Field"] = "changed while paused" runtime.resume(task.task_id) outcome = run_task(runtime, task.task_id) - assert outcome.state == "waiting_user" and "desktop context" in outcome.reason + assert outcome.state == "waiting_user" + assert "desktop context" in outcome.reason assert target.submissions == 0 assert len(runtime.store.actions(task.task_id)) == 1 @@ -54,9 +55,11 @@ def test_profiled_desktop_prefix_is_refused_before_any_helper_launch(): profile = AgentTargetProfile(name="reviewed-desktop", kind="desktop", apps=("fixture",), allowed_tools=("AC_workflow_control",), settings={"platform": "windows"}) target = NativeDesktopTarget(profile=profile.to_dict()) + resumed = form() with pytest.raises(AgentHumanReview, match="desktop context"): - target.check_resume(form(), [{"status": "confirmed"}]) - assert target._channel is None and target._resource is None + target.check_resume(resumed, [{"status": "confirmed"}]) + assert target._channel is None + assert target._resource is None def test_uninterrupted_desktop_skill_keeps_its_context(tmp_path): diff --git a/test/unit_test/headless/test_agent_dialog_scope.py b/test/unit_test/headless/test_agent_dialog_scope.py index 69f990447..8fd03527d 100644 --- a/test/unit_test/headless/test_agent_dialog_scope.py +++ b/test/unit_test/headless/test_agent_dialog_scope.py @@ -10,8 +10,9 @@ def test_unsaved_modal_requires_human_review_even_with_a_known_file_title(monkeypatch, name): from je_auto_control.utils.agent_runtime import dialog_scope monkeypatch.setattr(dialog_scope, "_window_metadata", lambda _hwnd: {"modal": True, "class_name": "#32770"}) + controls = [SimpleNamespace(name=name)] with pytest.raises(AgentTaskError, match="unsaved"): - dialog_scope.check_review(10, "Save As", [SimpleNamespace(name=name)], workspace=False) + dialog_scope.check_review(10, "Save As", controls, workspace=False) def test_unknown_modal_is_never_treated_as_a_file_picker(monkeypatch): @@ -113,7 +114,8 @@ def observe(): with pytest.raises(AgentHumanReview): runtime.observe(task.task_id) assert runtime.get(task.task_id).state == "waiting_user" - assert not runtime._leases and cleanup + assert not runtime._leases + assert cleanup assert runtime.store.actions(task.task_id) == [] runtime.resume(task.task_id) assert runtime.get(task.task_id).state == "queued" @@ -158,7 +160,8 @@ def test_file_dialog_rebinds_only_to_confirmed_own_foreground_and_canonicalizes_ with operation_scope(TaskOperation(spec, lambda: None, None)): with file_dialog_scope.scoped_file_dialog(target, 11, 7, {}): assert handle_file_dialog("report.txt")["handled"] - assert focus == [20] and target._action_identity == (20, 7) + assert focus == [20] + assert target._action_identity == (20, 7) assert input_calls == [str((tmp_path / "report.txt").resolve()), "enter"] @@ -172,8 +175,9 @@ def test_review_error_type_survives_the_native_channel_boundary(): channel._replies = queue.Queue() channel._stop = threading.Event() channel._replies.put({"id": "fixture", "error_type": "AgentHumanReview", "error": "unsaved content"}) + deadline = time.monotonic() + 1 with pytest.raises(AgentHumanReview): - channel._receive("fixture", time.monotonic() + 1, lambda: None, None) + channel._receive("fixture", deadline, lambda: None, None) def test_local_http_review_type_and_waiting_state_share_the_same_gate(tmp_path): diff --git a/test/unit_test/headless/test_agent_dynamic_recovery.py b/test/unit_test/headless/test_agent_dynamic_recovery.py index 1bb5dde91..6cad50be0 100644 --- a/test/unit_test/headless/test_agent_dynamic_recovery.py +++ b/test/unit_test/headless/test_agent_dynamic_recovery.py @@ -45,15 +45,21 @@ def test_preview_does_not_invent_remaining_actions_or_contact_a_model(dynamic_so network = Mock(side_effect=AssertionError("preview contacted a model")) monkeypatch.setattr("je_auto_control.utils.agent_runtime.ollama.request_json", network) view = runtime.recovery.control("preview", {"task_id": source}) - assert view["source_mode"] == mode and view["draft"] is None - assert view["completed_steps"] is None and view["attempted_steps"] == 1 - assert view["remaining_actions"] == [] and view["rows"] == [] + assert view["source_mode"] == mode + assert view["draft"] is None + assert view["completed_steps"] is None + assert view["attempted_steps"] == 1 + assert view["remaining_actions"] == [] + assert view["rows"] == [] assert view["issues"] == ["new_plan_required"] template = view["review_template"] - assert template["goal"] == "" and template["mode"] == mode and template["actions"] == [] + assert template["goal"] == "" + assert template["mode"] == mode + assert template["actions"] == [] assert template["options"] == runtime.store.get(source)["spec"]["options"] assert target.calls == ["AC_increment"] - assert (runtime.store.get(source), runtime.receipts(source)) == before + after = runtime.store.get(source), runtime.receipts(source) + assert after == before network.assert_not_called() @@ -77,8 +83,10 @@ def decide(self, _observation, history, **_kwargs): if not history else {"stop": True}) assert run_task(runtime, child, backend=Model()).state == "succeeded" - assert histories[0] == [] and len(histories[1]) == 1 - assert len(target.calls) == 2 and runtime.get(source).state == "paused" + assert histories[0] == [] + assert len(histories[1]) == 1 + assert len(target.calls) == 2 + assert runtime.get(source).state == "paused" assert runtime.get(child).metadata["recovery"]["source_task"] == source assert runtime.recovery.control("create", request)["task_id"] == child with pytest.raises(AgentTaskError, match="reviewed successor"): @@ -103,7 +111,8 @@ def test_dynamic_approval_requires_a_new_goal_without_switching_execution_author request["spec"]["options"]["endpoint"] = "http://unapproved.invalid" with pytest.raises(AgentTaskError): runtime.recovery.control("create", request) - assert len(runtime.list()) == 1 and runtime.get(source).state == "paused" + assert len(runtime.list()) == 1 + assert runtime.get(source).state == "paused" def test_explicit_approval_can_retain_goal_wording_without_inheriting_source_progress(dynamic_source): @@ -113,7 +122,8 @@ def test_explicit_approval_can_retain_goal_wording_without_inheriting_source_pro request["note"] = "Reviewed the same overall goal with a new application context and no receipt inheritance" child = runtime.recovery.control("create", request)["task_id"] assert runtime.store.actions(child) == [] - assert runtime.get(child).state == "queued" and runtime.get(source).state == "paused" + assert runtime.get(child).state == "queued" + assert runtime.get(source).state == "paused" def test_new_dynamic_goal_is_not_accepted_while_source_effects_are_unknown(dynamic_source): @@ -139,7 +149,9 @@ def test_dormant_dynamic_action_arrays_do_not_claim_finite_progress(dynamic_sour with runtime.store.connection(write=True) as connection: connection.execute("UPDATE agent_tasks SET spec=? WHERE id=?", (json.dumps(data), source)) view = runtime.recovery.control("preview", {"task_id": source}) - assert view["completed_steps"] is None and view["rows"] == [] and view["remaining_actions"] == [] + assert view["completed_steps"] is None + assert view["rows"] == [] + assert view["remaining_actions"] == [] assert view["review_template"]["actions"] == [] @@ -154,7 +166,8 @@ def test_finish_without_new_results_cannot_inherit_a_matching_source_result(dyna result = run_task(runtime, child, backend=stop) else: result = runtime.finish(child) - assert result.state == "waiting_user" and result.verification[0]["status"] == "unknown" + assert result.state == "waiting_user" + assert result.verification[0]["status"] == "unknown" assert runtime.store.actions(child) == [] assert runtime.store.actions(source)[0]["result"] == {"value": 3} @@ -209,13 +222,16 @@ def test_explicit_local_replan_uses_the_pinned_http_model_with_fresh_goal_and_hi while runtime.get(child).state in {"queued", "running", "verifying"} and time.monotonic() < deadline: time.sleep(0.01) assert runtime.get(child).state == "succeeded" - assert [path for path, _ in server.calls] == ["/api/show", "/api/chat", "/api/chat"] + paths = [path for path, _ in server.calls] + assert paths == ["/api/show", "/api/chat", "/api/chat"] assert all(body["model"] == "fixture-tools" for _, body in server.calls) first = json.loads(server.calls[1][1]["messages"][1]["content"]) second = json.loads(server.calls[2][1]["messages"][1]["content"]) - assert first["goal"] == reviewed["goal"] and first["recent_results"] == [] + assert first["goal"] == reviewed["goal"] + assert first["recent_results"] == [] assert len(second["recent_results"]) == 1 - assert len(runtime.store.actions(child)) == 1 and len(runtime.store.actions(source.task_id)) == 1 + assert len(runtime.store.actions(child)) == 1 + assert len(runtime.store.actions(source.task_id)) == 1 assert runtime.get(source.task_id).state == "paused" finally: jobs.close() diff --git a/test/unit_test/headless/test_agent_evidence_cache_legacy.py b/test/unit_test/headless/test_agent_evidence_cache_legacy.py index 64570950e..5430e7ee3 100644 --- a/test/unit_test/headless/test_agent_evidence_cache_legacy.py +++ b/test/unit_test/headless/test_agent_evidence_cache_legacy.py @@ -54,7 +54,8 @@ def current_only(path): with monkeypatch.context() as scoped: scoped.setattr(Path, 'iterdir', current_only) result = legacy.runtime.get(legacy.task) - assert result.state == 'succeeded' and result.metadata['artifact_cache_state'] == 'historical' + assert result.state == 'succeeded' + assert result.metadata['artifact_cache_state'] == 'historical' assert result.metadata['journal_path'] == str(legacy.directory / 'journal.jsonl') assert result.to_dict()['metadata']['artifacts'] == [] assert not legacy.runtime.evidence.directory(legacy.task).exists() diff --git a/test/unit_test/headless/test_agent_evidence_cache_ownership.py b/test/unit_test/headless/test_agent_evidence_cache_ownership.py index 8be30e0cc..d43721c5a 100644 --- a/test/unit_test/headless/test_agent_evidence_cache_ownership.py +++ b/test/unit_test/headless/test_agent_evidence_cache_ownership.py @@ -78,7 +78,8 @@ def test_backup_publication_creates_own_segment_without_mutating_original_histor assert Path(new_link['journal_path']).parent == backup.evidence.directory(source.task) assert backup.evidence.directory(source.task) != source.runtime.evidence.directory(source.task) runs = backup.evidence.history.list_runs() - assert len(runs) == 2 and old_run in runs + assert len(runs) == 2 + assert old_run in runs old_journals = backup.evidence.history.journals_for_run(old_run.id) assert [item.journal_path for item in old_journals] == [old_link['journal_path']] backup.evidence.publish(source.task) diff --git a/test/unit_test/headless/test_agent_evidence_cache_paths.py b/test/unit_test/headless/test_agent_evidence_cache_paths.py index b77e717ac..ac238d039 100644 --- a/test/unit_test/headless/test_agent_evidence_cache_paths.py +++ b/test/unit_test/headless/test_agent_evidence_cache_paths.py @@ -20,7 +20,8 @@ def test_directory_redirection_preserves_foreign_files(source, kind): sentinel = foreign / 'report.json' sentinel.write_bytes(b'foreign bytes must not change') assert link.resolve().is_relative_to(source.root.resolve()) - assert holding.resolve().is_relative_to(source.root.resolve()) and not holding.exists() + assert holding.resolve().is_relative_to(source.root.resolve()) + assert not holding.exists() link.rename(holding) _junction(link, foreign) try: @@ -46,9 +47,14 @@ def test_hardlinked_artifact_refuses_mutation_and_cleanup(source, name, operatio os.link(foreign, path) if operation == 'prune': expire(source.runtime, source.task) + mutate = source.runtime.prune + else: + def mutate(): + return source.runtime.evidence.publish(source.task) with pytest.raises(AgentTaskError, match='ownership'): - source.runtime.prune() if operation == 'prune' else source.runtime.evidence.publish(source.task) - assert foreign.read_bytes() == b'keep this separate file' and path.exists() + mutate() + assert foreign.read_bytes() == b'keep this separate file' + assert path.exists() assert source.runtime.store.get(source.task)['state'] == 'succeeded' diff --git a/test/unit_test/headless/test_agent_evidence_handoff.py b/test/unit_test/headless/test_agent_evidence_handoff.py index 74561b66a..fffb7e3c2 100644 --- a/test/unit_test/headless/test_agent_evidence_handoff.py +++ b/test/unit_test/headless/test_agent_evidence_handoff.py @@ -110,7 +110,8 @@ def forbidden(*_args, **_kwargs): assert review['approval_required'] is True grant = runtime.recovery.control('create_handoff', {**request_for(handoff), 'review': review['handoff_review'], 'note': 'Share only this original DAG completion evidence'})['handoff'] - assert 'store_path' not in json.dumps(review) and 'arguments' not in json.dumps(grant) + assert 'store_path' not in json.dumps(review) + assert 'arguments' not in json.dumps(grant) with pytest.raises(AgentTaskError, match='another caller'): runtime.get(handoff.child) decision = {'node_id': 'a', 'action': 'successor', 'task_id': handoff.child, 'handoff_id': grant['id']} @@ -124,8 +125,10 @@ def forbidden(*_args, **_kwargs): 'definition': handoff.definition, 'approval_id': approved['approval_id'], 'max_parallel': 4}) completed = runtime.batches.control('validate_node', {'batch_id': handoff.batch_id, 'node_id': 'a', 'attempt': started['attempt']}) - assert completed['outcome'] == 'completed' and completed['task_id'] == handoff.child - assert runtime.get(handoff.source).state == 'paused' and handoff.target.calls == calls + assert completed['outcome'] == 'completed' + assert completed['task_id'] == handoff.child + assert runtime.get(handoff.source).state == 'paused' + assert handoff.target.calls == calls for operation in (runtime.get, runtime.receipts, runtime.observe, runtime.resume): with pytest.raises(AgentTaskError, match='another caller'): operation(handoff.child) diff --git a/test/unit_test/headless/test_agent_evidence_handoff_files.py b/test/unit_test/headless/test_agent_evidence_handoff_files.py index a8b9ac6a9..973319da8 100644 --- a/test/unit_test/headless/test_agent_evidence_handoff_files.py +++ b/test/unit_test/headless/test_agent_evidence_handoff_files.py @@ -92,6 +92,7 @@ def forbidden(*_args, **_kwargs): assert files.runtime.store.get(files.child)['owner']['user_id'] == 'reviewer' with pytest.raises(AgentTaskError, match='another caller'): files.runtime.get(files.child) - assert files.manifest.read_bytes() == before and files.runtime.store.actions(files.child) == [] + assert files.manifest.read_bytes() == before + assert files.runtime.store.actions(files.child) == [] finally: server.stop() diff --git a/test/unit_test/headless/test_agent_evidence_handoff_gui.py b/test/unit_test/headless/test_agent_evidence_handoff_gui.py index 0fcda9897..2b4e2eebc 100644 --- a/test/unit_test/headless/test_agent_evidence_handoff_gui.py +++ b/test/unit_test/headless/test_agent_evidence_handoff_gui.py @@ -22,9 +22,11 @@ def test_actions_only_prepare_explicit_handoff_requests(monkeypatch, operation): assert request == {'handoff_id': ''} else: assert request['task_id'] == '' - assert request['batch_id'] == '' and request['node_id'] == '' + assert request['batch_id'] == '' + assert request['node_id'] == '' if operation == 'create_handoff': - assert request['review'] == '' and request['note'] == '' + assert request['review'] == '' + assert request['note'] == '' assert tab._pages.currentIndex() == tab._page_keys.index('agent_page_handoffs') finally: tab.close() diff --git a/test/unit_test/headless/test_agent_evidence_handoff_lifecycle.py b/test/unit_test/headless/test_agent_evidence_handoff_lifecycle.py index f4e79a213..bbb92a0d7 100644 --- a/test/unit_test/headless/test_agent_evidence_handoff_lifecycle.py +++ b/test/unit_test/headless/test_agent_evidence_handoff_lifecycle.py @@ -97,7 +97,9 @@ def test_completion_projection_and_public_consent_omit_private_task_fields(hando grant = grant_for(handoff) result = consume(handoff, grant['id']) combined = json.dumps([grant, result]) - assert secret not in combined and 'store_path' not in combined and 'arguments' not in combined + assert secret not in combined + assert 'store_path' not in combined + assert 'arguments' not in combined assert all(set(report) == {'kind', 'status'} for report in result[0]['verification']) @@ -120,8 +122,10 @@ def test_consent_survives_restart_but_does_not_reassign_or_replay_tasks(handoff) def test_malformed_historical_admitting_actor_is_a_typed_refusal(handoff): with handoff.runtime.store.connection(write=True) as connection: connection.execute('UPDATE agent_dag_local_tasks SET actor=? WHERE task_id=?', ('{', handoff.source)) - with authorization_scope(handoff.reviewer), pytest.raises(AgentTaskError): - handoff.runtime.recovery.control('review_handoff', request_for(handoff)) + request = request_for(handoff) + with authorization_scope(handoff.reviewer): + with pytest.raises(AgentTaskError): + handoff.runtime.recovery.control('review_handoff', request) @pytest.mark.parametrize(('field', 'value'), [('issuer', []), ('leaf_owner', None), diff --git a/test/unit_test/headless/test_agent_evidence_handoff_security.py b/test/unit_test/headless/test_agent_evidence_handoff_security.py index f6d6980f0..1a18510cc 100644 --- a/test/unit_test/headless/test_agent_evidence_handoff_security.py +++ b/test/unit_test/headless/test_agent_evidence_handoff_security.py @@ -35,9 +35,10 @@ def test_grant_never_applies_to_another_binding(handoff, field): def test_recipient_cannot_issue_its_own_grant_using_an_admins_review_token(handoff): with authorization_scope(handoff.reviewer): review = handoff.runtime.recovery.control('review_handoff', request_for(handoff)) + request = {**request_for(handoff), 'review': review['handoff_review'], + 'note': 'Cannot approve someone else\'s evidence'} with pytest.raises(AgentTaskError, match='another caller'): - handoff.runtime.recovery.control('create_handoff', {**request_for(handoff), - 'review': review['handoff_review'], 'note': 'Cannot approve someone else\'s evidence'}) + handoff.runtime.recovery.control('create_handoff', request) with handoff.runtime.store.connection() as connection: assert connection.execute('SELECT COUNT(*) FROM agent_evidence_handoffs').fetchone()[0] == 0 @@ -114,15 +115,17 @@ def test_a_consent_record_never_extends_the_terminal_evidence_deadline(handoff, assert handoff.runtime.recovery.control('get_handoff', {'handoff_id': grant['id']})['handoff']['state'] == 'expired' with pytest.raises(AgentTaskError, match='expired'): consume(handoff, grant['id']) + request = request_for(handoff) with authorization_scope(handoff.reviewer), pytest.raises(AgentTaskError, match='expired'): - handoff.runtime.recovery.control('review_handoff', request_for(handoff)) + handoff.runtime.recovery.control('review_handoff', request) def test_grants_require_private_original_admission_not_the_existing_public_tags(handoff): with handoff.runtime.store.connection(write=True) as connection: connection.execute('DELETE FROM agent_dag_local_tasks WHERE task_id=?', (handoff.source,)) + request = request_for(handoff) with authorization_scope(handoff.reviewer), pytest.raises(AgentTaskError, match='real bound local DAG admission'): - handoff.runtime.recovery.control('review_handoff', request_for(handoff)) + handoff.runtime.recovery.control('review_handoff', request) @pytest.mark.parametrize('field', ['budget', 'inputs', 'roots', 'success_criteria']) @@ -138,17 +141,19 @@ def test_changed_scope_cannot_issue_a_handoff_even_when_the_child_succeeded(hand row['spec']['success_criteria'][-1]['value'] = 'a different goal' with handoff.runtime.store.connection(write=True) as connection: connection.execute('UPDATE agent_tasks SET spec=? WHERE id=?', (json.dumps(row['spec']), handoff.child)) + request = request_for(handoff) with authorization_scope(handoff.reviewer), pytest.raises(AgentTaskError): - handoff.runtime.recovery.control('review_handoff', request_for(handoff)) + handoff.runtime.recovery.control('review_handoff', request) @pytest.mark.parametrize('participant', ['source', 'child']) def test_unwinding_input_never_supplies_handoff_evidence(handoff, participant): identifier = getattr(handoff, participant) + request = request_for(handoff) handoff.runtime._executing.add(identifier) try: with authorization_scope(handoff.reviewer), pytest.raises(AgentTaskError, match='input has not retired'): - handoff.runtime.recovery.control('review_handoff', request_for(handoff)) + handoff.runtime.recovery.control('review_handoff', request) finally: handoff.runtime._executing.discard(identifier) @@ -163,11 +168,12 @@ def test_grant_chain_bound_is_sixteen_real_links(handoff): current = connection.execute('SELECT source_task FROM agent_task_recoveries WHERE task_id=?', (current,)).fetchone()[0] links += 1 + request = request_for(handoff) if links == 16: - assert handoff.runtime.recovery.control('review_handoff', request_for(handoff))['approval_required'] + assert handoff.runtime.recovery.control('review_handoff', request)['approval_required'] else: with pytest.raises(AgentTaskError, match='bounded real recovery chain'): - handoff.runtime.recovery.control('review_handoff', request_for(handoff)) + handoff.runtime.recovery.control('review_handoff', request) def test_exact_concurrent_consent_retries_create_one_active_grant(handoff): diff --git a/test/unit_test/headless/test_agent_evidence_handoff_surfaces.py b/test/unit_test/headless/test_agent_evidence_handoff_surfaces.py index 5d62f8a6d..50862c19e 100644 --- a/test/unit_test/headless/test_agent_evidence_handoff_surfaces.py +++ b/test/unit_test/headless/test_agent_evidence_handoff_surfaces.py @@ -49,7 +49,8 @@ def test_shared_surfaces_review_issue_inspect_and_revoke_only_the_exact_handoff( review = invoke(surface, connection, tmp_path, 'review_handoff', request_for(handoff)) grant = invoke(surface, connection, tmp_path, 'create_handoff', {**request_for(handoff), 'review': review['handoff_review'], 'note': 'Explicit immutable evidence-only consent'})['handoff'] - assert grant['evidence_only'] is True and grant['input_authorized'] is False + assert grant['evidence_only'] is True + assert grant['input_authorized'] is False assert invoke(surface, connection, tmp_path, 'get_handoff', {'handoff_id': grant['id']})['handoff'] == grant listed = invoke(surface, connection, tmp_path, 'list_handoffs', {'batch_id': handoff.batch_id, 'node_id': 'a'}) assert [item['id'] for item in listed['handoffs']] == [grant['id']] diff --git a/test/unit_test/headless/test_agent_helper_owner_linux.py b/test/unit_test/headless/test_agent_helper_owner_linux.py index 3da6699df..c9f5d256a 100644 --- a/test/unit_test/headless/test_agent_helper_owner_linux.py +++ b/test/unit_test/headless/test_agent_helper_owner_linux.py @@ -179,7 +179,8 @@ def _until(predicate, seconds=3.0): def test_wire_layout_matches_the_native_frames(): assert (wire.REQUEST_SIZE, wire.REPLY_SIZE) == (9384, 8312) - assert wire.REQUEST_OFFSETS["data"] == 1188 and wire.REPLY_OFFSETS["data"] == 120 + assert wire.REQUEST_OFFSETS["data"] == 1188 + assert wire.REPLY_OFFSETS["data"] == 120 frame = wire.ServiceRequest(wire.OP_WRITE, (1, 2), ticket=((3, 4), (5, 6)), data=b"abc").pack() assert len(frame) == wire.REQUEST_SIZE assert frame[wire.REQUEST_OFFSETS["ticket"]:wire.REQUEST_OFFSETS["ticket"] + 8] == struct.pack("= 1 + assert not channel.retired + assert service.calls.count(wire.OP_CLOSE) >= 1 with pytest.raises(AgentTaskError, match="unknown start"): channel.close() assert not channel.retired @@ -425,7 +440,8 @@ def test_connection_turned_away_before_a_lease_is_busy_not_broken(): channel = OwnerServiceChannel(service, 1) with pytest.raises(AgentControlBusy, match="turned the connection away"): channel.request("echo", 1, timeout=5) - assert channel.retired and not service.calls + assert channel.retired + assert not service.calls def test_renewer_survives_a_failed_connection_without_a_thread_error(monkeypatch): @@ -437,7 +453,8 @@ def test_renewer_survives_a_failed_connection_without_a_thread_error(monkeypatch channel = OwnerServiceChannel(service, 1) assert channel.request("echo", 1, timeout=5) == 1 assert _until(lambda: not channel._renewer.is_alive()) - assert not raised and channel._lease_lost + assert not raised + assert channel._lease_lost def test_close_learns_a_lost_start_ticket_by_replay_and_retires(): @@ -449,4 +466,5 @@ def test_close_learns_a_lost_start_ticket_by_replay_and_retires(): assert not channel.retired service.drop_always = set() # The service answers again: the replay returns the kept ticket. channel.close() - assert channel.retired and wire.OP_OBSERVE in service.calls + assert channel.retired + assert wire.OP_OBSERVE in service.calls diff --git a/test/unit_test/headless/test_agent_maintenance_lease.py b/test/unit_test/headless/test_agent_maintenance_lease.py index fd5ec5995..5887716c0 100644 --- a/test/unit_test/headless/test_agent_maintenance_lease.py +++ b/test/unit_test/headless/test_agent_maintenance_lease.py @@ -59,7 +59,8 @@ def maintain(): users.set_role("owner", "viewer") proceed.set() watcher.join(5) - assert not watcher.is_alive() and not failures + assert not watcher.is_alive() + assert not failures assert runtime.store.get(task) == before, "A stale watcher cannot rewrite retired recovery evidence" assert target.calls == [] finally: @@ -76,7 +77,8 @@ def test_current_input_owner_still_retires_on_expiry_or_revocation(owned, monkey users.set_role("owner", "viewer") runtime.maintain() assert runtime.store.get(task)["state"] == ("timed_out" if cause == "expired" else "cancelled") - assert task not in runtime._leases and target.calls == [] + assert task not in runtime._leases + assert target.calls == [] @pytest.mark.parametrize("reacquire", [False, True]) @@ -105,9 +107,11 @@ def changed(identifier, state, **options): monkeypatch.setattr(runtime.clock, "remaining", expired_once) monkeypatch.setattr(runtime.store, "request_stop", changed) runtime.maintain() - assert snapshots and runtime.store.get(task) == snapshots[0] + assert snapshots + assert runtime.store.get(task) == snapshots[0] if reacquire: - assert task in runtime._leases and not runtime._token(task).is_cancelled + assert task in runtime._leases + assert not runtime._token(task).is_cancelled else: assert task not in runtime._leases assert target.calls == [] @@ -158,8 +162,10 @@ def pause(): finally: proceed.set() worker.join(5) - assert not worker.is_alive() and not failures - assert results[0].state == "paused" and task not in runtime._leases + assert not worker.is_alive() + assert not failures + assert results[0].state == "paused" + assert task not in runtime._leases def test_revocation_reason_is_final_when_cancellation_is_published(owned, monkeypatch): @@ -176,5 +182,6 @@ def capture(identifier, expected, state, **options): monkeypatch.setattr(runtime.store, "transition", capture) users.set_role("owner", "viewer") runtime.maintain() - assert published and published[0]["reason"] == "owner authorization or origin scope requires review" + assert published + assert published[0]["reason"] == "owner authorization or origin scope requires review" assert runtime.store.get(task) == published[0], "Terminal publication must leave a stable recovery snapshot" diff --git a/test/unit_test/headless/test_agent_model_grounding.py b/test/unit_test/headless/test_agent_model_grounding.py index e19eaf84e..0650ac2c0 100644 --- a/test/unit_test/headless/test_agent_model_grounding.py +++ b/test/unit_test/headless/test_agent_model_grounding.py @@ -64,7 +64,8 @@ def transport(call): assert content["observation"]["image_geometry"]["original_size"] == [400, 200] assert "data" not in content["observation"]["image_geometry"] offered = payload["tools"][0]["function"]["parameters"]["properties"] - assert "observed_target" in offered and "observed_target" not in original[0]["function"]["parameters"]["properties"] + assert "observed_target" in offered + assert "observed_target" not in original[0]["function"]["parameters"]["properties"] def test_model_reference_is_resolved_once_by_shared_runtime(tmp_path): @@ -96,7 +97,9 @@ def test_continuous_geometry_change_exhausts_retries_without_preparing_any_effec try: task = runtime.submit(specification()) assert run_task(runtime, task.task_id, backend=backend).state == "waiting_user" - assert backend.decisions == 3 and target.calls == [] and runtime.store.actions(task.task_id) == [] + assert backend.decisions == 3 + assert target.calls == [] + assert runtime.store.actions(task.task_id) == [] finally: runtime.close() @@ -107,7 +110,8 @@ def test_adapter_without_safe_reobservation_does_not_replan_past_held_input(tmp_ try: task = runtime.submit(specification()) assert run_task(runtime, task.task_id, backend=backend).state == "waiting_user" - assert backend.decisions == 1 and target.calls == [] + assert backend.decisions == 1 + assert target.calls == [] finally: runtime.close() diff --git a/test/unit_test/headless/test_agent_native_channel.py b/test/unit_test/headless/test_agent_native_channel.py index 7d0d01807..25aa75f53 100644 --- a/test/unit_test/headless/test_agent_native_channel.py +++ b/test/unit_test/headless/test_agent_native_channel.py @@ -103,7 +103,8 @@ def wait_for_reply(): waiter.join(timeout=5) assert not waiter.is_alive() assert closed_after < 5 - assert "result" not in outcome and "closed" in str(outcome["error"]) + assert "result" not in outcome + assert "closed" in str(outcome["error"]) assert worker.retired @@ -131,7 +132,8 @@ def close_once_reply_is_dequeued(): with pytest.raises(AgentTaskError, match="closed"): worker.request("echo", {"value": 1}, timeout=5, check=close_once_reply_is_dequeued) - assert closers and worker.retired + assert closers + assert worker.retired def test_closed_channel_refuses_new_requests(): diff --git a/test/unit_test/headless/test_agent_native_job.py b/test/unit_test/headless/test_agent_native_job.py index a0af028a4..1086c04e6 100644 --- a/test/unit_test/headless/test_agent_native_job.py +++ b/test/unit_test/headless/test_agent_native_job.py @@ -60,13 +60,16 @@ def CloseHandle(self, handle): return self.close_ok def OpenProcess(self, access, inherit, pid): - assert access == 0x101000 and not inherit and pid in self.processes + assert access == 0x101000 + assert not inherit + assert pid in self.processes handle = 0x200000000 + pid self.handles[handle] = pid return handle def IsProcessInJob(self, process, job, pointer): - assert process in self.handles and job == 0x100000001 + assert process in self.handles + assert job == 0x100000001 ctypes.cast(pointer, ctypes.POINTER(ctypes.c_int)).contents.value = self.belongs return True @@ -117,8 +120,10 @@ def sleep(delay): monkeypatch.setattr(module, 'time', SimpleNamespace(monotonic=lambda: now[0], sleep=sleep), raising=False) with pytest.raises(AgentTaskError): job.close(timeout=0.03) - assert now[0] <= 0.03 and sleeps - assert job._handle == 0x100000001 and 'close' not in kernel.events + assert now[0] <= 0.03 + assert sleeps + assert job._handle == 0x100000001 + assert 'close' not in kernel.events kernel.counts = [0] job.close() assert job._handle is None @@ -161,13 +166,15 @@ def test_process_objects_must_signal_even_after_the_job_reports_no_active_proces kernel.signals[202] = 258 with pytest.raises(AgentTaskError): job.close(timeout=0.03) - assert job._handle == 0x100000001 and 'close' not in kernel.events + assert job._handle == 0x100000001 + assert 'close' not in kernel.events assert any(event[:2] == ('process-wait', 202) for event in kernel.events if isinstance(event, tuple)) assert kernel.handles kernel.processes = [] kernel.signals[202] = 0 job.close() - assert job._handle is None and not kernel.handles + assert job._handle is None + assert not kernel.handles def test_process_object_queries_may_never_select_a_process_outside_the_actual_owned_job(monkeypatch): @@ -175,7 +182,8 @@ def test_process_object_queries_may_never_select_a_process_outside_the_actual_ow kernel.processes, kernel.belongs = [201], False with pytest.raises(AgentTaskError): job.close() - assert 'terminate' in kernel.events and 'close' not in kernel.events + assert 'terminate' in kernel.events + assert 'close' not in kernel.events assert not kernel.handles @@ -193,8 +201,10 @@ def delayed(*args): monkeypatch.setattr(module, 'time', SimpleNamespace(monotonic=lambda: now[0])) with pytest.raises(AgentTaskError): job.close(timeout=0.03) - assert now[0] <= 0.03 and 'terminate' in kernel.events - assert job._handle is not None and 'close' not in kernel.events + assert now[0] <= 0.03 + assert 'terminate' in kernel.events + assert job._handle is not None + assert 'close' not in kernel.events def test_already_exhausted_deadline_still_terminates_only_the_owned_job(monkeypatch): @@ -202,5 +212,6 @@ def test_already_exhausted_deadline_still_terminates_only_the_owned_job(monkeypa kernel.counts = [1] with pytest.raises(AgentTaskError): job.close(timeout=0) - assert 'terminate' in kernel.events and 'close' not in kernel.events + assert 'terminate' in kernel.events + assert 'close' not in kernel.events assert job._handle == 0x100000001 diff --git a/test/unit_test/headless/test_agent_observation_gui.py b/test/unit_test/headless/test_agent_observation_gui.py index fc60e6fc8..ec1d715f0 100644 --- a/test/unit_test/headless/test_agent_observation_gui.py +++ b/test/unit_test/headless/test_agent_observation_gui.py @@ -66,7 +66,8 @@ def test_android_semantic_selection_uses_the_shared_action_without_native_input( tab._present_result(frame) tab._observation._elements.cellClicked.emit(0, 0) action = json.loads(tab._action.toPlainText()) - assert action['tool'] == 'AC_android_tap' and action['arguments'] == {} + assert action['tool'] == 'AC_android_tap' + assert action['arguments'] == {} assert calls == [] resolved, _ground = resolve_action(action['tool'], action['arguments'], action['reference'], frame['data']) assert resolved == {'x': 25, 'y': 40} diff --git a/test/unit_test/headless/test_agent_observed_targets.py b/test/unit_test/headless/test_agent_observed_targets.py index 57f1f2ea7..cb2aae742 100644 --- a/test/unit_test/headless/test_agent_observed_targets.py +++ b/test/unit_test/headless/test_agent_observed_targets.py @@ -80,7 +80,8 @@ def test_invalid_or_outside_observation_references_never_dispatch(managed, refer with pytest.raises(AgentTaskError): runtime.act(task_id, {"tool": "AC_click_mouse", "reference": reference, "observation_id": frame.observation_id}) - assert target.calls == [] and runtime.store.actions(task_id) == [] + assert target.calls == [] + assert runtime.store.actions(task_id) == [] def test_reference_and_explicit_coordinates_cannot_conflict(managed): @@ -124,7 +125,8 @@ def test_image_metadata_cannot_supply_a_different_scale_from_the_actual_pixels(m with pytest.raises(AgentTaskError, match="actual pixels"): runtime.act(task_id, {"tool": "AC_click_mouse", "arguments": {"mouse_keycode": "left"}, "reference": {"image_point": [10, 5]}, "observation_id": frame.observation_id}) - assert target.calls == [] and runtime.store.actions(task_id) == [] + assert target.calls == [] + assert runtime.store.actions(task_id) == [] def test_legacy_explicit_coordinates_are_preserved(managed): diff --git a/test/unit_test/headless/test_agent_operation_context.py b/test/unit_test/headless/test_agent_operation_context.py index 1167de1a0..30b6de18c 100644 --- a/test/unit_test/headless/test_agent_operation_context.py +++ b/test/unit_test/headless/test_agent_operation_context.py @@ -74,7 +74,8 @@ def request(*_args, **_options): assert context.current_takeover() is parent.takeover captured() assert calls == ["bound"] - assert context.current_operation() is None and context.current_takeover() is None + assert context.current_operation() is None + assert context.current_takeover() is None def test_nested_operation_cannot_borrow_outer_readiness_callback(): @@ -107,10 +108,12 @@ def ready(self, _spec): assert context.current_operation() is parent raise AgentTaskError("owned failure") + target = Target() with context.operation_scope(parent): with pytest.raises(AgentTaskError, match="owned failure"): - context.ready_with_takeover(Target(), spec, lambda: None) - assert context.current_operation() is parent and context.current_takeover() is parent.takeover + context.ready_with_takeover(target, spec, lambda: None) + assert context.current_operation() is parent + assert context.current_takeover() is parent.takeover assert context.current_takeover() is None diff --git a/test/unit_test/headless/test_agent_origin_policy.py b/test/unit_test/headless/test_agent_origin_policy.py index 20c764a7e..3dab3ec49 100644 --- a/test/unit_test/headless/test_agent_origin_policy.py +++ b/test/unit_test/headless/test_agent_origin_policy.py @@ -30,7 +30,8 @@ def test_removed_origin_root_pauses_before_the_next_action(tmp_path): with pytest.raises(AgentTaskError, match="origin roots changed"): runtime.act(task.task_id, {"tool": "AC_increment", "observation_id": frame.observation_id}) assert target.calls == [] - assert runtime.get(task.task_id).state == "paused" and not runtime._leases + assert runtime.get(task.task_id).state == "paused" + assert not runtime._leases assert "scope requires review" in runtime.get(task.task_id).reason finally: runtime.close() @@ -45,7 +46,8 @@ def test_origin_revocation_retires_active_input_between_requests(tmp_path): runtime.observe(task.task_id) policy.revoke_managed_origin() runtime.maintain() - assert runtime.get(task.task_id).state == "cancelled" and not runtime._leases + assert runtime.get(task.task_id).state == "cancelled" + assert not runtime._leases finally: runtime.close() @@ -77,9 +79,10 @@ def test_live_origin_survives_runtime_restart_and_task_scope_never_expands(tmp_p @pytest.mark.parametrize("value", ["not-an-array", [{}], [True], ["../outside"], ["a" * 32] * 17]) def test_malformed_origin_references_fail_at_submission(tmp_path, value): runtime = AgentRuntime(tmp_path / "tasks.sqlite", targets={"fixture": FakeTarget()}) + malformed = specification(options={"origin_policies": value}) try: with pytest.raises(AgentTaskError): - runtime.submit(specification(options={"origin_policies": value})) + runtime.submit(malformed) finally: runtime.close() @@ -161,8 +164,9 @@ def test_nested_origin_scopes_intersect_instead_of_overriding_outer_authority(tm runtime = AgentRuntime(tmp_path / "tasks.sqlite", targets={"fixture": FakeTarget()}) try: with origin_scope(restrictive.managed_origin()), origin_scope(broad.managed_origin()): + escaping = specification(roots=[str(outside)]) with pytest.raises(AgentTaskError, match="origin roots changed"): - runtime.submit(specification(roots=[str(outside)])) + runtime.submit(escaping) task = runtime.submit(specification(roots=[str(allowed)])) assert len(runtime.store.get(task.task_id)["spec"]["options"]["origin_policies"]) == 2 finally: @@ -181,7 +185,8 @@ def test_legacy_mcp_tool_cannot_submit_a_task_beyond_its_path_ceiling(tmp_path): context = ToolCallContext(1, path_policy=PathPolicy([allowed])) with pytest.raises(AgentTaskError, match="origin roots changed"): tool.invoke({}, ctx=context) - assert runtime.list() == [] and origin_policy.current_origin() is None + assert runtime.list() == [] + assert origin_policy.current_origin() is None finally: runtime.close() diff --git a/test/unit_test/headless/test_agent_owned_channel_factory.py b/test/unit_test/headless/test_agent_owned_channel_factory.py index d7cb4a426..22030c19c 100644 --- a/test/unit_test/headless/test_agent_owned_channel_factory.py +++ b/test/unit_test/headless/test_agent_owned_channel_factory.py @@ -69,7 +69,9 @@ def test_linux_binding_launches_only_through_the_owner_service(monkeypatch, tmp_ monkeypatch.setattr(helper_owner_linux, "SeqpacketTransport", FakeTransport) channel = factory.owned_channel(ARGV) assert isinstance(channel, helper_owner_linux.OwnerServiceChannel) - assert channel._transport.endpoint == VALID["endpoint"] and channel._policy == 3 and channel._arguments == () + assert channel._transport.endpoint == VALID["endpoint"] + assert channel._policy == 3 + assert channel._arguments == () assert not local.calls @@ -110,7 +112,8 @@ def test_untrusted_binding_refuses(monkeypatch, tmp_path): def test_valid_binding_parses_exact_rows(monkeypatch, tmp_path): path = bind(monkeypatch, tmp_path, VALID) binding = factory.load_binding(str(path)) - assert binding.endpoint == VALID["endpoint"] and binding.policies == {tuple(ARGV): 3} + assert binding.endpoint == VALID["endpoint"] + assert binding.policies == {tuple(ARGV): 3} def test_every_consumer_obtains_its_channel_through_the_factory(): @@ -118,7 +121,8 @@ def test_every_consumer_obtains_its_channel_through_the_factory(): for name in ("helper_environment.py", "native_target.py", "remote_target.py", "presence_pool.py", "presence_worker.py"): source = (package / name).read_text(encoding="utf-8") - assert "owned_channel" in source and "OwnedChannel(" not in source, name + assert "owned_channel" in source, name + assert "OwnedChannel(" not in source, name def test_nested_grant_reaches_the_service_channel_and_refuses_elsewhere(monkeypatch, tmp_path, local): @@ -152,12 +156,15 @@ def test_containment_status_on_linux_never_claims_native_evidence(monkeypatch, t assert factory.helper_containment()["status"] == "not_configured" bind(monkeypatch, tmp_path, {"endpoint": "relative"}) refused = factory.helper_containment() - assert refused["status"] == "binding_refused" and refused["mode"] == "owner_service" and refused["reason"] + assert refused["status"] == "binding_refused" + assert refused["mode"] == "owner_service" + assert refused["reason"] bind(monkeypatch, tmp_path, VALID) for reachable, status in ((False, "unreachable"), (True, "reachable_unverified")): monkeypatch.setattr(factory, "_endpoint_reachable", lambda _endpoint, value=reachable: value) result = factory.helper_containment() - assert result["status"] == status and result["policies"] == 1 + assert result["status"] == status + assert result["policies"] == 1 assert result["native_evidence"] == "not_recorded" @@ -165,7 +172,8 @@ def test_runtime_status_carries_the_containment_section(monkeypatch, tmp_path): from je_auto_control.utils.agent_runtime import api monkeypatch.setattr(api, "default_connection_path", lambda: tmp_path / "absent.json") status = api.agent_runtime_status() - assert status["configured"] is False and status["helper_containment"]["native_evidence"] == "not_recorded" + assert status["configured"] is False + assert status["helper_containment"]["native_evidence"] == "not_recorded" def test_binding_under_a_writable_directory_refuses(monkeypatch, tmp_path): diff --git a/test/unit_test/headless/test_agent_owner_service_nested.py b/test/unit_test/headless/test_agent_owner_service_nested.py index 1ebd88816..a762e3d78 100644 --- a/test/unit_test/headless/test_agent_owner_service_nested.py +++ b/test/unit_test/headless/test_agent_owner_service_nested.py @@ -28,10 +28,12 @@ def test_frame_layout_matches_the_native_record(): def test_valid_frame_is_read_once_and_its_descriptor_closed(): fd = piped(frame()) grant = nested.read_nested_bootstrap(fd) - assert grant.parent == ((142, 1), (0, 3)) and grant.capability == CAPABILITY + assert grant.parent == ((142, 1), (0, 3)) + assert grant.capability == CAPABILITY with pytest.raises(OSError): os.fstat(fd) - assert "capability=<32 bytes>" in repr(grant) and str(CAPABILITY[0]) not in repr(grant).split("capability")[1] + assert "capability=<32 bytes>" in repr(grant) + assert str(CAPABILITY[0]) not in repr(grant).split("capability")[1] def test_absent_descriptor_means_not_nested(): diff --git a/test/unit_test/headless/test_agent_presence_binding.py b/test/unit_test/headless/test_agent_presence_binding.py index 3f6fb159c..65c9fe7a2 100644 --- a/test/unit_test/headless/test_agent_presence_binding.py +++ b/test/unit_test/headless/test_agent_presence_binding.py @@ -61,8 +61,9 @@ def payload(binding=BINDING): @pytest.mark.parametrize('binding', [None, {}, {'generation': 'a' * 32}, {'generation': True, 'lease_id': 'b' * 32}, {**BINDING, 'pid': 42}]) def test_standard_desktop_requires_exact_host_binding_without_starting_old_fallback(worker, binding): + unbound = payload(binding) with pytest.raises(AgentTaskError): - worker._dispatch('configure', payload(binding)) + worker._dispatch('configure', unbound) assert not Gate.instances @@ -70,7 +71,8 @@ def test_repeated_configure_reuses_one_reader_and_never_acquires_a_new_capabilit assert worker._dispatch('configure', payload())['ready'] assert worker._dispatch('configure', payload())['ready'] assert len(Gate.instances) == 1 - assert Gate.instances[0].binding == BINDING and Gate.instances[0].readies == 2 + assert Gate.instances[0].binding == BINDING + assert Gate.instances[0].readies == 2 @pytest.mark.parametrize('change', ['binding', 'spec', 'owner']) @@ -85,22 +87,26 @@ def test_configured_reader_cannot_change_capability_or_host_scope_without_retire altered['owner'] = {'user_id': 'other', 'role': 'admin', 'store_path': 'not-an-authority'} with pytest.raises(AgentTaskError): worker._dispatch('configure', altered) - assert len(Gate.instances) == 1 and Gate.instances[0].binding == BINDING + assert len(Gate.instances) == 1 + assert Gate.instances[0].binding == BINDING def test_release_only_cleanup_retires_reader_and_notice_thread_without_rearming(worker): worker._dispatch('configure', payload()) assert worker._release_owned({'keys': [], 'buttons': []}) == {'cleaned': True} - assert Gate.instances[0].retired and not worker._notice_thread.is_alive() + assert Gate.instances[0].retired + assert not worker._notice_thread.is_alive() + rearm = payload() with pytest.raises(AgentTaskError): - worker._dispatch('configure', payload()) + worker._dispatch('configure', rearm) assert len(Gate.instances) == 1 def test_owner_eof_closes_actual_worker_reader_and_notice_lifetime(worker): worker._dispatch('configure', payload()) worker.run() - assert Gate.instances[0].retired and not worker._notice_thread.is_alive() + assert Gate.instances[0].retired + assert not worker._notice_thread.is_alive() def test_notice_start_failure_is_typed_and_closes_unstarted_reader_without_rearming(worker, monkeypatch): @@ -111,11 +117,12 @@ def start(thread): raise RuntimeError('notice admission failed') original(thread) + first, rearm = payload(), payload() with monkeypatch.context() as patch: patch.setattr(threading.Thread, 'start', start) with pytest.raises(AgentTaskError, match='notice'): - worker._dispatch('configure', payload()) + worker._dispatch('configure', first) assert Gate.instances[0].retired with pytest.raises(AgentTaskError): - worker._dispatch('configure', payload()) + worker._dispatch('configure', rearm) assert len(Gate.instances) == 1 diff --git a/test/unit_test/headless/test_agent_presence_broker.py b/test/unit_test/headless/test_agent_presence_broker.py index 3b8738b6a..443c0fb64 100644 --- a/test/unit_test/headless/test_agent_presence_broker.py +++ b/test/unit_test/headless/test_agent_presence_broker.py @@ -92,7 +92,8 @@ def test_constructor_close_and_unbound_requests_never_discover_or_launch(monkeyp with pytest.raises(AgentTaskError): broker.request('inspect', {}) broker.close() - assert not pipe.calls and not children + assert not pipe.calls + assert not children def test_missing_source_launches_once_and_busy_is_normal_without_resetting_generation(monkeypatch): @@ -100,9 +101,11 @@ def test_missing_source_launches_once_and_busy_is_normal_without_resetting_gener broker = create() try: ready = broker.bootstrap() - assert ready['ready'] and len(children) == 1 + assert ready['ready'] + assert len(children) == 1 assert broker.request('acquire', {})['status'] == 'busy' - assert broker.bootstrap() == ready and not children[0].closed + assert broker.bootstrap() == ready + assert not children[0].closed source.advance(60) assert broker.request('acquire', {})['result']['generation'] == ready['generation'] assert len(children) == 1 @@ -180,7 +183,8 @@ def request(operation, body): broker = create() try: assert broker.bootstrap() == {'ready': False, 'status': 'busy'} - assert len(children) == 1 and not children[0].closed + assert len(children) == 1 + assert not children[0].closed assert broker.bootstrap()['generation'] == source.status()['generation'] assert len(children) == 1 finally: @@ -194,7 +198,8 @@ def test_wire_bootstrap_cannot_select_namespace_role_process_or_source(monkeypat with pytest.raises(AgentTaskError): broker.dispatch('bootstrap', payload) broker.close() - assert not pipe.calls and not children + assert not pipe.calls + assert not children @pytest.mark.parametrize('mutation', [{'request_id': True}, {'status': 'ok', 'result': {'generation': 'x'}}, @@ -211,4 +216,5 @@ def mutate(value): with pytest.raises(AgentTaskError): broker.request('inspect', {}) broker.close() - assert children[0].closed and len(children) == 1 + assert children[0].closed + assert len(children) == 1 diff --git a/test/unit_test/headless/test_agent_presence_channel_guards.py b/test/unit_test/headless/test_agent_presence_channel_guards.py index 02680ee5b..f13942045 100644 --- a/test/unit_test/headless/test_agent_presence_channel_guards.py +++ b/test/unit_test/headless/test_agent_presence_channel_guards.py @@ -10,7 +10,8 @@ @pytest.mark.parametrize('platform', ['linux', 'darwin', 'freebsd']) def test_transport_types_do_not_enable_non_windows_presence_or_input(monkeypatch, platform): - assert LinuxChannel.connect and LinuxListener.create + assert LinuxChannel.connect + assert LinuxListener.create monkeypatch.setattr(sys, 'platform', platform) with pytest.raises(AgentControlBusy, match='Windows console session'): presence_host.require_windows() diff --git a/test/unit_test/headless/test_agent_presence_channel_linux.py b/test/unit_test/headless/test_agent_presence_channel_linux.py index 483989749..0b84e546d 100644 --- a/test/unit_test/headless/test_agent_presence_channel_linux.py +++ b/test/unit_test/headless/test_agent_presence_channel_linux.py @@ -331,7 +331,7 @@ def receive(channel): with self.assertRaisesRegex(AgentTaskError, 'retirement is unconfirmed'): channel.close(timeout=0.05) self.assertLess(time.monotonic() - start, 0.5) - self.assertTrue(owned <= descriptors()) + self.assertLessEqual(owned, descriptors()) finally: release.set() thread.join(5) diff --git a/test/unit_test/headless/test_agent_presence_handshake.py b/test/unit_test/headless/test_agent_presence_handshake.py index 8d693fa0c..465fec597 100644 --- a/test/unit_test/headless/test_agent_presence_handshake.py +++ b/test/unit_test/headless/test_agent_presence_handshake.py @@ -50,7 +50,8 @@ def fileno(self): def send_bytes(self, raw): self.security.events.append('send') - assert isinstance(raw, bytes) and len(raw) <= 4096 + assert isinstance(raw, bytes) + assert len(raw) <= 4096 self.sent.append(json.loads(raw)) def recv_bytes(self, maximum): @@ -87,25 +88,33 @@ def test_client_opens_actual_peer_before_read_and_retains_query_handle_through_b security = Security() channel = Channel(security, [hello(), client_final]) generation, peer = module().client_handshake(channel, security) - assert generation == GENERATION and peer is security.process + assert generation == GENERATION + assert peer is security.process assert security.events[0] == ('peer-open', False) - assert not peer.closed and not channel.closed + assert not peer.closed + assert not channel.closed assert len(channel.sent) == 1 proof, = channel.sent assert set(proof) == {'protocol', 'server_challenge', 'challenge', 'birth'} - assert proof['protocol'] == 1 and proof['birth'] == 11 and proof['server_challenge'] == REMOTE_NONCE - assert len(proof['challenge']) == 32 and proof['challenge'] != REMOTE_NONCE + assert proof['protocol'] == 1 + assert proof['birth'] == 11 + assert proof['server_challenge'] == REMOTE_NONCE + assert len(proof['challenge']) == 32 + assert proof['challenge'] != REMOTE_NONCE def test_server_opens_actual_peer_before_issuing_challenge_and_echoes_only_fresh_client_proof(): security = Security() channel = Channel(security, [server_proof]) peer = module().server_handshake(channel, security, GENERATION) - assert peer is security.process and not peer.closed and not channel.closed + assert peer is security.process + assert not peer.closed + assert not channel.closed assert security.events[0] == ('peer-open', True) first, last = channel.sent assert set(first) == set(last) == {'protocol', 'challenge', 'birth', 'generation'} - assert first['challenge'] != REMOTE_NONCE and len(first['challenge']) == 32 + assert first['challenge'] != REMOTE_NONCE + assert len(first['challenge']) == 32 assert first['birth'] == last['birth'] == 11 assert first['generation'] == last['generation'] == GENERATION assert last['challenge'] == REMOTE_NONCE @@ -118,9 +127,12 @@ def test_client_refuses_invalid_first_frame_without_disclosing_proof(field, valu frame = hello() frame[field] = value channel = Channel(security, [frame]) + handshake = module() with pytest.raises(AgentTaskError): - module().client_handshake(channel, security) - assert not channel.sent and channel.closed and security.process.closed + handshake.client_handshake(channel, security) + assert not channel.sent + assert channel.closed + assert security.process.closed @pytest.mark.parametrize('field,value', [('challenge', REMOTE_NONCE), ('birth', 23), @@ -134,9 +146,11 @@ def forged(channel): return frame channel = Channel(security, [hello(), forged]) + handshake = module() with pytest.raises(AgentTaskError): - module().client_handshake(channel, security) - assert channel.closed and security.process.closed + handshake.client_handshake(channel, security) + assert channel.closed + assert security.process.closed @pytest.mark.parametrize('field,value', [('server_challenge', REMOTE_NONCE), ('birth', 23), @@ -150,9 +164,12 @@ def forged(channel): return frame channel = Channel(security, [forged]) + handshake = module() with pytest.raises(AgentTaskError): - module().server_handshake(channel, security, GENERATION) - assert len(channel.sent) == 1 and channel.closed and security.process.closed + handshake.server_handshake(channel, security, GENERATION) + assert len(channel.sent) == 1 + assert channel.closed + assert security.process.closed @pytest.mark.parametrize('server', [True, False]) @@ -164,12 +181,15 @@ def dying(channel): return server_proof(channel) if server else client_final(channel) channel = Channel(security, [dying] if server else [hello(), dying]) + handshake = module() + if server: + perform, arguments = handshake.server_handshake, (channel, security, GENERATION) + else: + perform, arguments = handshake.client_handshake, (channel, security) with pytest.raises(AgentTaskError): - if server: - module().server_handshake(channel, security, GENERATION) - else: - module().client_handshake(channel, security) - assert channel.closed and security.process.closed + perform(*arguments) + assert channel.closed + assert security.process.closed @pytest.mark.parametrize('raw', [b'\xff', b'{"protocol":1,"protocol":1}', b' ' * 4097], @@ -177,6 +197,8 @@ def dying(channel): def test_handshake_uses_closed_bounded_json_frames_and_closes_on_parse_failure(raw): security = Security() channel = Channel(security, [raw]) + handshake = module() with pytest.raises(AgentTaskError): - module().client_handshake(channel, security) - assert channel.closed and security.process.closed + handshake.client_handshake(channel, security) + assert channel.closed + assert security.process.closed diff --git a/test/unit_test/headless/test_agent_presence_messages_linux.py b/test/unit_test/headless/test_agent_presence_messages_linux.py index 738fb3151..d92a9128b 100644 --- a/test/unit_test/headless/test_agent_presence_messages_linux.py +++ b/test/unit_test/headless/test_agent_presence_messages_linux.py @@ -72,12 +72,12 @@ def test_missing_extra_duplicate_and_unknown_controls_never_reach_identity_queri 'malformed_pidfd': [credentials, malformed], }[shape] received = module._owned_descriptors(controls, module._ancillary_abi()) - connection = Mock() + connection, peer = Mock(), Mock() connection.recvmsg.return_value = (b'{}', controls, 0, None) try: with self.subTest(shape=shape), patch.object(module, '_snapshot') as snapshot: with self.assertRaises(AgentTaskError): - module._receive(connection, Mock()) + module._receive(connection, peer) snapshot.assert_not_called() for descriptor in received: with self.assertRaises(OSError): @@ -101,20 +101,20 @@ def test_negative_pidfd_never_enters_owned_set_or_queries_original_peer(self): peer.alive.assert_not_called() def test_unknown_abi_and_missing_cmsg_cloexec_refuse_before_consumption(self): - connection = Mock(spec=socket.socket) + connection, peer = Mock(spec=socket.socket), Mock() with patch.object(socket, 'MSG_CMSG_CLOEXEC', 0): with self.assertRaises(AgentTaskError): module._configure(connection) with patch.object(socket, 'SCM_PIDFD', None, create=True), patch( 'je_auto_control.utils.agent_runtime.presence_peer_linux.platform.machine', return_value='sparc64'): with self.assertRaises(AgentTaskError): - module._receive(connection, Mock()) + module._receive(connection, peer) connection.recvmsg.assert_not_called() def test_missing_descriptor_control_abi_refuses_before_receiving_owned_fds(self): - connection = Mock(spec=socket.socket) + connection, peer = Mock(spec=socket.socket), Mock() for name in ('SCM_CREDENTIALS', 'SCM_RIGHTS'): with self.subTest(name=name), patch.object(socket, name, None): with self.assertRaises(AgentTaskError): - module._receive(connection, Mock()) + module._receive(connection, peer) connection.recvmsg.assert_not_called() diff --git a/test/unit_test/headless/test_agent_presence_pipe.py b/test/unit_test/headless/test_agent_presence_pipe.py index 5ab2915b2..d4b36d797 100644 --- a/test/unit_test/headless/test_agent_presence_pipe.py +++ b/test/unit_test/headless/test_agent_presence_pipe.py @@ -53,7 +53,9 @@ def allocate(self): def create(self, *args): from je_auto_control.utils.agent_runtime.presence_security_windows import _SecurityAttributes attributes = ctypes.cast(args[-1], ctypes.POINTER(_SecurityAttributes)).contents - assert self.security.active and attributes.descriptor == 12345 and attributes.inherit == 0 + assert self.security.active + assert attributes.descriptor == 12345 + assert attributes.inherit == 0 self.calls.append('create') return ctypes.c_void_p(-1).value if self.fail_create else self.allocate() @@ -75,7 +77,8 @@ def close(self, handle): return True def connect(self, handle, *, overlapped): - assert handle in self.handles and overlapped is True + assert handle in self.handles + assert overlapped is True self.calls.append(('accept', handle)) def wait(block): @@ -107,13 +110,15 @@ def factory(monkeypatch): def test_constructor_is_inert_and_first_instance_uses_actual_noninheritable_descriptor(monkeypatch): api, native, security = factory(monkeypatch) - assert not native.calls and not security.names + assert not native.calls + assert not security.names listener = api.listen() try: args, = native.kernel.CreateNamedPipeW.calls assert args[0] == r'\\.\pipe\je-auto-presence-v1-test' assert args[1:7] == (0x40080003, 14, 17, 4096, 4096, 0) - assert security.names == ['a' * 32] and not security.active + assert security.names == ['a' * 32] + assert not security.active assert native.kernel.CreateNamedPipeW.restype is ctypes.c_void_p finally: listener.close() @@ -126,7 +131,8 @@ def test_accept_reserves_next_instance_before_wait_and_transfers_only_connected_ connection = listener.accept() assert native.calls[:3] == ['create', 'create', ('accept', 0x100000001)] assert native.kernel.CreateNamedPipeW.calls[1][1] == 0x40000003 - assert connection.fileno() == 0x100000001 and len(native.handles) == 2 + assert connection.fileno() == 0x100000001 + assert len(native.handles) == 2 listener.close() listener.close() assert native.handles == {0x100000001} @@ -154,7 +160,8 @@ def test_only_verified_missing_endpoint_may_be_reported_as_absent(monkeypatch): api, native, _security = factory(monkeypatch) native.fail_create, native.error = True, 2 assert api.connect_if_present() is None - assert len(native.kernel.CreateFileW.calls) == 1 and not native.handles + assert len(native.kernel.CreateFileW.calls) == 1 + assert not native.handles @pytest.mark.parametrize('error,kind', [(5, AgentTaskError), (231, AgentControlBusy), (53, AgentTaskError)]) @@ -163,7 +170,8 @@ def test_denied_busy_or_unknown_endpoint_is_never_reported_missing(monkeypatch, native.fail_create, native.error = True, error with pytest.raises(kind): api.connect_if_present() - assert len(native.kernel.CreateFileW.calls) == 1 and not native.handles + assert len(native.kernel.CreateFileW.calls) == 1 + assert not native.handles @pytest.mark.parametrize('listen', [True, False]) @@ -171,10 +179,12 @@ def test_open_failure_never_wraps_or_borrows_a_foreign_handle(monkeypatch, liste api, native, _ = factory(monkeypatch) native.fail_create = True native.error = 5 + open_pipe = api.listen if listen else api.connect with pytest.raises(AgentControlBusy if listen else AgentTaskError): - api.listen() if listen else api.connect() + open_pipe() assert len(native.kernel.CreateNamedPipeW.calls if listen else native.kernel.CreateFileW.calls) == 1 - assert not native.handles and not native.async_api.ConnectNamedPipe.calls + assert not native.handles + assert not native.async_api.ConnectNamedPipe.calls @pytest.mark.parametrize('failure', ['fail_state', 'fail_wrap']) @@ -232,8 +242,9 @@ def test_internal_factory_refuses_unowned_native_handles_before_any_native_opera listener = api.listen() try: before = list(native.calls) + method = getattr(api, operation) with pytest.raises(AgentTaskError): - getattr(api, operation)(123) + method(123) assert native.calls == before finally: listener.close() @@ -248,7 +259,8 @@ def test_connection_only_sends_bounded_immutable_bytes(monkeypatch, raw): with pytest.raises(AgentTaskError): connection.send_bytes(raw) assert not any(isinstance(item, tuple) and item[0] == 'send' for item in native.calls) - assert not hasattr(connection, 'send') and not hasattr(connection, 'recv') + assert not hasattr(connection, 'send') + assert not hasattr(connection, 'recv') connection.send_bytes(b'{}') assert ('send', b'{}') in native.calls assert len(connection.recv_bytes(4096)) == 4096 diff --git a/test/unit_test/headless/test_agent_presence_pool.py b/test/unit_test/headless/test_agent_presence_pool.py index 26007b9f2..a16b5e458 100644 --- a/test/unit_test/headless/test_agent_presence_pool.py +++ b/test/unit_test/headless/test_agent_presence_pool.py @@ -58,11 +58,13 @@ def test_pool_reference_status_and_unstarted_close_do_not_create_any_helper(): calls = [] instance = pool(lambda _argv: calls.append('created')) first, second = instance.reference(), instance.reference() - assert instance.status()['state'] == 'not_started' and not calls + assert instance.status()['state'] == 'not_started' + assert not calls first.close() assert not calls second.close() - assert instance.retired and not calls + assert instance.retired + assert not calls def test_two_references_share_one_broker_and_closing_one_keeps_the_other_lease(): @@ -78,10 +80,12 @@ def create(argv): first, second = instance.reference(), instance.reference() try: left, right = first.acquire(noop, 2), second.acquire(noop, 2) - assert left['generation'] == right['generation'] and len(channels) == 1 + assert left['generation'] == right['generation'] + assert len(channels) == 1 first.close() another = second.acquire(noop, 2) - assert another['generation'] == right['generation'] and not channels[0].retired + assert another['generation'] == right['generation'] + assert not channels[0].retired assert channels[0].source.released.wait(1) assert len(channels[0].threads) == 1 with pytest.raises(AgentTaskError): @@ -89,7 +93,8 @@ def create(argv): finally: first.close() second.close() - assert channels[0].retired and instance.retired + assert channels[0].retired + assert instance.retired def test_busy_acquisition_does_not_retire_or_restart_the_source(): @@ -129,7 +134,8 @@ def check(): channel.allow.set() token = second.acquire(noop, 2) assert channel.source.released.wait(1) - assert token['generation'] == channel.protocol.generation and not channel.retired + assert token['generation'] == channel.protocol.generation + assert not channel.retired finally: channel.allow.set() first.close() @@ -145,7 +151,8 @@ def test_individual_wait_deadline_abandons_late_grant_without_killing_shared_sou started = time.monotonic() with pytest.raises(AgentTaskError, match='deadline'): first.acquire(noop, 0.1) - assert time.monotonic() - started < 0.5 and not channel.retired + assert time.monotonic() - started < 0.5 + assert not channel.retired channel.allow.set() assert second.acquire(noop, 2)['generation'] == channel.protocol.generation assert channel.source.released.wait(1) @@ -165,7 +172,9 @@ def test_last_close_cancels_shared_lifetime_and_proves_owned_thread_retirement() assert channel.entered.wait(1) started = time.monotonic() reference.close() - assert time.monotonic() - started < 3 and channel.retired and instance.retired + assert time.monotonic() - started < 3 + assert channel.retired + assert instance.retired with pytest.raises(AgentTaskError): waiting.result(timeout=1) with pytest.raises(AgentTaskError): @@ -195,7 +204,9 @@ def create(_argv): closing.result(timeout=3) with pytest.raises(AgentTaskError): acquiring.result(timeout=1) - assert channel.retired and not channel.calls and instance.retired + assert channel.retired + assert not channel.calls + assert instance.retired def test_fatal_source_rpc_does_not_rearm_an_existing_reference(): @@ -236,10 +247,12 @@ def close(): reference.acquire(noop, 2) with pytest.raises(AgentTaskError): reference.close() - assert not instance.retired and not channel.retired + assert not instance.retired + assert not channel.retired fail[0] = False reference.close() - assert instance.retired and channel.retired + assert instance.retired + assert channel.retired with pytest.raises(AgentTaskError): reference.acquire(noop, 2) @@ -257,7 +270,8 @@ def fail_start(_thread): with pytest.raises(AgentTaskError, match='thread'): reference.acquire(noop, 2) reference.close() - assert instance.retired and not calls + assert instance.retired + assert not calls with pytest.raises(AgentTaskError): reference.acquire(noop, 2) @@ -283,10 +297,12 @@ def fail_start(_thread): patch.setattr(threading.Thread, 'start', fail_start) with pytest.raises(AgentTaskError, match='thread'): reference.close() - assert not instance.retired and not channel.retired + assert not instance.retired + assert not channel.retired channel.close = original reference.close() - assert instance.retired and channel.retired + assert instance.retired + assert channel.retired def registry(monkeypatch, channels): @@ -308,10 +324,12 @@ def create(): def test_shared_registry_is_passive_and_replaces_only_a_retired_closed_lifetime(monkeypatch): channels = [Channel(), Channel()] module, pools = registry(monkeypatch, channels) - assert module.shared_status()['state'] == 'not_started' and not pools + assert module.shared_status()['state'] == 'not_started' + assert not pools first, second = module.shared_reference(), module.shared_reference() try: - assert len(pools) == 1 and not channels[0].calls + assert len(pools) == 1 + assert not channels[0].calls old = first.acquire(noop, 2) first.close() assert second.acquire(noop, 2)['generation'] == old['generation'] @@ -321,7 +339,8 @@ def test_shared_registry_is_passive_and_replaces_only_a_retired_closed_lifetime( second.close() fresh = module.shared_reference() try: - assert len(pools) == 2 and pools[0].retired + assert len(pools) == 2 + assert pools[0].retired assert fresh.acquire(noop, 2)['generation'] != old['generation'] first.close() second.close() @@ -374,12 +393,15 @@ def fail_close(): old.close() with pytest.raises(AgentTaskError): module.shared_reference() - assert len(pools) == 1 and not channels[0].retired and not channels[1].calls + assert len(pools) == 1 + assert not channels[0].retired + assert not channels[1].calls finally: channels[0].close = original old.close() fresh = module.shared_reference() try: - assert len(pools) == 2 and pools[0].retired + assert len(pools) == 2 + assert pools[0].retired finally: fresh.close() diff --git a/test/unit_test/headless/test_agent_presence_pool_ipc.py b/test/unit_test/headless/test_agent_presence_pool_ipc.py index f213756f7..4dc22535f 100644 --- a/test/unit_test/headless/test_agent_presence_pool_ipc.py +++ b/test/unit_test/headless/test_agent_presence_pool_ipc.py @@ -47,7 +47,9 @@ def test_actual_pool_retains_one_source_across_reference_and_borrower_replacemen wait_lost(verifier, left) assert verifier.request('check', right, timeout=3)['status'] == 'ok' other.close() - assert borrower.retired and channels[1].retired and not channels[0].retired + assert borrower.retired + assert channels[1].retired + assert not channels[0].retired wait_lost(verifier, borrowed) fresh = instance(namespace, clock, instances, channels).reference() try: @@ -61,7 +63,8 @@ def test_actual_pool_retains_one_source_across_reference_and_borrower_replacemen second.close() if verifier is not None: verifier.close() - assert owner.retired and all(channel.retired for channel in channels) + assert owner.retired + assert all(channel.retired for channel in channels) def test_actual_pool_owner_loss_never_rearms_old_reference_or_imports_old_idle(tmp_path): @@ -81,7 +84,8 @@ def test_actual_pool_owner_loss_never_rearms_old_reference_or_imports_old_idle(t borrower.acquire(noop, 3) with pytest.raises(AgentTaskError): borrower.acquire(noop, 3) - assert len(channels) == 2 and all(channel.retired for channel in channels) + assert len(channels) == 2 + assert all(channel.retired for channel in channels) replacement = instance(namespace, clock, instances, channels).reference() with pytest.raises(AgentControlBusy): replacement.acquire(noop, 15) @@ -128,11 +132,13 @@ def check(): verifier = create(namespace, clock, instances) verifier.request('bootstrap', {}, timeout=15) assert verifier.request('check', original, timeout=3)['status'] == 'ok' - assert len(instances.read_text().splitlines()) == 1 and len(channels) == 1 + assert len(instances.read_text().splitlines()) == 1 + assert len(channels) == 1 finally: gate.unlink(missing_ok=True) first.close() second.close() if verifier is not None: verifier.close() - assert shared.retired and channels[0].retired + assert shared.retired + assert channels[0].retired diff --git a/test/unit_test/headless/test_agent_presence_protocol.py b/test/unit_test/headless/test_agent_presence_protocol.py index ca72812d5..7fc409d39 100644 --- a/test/unit_test/headless/test_agent_presence_protocol.py +++ b/test/unit_test/headless/test_agent_presence_protocol.py @@ -54,8 +54,9 @@ def request(connection, sequence, operation, body=None): b'{"body":{"x":1,"x":2}}', b'{"x":NaN}', b'{"x":Infinity}', b'{"x":1e309}', b'{"x":"\xff"}', b'{"x":"\\ud800"}', b'{' + b'x' * 4096]) def test_wire_rejects_nonobjects_duplicates_nonfinite_unicode_and_oversized_data(raw): + codec = frames() with pytest.raises(AgentTaskError): - frames().decode_frame(raw) + codec.decode_frame(raw) def test_exact_frame_bound_and_nested_finite_roundtrip(): @@ -63,22 +64,25 @@ def test_exact_frame_bound_and_nested_finite_roundtrip(): encoded = frames().encode_frame(value) assert len(encoded) == 4096 assert frames().decode_frame(encoded) == value + codec = frames() with pytest.raises(AgentTaskError): - frames().encode_frame({'padding': 'x' * 4083}) + codec.encode_frame({'padding': 'x' * 4083}) value = {'values': [1, 0.25, True, '中文', None]} assert frames().decode_frame(frames().encode_frame(value)) == value @pytest.mark.parametrize('value', [[], {'x': float('nan')}, {'x': float('inf')}, {'x': object()}]) def test_wire_encoder_does_not_serialize_objects_or_nonfinite_values(value): + codec = frames() with pytest.raises(AgentTaskError): - frames().encode_frame(value) + codec.encode_frame(value) @pytest.mark.parametrize('sequence', [True, False, 0, -1, 1.0, '1', 2 ** 63]) def test_request_sequence_requires_a_bounded_positive_integer(sequence): + codec = frames() with pytest.raises(AgentTaskError): - frames().validate_request({'request_id': sequence, 'operation': 'inspect', 'body': {}}) + codec.validate_request({'request_id': sequence, 'operation': 'inspect', 'body': {}}) @pytest.mark.parametrize('mutation', [{'pid': 123}, {'operation': 'stop'}, {'body': {'idle_seconds': 60}}, @@ -86,8 +90,9 @@ def test_request_sequence_requires_a_bounded_positive_integer(sequence): def test_closed_request_cannot_select_processes_methods_or_fake_idle(mutation): value = {'request_id': 1, 'operation': 'inspect', 'body': {}} value.update(mutation) + codec = frames() with pytest.raises(AgentTaskError): - frames().validate_request(value) + codec.validate_request(value) def test_busy_is_a_normal_result_and_source_survives_until_it_is_idle(): @@ -179,8 +184,9 @@ def test_response_snapshot_is_closed_typed_and_bound_to_the_live_generation(fiel generation = snapshot['generation'] snapshot[field] = value response = {'request_id': 1, 'status': 'ok', 'result': snapshot} + codec = frames() with pytest.raises(AgentTaskError): - frames().validate_response(response, 'inspect', 1, generation) + codec.validate_response(response, 'inspect', 1, generation) @pytest.mark.parametrize('response', [ @@ -190,15 +196,17 @@ def test_response_snapshot_is_closed_typed_and_bound_to_the_live_generation(fiel {'request_id': 1, 'status': 'unexpected', 'result': None}, {'request_id': 1, 'status': 'busy', 'result': None, 'input_authorized': True}]) def test_response_status_sequence_and_fields_cannot_forge_authority(response): + codec = frames() with pytest.raises(AgentTaskError): - frames().validate_response(response, 'inspect', 1, 'a' * 32) + codec.validate_response(response, 'inspect', 1, 'a' * 32) def test_release_success_requires_actual_true_and_acquire_token_shape_is_exact(): + codec = frames() for value in (1, 1.0, False): with pytest.raises(AgentTaskError): - frames().validate_response({'request_id': 1, 'status': 'ok', 'result': {'released': value}}, - 'release', 1, 'a' * 32) + codec.validate_response({'request_id': 1, 'status': 'ok', 'result': {'released': value}}, + 'release', 1, 'a' * 32) result = {'generation': 'a' * 32, 'lease_id': 'b' * 32} response = {'request_id': 1, 'status': 'ok', 'result': result} assert frames().validate_response(response, 'acquire', 1, 'a' * 32) == response diff --git a/test/unit_test/headless/test_agent_presence_quartz.py b/test/unit_test/headless/test_agent_presence_quartz.py index c3b70c614..1a544e69f 100644 --- a/test/unit_test/headless/test_agent_presence_quartz.py +++ b/test/unit_test/headless/test_agent_presence_quartz.py @@ -90,7 +90,8 @@ def test_only_a_process_posting_from_a_non_hid_state_counts_as_injected(pid, sta def test_hardware_events_count_and_track_held_keys_and_buttons(opened): api, quartz, physical = opened quartz.pending[:] = [event(10, keycode=4), event(1)] - assert api.poll(0) == (True, True) and physical == [1, 1] + assert api.poll(0) == (True, True) + assert physical == [1, 1] quartz.pending[:] = [event(11, keycode=4), event(2)] assert api.poll(0) == (True, False) @@ -98,7 +99,8 @@ def test_hardware_events_count_and_track_held_keys_and_buttons(opened): def test_events_a_process_posted_never_count(opened): api, quartz, physical = opened quartz.pending[:] = [event(10, pid=4242, state=0, keycode=4), event(5, pid=4242, state=-1)] - assert api.poll(0) == (True, False) and physical == [] + assert api.poll(0) == (True, False) + assert physical == [] def test_a_key_down_before_the_tap_started_still_counts_as_held(opened): @@ -123,8 +125,9 @@ def test_the_console_is_usable_only_on_console_and_unlocked(opened, session, int def test_a_missing_permission_refuses_to_start(): + api = QuartzPresenceAPI(lambda: FakeQuartz(tap=False)) with pytest.raises(AgentTaskError, match='Accessibility'): - QuartzPresenceAPI(lambda: FakeQuartz(tap=False)).open(lambda: None) + api.open(lambda: None) def test_a_disabled_tap_fails_the_source(opened): diff --git a/test/unit_test/headless/test_agent_presence_reader.py b/test/unit_test/headless/test_agent_presence_reader.py index 4beaf84be..7967b7a92 100644 --- a/test/unit_test/headless/test_agent_presence_reader.py +++ b/test/unit_test/headless/test_agent_presence_reader.py @@ -1,5 +1,6 @@ """Fresh capability checks and finite reader/watch retirement without native desktop observation.""" from concurrent.futures import ThreadPoolExecutor +import gc import json import threading import time @@ -102,11 +103,22 @@ def close(self): @pytest.fixture def harness(monkeypatch): - value = Harness(monkeypatch) + # The reader must answer each probe within 0.25 s. Late in the suite, under coverage and gdb, one + # collection of the whole heap takes longer than that, and a fixed test order makes it land in the + # same window on every run: ready() failed twice in a row on the 1.2.0 release PR. Collect first and + # hold the collector off, so these tests measure the reader, not the size of the suite's heap. + enabled = gc.isenabled() + gc.collect() + gc.disable() try: - yield value + value = Harness(monkeypatch) + try: + yield value + finally: + value.cleanup() finally: - value.cleanup() + if enabled: + gc.enable() def test_reader_construction_status_check_before_ready_and_unstarted_close_are_inert(harness): @@ -118,7 +130,8 @@ def test_reader_construction_status_check_before_ready_and_unstarted_close_are_i reader.close() with pytest.raises(AgentTaskError): reader.ready() - assert not harness.events and reader.retired + assert not harness.events + assert reader.retired @pytest.mark.parametrize('binding', [{}, {'generation': 'a' * 32}, {'generation': True, 'lease_id': 'b' * 32}, @@ -140,8 +153,11 @@ def test_each_ready_and_check_is_fresh_serial_io_for_the_same_detached_capabilit assert [item['request_id'] for item in harness.sent] == list(range(1, len(harness.sent) + 1)) assert all(item['operation'] == 'check' and item['body'] == original for item in harness.sent) harness.reader.close() - assert len(harness.threads) == 1 and threading.get_ident() not in harness.threads - assert harness.reader.retired and harness.peer_closed and harness.connection_closed + assert len(harness.threads) == 1 + assert threading.get_ident() not in harness.threads + assert harness.reader.retired + assert harness.peer_closed + assert harness.connection_closed assert not harness.notice.is_set() assert harness.issuer.request({'request_id': 2, 'operation': 'check', 'body': original})['status'] == 'ok' @@ -155,7 +171,8 @@ def test_physical_revision_revokes_reader_between_actions_and_never_rearms(harne harness.reader.check() with pytest.raises(AgentTaskError): harness.reader.ready() - assert harness.events.count('notice') == 1 and harness.events.count('connect') == 1 + assert harness.events.count('notice') == 1 + assert harness.events.count('connect') == 1 def mutate(value, change): @@ -187,7 +204,8 @@ def test_gate_refuses_revoked_malformed_stale_or_changed_source_replies_once(har harness.transform = lambda value: value with pytest.raises(AgentTaskError): harness.reader.ready() - assert harness.events.count('notice') == 1 and harness.events.count('connect') == 1 + assert harness.events.count('notice') == 1 + assert harness.events.count('connect') == 1 def test_existing_lease_may_keep_its_owned_held_input_without_new_acquisition(harness): @@ -208,8 +226,10 @@ def test_first_bind_blocks_are_bounded_and_late_replies_never_authorize(harness, start = time.monotonic() with pytest.raises(AgentTaskError): harness.reader.ready() - assert time.monotonic() - start < 0.7 and harness.entered.is_set() - assert harness.notice.wait(0.3) and not harness.reader.retired + assert time.monotonic() - start < 0.7 + assert harness.entered.is_set() + assert harness.notice.wait(0.3) + assert not harness.reader.retired start = time.monotonic() with pytest.raises(AgentTaskError): harness.reader.close() @@ -246,8 +266,10 @@ def test_close_during_late_connect_retires_only_owned_resources_without_handshak with pytest.raises(AgentTaskError): waiting.result(timeout=1) harness.reader.close() - assert harness.reader.retired and harness.connection_closed - assert 'handshake' not in harness.events and not harness.sent + assert harness.reader.retired + assert harness.connection_closed + assert 'handshake' not in harness.events + assert not harness.sent def test_failed_native_close_retains_handle_and_explicit_retry_never_reconnects(harness): @@ -255,10 +277,14 @@ def test_failed_native_close_retains_handle_and_explicit_retry_never_reconnects( harness.fail_close = True with pytest.raises(AgentTaskError): harness.reader.close() - assert harness.connection_closed and not harness.peer_closed and not harness.reader.retired + assert harness.connection_closed + assert not harness.peer_closed + assert not harness.reader.retired harness.fail_close = False harness.reader.close() - assert harness.peer_closed and harness.reader.retired and harness.events.count('connect') == 1 + assert harness.peer_closed + assert harness.reader.retired + assert harness.events.count('connect') == 1 assert not harness.notice.is_set() @@ -276,7 +302,9 @@ def start(thread): with pytest.raises(AgentTaskError, match='thread'): harness.reader.ready() harness.reader.close() - assert harness.reader.retired and not harness.sent and harness.events.count('notice') == 1 + assert harness.reader.retired + assert not harness.sent + assert harness.events.count('notice') == 1 def test_cleanup_thread_start_failure_retains_unknown_query_handle_for_explicit_retry(harness, monkeypatch): @@ -293,9 +321,11 @@ def start(_thread): patch.setattr(threading.Thread, 'start', start) with pytest.raises(AgentTaskError, match='thread'): harness.reader.close() - assert not harness.reader.retired and not harness.peer_closed + assert not harness.reader.retired + assert not harness.peer_closed harness.reader.close() - assert harness.reader.retired and harness.events.count('connect') == 1 + assert harness.reader.retired + assert harness.events.count('connect') == 1 def test_reader_checks_actual_peer_liveness_without_connecting_to_another_process(harness): @@ -304,7 +334,8 @@ def test_reader_checks_actual_peer_liveness_without_connecting_to_another_proces with pytest.raises(AgentTaskError): harness.reader.check() assert harness.notice.wait(0.3) - assert harness.events.count('notice') == 1 and harness.events.count('connect') == 1 + assert harness.events.count('notice') == 1 + assert harness.events.count('connect') == 1 @pytest.mark.parametrize('boundary_name', ['on_ack', 'on_prepare']) @@ -313,11 +344,15 @@ def test_actual_reader_revision_checks_fence_worker_ack_and_pointer_preparation( harness.reader.ready() boundary.worker._activity = harness.reader setattr(boundary, boundary_name, harness.source.state.physical) - with pytest.raises(AutoControlException): + + def press_through_the_boundary(): if boundary_name == 'on_prepare': boundary.worker._input({'kind': 'mouse_press', 'keycode': 'left', 'move_before_press': True}) else: boundary.press() + + with pytest.raises(AutoControlException): + press_through_the_boundary() assert 'pressed' not in boundary.events assert harness.notice.wait(0.3) assert harness.events.count('connect') == 1 @@ -330,6 +365,8 @@ def test_reader_loss_does_not_block_worker_owned_release_only_ack_cleanup(harnes harness.source.state.physical() assert harness.notice.wait(0.6) result = boundary.worker._release_owned({'keys': [{'keycode': 65, 'is_shift': False}], 'buttons': []}) - assert result == {'cleaned': True} and 'released' in boundary.events and 'cleaned' in boundary.events + assert result == {'cleaned': True} + assert 'released' in boundary.events + assert 'cleaned' in boundary.events with pytest.raises(AgentTaskError): harness.reader.check() diff --git a/test/unit_test/headless/test_agent_presence_reader_ipc.py b/test/unit_test/headless/test_agent_presence_reader_ipc.py index 7570984fe..9c90cb04f 100644 --- a/test/unit_test/headless/test_agent_presence_reader_ipc.py +++ b/test/unit_test/headless/test_agent_presence_reader_ipc.py @@ -123,7 +123,9 @@ def test_actual_reader_replacement_checks_same_capability_and_never_releases_iss for child in (first, second, owner): if child is not None: child.close() - assert first.retired and second.retired and owner.retired + assert first.retired + assert second.retired + assert owner.retired @pytest.mark.parametrize('mode', ['send', 'receive']) @@ -138,9 +140,12 @@ def test_actual_blocked_watch_retires_owned_reader_job_without_stopping_source(t gate.write_text('block', encoding='utf-8') assert not wait_notice(child)['retired'] closing = child.request('close', {}, timeout=3) - assert not closing['ok'] and not closing['retired'] and closing['elapsed'] < 0.7 + assert not closing['ok'] + assert not closing['retired'] + assert closing['elapsed'] < 0.7 child.close() - assert child.retired and not owner.retired + assert child.retired + assert not owner.retired assert owner.request('check', binding, timeout=3)['status'] == 'ok' assert len(instances.read_text().splitlines()) == 1 finally: @@ -159,12 +164,17 @@ def test_actual_first_bind_and_uncertain_handoff_never_claim_reader_retirement(t gate.write_text('block', encoding='utf-8') child = reader(namespace, gate, mode) initial = child.request('bootstrap', binding, timeout=15) - assert not initial['ok'] and initial['notice'] and initial['elapsed'] < 0.7 + assert not initial['ok'] + assert initial['notice'] + assert initial['elapsed'] < 0.7 assert not initial['retired'] closing = child.request('close', {}, timeout=3) - assert not closing['ok'] and not closing['retired'] and closing['elapsed'] < 0.7 + assert not closing['ok'] + assert not closing['retired'] + assert closing['elapsed'] < 0.7 child.close() - assert child.retired and not owner.retired + assert child.retired + assert not owner.retired assert owner.request('check', binding, timeout=3)['status'] == 'ok' finally: if child is not None: diff --git a/test/unit_test/headless/test_agent_presence_runtime.py b/test/unit_test/headless/test_agent_presence_runtime.py index ff1ac2856..8ce50c083 100644 --- a/test/unit_test/headless/test_agent_presence_runtime.py +++ b/test/unit_test/headless/test_agent_presence_runtime.py @@ -73,12 +73,14 @@ def test_two_runtime_databases_keep_one_source_across_input_task_and_borrower_re first, second = fixture.runtime('one'), fixture.runtime('two') try: left, right = first.submit(spec()), second.submit(spec()) - assert not fixture.source.calls and not fixture.helpers + assert not fixture.source.calls + assert not fixture.helpers first.observe(left.task_id) first.pause(left.task_id) second.observe(right.task_id) bindings = fixture.bindings() - assert len(bindings) == 2 and bindings[0]['generation'] == bindings[1]['generation'] + assert len(bindings) == 2 + assert bindings[0]['generation'] == bindings[1]['generation'] assert bindings[0]['lease_id'] != bindings[1]['lease_id'] assert sum(name == 'bootstrap' for name, _body in fixture.source.calls) == 1 first.close() @@ -89,7 +91,9 @@ def test_two_runtime_databases_keep_one_source_across_input_task_and_borrower_re finally: first.close() second.close() - assert fixture.source.retired and fixture.pool.retired and all(helper.retired for helper in fixture.helpers) + assert fixture.source.retired + assert fixture.pool.retired + assert all(helper.retired for helper in fixture.helpers) def test_new_task_uses_a_new_lease_without_resetting_runtime_source_idle(fixture): @@ -100,8 +104,10 @@ def test_new_task_uses_a_new_lease_without_resetting_runtime_source_idle(fixture second = runtime.submit(spec()) runtime.observe(second.task_id) left, right = fixture.bindings() - assert left['generation'] == right['generation'] and left['lease_id'] != right['lease_id'] - assert len(fixture.helpers) == 2 and fixture.helpers[0].retired + assert left['generation'] == right['generation'] + assert left['lease_id'] != right['lease_id'] + assert len(fixture.helpers) == 2 + assert fixture.helpers[0].retired def test_custom_reviewed_worker_and_passive_runtime_paths_never_bind_presence(fixture): @@ -110,7 +116,8 @@ def test_custom_reviewed_worker_and_passive_runtime_paths_never_bind_presence(fi runtime.list() runtime.get(task.task_id) runtime.observe(task.task_id) - assert not fixture.source.calls and not fixture.bindings() + assert not fixture.source.calls + assert not fixture.bindings() def test_failed_source_retirement_keeps_runtime_and_store_owned_for_explicit_retry(fixture, monkeypatch): @@ -127,14 +134,16 @@ def fail(): monkeypatch.setattr(reference, 'close', fail) with pytest.raises(AgentTaskError, match='unproven'): runtime.close() - assert not runtime._closed and not runtime._leases + assert not runtime._closed + assert not runtime._leases assert all(helper.retired for helper in fixture.helpers) with pytest.raises(AgentControlBusy): fixture.runtime('one') finally: monkeypatch.setattr(reference, 'close', original) runtime.close() - assert runtime._closed and fixture.pool.retired + assert runtime._closed + assert fixture.pool.retired def test_runtime_close_during_first_acquire_cancels_caller_before_final_source_closure(fixture): @@ -149,11 +158,13 @@ def test_runtime_close_during_first_acquire_cancels_caller_before_final_source_c runtime.close() with pytest.raises(CancelledError): waiting.result(timeout=1) - assert not fixture.source.retired and not runtime._closed + assert not fixture.source.retired + assert not runtime._closed finally: fixture.source.allow.set() runtime.close() - assert fixture.pool.retired and all(helper.retired for helper in fixture.helpers) + assert fixture.pool.retired + assert all(helper.retired for helper in fixture.helpers) def test_cancelled_host_scope_never_opens_input_resource_or_helper_before_bind(fixture): @@ -167,7 +178,9 @@ def cancelled(): with pytest.raises(CancelledError): adapter.ready(definition) try: - assert adapter._resource is None and adapter._channel is None and not fixture.helpers + assert adapter._resource is None + assert adapter._channel is None + assert not fixture.helpers assert not fixture.source.calls finally: adapter.cleanup() @@ -178,7 +191,8 @@ def test_passive_target_description_exposes_coarse_presence_without_creating_a_s summary, = runtime.target_profiles.describe()['targets'] assert summary['presence']['state'] in {'not_bound', 'not_supported'} assert summary['presence']['native_evidence'] == 'not_recorded' - assert not fixture.source.calls and not fixture.helpers + assert not fixture.source.calls + assert not fixture.helpers def test_unsupported_default_desktop_refuses_before_helper_or_input_journal(fixture, monkeypatch): @@ -189,7 +203,8 @@ def test_unsupported_default_desktop_refuses_before_helper_or_input_journal(fixt task = runtime.submit(spec()) with pytest.raises(AgentControlBusy, match='Windows'): runtime.observe(task.task_id) - assert not fixture.helpers and not fixture.source.calls + assert not fixture.helpers + assert not fixture.source.calls def test_named_owner_takeover_is_negative_only_and_old_helper_notice_cannot_stop_resumed_lease(fixture): @@ -211,7 +226,8 @@ def test_named_owner_takeover_is_negative_only_and_old_helper_notice_cannot_stop deadline = time.monotonic() + 1 while runtime.get(task.task_id).state != 'paused' and time.monotonic() < deadline: time.sleep(0.01) - assert runtime.get(task.task_id).state == 'paused' and not runtime._leases + assert runtime.get(task.task_id).state == 'paused' + assert not runtime._leases def test_task_options_cannot_select_source_provider_or_presence_capability(fixture): diff --git a/test/unit_test/headless/test_agent_presence_runtime_ipc.py b/test/unit_test/headless/test_agent_presence_runtime_ipc.py index 8f067685c..deba4f669 100644 --- a/test/unit_test/headless/test_agent_presence_runtime_ipc.py +++ b/test/unit_test/headless/test_agent_presence_runtime_ipc.py @@ -118,14 +118,17 @@ def test_actual_two_runtime_databases_and_standard_helpers_share_idle_and_source second, right_target = fixture.runtime('two') try: left, right = first.submit(spec()), second.submit(spec()) - assert not fixture.sources and not fixture.helpers + assert not fixture.sources + assert not fixture.helpers fixture.first_observation(first, left) old = dict(left_target._presence._grant.binding) first.pause(left.task_id) second.observe(right.task_id) current = dict(right_target._presence._grant.binding) - assert current['generation'] == old['generation'] and current['lease_id'] != old['lease_id'] - assert len(fixture.sources) == 1 and len(fixture.instances.read_text().splitlines()) == 1 + assert current['generation'] == old['generation'] + assert current['lease_id'] != old['lease_id'] + assert len(fixture.sources) == 1 + assert len(fixture.instances.read_text().splitlines()) == 1 first.close() assert not fixture.sources[0].retired second.observe(right.task_id) @@ -148,8 +151,10 @@ def test_actual_worker_watch_notice_pauses_and_explicit_resume_waits_fresh_idle( deadline = time.monotonic() + 3 while runtime.get(task.task_id).state != 'paused' and time.monotonic() < deadline: time.sleep(0.01) - assert runtime.get(task.task_id).state == 'paused' and not runtime._leases - assert target._channel is None and target._ledger.empty + assert runtime.get(task.task_id).state == 'paused' + assert not runtime._leases + assert target._channel is None + assert target._ledger.empty assert not fixture.sources[0].retired runtime.resume(task.task_id) with pytest.raises(AgentControlBusy): @@ -157,8 +162,10 @@ def test_actual_worker_watch_notice_pauses_and_explicit_resume_waits_fresh_idle( fixture.clock.write_text('120', encoding='utf-8') runtime.observe(task.task_id) current = target._presence._grant.binding - assert current['generation'] == original['generation'] and current['lease_id'] != original['lease_id'] - assert len(fixture.sources) == 1 and len(fixture.instances.read_text().splitlines()) == 1 + assert current['generation'] == original['generation'] + assert current['lease_id'] != original['lease_id'] + assert len(fixture.sources) == 1 + assert len(fixture.instances.read_text().splitlines()) == 1 assert fixture.retired() @@ -174,15 +181,18 @@ def test_actual_source_owner_loss_requires_new_generation_and_explicit_fresh_idl while (runtime.get(task.task_id).state != 'paused' or not fixture.pools[0].retired ) and time.monotonic() < deadline: time.sleep(0.01) - assert runtime.get(task.task_id).state == 'paused' and fixture.pools[0].retired - assert len(fixture.sources) == 1 and target._channel is None + assert runtime.get(task.task_id).state == 'paused' + assert fixture.pools[0].retired + assert len(fixture.sources) == 1 + assert target._channel is None runtime.resume(task.task_id) with pytest.raises(AgentControlBusy): runtime.observe(task.task_id) fixture.clock.write_text('120', encoding='utf-8') runtime.observe(task.task_id) assert target._presence._grant.binding['generation'] != previous['generation'] - assert len(fixture.sources) == 2 and len(fixture.instances.read_text().splitlines()) == 2 + assert len(fixture.sources) == 2 + assert len(fixture.instances.read_text().splitlines()) == 2 assert fixture.retired() @@ -202,12 +212,15 @@ def test_actual_reader_loss_cannot_release_input_lease_until_owned_ack_cleanup_s deadline = time.monotonic() + 3 while (target._channel is not None and not target._channel.retired) and time.monotonic() < deadline: time.sleep(0.01) - assert runtime.get(task.task_id).state == 'pausing' and runtime._leases + assert runtime.get(task.task_id).state == 'pausing' + assert runtime._leases deadline = time.monotonic() + 1 while target.presence_status['takeover']['cleanup_error'] is None and time.monotonic() < deadline: time.sleep(0.01) assert target.presence_status['takeover']['cleanup_error'] == 'AgentTaskError' - assert not target._ledger.empty and target._resource is not None and target._presence._grant is not None + assert not target._ledger.empty + assert target._resource is not None + assert target._presence._grant is not None with pytest.raises(AgentTaskError): runtime.resume(task.task_id) other = runtime.submit(spec()) @@ -215,8 +228,11 @@ def test_actual_reader_loss_cannot_release_input_lease_until_owned_ack_cleanup_s runtime.observe(other.task_id) failed.unlink() runtime.pause(task.task_id) - assert runtime.get(task.task_id).state == 'paused' and not runtime._leases and target._ledger.empty - assert target._resource is None and target._presence._grant is None + assert runtime.get(task.task_id).state == 'paused' + assert not runtime._leases + assert target._ledger.empty + assert target._resource is None + assert target._presence._grant is None fixture.clock.write_text('120', encoding='utf-8') runtime.observe(other.task_id) assert (tmp_path / 'input-events.txt').read_text().splitlines() == ['pressed', 'released'] diff --git a/test/unit_test/headless/test_agent_presence_security.py b/test/unit_test/headless/test_agent_presence_security.py index 95798dd94..ce748c786 100644 --- a/test/unit_test/headless/test_agent_presence_security.py +++ b/test/unit_test/headless/test_agent_presence_security.py @@ -38,7 +38,8 @@ def __init__(self, module): ConvertStringSecurityDescriptorToSecurityDescriptorW=Function(self.descriptor)) def open(self, access, inherit, pid): - assert access == 0x101000 and not inherit + assert access == 0x101000 + assert not inherit handle = 0x100000000 + pid self.handles[handle] = pid return handle @@ -114,7 +115,8 @@ def test_constructor_is_inert_and_current_identity_releases_all_query_handles(): assert not fake.kernel.OpenProcess.calls value = api.current() assert (value.pid, value.sid, value.session, value.birth) == (10, fake.sid, 2, (1 << 32) + 10) - assert not fake.handles and not fake.buffers + assert not fake.handles + assert not fake.buffers assert fake.kernel.OpenProcess.restype is ctypes.c_void_p @@ -122,7 +124,8 @@ def test_constructor_is_inert_and_current_identity_releases_all_query_handles(): def test_peer_identity_comes_from_connected_pipe_and_only_owned_handles_are_closed(server): api, fake = native() peer = api.peer(999, server=server) - assert peer.identity.pid == 11 and peer.identity.birth == (1 << 32) + 11 + assert peer.identity.pid == 11 + assert peer.identity.birth == (1 << 32) + 11 assert peer.alive() getter = fake.kernel.GetNamedPipeClientProcessId if server else fake.kernel.GetNamedPipeServerProcessId assert len(getter.calls) == 1 @@ -130,7 +133,8 @@ def test_peer_identity_comes_from_connected_pipe_and_only_owned_handles_are_clos peer.close() peer.close() assert not peer.alive() - assert not fake.handles and not fake.buffers + assert not fake.handles + assert not fake.buffers @pytest.mark.parametrize('attribute,value', [('peer_sid', 'S-1-5-21-2'), ('peer_session', 3), ('live', False)]) @@ -139,7 +143,8 @@ def test_wrong_os_user_session_or_dead_peer_is_refused_without_process_terminati setattr(fake, attribute, value) with pytest.raises(AgentTaskError): api.peer(999, server=False) - assert not fake.handles and not fake.buffers + assert not fake.handles + assert not fake.buffers @pytest.mark.parametrize('attribute,value', [('token_ok', False), ('info_ok', False), @@ -149,7 +154,8 @@ def test_invalid_token_data_cannot_select_endpoint_or_descriptor_and_keeps_no_ha setattr(fake, attribute, value) with pytest.raises(AgentTaskError): api.current() - assert not fake.handles and not fake.buffers + assert not fake.handles + assert not fake.buffers assert not fake.advapi.ConvertStringSecurityDescriptorToSecurityDescriptorW.calls @@ -157,11 +163,13 @@ def test_pipe_security_descriptor_grants_only_current_user_system_and_is_not_inh api, fake = native() with api.attributes() as value: assert value.length == ctypes.sizeof(type(value)) - assert value.inherit == 0 and value.descriptor + assert value.inherit == 0 + assert value.descriptor assert len(fake.buffers) == 1 text = fake.advapi.ConvertStringSecurityDescriptorToSecurityDescriptorW.calls[0][0] assert text == 'D:P(A;;GA;;;SY)(A;;GA;;;S-1-5-21-1)' - assert not fake.handles and not fake.buffers + assert not fake.handles + assert not fake.buffers def test_endpoint_is_derived_from_actual_sid_session_and_survives_process_restart(): @@ -182,6 +190,9 @@ def test_real_current_process_identity_is_query_only_and_never_starts_observatio api = WindowsPresenceSecurity() value = api.current() assert value.pid == os.getpid() - assert value.sid.startswith('S-1-') and value.birth > 0 and value.session >= 0 + assert value.sid.startswith('S-1-') + assert value.birth > 0 + assert value.session >= 0 with api.attributes() as attributes: - assert attributes.descriptor and not attributes.inherit + assert attributes.descriptor + assert not attributes.inherit diff --git a/test/unit_test/headless/test_agent_presence_server.py b/test/unit_test/headless/test_agent_presence_server.py index c26b4a7cd..b7f48957e 100644 --- a/test/unit_test/headless/test_agent_presence_server.py +++ b/test/unit_test/headless/test_agent_presence_server.py @@ -86,7 +86,8 @@ def connected(pipe): def test_constructor_close_and_status_do_not_reserve_or_construct_an_observer(monkeypatch): server, _source, _pipe, events = service(monkeypatch) - assert not server.status()['running'] and not events + assert not server.status()['running'] + assert not events server.close() assert not events with pytest.raises(AgentTaskError): @@ -107,7 +108,8 @@ def test_reservation_precedes_observation_and_failed_start_keeps_no_ready_servic with pytest.raises(AgentTaskError): server.start() assert events[:3] == ['reserve', 'construct', 'start'] - assert 'listener-close' in events and 'source-close' in events + assert 'listener-close' in events + assert 'source-close' in events assert not server.status()['running'] @@ -122,10 +124,12 @@ def test_busy_preserves_shared_source_and_disconnect_revokes_only_issued_tokens( assert lease['generation'] == ready['generation'] assert second.request(1, 'check', lease)['status'] == 'ok' first.close() - assert source.released.wait(1) and first.peer_closed.wait(1) + assert source.released.wait(1) + assert first.peer_closed.wait(1) assert second.request(2, 'check', lease)['status'] == 'lease_lost' assert second.request(3, 'inspect')['result']['generation'] == ready['generation'] - assert 'source-close' not in events and server.status()['running'] + assert 'source-close' not in events + assert server.status()['running'] finally: server.close() @@ -172,7 +176,8 @@ def test_close_fences_leases_and_retires_only_this_server_resources(monkeypatch) server.close() server.close() assert time.monotonic() - started < 2 - assert client.closed.is_set() and 'listener-close' in events + assert client.closed.is_set() + assert 'listener-close' in events assert not server.status()['running'] with pytest.raises(AgentTaskError): source.state.check(**lease) diff --git a/test/unit_test/headless/test_agent_presence_service_ipc.py b/test/unit_test/headless/test_agent_presence_service_ipc.py index 337b86ef5..765b2db01 100644 --- a/test/unit_test/headless/test_agent_presence_service_ipc.py +++ b/test/unit_test/headless/test_agent_presence_service_ipc.py @@ -102,7 +102,8 @@ def test_two_actual_brokers_share_generation_busy_and_borrower_only_token_cleanu ready = owner.request('bootstrap', {}, timeout=15) assert borrower.request('bootstrap', {}, timeout=15) == ready assert owner.request('acquire', {}, timeout=3)['status'] == 'busy' - assert not owner.retired and len(instances.read_text().splitlines()) == 1 + assert not owner.retired + assert len(instances.read_text().splitlines()) == 1 clock.write_text('60', encoding='utf-8') first = owner.request('acquire', {}, timeout=3)['result'] second = borrower.request('acquire', {}, timeout=3)['result'] @@ -115,7 +116,8 @@ def test_two_actual_brokers_share_generation_busy_and_borrower_only_token_cleanu finally: borrower.close() owner.close() - assert borrower.retired and owner.retired + assert borrower.retired + assert owner.retired def test_actual_owner_loss_refuses_old_leases_and_replacement_observes_fresh_idle(tmp_path): @@ -130,7 +132,8 @@ def test_actual_owner_loss_refuses_old_leases_and_replacement_observes_fresh_idl owner.close() with pytest.raises(AgentTaskError): borrower.request('check', old, timeout=3) - assert borrower.retired and len(instances.read_text().splitlines()) == 1 + assert borrower.retired + assert len(instances.read_text().splitlines()) == 1 replacement = create(namespace, clock, instances) new = replacement.request('bootstrap', {}, timeout=15) assert new['generation'] != ready['generation'] @@ -155,7 +158,8 @@ def test_concurrent_actual_startup_constructs_only_the_first_instance_source(tmp right = pool.submit(second.request, 'bootstrap', {}, timeout=15) assert left.result(timeout=16) == right.result(timeout=16) records = [json.loads(line) for line in instances.read_text().splitlines()] - assert len(records) == 1 and records[0]['generation'] == left.result()['generation'] + assert len(records) == 1 + assert records[0]['generation'] == left.result()['generation'] assert first.request('acquire', {}, timeout=3)['status'] == 'busy' assert second.request('acquire', {}, timeout=3)['status'] == 'busy' finally: @@ -187,7 +191,8 @@ def test_borrower_lost_during_acquire_revokes_late_token_without_stopping_the_ow wait_file(tmp_path / 'acquire-entered') with pytest.raises(AgentTaskError, match='deadline'): waiting.result(timeout=4) - assert borrower.retired and not owner.retired + assert borrower.retired + assert not owner.retired gate.unlink() wait_file(tmp_path / 'releases.txt') assert owner.request('check', lease, timeout=3)['status'] == 'ok' diff --git a/test/unit_test/headless/test_agent_presence_service_linux.py b/test/unit_test/headless/test_agent_presence_service_linux.py index a258ee4f2..800864db2 100644 --- a/test/unit_test/headless/test_agent_presence_service_linux.py +++ b/test/unit_test/headless/test_agent_presence_service_linux.py @@ -46,8 +46,9 @@ def test_an_endpoint_round_trips_through_its_wire_form(): {"listener_process": {"pid": 42, "uid": 1000, "gid": 1000, "birth": 99, "user_namespace": "n"}}, ]) def test_a_malformed_endpoint_is_refused_before_any_connection(change): + malformed = _wire(**change) with pytest.raises(AgentTaskError): - endpoint_from_wire(_wire(**change)) + endpoint_from_wire(malformed) def test_the_wire_form_names_every_field_of_the_endpoint_types(): @@ -68,8 +69,9 @@ def test_without_an_owned_launch_there_is_nothing_to_connect_to(): def test_listening_needs_a_private_runtime_directory(): + pipe = LinuxPresencePipe(runtime_directory="") with pytest.raises(AgentTaskError, match="XDG_RUNTIME_DIR"): - LinuxPresencePipe(runtime_directory="").listen() + pipe.listen() def _broker_script(clock): @@ -119,7 +121,8 @@ def test_an_owned_worker_hands_its_endpoint_to_the_broker_and_serves_presence(tm broker = OwnedChannel([sys.executable, '-u', '-c', _broker_script(clock)]) try: ready = broker.request('bootstrap', {}, timeout=15) - assert ready['ready'] is True and len(ready['generation']) == 32, json.dumps(ready) + assert ready['ready'] is True, json.dumps(ready) + assert len(ready['generation']) == 32, json.dumps(ready) assert broker.request('acquire', {}, timeout=3)['status'] == 'busy' clock.write_text('60', encoding='utf-8') lease = broker.request('acquire', {}, timeout=3)['result'] diff --git a/test/unit_test/headless/test_agent_presence_session.py b/test/unit_test/headless/test_agent_presence_session.py index 83ab5f84a..1aa93799e 100644 --- a/test/unit_test/headless/test_agent_presence_session.py +++ b/test/unit_test/headless/test_agent_presence_session.py @@ -29,7 +29,8 @@ def metric(self, code): return self.metric_remote if code == 0x1000 else self.metric_shadow def query(self, server, session, info, pointer, count): - assert server is None and session == 0xFFFFFFFF + assert server is None + assert session == 0xFFFFFFFF value = ctypes.c_uint16(self.protocol) if info == 16 else ctypes.c_int32( self.remote if info == 29 else self.state) assert info in (16, 29, 8) @@ -52,7 +53,8 @@ def console(native): def test_console_guard_construction_is_inert_and_all_checks_use_current_local_session_queries(): native = Native() guard = console(native) - assert not native.kernel.GetCurrentProcessId.calls and not native.freed + assert not native.kernel.GetCurrentProcessId.calls + assert not native.freed guard.check() guard.check() assert [row[2] for row in native.wts.WTSQuerySessionInformationW.calls] == [16, 29, 8] * 2 @@ -69,8 +71,9 @@ def test_console_guard_construction_is_inert_and_all_checks_use_current_local_se def test_remote_shadow_unknown_detaching_and_malformed_native_queries_refuse_permission(field, value): native = Native() setattr(native, field, value) + session = console(native) with pytest.raises(AgentTaskError): - console(native).check() + session.check() if native.wts.WTSQuerySessionInformationW.calls and not native.null: assert native.freed @@ -88,8 +91,9 @@ def test_console_guard_rechecks_session_change_instead_of_caching_initial_author def test_unsupported_platform_never_loads_dlls(monkeypatch): from je_auto_control.utils.agent_runtime import presence_session_windows monkeypatch.setattr(presence_session_windows, 'sys', SimpleNamespace(platform='linux')) + session = presence_session_windows.WindowsConsoleSession() with pytest.raises(AgentTaskError): - presence_session_windows.WindowsConsoleSession().check() + session.check() def test_observer_refuses_session_before_creating_any_window_or_raw_registration(monkeypatch): @@ -101,7 +105,8 @@ def refuse(): api._support.session = refuse with pytest.raises(AgentTaskError, match='console'): api.open(lambda: None) - assert not native.user.CreateWindowExW.calls and not native.user.RegisterClassW.calls + assert not native.user.CreateWindowExW.calls + assert not native.user.RegisterClassW.calls def test_observer_rechecks_supported_session_for_every_fresh_poll(monkeypatch): diff --git a/test/unit_test/headless/test_agent_presence_transport.py b/test/unit_test/headless/test_agent_presence_transport.py index 09d52270d..c5d027104 100644 --- a/test/unit_test/headless/test_agent_presence_transport.py +++ b/test/unit_test/headless/test_agent_presence_transport.py @@ -85,7 +85,8 @@ def listening(namespace): try: ready = server.request('listen', {'namespace': namespace}, timeout=15) expected = Path(__file__).resolve().parents[3] / 'je_auto_control/utils/agent_runtime/presence_pipe_windows.py' - assert ready['ready'] and Path(ready['origin']) == expected + assert ready['ready'] + assert Path(ready['origin']) == expected return server, ready except BaseException: server.close() @@ -111,7 +112,8 @@ def pair(): finally: client.close() server.close() - assert client.retired and server.retired + assert client.retired + assert server.retired def test_actual_background_peer_challenges_and_bounded_bytes_round_trip(pair): @@ -131,7 +133,8 @@ def test_actual_first_instance_rejects_contender_without_retiring_existing_owner try: with pytest.raises(AgentControlBusy): contender.request('listen', {'namespace': namespace}, timeout=15) - assert contender.retired and not server.retired + assert contender.retired + assert not server.retired assert connect(server, client, namespace) == ready['generation'] finally: client.close() @@ -145,7 +148,8 @@ def test_blocked_actual_read_or_write_retires_only_that_owned_helper(pair, opera started = time.monotonic() with pytest.raises(AgentTaskError, match='deadline'): client.request(operation, {}, timeout=0.25) - assert client.retired and time.monotonic() - started < 5 + assert client.retired + assert time.monotonic() - started < 5 assert server.request('alive', {}, timeout=3) == {'alive': False} assert not server.retired @@ -157,7 +161,8 @@ def test_blocked_actual_accept_retires_owner_and_allows_new_first_instance(): started = time.monotonic() with pytest.raises(AgentTaskError, match='deadline'): server.request('accept', {}, timeout=0.25) - assert server.retired and time.monotonic() - started < 5 + assert server.retired + assert time.monotonic() - started < 5 replacement, new = listening(namespace) try: assert new['generation'] != old['generation'] @@ -183,11 +188,13 @@ def test_actual_half_handshake_is_bounded_by_owned_process_retirement(waiting_se with pytest.raises(AgentTaskError, match='deadline'): client.request('connect', {'namespace': namespace}, timeout=3) assert accepted.result(timeout=16) - assert client.retired and not server.retired + assert client.retired + assert not server.retired return with pytest.raises(AgentTaskError, match='deadline'): waiting.request(operation, payload, timeout=0.25) - assert waiting.retired and not client.retired + assert waiting.retired + assert not client.retired finally: client.close() server.close() diff --git a/test/unit_test/headless/test_agent_presence_win32.py b/test/unit_test/headless/test_agent_presence_win32.py index b9a83cb07..c5aa2e788 100644 --- a/test/unit_test/headless/test_agent_presence_win32.py +++ b/test/unit_test/headless/test_agent_presence_win32.py @@ -225,8 +225,9 @@ def test_poll_and_cleanup_cannot_run_on_a_different_window_owner_thread(monkeypa api.open(lambda: None) with ThreadPoolExecutor(max_workers=1) as executor: for operation in (lambda: api.poll(0), api.close): + future = executor.submit(operation) with pytest.raises(AgentTaskError, match='thread'): - executor.submit(operation).result(timeout=1) + future.result(timeout=1) assert fake.user.DestroyWindow.calls == [] finally: api.close() diff --git a/test/unit_test/headless/test_agent_presence_worker.py b/test/unit_test/headless/test_agent_presence_worker.py index 1d1709d48..08310d560 100644 --- a/test/unit_test/headless/test_agent_presence_worker.py +++ b/test/unit_test/headless/test_agent_presence_worker.py @@ -36,9 +36,11 @@ def transport(*, namespace): module().run(io.BytesIO(packet()), output, source_factory=construct, pipe_factory=transport) response, = [json.loads(line) for line in output.getvalue().splitlines()] - assert response['id'] == 'a' * 32 and response['result']['ready'] is True + assert response['id'] == 'a' * 32 + assert response['result']['ready'] is True assert events[:3] == ['reserve', 'construct', 'start'] - assert 'source-close' in events and 'listener-close' in events + assert 'source-close' in events + assert 'listener-close' in events def test_worker_does_nothing_before_bootstrap_or_when_endpoint_is_already_owned(): @@ -62,7 +64,8 @@ def test_worker_rejects_untrusted_fields_before_any_transport_or_source_is_const calls, output = [], io.BytesIO() module().run(io.BytesIO(packet(payload)), output, source_factory=lambda: calls.append('source'), pipe_factory=lambda **_kwargs: calls.append('pipe')) - assert not calls and json.loads(output.getvalue())['error_type'] == 'AgentTaskError' + assert not calls + assert json.loads(output.getvalue())['error_type'] == 'AgentTaskError' def test_source_process_is_inert_and_uses_fixed_owned_channel_argv_before_bootstrap(): @@ -91,7 +94,8 @@ def create(argv): b'{"id":"x","id":"x"}\n', b'[]\n']) def test_private_owner_envelope_refuses_incomplete_or_invalid_identity_before_startup(raw): calls = [] + worker, incoming, outgoing = module(), io.BytesIO(raw), io.BytesIO() with pytest.raises(AgentTaskError): - module().run(io.BytesIO(raw), io.BytesIO(), source_factory=lambda: calls.append('source'), - pipe_factory=lambda **_kwargs: calls.append('pipe')) + worker.run(incoming, outgoing, source_factory=lambda: calls.append('source'), + pipe_factory=lambda **_kwargs: calls.append('pipe')) assert not calls diff --git a/test/unit_test/headless/test_agent_presence_x11.py b/test/unit_test/headless/test_agent_presence_x11.py index 50f3afef6..71ca03a63 100644 --- a/test/unit_test/headless/test_agent_presence_x11.py +++ b/test/unit_test/headless/test_agent_presence_x11.py @@ -93,9 +93,11 @@ def test_xtest_events_never_count_as_physical(opened): def test_real_device_events_count_and_track_what_is_held(opened): api, display, physical = opened display.queue[:] = [raw(13, KEYBOARD, 38)] - assert api.poll(0) == (True, True) and physical == [1] + assert api.poll(0) == (True, True) + assert physical == [1] display.queue[:] = [raw(14, KEYBOARD, 38)] - assert api.poll(0) == (True, False) and physical == [1, 1] + assert api.poll(0) == (True, False) + assert physical == [1, 1] def test_core_state_counts_as_held_even_without_a_tracked_press(opened): @@ -111,7 +113,8 @@ def test_an_unknown_source_refreshes_devices_and_counts_once_it_is_a_real_slave( display.devices.append(SimpleNamespace(deviceid=12, use=3, name='USB mouse')) display.queue[:] = [raw(17, 12)] api.poll(0) - assert physical == [1] and display.queries == 2 + assert physical == [1] + assert display.queries == 2 def test_a_master_device_is_never_a_source(opened): @@ -135,8 +138,9 @@ def test_poll_deadlines_are_bounded(opened, timeout): def test_xinput_one_and_a_second_open_are_refused(): + legacy = X11PresenceAPI(lambda: FakeDisplay(version=1), interactive=lambda: True) with pytest.raises(AgentTaskError, match='could not start|XInput 2'): - X11PresenceAPI(lambda: FakeDisplay(version=1), interactive=lambda: True).open(lambda: None) + legacy.open(lambda: None) api = X11PresenceAPI(FakeDisplay, interactive=lambda: True) api.open(lambda: None) with pytest.raises(AgentTaskError, match='cannot restart'): @@ -156,9 +160,11 @@ def foreign_close(): worker = threading.Thread(target=foreign_close) worker.start() worker.join() - assert outcome == ['refused'] and not display.closed + assert outcome == ['refused'] + assert not display.closed api.close() - assert display.selected[-1] == [(0, 0)] and display.closed + assert display.selected[-1] == [(0, 0)] + assert display.closed with pytest.raises(AgentTaskError): api.poll(0) @@ -199,13 +205,15 @@ def test_logind_is_queried_with_a_fixed_argv_and_cached_for_its_interval(monkeyp monkeypatch.setattr(presence_x11.shutil, 'which', lambda _name: '/usr/bin/loginctl') calls, now = [], [0.0] session = LogindSession('c2', runner=_runner('Active=yes\nLockedHint=no', calls=calls), clock=lambda: now[0]) - assert session.interactive() and session.interactive() + assert session.interactive() + assert session.interactive() now[0] = 1.5 session.interactive() assert len(calls) == 2 argv, kwargs = calls[0] assert argv == ['/usr/bin/loginctl', 'show-session', 'c2', '-p', 'Active', '-p', 'LockedHint'] - assert kwargs['shell'] is False and kwargs['timeout'] == 1 + assert kwargs['shell'] is False + assert kwargs['timeout'] == 1 @pytest.mark.skipif(not sys.platform.startswith('linux') or not os.environ.get('DISPLAY'), diff --git a/test/unit_test/headless/test_agent_readiness.py b/test/unit_test/headless/test_agent_readiness.py index b9f864291..3d54f83de 100644 --- a/test/unit_test/headless/test_agent_readiness.py +++ b/test/unit_test/headless/test_agent_readiness.py @@ -25,7 +25,8 @@ def test_every_area_uses_the_closed_vocabulary_and_records_no_native_evidence(): ('linux', {'WAYLAND_DISPLAY': 'wayland-0', 'XDG_SESSION_TYPE': 'wayland'}), ('sunos5', {})): report = _report(platform, environ) - assert report['passive'] is True and report['states'] == list(readiness.STATES) + assert report['passive'] is True + assert report['states'] == list(readiness.STATES) assert {item['state'] for item in report['areas']} <= set(readiness.STATES) assert all(item['native_evidence'] == 'not_recorded' for item in report['areas']) assert 'measured' not in {item['state'] for item in report['areas']} @@ -68,7 +69,8 @@ def test_only_a_platform_with_a_presence_source_reports_one(platform, environ, s def test_a_local_capability_observation_maps_without_becoming_measured(observed, state): status = CapabilityStatus('input', observed, 'win32', 'observed locally', 'none', True) area = _area(_report(input_state=status), 'desktop_input') - assert area['state'] == state and area['observed'] == observed + assert area['state'] == state + assert area['observed'] == observed def test_the_report_never_contacts_a_device_or_imports_the_browser_package(): diff --git a/test/unit_test/headless/test_agent_readonly_completion.py b/test/unit_test/headless/test_agent_readonly_completion.py index ff6d0b57c..540a026e1 100644 --- a/test/unit_test/headless/test_agent_readonly_completion.py +++ b/test/unit_test/headless/test_agent_readonly_completion.py @@ -29,15 +29,19 @@ def test_standard_file_reader_is_fresh_scope_bound_and_retires_without_calling_o parent = TaskOperation(definition, lambda: None, None) with operation_scope(parent): with source.completion_reader(definition) as reader: - assert reader.observe()["identity"] == "local-files" and not reader.retired + assert reader.observe()["identity"] == "local-files" + assert not reader.retired with operation_scope(TaskOperation(definition, lambda: None, None)), pytest.raises(AgentTaskError): reader.observe() assert reader.retired with pytest.raises(AgentTaskError, match="closed"): reader.observe() - assert source._channel is None and source._resource is None + assert source._channel is None + assert source._resource is None witness = InputJournal(tmp_path / "input.lock").read() - assert witness["owner"] is None and witness["held"] == {} and witness["in_flight"] is None + assert witness["owner"] is None + assert witness["held"] == {} + assert witness["in_flight"] is None source.observe.assert_not_called() source.perform.assert_not_called() @@ -49,9 +53,9 @@ def test_unknown_input_journal_is_never_cleared_by_opening_a_reader(tmp_path): before = journal.path.read_bytes() source = NativeDesktopTarget(resource_path=path) definition = spec(tmp_path) - with (operation_scope(TaskOperation(definition, lambda: None, None)), - pytest.raises(AgentTaskError, match="recovery")): - with source.completion_reader(definition): + with operation_scope(TaskOperation(definition, lambda: None, None)): + session = source.completion_reader(definition) + with pytest.raises(AgentTaskError, match="recovery"), session: pytest.fail("unknown witness was accepted") assert journal.path.read_bytes() == before @@ -72,7 +76,8 @@ def test_worker_refuses_native_ack_and_nonempty_cleanup_without_any_target_call( worker._input({"kind": "mouse_press", "x": 1, "y": 2}) with pytest.raises(AgentTaskError, match="cleanup input"): worker._dispatch("cleanup", {"keys": [{"keycode": 65}], "buttons": []}) - assert sink.getvalue() == b"" and not target.method_calls + assert sink.getvalue() == b"" + assert not target.method_calls HELPER = ''' @@ -97,14 +102,15 @@ def test_unexpected_input_from_a_helper_is_not_acked_and_retains_quarantine(tmp_ adapter = ReadOnlyNativeTarget(argv=[sys.executable, "-u", "-c", HELPER], resource_path=path) with operation_scope(TaskOperation(definition, lambda: None, None)): reader = CompletionReader(adapter, definition) - with pytest.raises(AgentTaskError): - with reader.session(): - reader.observe() + session = reader.session() + with pytest.raises(AgentTaskError), session: + reader.observe() assert not reader.retired with pytest.raises(AgentTaskError, match="closed"): reader.observe() witness = InputJournal(path).read() - assert witness["owner"] and witness["in_flight"] == "readonly_input_violation" + assert witness["owner"] + assert witness["in_flight"] == "readonly_input_violation" assert json.loads(path.with_suffix(".lock.journal").read_text())["owner"] @@ -117,9 +123,9 @@ def test_reader_refuses_nonexact_capabilities_before_any_configuration(tmp_path, definition = spec(tmp_path) with operation_scope(TaskOperation(definition, lambda: None, None)): reader = CompletionReader(adapter, definition) - with pytest.raises(AgentTaskError, match="contract"): - with reader.session(): - pytest.fail("bad capability accepted") + session = reader.session() + with pytest.raises(AgentTaskError, match="contract"), session: + pytest.fail("bad capability accepted") adapter.ready.assert_not_called() adapter.cleanup.assert_called_once() @@ -166,7 +172,8 @@ def supports_readonly_completion(self): try: report = runtime.target_profiles.describe()["targets"][0] assert report["readonly_completion"] == {"status": "not_supported"} - assert report["readiness"] == "not_probed" and report["native_evidence"] == "not_recorded" + assert report["readiness"] == "not_probed" + assert report["native_evidence"] == "not_recorded" finally: runtime.close() @@ -258,8 +265,9 @@ def test_reader_cannot_be_borrowed_under_another_owner_even_in_the_same_operatio def test_direct_readonly_proxy_uses_only_the_readonly_worker_and_refuses_unscoped_startup(tmp_path): adapter = ReadOnlyNativeTarget(resource_path=tmp_path / "input.lock") assert adapter._argv[-1] == "je_auto_control.utils.agent_runtime.readonly_worker" + definition = spec(tmp_path) with pytest.raises(AgentTaskError, match="scope"): - adapter.ready(spec(tmp_path)) + adapter.ready(definition) assert not InputJournal(tmp_path / "input.lock").path.exists() diff --git a/test/unit_test/headless/test_agent_reauthorization.py b/test/unit_test/headless/test_agent_reauthorization.py index 5c0ccaee9..1baa26585 100644 --- a/test/unit_test/headless/test_agent_reauthorization.py +++ b/test/unit_test/headless/test_agent_reauthorization.py @@ -152,7 +152,8 @@ def test_host_reapproval_without_a_client_origin_is_explicit_and_still_source_bo assert "origin_policies" not in request["spec"]["options"] child = runtime.recovery.control("create_authorized", request)["task_id"] proof = runtime.get(child).metadata["recovery"]["reauthorization"] - assert proof["actor"] == "local" and list(proof["context"]["current_origins"]) == [] + assert proof["actor"] == "local" + assert list(proof["context"]["current_origins"]) == [] assert list(proof["context"]["replaced_origins"]) == [old_id] @@ -273,9 +274,12 @@ def test_dynamic_tasks_reauthorize_without_inventing_steps_or_contacting_a_model monkeypatch.setattr("je_auto_control.utils.agent_runtime.ollama.request_json", network) with origin_scope(current_id): view = runtime.recovery.control("review_authorization", {"task_id": source}) - assert view["draft"] is None and view["completed_steps"] is None and view["remaining_actions"] == [] + assert view["draft"] is None + assert view["completed_steps"] is None + assert view["remaining_actions"] == [] spec = view["review_template"] - assert spec["goal"] == "" and spec["mode"] == mode + assert spec["goal"] == "" + assert spec["mode"] == mode spec["goal"] = "Process only the remaining field under the new report scope" child = runtime.recovery.control("create_authorized", {"task_id": source, "snapshot": view["snapshot"], "authorization_review": view["authorization_review"], "spec": spec, "note": "Reviewed the new source"}) @@ -291,7 +295,8 @@ def test_inaccessible_completion_scope_produces_no_runnable_authorization_draft( connection.execute("UPDATE agent_tasks SET spec=? WHERE id=?", (json.dumps(data), source)) with origin_scope(current_id): view = runtime.recovery.control("review_authorization", {"task_id": source}) - assert view["draft"] is None and view["review_template"] is None + assert view["draft"] is None + assert view["review_template"] is None assert view["issues"] == ["authorization_scope_unavailable"] assert len(runtime.list()) == 1 diff --git a/test/unit_test/headless/test_agent_reauthorization_surfaces.py b/test/unit_test/headless/test_agent_reauthorization_surfaces.py index 9eab882ff..18175b567 100644 --- a/test/unit_test/headless/test_agent_reauthorization_surfaces.py +++ b/test/unit_test/headless/test_agent_reauthorization_surfaces.py @@ -16,7 +16,8 @@ def test_cli_exposes_distinct_review_and_approval_operations_without_startup(): parser = build_parser() for operation in ("review_authorization", "create_authorized"): parsed = parser.parse_args(["agent-task", "recovery", operation, "review.json"]) - assert parsed.recovery_operation == operation and parsed.request == "review.json" + assert parsed.recovery_operation == operation + assert parsed.request == "review.json" def test_authenticated_http_reapproval_keeps_owner_and_scope_bound(service, tmp_path, monkeypatch): diff --git a/test/unit_test/headless/test_agent_recovery_ownership.py b/test/unit_test/headless/test_agent_recovery_ownership.py index 8648390dd..773af2ba6 100644 --- a/test/unit_test/headless/test_agent_recovery_ownership.py +++ b/test/unit_test/headless/test_agent_recovery_ownership.py @@ -63,7 +63,8 @@ def owned_request(stopped, *, reauthorize=False): def test_admin_source_owned_child_is_adopted_without_broadening_reads(stopped, monkeypatch): client, host, target, _users, owner, actor, batch, definition, _source = stopped value, request = owned_request(stopped) - assert value["ownership"]["owner"] == "owner" and value["ownership"]["actor"] == "reviewer" + assert value["ownership"]["owner"] == "owner" + assert value["ownership"]["actor"] == "reviewer" assert "store_path" not in str(value["ownership"]) child = recovery(stopped, "create", request)["task_id"] assert recovery(stopped, "create", request)["task_id"] == child @@ -79,7 +80,8 @@ def test_admin_source_owned_child_is_adopted_without_broadening_reads(stopped, m selected = review(client, batch, definition, decisions=selected["decisions"]) assert selected["can_approve"] result = run_dag(definition, batch_id=batch["batch_id"], recovery=approval(selected)) - assert result.succeeded and target.count == 3 + assert result.succeeded + assert target.count == 3 def test_default_admin_recovery_keeps_legacy_caller_ownership(stopped): @@ -105,7 +107,8 @@ def test_owned_recovery_rejects_unreviewed_and_caller_authored_identity(stopped, request.pop("owner_policy") with pytest.raises(AgentTaskError): recovery(stopped, "create", request) - assert len(stopped[1].runtime.list()) == 1 and stopped[2].count == 1 + assert len(stopped[1].runtime.list()) == 1 + assert stopped[2].count == 1 @pytest.mark.parametrize("user,role", [("owner", "viewer"), ("reviewer", "viewer"), ("reviewer", "operator")]) @@ -129,7 +132,9 @@ def test_maintenance_retires_owned_successor_when_reviewer_loses_cross_owner_aut # cancelling and a later one settles it. until(lambda: runtime.maintain() or runtime.get(child).state != "cancelling") result = runtime.get(child) - assert result.state == "cancelled" and result.metadata["input_released"] is True and stopped[2].count == 1 + assert result.state == "cancelled" + assert result.metadata["input_released"] is True + assert stopped[2].count == 1 def test_revocation_during_grounding_cannot_reach_successor_input(stopped, monkeypatch): @@ -150,7 +155,8 @@ def revoked(): # successor's input is refused before dispatch, which is what the assertions below hold. with pytest.raises((AuthorizationError, CancelledError)): instance.act(child, {"tool": "AC_increment", "observation_id": frame.observation_id}) - assert target.count == 1 and [item["status"] for item in instance.store.actions(child)] == ["failed"] + assert target.count == 1 + assert [item["status"] for item in instance.store.actions(child)] == ["failed"] @pytest.mark.parametrize("reauthorize", [False, True]) @@ -164,7 +170,8 @@ def test_owned_admission_failure_rolls_back_task_lineage_and_both_audits(stopped with stopped[1].runtime.store.connection() as connection: for table in ("agent_task_recoveries", "agent_task_authorizations", "agent_recovery_owners"): assert connection.execute("SELECT COUNT(*) FROM " + table).fetchone()[0] == 0 - assert len(stopped[1].runtime.list()) == 1 and stopped[2].count == 1 + assert len(stopped[1].runtime.list()) == 1 + assert stopped[2].count == 1 def test_owned_and_default_recovery_retries_cannot_switch_accepted_policy(stopped): @@ -220,7 +227,8 @@ def test_changed_live_owner_role_requires_a_fresh_ownership_review_even_when_inp recovery(stopped, "create", request) _value, fresh = owned_request(stopped) assert fresh["ownership_review"] != request["ownership_review"] - assert recovery(stopped, "create", fresh)["state"] == "queued" and stopped[2].count == 1 + assert recovery(stopped, "create", fresh)["state"] == "queued" + assert stopped[2].count == 1 def test_ownership_audit_outlives_child_retention_until_source_retention_cascades_it(standalone): diff --git a/test/unit_test/headless/test_agent_recovery_surfaces.py b/test/unit_test/headless/test_agent_recovery_surfaces.py index 5e8b655a3..30e23f77f 100644 --- a/test/unit_test/headless/test_agent_recovery_surfaces.py +++ b/test/unit_test/headless/test_agent_recovery_surfaces.py @@ -26,8 +26,9 @@ def test_public_facades_and_cli_forward_the_same_review_request(tmp_path, monkey arguments = build_parser().parse_args(["agent-task", "recovery", "preview", str(path)]) assert run(arguments) == {"approval_required": True} assert call.call_count == 2 + parser = build_parser() with pytest.raises(SystemExit): - build_parser().parse_args(["agent-task", "recovery", "preview"]) + parser.parse_args(["agent-task", "recovery", "preview"]) def test_real_service_authenticates_preview_and_deduplicates_explicit_creation(service, monkeypatch): @@ -72,7 +73,8 @@ def test_http_dynamic_replan_preserves_scope_and_waits_for_new_results(service, client.call("pause", {"task_id": source.task_id}) view = client.call("recovery", {"operation": "preview", "request": {"task_id": source.task_id}}) instance.runtime.background.start.assert_not_called() - assert view["draft"] is None and view["review_template"]["goal"] == "" + assert view["draft"] is None + assert view["review_template"]["goal"] == "" reviewed = view["review_template"] reviewed["goal"] = "Process only the remaining field and read its final value" child = client.call("recovery", {"operation": "create", "request": {"task_id": source.task_id, diff --git a/test/unit_test/headless/test_agent_remote_effect.py b/test/unit_test/headless/test_agent_remote_effect.py index 16a39bf22..ff7d570d0 100644 --- a/test/unit_test/headless/test_agent_remote_effect.py +++ b/test/unit_test/headless/test_agent_remote_effect.py @@ -49,12 +49,14 @@ def notify(event): ]) def test_wrong_incomplete_or_coerced_settlement_never_emits_end_or_allows_reuse(proof): controller, calls, events = RemoteEffectController(GUARD, DEVICE), [], [] + settling = action(calls, proof=proof) with pytest.raises(AgentTaskError): - controller.perform(action(calls, proof=proof), events.append, lambda: None) + controller.perform(settling, events.append, lambda: None) assert calls == ['invoke'] assert [event['kind'] for event in events] == ['remote_begin'] + reuse = action(calls) with pytest.raises(AgentTaskError, match='recovery'): - controller.perform(action(calls), events.append, lambda: None) + controller.perform(reuse, events.append, lambda: None) assert calls == ['invoke'] @@ -73,11 +75,13 @@ def notify(event): if event['kind'] == fail_phase: raise AgentTaskError('fixture lost ACK') + first = action(calls) with pytest.raises(AgentTaskError, match='ACK'): - controller.perform(action(calls), notify, lambda: None) + controller.perform(first, notify, lambda: None) assert calls == ([] if fail_phase == 'remote_begin' else ['invoke']) + second = action(calls) with pytest.raises(AgentTaskError, match='recovery'): - controller.perform(action(calls), notify, lambda: None) + controller.perform(second, notify, lambda: None) class Adapter: @@ -130,7 +134,8 @@ def test_child_configuration_cannot_equate_json_boolean_or_float_to_integer(valu sink, adapter = io.BytesIO(), Adapter() RemoteWorker(source, sink, adapter).run() replies = [json.loads(line) for line in sink.getvalue().splitlines()] - assert replies[0]['result']['ready'] is True and 'error' in replies[1] + assert replies[0]['result']['ready'] is True + assert 'error' in replies[1] assert not adapter.calls diff --git a/test/unit_test/headless/test_agent_remote_journal.py b/test/unit_test/headless/test_agent_remote_journal.py index 479762765..238c36e6e 100644 --- a/test/unit_test/headless/test_agent_remote_journal.py +++ b/test/unit_test/headless/test_agent_remote_journal.py @@ -43,14 +43,16 @@ def test_settled_events_advance_sequence_without_persisting_tool_arguments(journ ]) def test_bad_events_never_change_durable_state(journal, change): before = journal.path.read_bytes() + bad = event(journal, **change) with pytest.raises(AgentTaskError): - journal.consume(event(journal, **change)) + journal.consume(bad) assert journal.path.read_bytes() == before def test_duplicate_begin_and_unmatched_end_keep_quarantine(journal): + unmatched = event(journal, "remote_end") with pytest.raises(AgentTaskError): - journal.consume(event(journal, "remote_end")) + journal.consume(unmatched) journal.consume(event(journal)) for proposal in (event(journal), event(journal, "remote_end", epoch="different")): with pytest.raises(AgentTaskError): @@ -72,8 +74,9 @@ def test_pending_effect_cannot_lose_its_operation_marker(journal, operation): @pytest.mark.parametrize("operation", [None, "observe", "configure"]) def test_read_only_operations_cannot_authorize_effects(journal, operation): journal.update({}, operation) + begin = event(journal) with pytest.raises(AgentTaskError, match="perform"): - journal.consume(event(journal)) + journal.consume(begin) def test_recovery_baseline_is_after_retirement_not_the_input_epoch(journal): @@ -105,8 +108,9 @@ def test_untrusted_or_incomplete_recovery_evidence_never_clears_effect(journal, def test_remote_witness_blocks_other_owners_and_generic_local_recovery(journal): journal.consume(event(journal)) resource = journal.path.with_suffix("") + other = RemoteInputJournal(resource, DEVICE) with pytest.raises(AgentTaskError, match="recovery"): - RemoteInputJournal(resource, DEVICE).acquire() + other.acquire() local = InputJournal(resource) with pytest.raises(AgentTaskError, match="device-specific"): local.acquire() @@ -121,8 +125,9 @@ def test_corrupt_or_redirected_witness_is_never_acquired(journal, field, value): data = journal.read() data[field] = value journal.path.write_text(json.dumps(data), encoding="utf-8") + redirected = RemoteInputJournal(journal.path.with_suffix(""), DEVICE) with pytest.raises(AgentTaskError): - RemoteInputJournal(journal.path.with_suffix(""), DEVICE).acquire() + redirected.acquire() def test_identity_is_physical_not_endpoint_app_or_profile(): diff --git a/test/unit_test/headless/test_agent_remote_worker.py b/test/unit_test/headless/test_agent_remote_worker.py index cc1a5c9ba..653c17f44 100644 --- a/test/unit_test/headless/test_agent_remote_worker.py +++ b/test/unit_test/headless/test_agent_remote_worker.py @@ -91,7 +91,8 @@ def test_real_worker_brackets_single_sdk_calls_and_refreshes_sequence(tmp_path, assert adapter.ready(fixture_scope) == (True, '') assert adapter.perform('AC_increment', {}) == {'value': 1} assert (tmp_path / 'effects').read_text().splitlines() == ['invoke', 'invoke'] - assert adapter._journal.sequence == 3 and not adapter._journal.pending + assert adapter._journal.sequence == 3 + assert not adapter._journal.pending assert adapter.observe() == {'user': None, 'role': None} finally: release_fixture(adapter) @@ -127,7 +128,8 @@ def test_local_return_or_failure_cannot_clear_unproved_remote_effect(tmp_path, f adapter.perform('AC_increment', {'mode': mode}) assert 'private-password-and-command' not in str(error.value) assert (tmp_path / 'effects').read_text().splitlines() == ['invoke'] - assert adapter._channel.retired and adapter._journal.pending + assert adapter._channel.retired + assert adapter._journal.pending with pytest.raises(AgentTaskError, match='quarantined'): adapter.cleanup() finally: @@ -162,7 +164,8 @@ def checkpoint(): with pytest.raises((CancelledError, AgentTaskError)): adapter.perform('AC_increment', {'mode': 'hang'}) assert (tmp_path / 'effects').read_text().splitlines() == ['invoke'] - assert adapter._journal.pending and adapter._channel.retired + assert adapter._journal.pending + assert adapter._channel.retired assert not adapter.can_reobserve finally: release_fixture(adapter) @@ -239,6 +242,7 @@ def revoke(journal, event): with pytest.raises(AgentTaskError): adapter.perform('AC_increment', {}) assert not (tmp_path / 'effects').exists() - assert adapter._journal.pending and adapter._channel.retired + assert adapter._journal.pending + assert adapter._channel.retired finally: release_fixture(adapter) diff --git a/test/unit_test/headless/test_agent_request_lookup.py b/test/unit_test/headless/test_agent_request_lookup.py index 109a4f791..799e415fa 100644 --- a/test/unit_test/headless/test_agent_request_lookup.py +++ b/test/unit_test/headless/test_agent_request_lookup.py @@ -21,9 +21,11 @@ def test_lookup_miss_then_late_acceptance_does_not_create_or_dispatch_work(tmp_p assert runtime.list() == [] task = runtime.submit(specification(), idempotency_key="late-key") result = runtime.lookup("late-key") - assert result["found"] is True and result["task"]["task_id"] == task.task_id + assert result["found"] is True + assert result["task"]["task_id"] == task.task_id assert result["task"]["state"] == "queued" - assert target.calls == [] and not runtime._leases + assert target.calls == [] + assert not runtime._leases assert runtime.store.actions(task.task_id) == [] finally: runtime.close() @@ -89,7 +91,8 @@ def test_accepted_key_survives_service_restart_without_resume(tmp_path): replacement = AgentRuntime(tmp_path / "tasks.sqlite", targets={"fixture": FakeTarget()}) try: found = replacement.lookup("stable-key") - assert found["task"]["task_id"] == task.task_id and found["task"]["state"] == "queued" + assert found["task"]["task_id"] == task.task_id + assert found["task"]["state"] == "queued" assert not replacement._leases finally: replacement.close() @@ -115,12 +118,15 @@ def lose_submit(url, **kwargs): monkeypatch.setattr(client_module, "request_json", lose_submit) try: with authorization_scope(AuthorizationContext("alice", "operator")): + spec = specification().to_dict() with pytest.raises(AgentTaskError, match="lost accepted reply"): - client.call("submit", {"spec": specification().to_dict(), "idempotency_key": "lost-reply"}) + client.call("submit", {"spec": spec, "idempotency_key": "lost-reply"}) result = client.call("lookup", {"idempotency_key": "lost-reply"}) - assert result["found"] is True and result["task"]["state"] == "queued" + assert result["found"] is True + assert result["task"]["state"] == "queued" assert calls == ["submit", "lookup"] - assert len(service.runtime.list()) == 1 and not service.runtime._leases + assert len(service.runtime.list()) == 1 + assert not service.runtime._leases assert target.calls == [] with authorization_scope(AuthorizationContext("bob", "operator")): assert client.call("lookup", {"idempotency_key": "lost-reply"})["found"] is False @@ -143,7 +149,8 @@ def test_public_command_mcp_and_api_share_read_only_lookup_contract(): assert ac.lookup_agent_task is api.lookup_agent_task is core.lookup_agent_task is task_api.lookup_agent_task entry = CATALOG["AC_agent_task_lookup"] - assert entry[0] is ac.lookup_agent_task and entry[3] is True + assert entry[0] is ac.lookup_agent_task + assert entry[3] is True registry = build_default_tool_registry() tool = next((tool for tool in registry if tool.name == "ac_agent_task_lookup"), None) assert tool is not None diff --git a/test/unit_test/headless/test_agent_request_match.py b/test/unit_test/headless/test_agent_request_match.py index 46d671a35..42c58c500 100644 --- a/test/unit_test/headless/test_agent_request_match.py +++ b/test/unit_test/headless/test_agent_request_match.py @@ -23,9 +23,11 @@ def test_matching_normalizes_defaults_and_destination_roots_without_dispatch(tmp expected["roots"] = [str(root / "child" / "..")] expected = dict(reversed(list(expected.items()))) match = runtime.lookup("key", expected) - assert match["spec_match_contract"] == 1 and match["spec_match"] == "matches" + assert match["spec_match_contract"] == 1 + assert match["spec_match"] == "matches" assert match["task"]["task_id"] == accepted.task_id - assert target.calls == [] and not runtime._leases + assert target.calls == [] + assert not runtime._leases assert len(runtime.list()) == 1 finally: runtime.close() @@ -51,9 +53,12 @@ def test_changed_reviewed_spec_is_reported_without_resubmission(tmp_path, change else: expected["success_criteria"][0]["value"] = 2 result = runtime.lookup("key", expected) - assert result["found"] is True and result["spec_match"] == "differs" + assert result["found"] is True + assert result["spec_match"] == "differs" assert result["task"]["task_id"] == accepted.task_id - assert target.calls == [] and not runtime._leases and len(runtime.list()) == 1 + assert target.calls == [] + assert not runtime._leases + assert len(runtime.list()) == 1 finally: runtime.close() @@ -66,7 +71,8 @@ def test_lookup_remains_readable_but_matching_is_unverifiable_when_target_is_abs replacement = AgentRuntime(path, targets={}) try: result = replacement.lookup("key", specification()) - assert result["found"] is True and result["task"]["task_id"] == accepted.task_id + assert result["found"] is True + assert result["task"]["task_id"] == accepted.task_id assert result["spec_match"] == "unverifiable" assert not replacement._leases finally: @@ -80,7 +86,8 @@ def test_spec_match_does_not_search_another_owner_or_create_missing_requests(tmp runtime.submit(specification(), idempotency_key="key") with authorization_scope(AuthorizationContext("bob", "operator")): result = runtime.lookup("key", specification()) - assert result["found"] is False and result["spec_match"] == "not_found" + assert result["found"] is False + assert result["spec_match"] == "not_found" assert len(runtime.list()) == 1 finally: runtime.close() @@ -94,8 +101,10 @@ def test_http_spec_match_is_closed_and_never_dispatches(tmp_path): original = specification().to_dict() accepted = client.call("submit", {"spec": original, "idempotency_key": "key"}) result = client.call("lookup", {"idempotency_key": "key", "spec": original}) - assert result["spec_match"] == "matches" and result["task"]["task_id"] == accepted["task_id"] - assert not service.runtime._leases and service.runtime.store.actions(accepted["task_id"]) == [] + assert result["spec_match"] == "matches" + assert result["task"]["task_id"] == accepted["task_id"] + assert not service.runtime._leases + assert service.runtime.store.actions(accepted["task_id"]) == [] for invalid in (None, [], {"unknown": True}): with pytest.raises(AgentTaskError): client.call("lookup", {"idempotency_key": "key", "spec": invalid}) @@ -116,8 +125,9 @@ def reject(_spec): raise error monkeypatch.setattr(runtime, "_validated_spec", reject) + requested = specification() with pytest.raises(type(error)): - runtime.lookup("key", specification()) + runtime.lookup("key", requested) assert not runtime._leases finally: runtime.close() diff --git a/test/unit_test/headless/test_agent_resume_gate.py b/test/unit_test/headless/test_agent_resume_gate.py index a0032c9aa..aa27d6f87 100644 --- a/test/unit_test/headless/test_agent_resume_gate.py +++ b/test/unit_test/headless/test_agent_resume_gate.py @@ -78,10 +78,12 @@ def test_only_fresh_state_kinds_are_allowed_and_the_count_is_bounded(): def test_submission_rejects_an_invalid_resume_condition(managed): runtime, _target = managed + unknown = spec(resume_conditions=[{"kind": "actions_complete"}]) + incomplete = spec(resume_conditions=[{"kind": "screen"}]) with pytest.raises(AgentTaskError): - runtime.submit(spec(resume_conditions=[{"kind": "actions_complete"}])) + runtime.submit(unknown) with pytest.raises(AgentTaskError): - runtime.submit(spec(resume_conditions=[{"kind": "screen"}])) + runtime.submit(incomplete) def test_the_first_action_waits_while_the_state_is_not_re_established(managed): diff --git a/test/unit_test/headless/test_agent_retained_browser_collection.py b/test/unit_test/headless/test_agent_retained_browser_collection.py index 511e57c86..41f5e9c05 100644 --- a/test/unit_test/headless/test_agent_retained_browser_collection.py +++ b/test/unit_test/headless/test_agent_retained_browser_collection.py @@ -56,9 +56,11 @@ def test_distinct_effect_tasks_keep_their_own_process_on_one_profile(): assert pool.require(source("second")) is second assert first.channel is not second.channel pool.discard("first") - assert first.channel.retired and not second.channel.retired + assert first.channel.retired + assert not second.channel.retired + discarded = source() with pytest.raises(AgentTaskError, match="unavailable"): - pool.require(source()) + pool.require(discarded) pool.close() assert second.channel.retired @@ -85,8 +87,9 @@ def test_absolute_300_second_expiry_does_not_extend_on_borrow(): assert pool.borrow(source()) is entry pool.release(entry) clock[0] = 310 + expired = source() with pytest.raises(AgentTaskError, match="unavailable"): - pool.borrow(source()) + pool.borrow(expired) assert entry.channel.retired @@ -111,7 +114,8 @@ def test_capacity_counts_pending_reservations_and_never_evicts_a_borrower(): entries = [retain(pool, source(str(index))) for index in range(15)] borrowed = pool.borrow(source("0")) with pool.reserve(source("pending")) as pending: - assert pending is not None and pool.full + assert pending is not None + assert pool.full with pool.reserve(source("overflow")) as refused: assert refused is None assert not borrowed.channel.retired @@ -130,12 +134,14 @@ def test_unconfirmed_reaping_stays_tracked_unavailable_and_counts_against_capaci with pytest.raises(AgentTaskError): pool.discard("failed") assert pool.full + failed = source("failed") with pytest.raises(AgentTaskError): - pool.require(source("failed")) + pool.require(failed) entry.channel.failure = False entry.channel.confirmed = True pool.close() - assert entry.channel.retired and not pool.full + assert entry.channel.retired + assert not pool.full def test_collection_close_invalidates_borrow_and_closes_other_owned_processes_after_a_failure(): @@ -146,8 +152,9 @@ def test_collection_close_invalidates_borrow_and_closes_other_owned_processes_af with pytest.raises(AgentTaskError): pool.close() assert second.channel.retired + closed = source("borrowed") with pytest.raises(AgentTaskError): - pool.require(source("borrowed")) + pool.require(closed) pool.release(borrowed) first.channel.failure = False pool.close() @@ -157,20 +164,22 @@ def test_collection_close_invalidates_borrow_and_closes_other_owned_processes_af def test_invalid_proof_cannot_leave_an_orphaned_detached_process(): pool = collection([0]) process = OwnedProcess() - with pytest.raises(AgentTaskError): - with pool.reserve(source()) as entry: - pool.commit(entry, process, {**PROOF, "input_supported": True}) + row = source() + reservation = pool.reserve(row) + with pytest.raises(AgentTaskError), reservation as entry: + pool.commit(entry, process, {**PROOF, "input_supported": True}) assert process.retired with pytest.raises(AgentTaskError): - pool.require(source()) + pool.require(row) def test_lost_owned_process_never_launches_or_recreates_a_context(): pool = collection([0]) entry = retain(pool, source()) entry.channel.close() + lost = source() with pytest.raises(AgentTaskError, match="unavailable"): - pool.borrow(source()) + pool.borrow(lost) assert entry.channel.retired pool.close() @@ -178,9 +187,9 @@ def test_lost_owned_process_never_launches_or_recreates_a_context(): def test_closed_collection_cannot_create_new_context_reservations(): pool = collection([0]) pool.close() - with pytest.raises(AgentTaskError, match="closed"): - with pool.reserve(source("new")): - pytest.fail("A closed owner cannot accept more retained process ownership") + reservation = pool.reserve(source("new")) + with pytest.raises(AgentTaskError, match="closed"), reservation: + pytest.fail("A closed owner cannot accept more retained process ownership") def test_source_binding_preserves_json_value_types(): @@ -206,9 +215,10 @@ def test_detachment_time_cannot_extend_the_absolute_retained_lifetime(): clock[0] = 20 pool.commit(entry, process, PROOF) clock[0] = 300 + expired = source() try: with pytest.raises(AgentTaskError, match="unavailable"): - pool.borrow(source()) + pool.borrow(expired) assert process.retired finally: pool.close() @@ -229,7 +239,8 @@ def test_failed_detached_transfer_has_one_cleanup_owner_and_can_be_reaped_after_ assert pool.sources() == [row] process.failure = False maintain_retained(runtime) - assert process.retired and pool.sources() == [] + assert process.retired + assert pool.sources() == [] finally: process.failure = False pool.close() diff --git a/test/unit_test/headless/test_agent_retained_browser_detach.py b/test/unit_test/headless/test_agent_retained_browser_detach.py index 65e078ca3..50940dfbf 100644 --- a/test/unit_test/headless/test_agent_retained_browser_detach.py +++ b/test/unit_test/headless/test_agent_retained_browser_detach.py @@ -24,7 +24,8 @@ def request(self, operation, payload, **_options): if operation == "bootstrap": assert payload == {} return self.bootstrap - assert operation == "retire_browser" and payload == {} + assert operation == "retire_browser" + assert payload == {} if isinstance(self.response, Exception): raise self.response return self.response @@ -45,11 +46,13 @@ def close(self): def test_detach_transfers_actual_channel_after_proof_and_releases_only_input_resources(owned): channel, proof = owned.target.detach_readonly() - assert channel is owned.channel and proof == PROOF + assert channel is owned.channel + assert proof == PROOF assert owned.events == ["bootstrap", "journal-update", "retire_browser", "journal-release", "resource-close"] assert not channel.retired assert owned.target._channel is None - assert owned.target._resource is None and owned.target._journal is None + assert owned.target._resource is None + assert owned.target._journal is None assert owned.target._pending is None @@ -73,9 +76,11 @@ def test_unsettled_input_cannot_be_detached_or_falsely_released(owned, damage): def test_unsupported_sdk_keeps_original_normal_cleanup_owner(owned): owned.channel.response = {"supported": False} assert owned.target.detach_readonly() is None - assert owned.target._channel is owned.channel and not owned.channel.retired + assert owned.target._channel is owned.channel + assert not owned.channel.retired assert owned.target._pending is None - assert "journal-release" not in owned.events and "resource-close" not in owned.events + assert "journal-release" not in owned.events + assert "resource-close" not in owned.events @pytest.mark.parametrize("response", [ @@ -88,7 +93,8 @@ def test_failed_or_forged_retirement_preserves_source_ownership_and_prevents_fre with pytest.raises(AgentTaskError): owned.target.detach_readonly() assert owned.target._channel is owned.channel - assert owned.target._resource is not None and owned.target._journal is not None + assert owned.target._resource is not None + assert owned.target._journal is not None assert owned.target._pending == "retire_browser" before = list(owned.events) owned.channel.response = dict(PROOF) @@ -112,4 +118,5 @@ def test_legacy_helper_capability_does_not_break_normal_browser_cleanup(owned, v assert owned.target.detach_readonly() is None assert owned.events == ["bootstrap"] assert owned.target._pending is None - assert owned.target._channel is owned.channel and not owned.channel.retired + assert owned.target._channel is owned.channel + assert not owned.channel.retired diff --git a/test/unit_test/headless/test_agent_retained_browser_lifecycle.py b/test/unit_test/headless/test_agent_retained_browser_lifecycle.py index 434e2b37d..1f4282f87 100644 --- a/test/unit_test/headless/test_agent_retained_browser_lifecycle.py +++ b/test/unit_test/headless/test_agent_retained_browser_lifecycle.py @@ -41,11 +41,13 @@ def test_failed_retained_reaping_keeps_runtime_open_and_service_ownership_until_ try: with pytest.raises(AgentTaskError): runtime.close() - assert runtime._closed is False and not process.retired + assert runtime._closed is False + assert not process.retired finally: process.failure = False runtime.close() - assert runtime._closed is True and process.retired + assert runtime._closed is True + assert process.retired def test_nonbrowser_input_retirement_survives_unreadable_action_evidence(tmp_path, monkeypatch): diff --git a/test/unit_test/headless/test_agent_retained_browser_sdk.py b/test/unit_test/headless/test_agent_retained_browser_sdk.py index 78496198d..3da0c20d5 100644 --- a/test/unit_test/headless/test_agent_retained_browser_sdk.py +++ b/test/unit_test/headless/test_agent_retained_browser_sdk.py @@ -85,7 +85,8 @@ def test_input_lease_releases_while_runtime_still_owns_the_original_readonly_pro source = live.runtime.submit(live.definition).task_id assert run_task(live.runtime, source).state == "waiting_user" assert source not in live.runtime._leases - assert len(live.processes) == 1 and live.processes[0].poll() is None + assert len(live.processes) == 1 + assert live.processes[0].poll() is None live.runtime.close() assert live.processes[0].poll() is not None @@ -110,7 +111,8 @@ def test_older_sdk_closes_normally_but_cannot_create_a_completion_context(live): assert live.processes[0].poll() is not None with pytest.raises(AgentTaskError, match="context.*unavailable"): completion(live.runtime, source) - assert len(live.processes) == 1 and source not in live.runtime._leases + assert len(live.processes) == 1 + assert source not in live.runtime._leases def test_expired_context_is_reaped_without_replacement_or_historical_fallback(live, monkeypatch): @@ -119,7 +121,8 @@ def test_expired_context_is_reaped_without_replacement_or_historical_fallback(li monkeypatch.setattr(live.runtime._retained_browsers, "_clock", lambda: float("inf")) with pytest.raises(AgentTaskError, match="context.*unavailable"): completion(live.runtime, source) - assert live.processes[0].poll() is not None and len(live.processes) == 1 + assert live.processes[0].poll() is not None + assert len(live.processes) == 1 def test_disabled_target_maintenance_retires_the_original_owned_context(live): @@ -145,7 +148,8 @@ def test_forged_live_capability_description_cannot_revive_a_reaped_context(live, monkeypatch.setattr(entry.channel, "request", original) with pytest.raises(AgentTaskError, match="unavailable"): completion(runtime, source) - assert live.processes[0].poll() is not None and len(live.processes) == 1 + assert live.processes[0].poll() is not None + assert len(live.processes) == 1 def test_source_snapshot_change_during_live_read_cannot_publish_completion(live, monkeypatch): @@ -189,7 +193,8 @@ def cancelled(operation, payload, **options): with pytest.raises(CancelledError): runtime.finish(child) assert runtime.get(child).state == "cancelled" - assert live.processes[0].poll() is not None and len(live.processes) == 1 + assert live.processes[0].poll() is not None + assert len(live.processes) == 1 @pytest.fixture @@ -232,7 +237,8 @@ def test_skill_publication_uses_actual_trial_and_separate_same_context_completio accepted = runtime.skills.control("accept", { "candidate_id": value.candidate["id"], "task_id": value.trial, "completion_task_id": value.trial_reader, "expected_version": 0, "note": "Reviewed actual new trial and original-context independent evidence"}) - assert accepted["version"] == 1 and accepted["state"] == "accepted" + assert accepted["version"] == 1 + assert accepted["state"] == "accepted" assert accepted["publication"]["task_id"] == value.trial assert accepted["publication"]["completion_review"]["task_id"] == value.trial_reader assert runtime.store.actions(value.trial) == before @@ -264,7 +270,8 @@ def test_dead_source_helper_is_refused_by_actual_probe_without_new_process(live) live.processes[0].wait(timeout=3) with pytest.raises(AgentTaskError): completion(live.runtime, source) - assert live.processes[0].poll() is not None and len(live.processes) == 1 + assert live.processes[0].poll() is not None + assert len(live.processes) == 1 def test_explicit_private_scope_renewal_reads_original_context_without_reviving_old_origin(live, tmp_path): @@ -308,7 +315,8 @@ def test_exact_creation_retry_returns_existing_task_after_context_retirement_wit runtime._retained_browsers.discard(source) assert runtime.recovery.control("create_completion", request)["task_id"] == child assert runtime.get(child).state == "queued" - assert len(live.processes) == 1 and live.processes[0].poll() is not None + assert len(live.processes) == 1 + assert live.processes[0].poll() is not None def test_source_change_during_admission_probe_rolls_back_the_private_completion_task(live, monkeypatch): @@ -365,9 +373,11 @@ def test_public_surfaces_verify_the_actual_retained_browser_without_replaying_in assert run_tool_call("AC_agent_task_recovery", { "operation": "create_completion", "request": request})["task_id"] == child result = AgentRuntimeClient(connection).call("finish", {"task_id": child}) - assert result["state"] == "succeeded" and result["metadata"]["input_released"] is True + assert result["state"] == "succeeded" + assert result["metadata"]["input_released"] is True assert result["metadata"]["completion_review"]["effect_task"] == source - assert runtime.store.actions(source) == original and runtime.store.actions(child) == [] + assert runtime.store.actions(source) == original + assert runtime.store.actions(child) == [] assert len(live.processes) == 1 finally: server.stop() diff --git a/test/unit_test/headless/test_agent_retained_browser_worker.py b/test/unit_test/headless/test_agent_retained_browser_worker.py index 6f81bc63a..4b68516c4 100644 --- a/test/unit_test/headless/test_agent_retained_browser_worker.py +++ b/test/unit_test/headless/test_agent_retained_browser_worker.py @@ -181,8 +181,9 @@ def test_reader_scope_bind_does_not_reconfigure_original_input_or_change_context assert configured.worker._dispatch("bind_browser_reader", {"spec": reader_spec(configured)}) == PROOF assert configured.worker._spec is original assert configured.worker._dispatch("observe", {})["identity"] == configured.target.observe()["identity"] + reader = reader_spec(configured) with pytest.raises(AgentTaskError): - configured.worker._dispatch("configure", {"spec": reader_spec(configured)}) + configured.worker._dispatch("configure", {"spec": reader}) @pytest.mark.parametrize("changed", [ @@ -195,8 +196,9 @@ def test_reader_scope_bind_does_not_reconfigure_original_input_or_change_context ]) def test_readonly_scope_bind_cannot_broaden_original_permissions_or_change_evidence(configured, changed): configured.worker._dispatch("retire_browser", {}) + broadened = {**reader_spec(configured), **changed} with pytest.raises(AgentTaskError): - configured.worker._dispatch("bind_browser_reader", {"spec": {**reader_spec(configured), **changed}}) + configured.worker._dispatch("bind_browser_reader", {"spec": broadened}) assert configured.session.actions == [] diff --git a/test/unit_test/headless/test_agent_role_boundaries.py b/test/unit_test/headless/test_agent_role_boundaries.py index e9ed321f8..e1f72ca99 100644 --- a/test/unit_test/headless/test_agent_role_boundaries.py +++ b/test/unit_test/headless/test_agent_role_boundaries.py @@ -68,7 +68,8 @@ def test_an_injected_tool_outside_the_reviewed_scope_is_refused_before_dispatch( handle = runtime.submit(spec()) model = ObedientModel({"tool": "AC_shell_command", "arguments": {"command": "whoami"}}) assert run_task(runtime, handle.task_id, backend=model).state == "waiting_user" - assert target.calls == [] and INJECTION in model.seen + assert target.calls == [] + assert INJECTION in model.seen def test_scope_fields_smuggled_into_a_proposal_never_reach_the_spec_or_the_target(managed): @@ -79,16 +80,19 @@ def test_scope_fields_smuggled_into_a_proposal_never_reach_the_spec_or_the_targe run_task(runtime, handle.task_id, backend=model) assert target.calls == [("AC_increment", {})] stored = runtime.store.get(handle.task_id)["spec"] - assert stored["allowed_tools"] == ["AC_increment"] and stored["roots"] == [] + assert stored["allowed_tools"] == ["AC_increment"] + assert stored["roots"] == [] def test_a_planner_claiming_completion_is_not_evidence(managed): runtime, target = managed handle = runtime.submit(spec()) result = run_task(runtime, handle.task_id, backend=ObedientModel({"stop": True, "message": "task complete"})) - assert result.state == "waiting_user" and target.calls == [] + assert result.state == "waiting_user" + assert target.calls == [] reports = runtime.get(handle.task_id).verification - assert reports and all(item["status"] != "passed" for item in reports) + assert reports + assert all(item["status"] != "passed" for item in reports) def test_a_path_outside_the_reviewed_roots_is_refused_even_when_the_page_names_it(tmp_path): @@ -119,5 +123,6 @@ def forbidden(*_args, **_kwargs): "allowed_origins": ["https://allowed.test"], "url": "https://allowed.test/form", "fields": [{"column": "name", "selector": "#field"}], "new_record_selector": "#new", "submit_selector": "#save", "confirmation_selector": "#confirmation", "confirmation_value": "saved"}) - assert isinstance(draft, dict) and draft + assert isinstance(draft, dict) + assert draft assert not list(tmp_path.glob("*.sqlite*")) diff --git a/test/unit_test/headless/test_agent_runtime.py b/test/unit_test/headless/test_agent_runtime.py index 853b32dae..3c0375b25 100644 --- a/test/unit_test/headless/test_agent_runtime.py +++ b/test/unit_test/headless/test_agent_runtime.py @@ -156,9 +156,9 @@ def test_file_completion_cannot_escape_task_roots(managed, tmp_path): outside.write_text("secret", encoding="utf-8") root = tmp_path / "allowed" root.mkdir() + escaping = specification(roots=[str(root)], success_criteria=[{"kind": "file_exists", "path": str(outside)}]) with pytest.raises(AgentTaskError, match="root"): - runtime.submit(specification(roots=[str(root)], - success_criteria=[{"kind": "file_exists", "path": str(outside)}])) + runtime.submit(escaping) def test_pause_does_not_transfer_control_before_running_action_retires(managed): diff --git a/test/unit_test/headless/test_agent_runtime_runner.py b/test/unit_test/headless/test_agent_runtime_runner.py index 7701342f8..625b03c21 100644 --- a/test/unit_test/headless/test_agent_runtime_runner.py +++ b/test/unit_test/headless/test_agent_runtime_runner.py @@ -153,5 +153,6 @@ def transport(call): @pytest.mark.parametrize("options", [{"model": "some-cloud-model"}, {"model": "local", "endpoint": "https://example.org"}, {}]) def test_local_backend_rejects_remote_or_implicit_models(options): + spec = task_spec(mode="ollama", options=options) with pytest.raises(AgentTaskError): - OllamaTaskBackend(task_spec(mode="ollama", options=options)) + OllamaTaskBackend(spec) diff --git a/test/unit_test/headless/test_agent_runtime_service.py b/test/unit_test/headless/test_agent_runtime_service.py index b741d79c9..5aecbf201 100644 --- a/test/unit_test/headless/test_agent_runtime_service.py +++ b/test/unit_test/headless/test_agent_runtime_service.py @@ -80,9 +80,10 @@ def test_emergency_stop_keeps_other_principals_tasks(service): def test_viewer_cannot_become_local_operator(service): client, instance = service + request = {"spec": spec()} with authorization_scope(AuthorizationContext("reader", "viewer")): with pytest.raises(AgentTaskError): - client.call("submit", {"spec": spec()}) + client.call("submit", request) assert instance.runtime.list() == [] @@ -122,8 +123,9 @@ def test_abandoned_external_lease_expires_without_another_client_request(service def test_second_runtime_cannot_recover_live_tasks(service): _client, instance = service + targets = {"counter": Counter()} with pytest.raises(AgentTaskError, match="owns"): - AgentRuntimeService(instance.directory, targets={"counter": Counter()}) + AgentRuntimeService(instance.directory, targets=targets) def test_internal_skill_runs_after_submission(service): diff --git a/test/unit_test/headless/test_agent_screen_evidence.py b/test/unit_test/headless/test_agent_screen_evidence.py index 631715835..ab2f606f0 100644 --- a/test/unit_test/headless/test_agent_screen_evidence.py +++ b/test/unit_test/headless/test_agent_screen_evidence.py @@ -89,7 +89,8 @@ def test_bounded_desktop_labels_keep_safe_matches_and_exclude_mask_placeholders( monkeypatch.setattr(target, "_check_foreground", lambda: (1, "Owned", 2)) monkeypatch.setattr(accessibility_api, "list_accessibility_elements", lambda **_kwargs: controls) observation = target.observe() - assert observation["semantic_observation"] is True and observation["text_redacted"] is True + assert observation["semantic_observation"] is True + assert observation["text_redacted"] is True assert len(observation["elements"]) == 2 assert "SECRET" not in str(observation) assert status(observation, {"text_present": "Saved"}) == "passed" @@ -105,7 +106,8 @@ def test_bounded_browser_previews_never_prove_global_absence(frames): "elements": [{"selector": "#save", "text": "Saved", "is_visible": True, "is_enabled": True}, {"selector": "#hidden", "text": "Hidden", "is_visible": False}]}) observation = target.observe() - assert observation["semantic_observation"] is True and observation["text_redacted"] is True + assert observation["semantic_observation"] is True + assert observation["text_redacted"] is True assert status(observation, {"text_present": "Saved"}) == "passed" assert status(observation, {"text_absent": "Saved"}) == "failed" for text in ("Missing", "Other body text", "Hidden"): diff --git a/test/unit_test/headless/test_agent_skill_authorization_surfaces.py b/test/unit_test/headless/test_agent_skill_authorization_surfaces.py index ac166d25f..a5b4ca29b 100644 --- a/test/unit_test/headless/test_agent_skill_authorization_surfaces.py +++ b/test/unit_test/headless/test_agent_skill_authorization_surfaces.py @@ -20,7 +20,8 @@ def test_cli_exposes_explicit_skill_authorization_operations(): parser = build_parser() for operation in ("review_authorization", "create_authorized"): parsed = parser.parse_args(["agent-task", "skill", operation, "review.json"]) - assert parsed.skill_operation == operation and parsed.request == "review.json" + assert parsed.skill_operation == operation + assert parsed.request == "review.json" def test_authenticated_http_can_review_and_create_without_executing_or_publishing(published, monkeypatch): @@ -42,7 +43,8 @@ def test_authenticated_http_can_review_and_create_without_executing_or_publishin request = {"candidate_id": parent["id"], "snapshot": view["snapshot"], "spec": view["spec"], "authorization_review": view["authorization_review"], "note": "Reviewed report-root candidate"} child = client.call("skill", {"operation": "create_authorized", "request": request}) - assert child["state"] == "draft" and child["version"] == 0 + assert child["state"] == "draft" + assert child["version"] == 0 assert child["provenance"]["reauthorization"]["actor"] == "alice" start.assert_not_called() with origin_scope(current_id), authorization_scope(AuthorizationContext("bob", "operator")): diff --git a/test/unit_test/headless/test_agent_skill_completion.py b/test/unit_test/headless/test_agent_skill_completion.py index 6e7662259..ffdb6ce51 100644 --- a/test/unit_test/headless/test_agent_skill_completion.py +++ b/test/unit_test/headless/test_agent_skill_completion.py @@ -84,12 +84,15 @@ def test_publish_keeps_actual_validation_trial_and_separate_completion_provenanc receipts = runtime.store.actions(trial.task_id) manifest = trial.manifest.read_bytes() accepted = runtime.skills.control('accept', acceptance(trial)) - assert accepted['state'] == 'accepted' and accepted['version'] == 1 + assert accepted['state'] == 'accepted' + assert accepted['version'] == 1 assert accepted['publication']['task_id'] == trial.task_id review = accepted['publication']['completion_review'] - assert review['task_id'] == trial.child_id and review['effect_task'] == trial.task_id + assert review['task_id'] == trial.child_id + assert review['effect_task'] == trial.task_id assert runtime.get(trial.task_id).state == 'paused' - assert runtime.store.actions(trial.task_id) == receipts and runtime.store.actions(trial.child_id) == [] + assert runtime.store.actions(trial.task_id) == receipts + assert runtime.store.actions(trial.child_id) == [] assert trial.manifest.read_bytes() == manifest assert runtime.get(trial.child_id).metadata['completion_contract'] == 1 with runtime.store.connection() as connection: @@ -115,20 +118,23 @@ def changed(*, write=False): with original(write=write) as connection: yield connection monkeypatch.setattr(runtime.store, 'connection', changed) + request = acceptance(trial) with pytest.raises(AgentTaskError): - runtime.skills.control('accept', acceptance(trial)) + runtime.skills.control('accept', request) with original() as connection: state = connection.execute('SELECT state FROM agent_skill_candidates WHERE id=?', (trial.candidate['id'],)).fetchone()[0] publications = connection.execute('SELECT COUNT(*) FROM agent_skill_publications').fetchone()[0] - assert state == 'draft' and publications == 0 + assert state == 'draft' + assert publications == 0 @pytest.mark.parametrize('trial', ['retry'], indirect=True) def test_retried_reader_chain_retains_the_original_execution_trial(trial): value = trial.runtime.skills.control('accept', acceptance(trial)) review = value['publication']['completion_review'] - assert review['effect_task'] == trial.task_id and review['source_task'] != trial.task_id + assert review['effect_task'] == trial.task_id + assert review['source_task'] != trial.task_id assert trial.runtime.get(review['source_task']).state == 'cancelled' assert value['publication']['task_id'] == trial.task_id diff --git a/test/unit_test/headless/test_agent_skill_completion_authority.py b/test/unit_test/headless/test_agent_skill_completion_authority.py index 1a6e05d00..50e7f7855 100644 --- a/test/unit_test/headless/test_agent_skill_completion_authority.py +++ b/test/unit_test/headless/test_agent_skill_completion_authority.py @@ -22,8 +22,9 @@ def test_http_revoked_reviewer_is_typed_refusal_without_publication(trial, tmp_p connection = tmp_path / 'connection.json' connection.write_text(json.dumps({'url': server.base_url, 'token': server.token}), encoding='utf-8') trial.users.remove_user('reviewer') + client, request = AgentRuntimeClient(connection), acceptance(trial) with pytest.raises(AgentTaskError) as error: - AgentRuntimeClient(connection).call('skill', {'operation': 'accept', 'request': acceptance(trial)}) + client.call('skill', {'operation': 'accept', 'request': request}) assert '(409)' in str(error.value) assert_unpublished(trial) finally: @@ -37,8 +38,9 @@ def test_current_authority_is_required_before_adoption(trial, change): trial.users.set_role('reviewer', 'operator') else: trial.users.remove_user(change) + request = acceptance(trial) with pytest.raises((AgentTaskError, AuthorizationError)): - trial.runtime.skills.control('accept', acceptance(trial)) + trial.runtime.skills.control('accept', request) assert_unpublished(trial) @@ -50,13 +52,15 @@ def revoked(connection, candidate, row): check(connection, candidate, row) trial.users.remove_user('reviewer') monkeypatch.setattr(AgentSkills, '_check_trial', staticmethod(revoked)) + request = acceptance(trial) with pytest.raises((AgentTaskError, AuthorizationError)): - trial.runtime.skills.control('accept', acceptance(trial)) + trial.runtime.skills.control('accept', request) assert_unpublished(trial) @pytest.mark.parametrize('trial', ['named_caller'], indirect=True) def test_source_owner_cannot_implicitly_read_an_existing_admin_owned_child(trial): + request = acceptance(trial) with pytest.raises(AgentTaskError, match='another caller'): - trial.runtime.skills.control('accept', acceptance(trial)) + trial.runtime.skills.control('accept', request) assert_unpublished(trial) diff --git a/test/unit_test/headless/test_agent_skill_completion_security.py b/test/unit_test/headless/test_agent_skill_completion_security.py index 0a81f0297..0d2db2f23 100644 --- a/test/unit_test/headless/test_agent_skill_completion_security.py +++ b/test/unit_test/headless/test_agent_skill_completion_security.py @@ -39,15 +39,17 @@ def test_corrupt_or_stale_proof_cannot_publish(trial, damage): } with runtime.store.connection(write=True) as connection: connection.execute(*changes[damage]) + request = acceptance(trial) with pytest.raises(AgentTaskError): - runtime.skills.control('accept', acceptance(trial)) + runtime.skills.control('accept', request) assert_unpublished(trial) @pytest.mark.parametrize('identifier', [None, [], '', 'not-a-reviewed-completion']) def test_invalid_completion_identity_has_no_publication(trial, identifier): + request = {**acceptance(trial), 'completion_task_id': identifier} with pytest.raises(AgentTaskError): - trial.runtime.skills.control('accept', {**acceptance(trial), 'completion_task_id': identifier}) + trial.runtime.skills.control('accept', request) assert_unpublished(trial) @@ -62,8 +64,9 @@ def test_reader_cannot_masquerade_as_the_execution_trial(trial): def test_another_candidate_cannot_borrow_verified_effects(trial): other = trial.runtime.skills.control('record', {'task_id': trial.source_id, 'name': 'Different candidate'}) + request = {**acceptance(trial), 'candidate_id': other['id']} with pytest.raises(AgentTaskError, match='exact candidate'): - trial.runtime.skills.control('accept', {**acceptance(trial), 'candidate_id': other['id']}) + trial.runtime.skills.control('accept', request) assert_unpublished(trial) @@ -72,8 +75,9 @@ def test_unwinding_input_witness_blocks_publication(trial, which): identifier = getattr(trial, which) trial.runtime._executing.add(identifier) try: + request = acceptance(trial) with pytest.raises(AgentTaskError, match='retired|stopped'): - trial.runtime.skills.control('accept', acceptance(trial)) + trial.runtime.skills.control('accept', request) assert_unpublished(trial) finally: trial.runtime._executing.discard(identifier) @@ -85,8 +89,9 @@ def test_options_and_success_flags_do_not_create_private_completion_provenance(t definition['options'] = {**definition['options'], 'completion_review': {'effect_task': trial.task_id}} forged = runtime.submit(definition) runtime.store.update(forged.task_id, 'succeeded', verification=runtime.store.get(trial.child_id)['verification']) + request = {**acceptance(trial), 'completion_task_id': forged.task_id} with pytest.raises(AgentTaskError, match='private reviewed'): - runtime.skills.control('accept', {**acceptance(trial), 'completion_task_id': forged.task_id}) + runtime.skills.control('accept', request) assert_unpublished(trial) @@ -103,6 +108,7 @@ def test_malformed_private_binding_returns_typed_refusal(trial, damage): encoded = '{' if damage == 'invalid_json' else json.dumps(details) connection.execute('UPDATE agent_readonly_completions SET details=? WHERE task_id=?', (encoded, trial.child_id)) + request = acceptance(trial) with pytest.raises(AgentTaskError): - trial.runtime.skills.control('accept', acceptance(trial)) + trial.runtime.skills.control('accept', request) assert_unpublished(trial) diff --git a/test/unit_test/headless/test_agent_skill_completion_surfaces.py b/test/unit_test/headless/test_agent_skill_completion_surfaces.py index 2c9efe198..1554a5583 100644 --- a/test/unit_test/headless/test_agent_skill_completion_surfaces.py +++ b/test/unit_test/headless/test_agent_skill_completion_surfaces.py @@ -41,7 +41,8 @@ def test_shared_surfaces_publish_the_original_trial_with_explicit_completion(tri value = tools['ac_agent_skill'].invoke({'operation': 'accept', 'request': request}) else: value = AgentRuntimeClient(connection).call('skill', {'operation': 'accept', 'request': request}) - assert value['publication']['task_id'] == trial.task_id and value['version'] == 1 + assert value['publication']['task_id'] == trial.task_id + assert value['version'] == 1 assert value['publication']['completion_review']['task_id'] == trial.child_id assert trial.runtime.get(trial.task_id).state == 'paused' assert 'accept' in tools['ac_agent_skill'].input_schema['properties']['operation']['enum'] diff --git a/test/unit_test/headless/test_agent_skill_execution.py b/test/unit_test/headless/test_agent_skill_execution.py index a253fc0f1..f5f194141 100644 --- a/test/unit_test/headless/test_agent_skill_execution.py +++ b/test/unit_test/headless/test_agent_skill_execution.py @@ -76,7 +76,8 @@ def execution(tmp_path, request): with authorization_scope(reviewer): if kind.startswith('capture'): preview = runtime.recovery.control('preview', {'task_id': trial['task_id']}) - assert preview['draft'] is None and 'fresh_capture_required' in preview['issues'] + assert preview['draft'] is None + assert 'fresh_capture_required' in preview['issues'] reviewed = runtime.store.get(trial['task_id'])['spec'] reviewed['options'].pop('skill_pin') reviewed['actions'] = [edit('Last', 'different' if kind == 'capture_mismatch' else 'first')] @@ -116,17 +117,23 @@ def forbidden(*_args, **_kwargs): monkeypatch.setattr(execution.target, 'perform', forbidden) request = review_request(execution) review = runtime.skills.control('review_execution', request) - assert review['approval_required'] is True and review['publication_inherited'] is False + assert review['approval_required'] is True + assert review['publication_inherited'] is False assert runtime.skills.control('get', {'candidate_id': execution.candidate['id']})['state'] == 'draft' accepted = runtime.skills.control('accept', {**request, 'execution_review': review['execution_review'], 'expected_version': 0, 'note': 'Reviewed the exact full semantic effect chain'}) - assert accepted['version'] == 1 and accepted['publication']['task_id'] == execution.task_id + assert accepted['version'] == 1 + assert accepted['publication']['task_id'] == execution.task_id proof = accepted['publication']['execution_review'] - assert proof['task_id'] == execution.child_id and proof['source_task'] == execution.task_id - assert proof['verification_task'] == execution.child_id and len(proof['segments']) == 1 - assert 'store_path' not in json.dumps(proof) and 'arguments' not in json.dumps(proof) + assert proof['task_id'] == execution.child_id + assert proof['source_task'] == execution.task_id + assert proof['verification_task'] == execution.child_id + assert len(proof['segments']) == 1 + assert 'store_path' not in json.dumps(proof) + assert 'arguments' not in json.dumps(proof) assert runtime.get(execution.task_id).state == 'paused' - assert runtime.store.actions(execution.task_id) == receipts and execution.target.calls == calls + assert runtime.store.actions(execution.task_id) == receipts + assert execution.target.calls == calls with runtime.store.connection() as connection: trials = connection.execute('SELECT task_id FROM agent_skill_trials').fetchall() assert [row[0] for row in trials] == [execution.task_id] @@ -142,15 +149,17 @@ def accept_request(value, **changes): def test_multiple_real_recovery_segments_match_the_complete_original_plan(execution): accepted = execution.runtime.skills.control('accept', accept_request(execution)) proof = accepted['publication']['execution_review'] - assert len(proof['segments']) == 2 and proof['source_task'] == execution.task_id + assert len(proof['segments']) == 2 + assert proof['source_task'] == execution.task_id assert accepted['publication']['task_id'] == execution.task_id @pytest.mark.parametrize('execution', ['extra'], indirect=True) def test_a_successful_different_recovery_plan_cannot_publish_the_original_candidate(execution): assert execution.runtime.get(execution.child_id).state == 'succeeded' + request = review_request(execution) with pytest.raises(AgentTaskError, match='outside the original candidate'): - execution.runtime.skills.control('review_execution', review_request(execution)) + execution.runtime.skills.control('review_execution', request) assert execution.runtime.skills.control('get', {'candidate_id': execution.candidate['id']})['state'] == 'draft' diff --git a/test/unit_test/headless/test_agent_skill_execution_gui.py b/test/unit_test/headless/test_agent_skill_execution_gui.py index 063fead90..3331c76f5 100644 --- a/test/unit_test/headless/test_agent_skill_execution_gui.py +++ b/test/unit_test/headless/test_agent_skill_execution_gui.py @@ -24,7 +24,8 @@ def test_gui_prepares_original_trial_and_separate_execution_without_dispatch(mon 'execution_task_id': 'actual-final-executable-successor'} if operation == 'accept': expected.update(execution_review='', expected_version=0, note='') - assert request == expected and tab._skill_operation.currentData() == operation + assert request == expected + assert tab._skill_operation.currentData() == operation assert tab._pages.currentIndex() == tab._page_keys.index('agent_page_skills') finally: tab.close() diff --git a/test/unit_test/headless/test_agent_skill_execution_lifecycle.py b/test/unit_test/headless/test_agent_skill_execution_lifecycle.py index ae60b2e12..6e654cc58 100644 --- a/test/unit_test/headless/test_agent_skill_execution_lifecycle.py +++ b/test/unit_test/headless/test_agent_skill_execution_lifecycle.py @@ -24,8 +24,9 @@ def test_actual_capture_receipts_can_match_fresh_reviewed_literals_without_resto @pytest.mark.parametrize('execution', ['capture_mismatch'], indirect=True) def test_fresh_literal_recovery_cannot_claim_different_original_capture_semantics(execution): assert execution.runtime.get(execution.child_id).state == 'succeeded' + request = review_request(execution) with pytest.raises(AgentTaskError, match='semantic plan'): - execution.runtime.skills.control('review_execution', review_request(execution)) + execution.runtime.skills.control('review_execution', request) assert_unpublished(execution) diff --git a/test/unit_test/headless/test_agent_skill_execution_security.py b/test/unit_test/headless/test_agent_skill_execution_security.py index cb7581fb4..6f6f1cb38 100644 --- a/test/unit_test/headless/test_agent_skill_execution_security.py +++ b/test/unit_test/headless/test_agent_skill_execution_security.py @@ -26,7 +26,8 @@ def assert_unpublished(value): def test_zero_effect_original_trial_can_adopt_its_complete_actual_successor(execution): assert execution.runtime.store.actions(execution.task_id) == [] accepted = execution.runtime.skills.control('accept', accept_request(execution)) - assert accepted['publication']['task_id'] == execution.task_id and accepted['version'] == 1 + assert accepted['publication']['task_id'] == execution.task_id + assert accepted['version'] == 1 @pytest.mark.parametrize('execution', ['named'], indirect=True) @@ -38,8 +39,9 @@ def test_named_owner_can_adopt_explicit_source_owned_successor(execution): @pytest.mark.parametrize('execution', ['legacy_owner'], indirect=True) def test_original_owner_cannot_implicitly_adopt_an_admin_owned_successor(execution): + request = review_request(execution) with pytest.raises(AgentTaskError, match='another caller'): - execution.runtime.skills.control('review_execution', review_request(execution)) + execution.runtime.skills.control('review_execution', request) assert_unpublished(execution) @@ -72,9 +74,9 @@ def revoked(*arguments): @pytest.mark.parametrize('identifier', [None, [], {}, '', 'missing', 1, True]) def test_invalid_or_missing_executable_identity_does_not_publish(execution, identifier): + request = {**review_request(execution), 'execution_task_id': identifier} with pytest.raises(AgentTaskError): - execution.runtime.skills.control('review_execution', {**review_request(execution), - 'execution_task_id': identifier}) + execution.runtime.skills.control('review_execution', request) assert_unpublished(execution) @@ -89,8 +91,9 @@ def test_actual_private_recovery_chain_is_required(execution, corruption): else: connection.execute('UPDATE agent_task_recoveries SET source_task=? WHERE task_id=?', (execution.child_id, execution.child_id)) + request = review_request(execution) with pytest.raises(AgentTaskError): - execution.runtime.skills.control('review_execution', review_request(execution)) + execution.runtime.skills.control('review_execution', request) assert_unpublished(execution) @@ -98,8 +101,9 @@ def test_actual_private_recovery_chain_is_required(execution, corruption): def test_unknown_or_incomplete_final_effects_are_never_complete(execution, status): with execution.runtime.store.connection(write=True) as connection: connection.execute('UPDATE agent_actions SET status=? WHERE task_id=?', (status, execution.child_id)) + request = review_request(execution) with pytest.raises(AgentTaskError, match='semantic plan'): - execution.runtime.skills.control('review_execution', review_request(execution)) + execution.runtime.skills.control('review_execution', request) assert_unpublished(execution) @@ -110,8 +114,9 @@ def test_final_independent_completion_and_retirement_are_mandatory(execution, fi connection.execute('UPDATE agent_tasks SET verification=? WHERE id=?', ('[]', execution.child_id)) else: connection.execute('DELETE FROM agent_completion WHERE task_id=?', (execution.child_id,)) + request = review_request(execution) with pytest.raises(AgentTaskError, match='completion'): - execution.runtime.skills.control('review_execution', review_request(execution)) + execution.runtime.skills.control('review_execution', request) assert_unpublished(execution) @@ -156,8 +161,9 @@ def test_malformed_private_ownership_evidence_is_a_typed_refusal(execution, dama value['review'] = 'not-the-review' connection.execute('UPDATE agent_recovery_owners SET details=? WHERE task_id=?', ('{' if damage == 'json' else json.dumps(value), execution.child_id)) + request = review_request(execution) with pytest.raises(AgentTaskError): - execution.runtime.skills.control('review_execution', review_request(execution)) + execution.runtime.skills.control('review_execution', request) assert_unpublished(execution) @@ -176,10 +182,11 @@ def test_legacy_admission_reviewer_must_remain_live_for_an_independent_current_a @pytest.mark.parametrize('participant', ['root', 'leaf']) def test_inflight_input_witness_prevents_passive_review(execution, participant): identifier = execution.task_id if participant == 'root' else execution.child_id + request = review_request(execution) execution.runtime._executing.add(identifier) try: with pytest.raises(AgentTaskError, match='input has not retired'): - execution.runtime.skills.control('review_execution', review_request(execution)) + execution.runtime.skills.control('review_execution', request) finally: execution.runtime._executing.discard(identifier) assert_unpublished(execution) diff --git a/test/unit_test/headless/test_agent_skill_execution_surfaces.py b/test/unit_test/headless/test_agent_skill_execution_surfaces.py index fb473b16a..81f217009 100644 --- a/test/unit_test/headless/test_agent_skill_execution_surfaces.py +++ b/test/unit_test/headless/test_agent_skill_execution_surfaces.py @@ -46,11 +46,13 @@ def test_shared_surfaces_review_then_accept_complete_recovered_trial(execution, monkeypatch.setattr('je_auto_control.utils.agent_runtime.client.default_connection_path', lambda: connection) request = review_request(execution) review = invoke(surface, connection, tmp_path, 'review_execution', request) - assert review['approval_required'] is True and review['publication_inherited'] is False + assert review['approval_required'] is True + assert review['publication_inherited'] is False accepted = invoke(surface, connection, tmp_path, 'accept', {**request, 'execution_review': review['execution_review'], 'expected_version': 0, 'note': 'Reviewed exact full original execution chain'}) - assert accepted['publication']['task_id'] == execution.task_id and accepted['version'] == 1 + assert accepted['publication']['task_id'] == execution.task_id + assert accepted['version'] == 1 assert accepted['publication']['execution_review']['task_id'] == execution.child_id assert execution.runtime.get(execution.task_id).state == 'paused' finally: diff --git a/test/unit_test/headless/test_agent_skill_reauthorization.py b/test/unit_test/headless/test_agent_skill_reauthorization.py index d722f4f87..5da18965e 100644 --- a/test/unit_test/headless/test_agent_skill_reauthorization.py +++ b/test/unit_test/headless/test_agent_skill_reauthorization.py @@ -61,7 +61,8 @@ def test_renewal_requires_fresh_trial_and_publication_without_mutating_old_versi request = approval(runtime, parent) assert request["spec"]["roots"] == [str(narrow)] candidate = runtime.skills.control("create_authorized", request) - assert candidate["state"] == "draft" and candidate["version"] == 0 + assert candidate["state"] == "draft" + assert candidate["version"] == 0 assert candidate["parameters"] == parent["parameters"] assert candidate["spec"]["actions"] == parent["spec"]["actions"] assert candidate["provenance"]["reauthorization"]["context"]["replaced_origins"] == [old_id] @@ -262,6 +263,8 @@ def test_narrowed_scope_never_drops_an_inaccessible_completion_criterion(publish runtime, parent, _old_id, current_id, _allowed, _narrow = published with origin_scope(current_id): view = runtime.skills.control("review_authorization", {"candidate_id": parent["id"]}) - assert view["spec"] is None and view["issues"] == ["authorization_scope_unavailable"] - assert view["approval_required"] and view["validation_required"] + assert view["spec"] is None + assert view["issues"] == ["authorization_scope_unavailable"] + assert view["approval_required"] + assert view["validation_required"] assert runtime.skills.control("get", {"candidate_id": parent["id"]}) == parent diff --git a/test/unit_test/headless/test_agent_skill_recording_authority.py b/test/unit_test/headless/test_agent_skill_recording_authority.py index 88760ab33..af95ea2ee 100644 --- a/test/unit_test/headless/test_agent_skill_recording_authority.py +++ b/test/unit_test/headless/test_agent_skill_recording_authority.py @@ -17,8 +17,9 @@ def test_live_source_owner_and_reviewer_are_required(recording, change): recording.users.set_role('reviewer', 'operator') else: recording.users.remove_user(change) + request = request_for(recording) with pytest.raises(AgentTaskError, match='authority changed'): - recording.runtime.skills.control('record', request_for(recording)) + recording.runtime.skills.control('record', request) assert_no_draft(recording) @@ -31,15 +32,17 @@ def revoked(connection, candidate): recording.users.remove_user(principal) return identifier monkeypatch.setattr(AgentSkills, '_insert_candidate', staticmethod(revoked)) + request = request_for(recording) with pytest.raises(AgentTaskError, match='authority changed'): - recording.runtime.skills.control('record', request_for(recording)) + recording.runtime.skills.control('record', request) assert_no_draft(recording) @pytest.mark.parametrize('recording', ['named_caller'], indirect=True) def test_source_owner_cannot_implicitly_adopt_an_admin_owned_reader(recording): + request = request_for(recording) with pytest.raises(AgentTaskError, match='another caller'): - recording.runtime.skills.control('record', request_for(recording)) + recording.runtime.skills.control('record', request) assert_no_draft(recording) @@ -61,6 +64,8 @@ def revoked(connection, candidate): recording.users.remove_user('manager') return identifier monkeypatch.setattr(AgentSkills, '_insert_candidate', staticmethod(revoked)) - with authorization_scope(manager), pytest.raises(AgentTaskError, match='authority changed'): - recording.runtime.skills.control('record', request_for(recording)) + request = request_for(recording) + with authorization_scope(manager): + with pytest.raises(AgentTaskError, match='authority changed'): + recording.runtime.skills.control('record', request) assert_no_draft(recording) diff --git a/test/unit_test/headless/test_agent_skill_recording_completion.py b/test/unit_test/headless/test_agent_skill_recording_completion.py index cbc7e8eeb..2714fefef 100644 --- a/test/unit_test/headless/test_agent_skill_recording_completion.py +++ b/test/unit_test/headless/test_agent_skill_recording_completion.py @@ -85,14 +85,19 @@ def test_record_original_semantic_actions_without_replay_or_trial_fabrication(re receipts = runtime.store.actions(recording.task_id) manifest = recording.manifest.read_bytes() candidate = runtime.skills.control('record', request_for(recording)) - assert candidate['state'] == 'draft' and candidate['version'] == 0 and candidate['publication'] is None - assert candidate['spec']['mode'] == 'skill' and candidate['spec']['actions'] == [recording.action] + assert candidate['state'] == 'draft' + assert candidate['version'] == 0 + assert candidate['publication'] is None + assert candidate['spec']['mode'] == 'skill' + assert candidate['spec']['actions'] == [recording.action] assert candidate['provenance']['source_task'] == recording.task_id proof = candidate['provenance']['completion_review'] - assert proof['task_id'] == recording.child_id and proof['effect_task'] == recording.task_id + assert proof['task_id'] == recording.child_id + assert proof['effect_task'] == recording.task_id assert 'skill_pin' not in candidate['spec']['options'] assert runtime.get(recording.task_id).state == 'paused' - assert runtime.store.actions(recording.task_id) == receipts and runtime.store.actions(recording.child_id) == [] + assert runtime.store.actions(recording.task_id) == receipts + assert runtime.store.actions(recording.child_id) == [] assert recording.manifest.read_bytes() == manifest with runtime.store.connection() as connection: assert connection.execute('SELECT COUNT(*) FROM agent_skill_trials').fetchone()[0] == 0 @@ -128,7 +133,8 @@ def test_draft_publishes_only_after_its_own_new_validation(recording, monkeypatc assert run_task(runtime, trial['task_id']).state == 'succeeded' accepted = runtime.skills.control('accept', {'candidate_id': candidate['id'], 'task_id': trial['task_id'], 'expected_version': 0, 'note': 'Reviewed the new exact trial'}) - assert accepted['version'] == 1 and accepted['publication']['task_id'] == trial['task_id'] + assert accepted['version'] == 1 + assert accepted['publication']['task_id'] == trial['task_id'] assert runtime.get(recording.task_id).state == 'paused' with runtime.store.connection() as connection: identifiers = connection.execute('SELECT task_id FROM agent_skill_trials').fetchall() @@ -140,7 +146,8 @@ def test_added_criteria_do_not_inherit_the_readers_verified_result(recording, mo runtime = recording.runtime candidate = runtime.skills.control('record', request_for(recording, validation_criteria=[ {'kind': 'file_text', 'path': str(recording.output), 'value': 'not independently verified'}])) - assert runtime.get(recording.child_id).state == 'succeeded' and candidate['publication'] is None + assert runtime.get(recording.child_id).state == 'succeeded' + assert candidate['publication'] is None trial = runtime.skills.control('validate', {'candidate_id': candidate['id']}) monkeypatch.undo() assert run_task(runtime, trial['task_id']).state != 'succeeded' @@ -162,10 +169,12 @@ def test_reader_retention_and_restart_preserve_only_historical_draft_provenance( reopened = AgentRuntime(runtime.store.path, targets={'local-files': NativeDesktopTarget()}) try: current = reopened.skills.control('get', {'candidate_id': candidate['id']}) - assert current['state'] == 'draft' and current['provenance']['completion_review'] == proof + assert current['state'] == 'draft' + assert current['provenance']['completion_review'] == proof fresh = reopened.skills.control('validate', {'candidate_id': candidate['id']}) assert reopened.store.actions(fresh['task_id']) == [] + request = request_for(recording) with pytest.raises(AgentTaskError): - reopened.skills.control('record', request_for(recording)) + reopened.skills.control('record', request) finally: reopened.close() diff --git a/test/unit_test/headless/test_agent_skill_recording_security.py b/test/unit_test/headless/test_agent_skill_recording_security.py index e34d25096..b80d3af59 100644 --- a/test/unit_test/headless/test_agent_skill_recording_security.py +++ b/test/unit_test/headless/test_agent_skill_recording_security.py @@ -29,15 +29,17 @@ def test_damaged_completion_cannot_leave_a_recorded_draft(recording, damage): } with recording.runtime.store.connection(write=True) as connection: connection.execute(*changes[damage]) + request = request_for(recording) with pytest.raises(AgentTaskError): - recording.runtime.skills.control('record', request_for(recording)) + recording.runtime.skills.control('record', request) assert_no_draft(recording) @pytest.mark.parametrize('identifier', [None, [], '', 'missing-completion']) def test_invalid_reader_identity_creates_no_draft(recording, identifier): + request = request_for(recording, completion_task_id=identifier) with pytest.raises(AgentTaskError): - recording.runtime.skills.control('record', request_for(recording, completion_task_id=identifier)) + recording.runtime.skills.control('record', request) assert_no_draft(recording) @@ -46,16 +48,18 @@ def test_unwinding_source_or_reader_cannot_be_recorded(recording, which): identifier = getattr(recording, which) recording.runtime._executing.add(identifier) try: + request = request_for(recording) with pytest.raises(AgentTaskError): - recording.runtime.skills.control('record', request_for(recording)) + recording.runtime.skills.control('record', request) assert_no_draft(recording) finally: recording.runtime._executing.discard(identifier) def test_recording_without_host_completion_callbacks_refuses_before_writing(recording): + skills, request = AgentSkills(recording.runtime), request_for(recording) with pytest.raises(AgentTaskError, match='completion provider'): - AgentSkills(recording.runtime).control('record', request_for(recording)) + skills.control('record', request) assert_no_draft(recording) @@ -71,8 +75,9 @@ def changed(*, write=False): with original(write=write) as connection: yield connection monkeypatch.setattr(recording.runtime.store, 'connection', changed) + request = request_for(recording) with pytest.raises(AgentTaskError, match='snapshot'): - recording.runtime.skills.control('record', request_for(recording)) + recording.runtime.skills.control('record', request) assert_no_draft(recording) @@ -83,8 +88,9 @@ def test_forged_options_and_success_do_not_grant_recording_provenance(recording) forged = runtime.submit(spec) reports = runtime.store.get(recording.child_id)['verification'] runtime.store.update(forged.task_id, 'succeeded', verification=reports) + request = request_for(recording, completion_task_id=forged.task_id) with pytest.raises(AgentTaskError, match='private reviewed'): - runtime.skills.control('record', request_for(recording, completion_task_id=forged.task_id)) + runtime.skills.control('record', request) assert_no_draft(recording) @@ -92,22 +98,25 @@ def test_malformed_private_binding_returns_typed_refusal_without_a_draft(recordi with recording.runtime.store.connection(write=True) as connection: connection.execute('UPDATE agent_readonly_completions SET details=? WHERE task_id=?', (json.dumps({'contract': 1}), recording.child_id)) + request = request_for(recording) with pytest.raises(AgentTaskError): - recording.runtime.skills.control('record', request_for(recording)) + recording.runtime.skills.control('record', request) assert_no_draft(recording) def test_unrelated_source_cannot_borrow_a_real_readers_evidence(recording): runtime = recording.runtime other = runtime.submit(runtime.store.get(recording.task_id)['spec']) + request = request_for(recording, task_id=other.task_id) with pytest.raises(AgentTaskError, match='original workflow'): - runtime.skills.control('record', request_for(recording, task_id=other.task_id)) + runtime.skills.control('record', request) assert_no_draft(recording) @pytest.mark.parametrize('recording', ['origin'], indirect=True) def test_expired_source_scope_cannot_be_renewed_by_recording(recording): recording.origin.revoke() + request = request_for(recording) with pytest.raises(AgentTaskError): - recording.runtime.skills.control('record', request_for(recording)) + recording.runtime.skills.control('record', request) assert_no_draft(recording) diff --git a/test/unit_test/headless/test_agent_skill_recording_surfaces.py b/test/unit_test/headless/test_agent_skill_recording_surfaces.py index 9a06240a2..c6d454f30 100644 --- a/test/unit_test/headless/test_agent_skill_recording_surfaces.py +++ b/test/unit_test/headless/test_agent_skill_recording_surfaces.py @@ -43,7 +43,8 @@ def test_shared_surfaces_record_original_actions_as_an_unpublished_draft(recordi value = tools['ac_agent_skill'].invoke({'operation': 'record', 'request': request}) else: value = AgentRuntimeClient(connection).call('skill', {'operation': 'record', 'request': request}) - assert value['state'] == 'draft' and value['publication'] is None + assert value['state'] == 'draft' + assert value['publication'] is None assert value['provenance']['source_task'] == recording.task_id assert value['provenance']['completion_review']['task_id'] == recording.child_id assert value['spec']['actions'] == [recording.action] @@ -63,8 +64,10 @@ def test_http_revoked_reviewer_refuses_recording_with_409(recording, tmp_path): connection = tmp_path / 'connection.json' connection.write_text(json.dumps({'url': server.base_url, 'token': server.token}), encoding='utf-8') recording.users.remove_user('reviewer') + client = AgentRuntimeClient(connection) + request = {'operation': 'record', 'request': request_for(recording)} with pytest.raises(AgentTaskError) as error: - AgentRuntimeClient(connection).call('skill', {'operation': 'record', 'request': request_for(recording)}) + client.call('skill', request) assert '(409)' in str(error.value) assert_no_draft(recording) finally: diff --git a/test/unit_test/headless/test_agent_skill_versions.py b/test/unit_test/headless/test_agent_skill_versions.py index c10247ac1..e5d4308a0 100644 --- a/test/unit_test/headless/test_agent_skill_versions.py +++ b/test/unit_test/headless/test_agent_skill_versions.py @@ -68,9 +68,11 @@ def test_record_preview_validate_accept_and_run_pinned_version(managed): preview = runtime.skills.control("preview", {"candidate_id": candidate["id"], "parameters": {"value": "Ada"}}) assert preview["spec"]["actions"][0]["arguments"]["value"] == "Ada" assert candidate["spec"]["actions"][0]["arguments"]["value"] == "done" - assert candidate["state"] == "draft" and candidate["version"] == 0 + assert candidate["state"] == "draft" + assert candidate["version"] == 0 accepted = publish(runtime, candidate) - assert accepted["version"] == 1 and accepted["digest"] == candidate["digest"] + assert accepted["version"] == 1 + assert accepted["digest"] == candidate["digest"] assert accepted["publication"]["note"] == "Reviewed exact candidate trial" task = runtime.skills.control("run", {"skill_id": accepted["skill_id"], "version": 1, "parameters": {"value": "Grace"}}) @@ -281,6 +283,7 @@ def test_public_skill_api_and_http_service_share_verified_publication(tmp_path, threading.Event().wait(0.1) # At most 50 polls within the live service's request budget. accepted = api.manage_agent_skill("accept", {"candidate_id": candidate["id"], "task_id": trial["task_id"], "expected_version": 0, "note": "Reviewed HTTP fake-target evidence"}) - assert accepted["version"] == 1 and accepted["publication"]["task_id"] == trial["task_id"] + assert accepted["version"] == 1 + assert accepted["publication"]["task_id"] == trial["task_id"] finally: service.close() diff --git a/test/unit_test/headless/test_agent_stop_completion_race.py b/test/unit_test/headless/test_agent_stop_completion_race.py index 93b756bfc..e109c3a9b 100644 --- a/test/unit_test/headless/test_agent_stop_completion_race.py +++ b/test/unit_test/headless/test_agent_stop_completion_race.py @@ -38,5 +38,7 @@ def stop_after_read(): finally: published.set() result = pending.result(timeout=5) - assert result.state == "succeeded" and result.verification == completed.verification - assert result.metadata["input_released"] is True and target.calls == ["AC_increment"] + assert result.state == "succeeded" + assert result.verification == completed.verification + assert result.metadata["input_released"] is True + assert target.calls == ["AC_increment"] diff --git a/test/unit_test/headless/test_agent_target_profiles.py b/test/unit_test/headless/test_agent_target_profiles.py index c7a1919fc..90465b69a 100644 --- a/test/unit_test/headless/test_agent_target_profiles.py +++ b/test/unit_test/headless/test_agent_target_profiles.py @@ -53,8 +53,9 @@ def test_target_names_are_immutable_even_when_disabled(configured, tmp_path): runtime, _calls, _instances = configured runtime.target_profiles.control("register", profile(tmp_path)) runtime.target_profiles.control("disable", {"name": "archive"}) + renamed = profile(tmp_path, roots=[str(tmp_path.parent)]) with pytest.raises(AgentTaskError, match="already|immutable"): - runtime.target_profiles.control("register", profile(tmp_path, roots=[str(tmp_path.parent)])) + runtime.target_profiles.control("register", renamed) @pytest.mark.parametrize("updates", [ @@ -63,8 +64,9 @@ def test_target_names_are_immutable_even_when_disabled(configured, tmp_path): ]) def test_invalid_profiles_cannot_publish_an_adapter(configured, tmp_path, updates): runtime, calls, _instances = configured + invalid = profile(tmp_path, **updates) with pytest.raises(AgentTaskError): - runtime.target_profiles.control("register", profile(tmp_path, **updates)) + runtime.target_profiles.control("register", invalid) assert calls == [] @@ -72,15 +74,17 @@ def test_task_cannot_expand_profile_tools_roots_or_apps(configured, tmp_path): runtime, _calls, _instances = configured runtime.target_profiles.control("register", profile(tmp_path)) for updates in ({"roots": [str(tmp_path.parent)]}, {"apps": ["other"]}, {"allowed_tools": ["AC_write"]}): + expanded = task(tmp_path, **updates) with pytest.raises(AgentTaskError): - runtime.submit(task(tmp_path, **updates)) + runtime.submit(expanded) def test_profile_change_controls_require_host_authority(configured, tmp_path): runtime, calls, _instances = configured + config = profile(tmp_path) with authorization_scope(AuthorizationContext("operator", "operator")): with pytest.raises(AgentTaskError): - runtime.target_profiles.control("register", profile(tmp_path)) + runtime.target_profiles.control("register", config) assert calls == [] @@ -107,11 +111,12 @@ def test_restart_preserves_profile_scope_and_disabled_state(configured, tmp_path runtime.target_profiles.control("disable", {"name": "archive"}) runtime.close() restored = AgentRuntime(tmp_path / "runtime.sqlite", targets={}) + disabled = task(tmp_path) try: assert len(calls) == 2 assert restored.target_profiles.describe()["targets"][0]["enabled"] is False with pytest.raises(AgentTaskError): - restored.submit(task(tmp_path)) + restored.submit(disabled) finally: restored.close() @@ -123,8 +128,9 @@ def test_anonymous_mcp_context_cannot_reuse_local_host_authority(monkeypatch): calls = [] original = CATALOG["AC_agent_target"] monkeypatch.setitem(CATALOG, "AC_agent_target", (lambda **kwargs: calls.append(kwargs), *original[1:])) + handle, ctx = _handler("AC_agent_target"), ToolCallContext(1, session_id="anonymous") with pytest.raises(AgentTaskError, match="administrator"): - _handler("AC_agent_target")(ctx=ToolCallContext(1, session_id="anonymous"), operation="register", request={}) + handle(ctx=ctx, operation="register", request={}) assert calls == [] @@ -136,9 +142,9 @@ def test_anonymous_mcp_legacy_wrapper_cannot_expand_to_host_configuration(monkey calls = [] monkeypatch.setitem(executor.event_dict, "AC_agent_target", lambda **kwargs: calls.append(kwargs)) tool = next(item for item in build_default_tool_registry() if item.name == "ac_execute_actions") + ctx = ToolCallContext(1, session_id="anonymous") with pytest.raises(AuthorizationError): - tool.invoke({"actions": [["AC_agent_target", {"operation": "get", "request": {"name": "archive"}}]]}, - ctx=ToolCallContext(1, session_id="anonymous")) + tool.invoke({"actions": [["AC_agent_target", {"operation": "get", "request": {"name": "archive"}}]]}, ctx=ctx) assert calls == [] @@ -188,9 +194,10 @@ def test_profile_lifecycle_and_sensitive_configuration_are_protected_over_http(t try: service.start() client = AgentRuntimeClient(tmp_path / "agent_runtime.json") + unauthorized = {"operation": "register", "request": profile(tmp_path)} with authorization_scope(AuthorizationContext("operator", "operator")): with pytest.raises(AgentTaskError): - client.call("target", {"operation": "register", "request": profile(tmp_path)}) + client.call("target", unauthorized) with authorization_scope(AuthorizationContext("admin", "admin")): result = client.call("target", {"operation": "register", "request": profile(tmp_path)}) assert result["profile"]["roots"] == [str(tmp_path.resolve())] diff --git a/test/unit_test/headless/test_agent_task_handoff.py b/test/unit_test/headless/test_agent_task_handoff.py index d0c95f26c..cbe9d6632 100644 --- a/test/unit_test/headless/test_agent_task_handoff.py +++ b/test/unit_test/headless/test_agent_task_handoff.py @@ -16,7 +16,8 @@ def issue(value, source, leaf): with authorization_scope(value.reviewer): review = value.runtime.recovery.control('review_handoff', request) assert review['evidence']['source_task'] == source - assert 'batch_id' not in review['evidence'] and 'node_id' not in review['evidence'] + assert 'batch_id' not in review['evidence'] + assert 'node_id' not in review['evidence'] created = {**request, 'review': review['handoff_review'], 'note': 'Explicitly adopt only this actual non-DAG completion evidence'} return value.runtime.recovery.control('create_handoff', created)['handoff'] @@ -35,46 +36,56 @@ def assert_no_dag(runtime): @pytest.mark.parametrize('recording', ['named_caller'], indirect=True) def test_source_owner_records_other_caller_completion_only_with_an_explicit_task_grant(recording, monkeypatch): runtime = recording.runtime + request = request_for(recording) with pytest.raises(AgentTaskError, match='another caller'): - runtime.skills.control('record', request_for(recording)) + runtime.skills.control('record', request) before = runtime.store.actions(recording.task_id) manifest = recording.manifest.read_bytes() monkeypatch.setattr(NativeDesktopTarget, '_launch', no_helper) grant = issue(recording, recording.task_id, recording.child_id) candidate = runtime.skills.control('record', request_for(recording, handoff_id=grant['id'])) assert candidate['owner']['user_id'] == 'owner' - assert candidate['state'] == 'draft' and candidate['publication'] is None + assert candidate['state'] == 'draft' + assert candidate['publication'] is None proof = candidate['provenance']['completion_review'] - assert proof['task_id'] == recording.child_id and proof['effect_task'] == recording.task_id + assert proof['task_id'] == recording.child_id + assert proof['effect_task'] == recording.task_id assert proof['evidence_handoff']['id'] == grant['id'] assert runtime.get(recording.task_id).state == 'paused' assert runtime.store.get(recording.child_id)['owner']['user_id'] == 'reviewer' - assert runtime.store.actions(recording.task_id) == before and recording.manifest.read_bytes() == manifest + assert runtime.store.actions(recording.task_id) == before + assert recording.manifest.read_bytes() == manifest with pytest.raises(AgentTaskError, match='another caller'): runtime.get(recording.child_id) assert_no_dag(runtime) runtime.recovery.control('revoke_handoff', {'handoff_id': grant['id']}) + request = request_for(recording, handoff_id=grant['id'], name='Revoked grant') with pytest.raises(AgentTaskError): - runtime.skills.control('record', request_for(recording, handoff_id=grant['id'], name='Revoked grant')) + runtime.skills.control('record', request) assert len(runtime.skills.control('list')['candidates']) == 1 @pytest.mark.parametrize('trial', ['named_caller'], indirect=True) def test_original_validation_trial_publishes_with_other_caller_task_evidence_without_replay(trial, monkeypatch): runtime = trial.runtime + request = acceptance(trial) with pytest.raises(AgentTaskError, match='another caller'): - runtime.skills.control('accept', acceptance(trial)) + runtime.skills.control('accept', request) before = runtime.store.actions(trial.task_id) manifest = trial.manifest.read_bytes() monkeypatch.setattr(NativeDesktopTarget, '_launch', no_helper) grant = issue(trial, trial.task_id, trial.child_id) result = runtime.skills.control('accept', {**acceptance(trial), 'handoff_id': grant['id']}) - assert result['state'] == 'accepted' and result['version'] == 1 - assert result['owner']['user_id'] == 'owner' and result['publication']['task_id'] == trial.task_id + assert result['state'] == 'accepted' + assert result['version'] == 1 + assert result['owner']['user_id'] == 'owner' + assert result['publication']['task_id'] == trial.task_id proof = result['publication']['completion_review'] - assert proof['task_id'] == trial.child_id and proof['effect_task'] == trial.task_id + assert proof['task_id'] == trial.child_id + assert proof['effect_task'] == trial.task_id assert proof['evidence_handoff']['id'] == grant['id'] - assert runtime.get(trial.task_id).state == 'paused' and runtime.store.actions(trial.task_id) == before + assert runtime.get(trial.task_id).state == 'paused' + assert runtime.store.actions(trial.task_id) == before assert runtime.store.get(trial.child_id)['owner']['user_id'] == 'reviewer' assert trial.manifest.read_bytes() == manifest with pytest.raises(AgentTaskError, match='another caller'): diff --git a/test/unit_test/headless/test_agent_task_handoff_authority.py b/test/unit_test/headless/test_agent_task_handoff_authority.py index 5d59d3fd7..f25756f3e 100644 --- a/test/unit_test/headless/test_agent_task_handoff_authority.py +++ b/test/unit_test/headless/test_agent_task_handoff_authority.py @@ -22,8 +22,9 @@ def test_recording_retains_both_issuing_and_consuming_session_origins(recording) candidate = runtime.skills.control('record', request_for(recording, handoff_id=grant['id'])) assert {issuer.identifier, recipient.identifier} <= set(candidate['spec']['options']['origin_policies']) issuer.revoke() + request = request_for(recording, handoff_id=grant['id'], name='No lost ceiling') with pytest.raises(AgentTaskError): - runtime.skills.control('record', request_for(recording, handoff_id=grant['id'], name='No lost ceiling')) + runtime.skills.control('record', request) assert len(runtime.skills.control('list')['candidates']) == 1 assert runtime.recovery.control('get_handoff', {'handoff_id': grant['id']})['handoff']['id'] == grant['id'] listed = runtime.recovery.control('list_handoffs', {'source_task': recording.task_id}) diff --git a/test/unit_test/headless/test_agent_task_handoff_gui.py b/test/unit_test/headless/test_agent_task_handoff_gui.py index 806bf6791..8498e8f0d 100644 --- a/test/unit_test/headless/test_agent_task_handoff_gui.py +++ b/test/unit_test/headless/test_agent_task_handoff_gui.py @@ -36,7 +36,8 @@ def test_explicit_task_source_mode_prepares_a_real_source_request_without_fabric assert calls == [] monkeypatch.setattr('je_auto_control.gui.agent_tasks_tab._translate', lambda key: 'translated:' + key) widget.retranslate() - assert mode.currentData() == 'task' and mode.currentText().startswith('translated:') + assert mode.currentData() == 'task' + assert mode.currentText().startswith('translated:') request['source_task'] = 'actual-original-task' widget._handoff_request.setPlainText(json.dumps(request)) actions['agent_handoff_apply']() @@ -56,5 +57,6 @@ def test_skill_actions_prepare_selected_grant_adoption_without_issuing_or_publis actions[name]() request = json.loads(widget._skill_request.toPlainText()) assert widget._skill_operation.currentData() == operation - assert request['handoff_id'] == '' and request[selected] == 'reviewed-evidence-task' + assert request['handoff_id'] == '' + assert request[selected] == 'reviewed-evidence-task' assert calls == [] diff --git a/test/unit_test/headless/test_agent_task_handoff_lifecycle.py b/test/unit_test/headless/test_agent_task_handoff_lifecycle.py index f5fab5bf4..b01e1b32c 100644 --- a/test/unit_test/headless/test_agent_task_handoff_lifecycle.py +++ b/test/unit_test/headless/test_agent_task_handoff_lifecycle.py @@ -61,7 +61,8 @@ def test_actual_grant_retry_and_other_caller_completion_survive_runtime_restart( candidate = reopened.skills.control('record', request_for(recording, handoff_id=grant['id'])) assert candidate['state'] == 'draft' assert candidate['provenance']['completion_review']['evidence_handoff']['id'] == grant['id'] - assert reopened.get(recording.task_id).state == 'paused' and recording.manifest.read_bytes() == before + assert reopened.get(recording.task_id).state == 'paused' + assert recording.manifest.read_bytes() == before with pytest.raises(AgentTaskError, match='another caller'): reopened.get(recording.child_id) finally: @@ -84,10 +85,12 @@ def test_sixteen_real_renewed_readers_grant_completion_without_an_extra_link_or_ monkeypatch.setattr(NativeDesktopTarget, '_launch', no_helper) grant = issue(completed, completed.source, parent) access = runtime.recovery.task_handoff_evidence(completed.source, parent, grant['id']) - assert len(access.links) == 16 and len(access.rows) == 17 + assert len(access.links) == 16 + assert len(access.rows) == 17 candidate = runtime.skills.control('record', {'task_id': completed.source, 'completion_task_id': parent, 'handoff_id': grant['id'], 'name': 'Sixteen actual reviewed readers'}) - assert candidate['state'] == 'draft' and candidate['publication'] is None + assert candidate['state'] == 'draft' + assert candidate['publication'] is None assert candidate['provenance']['completion_review']['task_id'] == parent assert runtime.get(completed.source).state == 'paused' assert origin_policy._path(completed.old.identifier).read_bytes() == witness diff --git a/test/unit_test/headless/test_agent_task_handoff_mixed.py b/test/unit_test/headless/test_agent_task_handoff_mixed.py index dc4d6fb88..1e889e828 100644 --- a/test/unit_test/headless/test_agent_task_handoff_mixed.py +++ b/test/unit_test/headless/test_agent_task_handoff_mixed.py @@ -124,8 +124,10 @@ def test_actual_renewed_mixed_file_chain_keeps_original_trial_and_old_witness_wi 'execution_task_id': value.child_id, 'completion_task_id': value.reader, 'handoff_id': grant['id']} accepted = publish(value, request) proof = accepted['publication']['execution_review'] - assert accepted['publication']['task_id'] == value.task_id and accepted['version'] == 1 - assert proof['task_id'] == value.child_id and proof['verification_task'] == value.reader + assert accepted['publication']['task_id'] == value.task_id + assert accepted['version'] == 1 + assert proof['task_id'] == value.child_id + assert proof['verification_task'] == value.reader assert proof['completion_review']['effect_task'] == value.child_id assert proof['evidence_handoff']['id'] == grant['id'] assert runtime.get(value.task_id).state == 'paused' @@ -133,7 +135,8 @@ def test_actual_renewed_mixed_file_chain_keeps_original_trial_and_old_witness_wi assert runtime.store.actions(value.reader) == [] assert [path.read_bytes() for path in value.manifests] == before assert origin_policy._path(value.old.identifier).read_bytes() == witness - assert 'arguments' not in json.dumps(proof) and 'store_path' not in json.dumps(proof) + assert 'arguments' not in json.dumps(proof) + assert 'store_path' not in json.dumps(proof) with pytest.raises(AgentTaskError, match='another caller'): runtime.get(value.child_id) with pytest.raises(AgentTaskError, match='managed origin'): diff --git a/test/unit_test/headless/test_agent_task_handoff_revocation.py b/test/unit_test/headless/test_agent_task_handoff_revocation.py index cb90bee8f..05b8dc591 100644 --- a/test/unit_test/headless/test_agent_task_handoff_revocation.py +++ b/test/unit_test/headless/test_agent_task_handoff_revocation.py @@ -60,10 +60,12 @@ def test_revocation_inside_publication_sql_rolls_back_acceptance_and_its_provena connection.execute('CREATE TRIGGER owned_publication_revoke AFTER INSERT ON agent_skill_publications ' "BEGIN UPDATE agent_task_handoffs SET state='revoked'; " "INSERT INTO agent_task_handoff_revocations SELECT id,'{}',0 FROM agent_task_handoffs; END") + request = {**acceptance(trial), 'handoff_id': grant['id']} with pytest.raises(AgentTaskError, match='revoked'): - runtime.skills.control('accept', {**acceptance(trial), 'handoff_id': grant['id']}) + runtime.skills.control('accept', request) candidate = runtime.skills.control('get', {'candidate_id': trial.candidate['id']}) - assert candidate['state'] == 'draft' and candidate['publication'] is None + assert candidate['state'] == 'draft' + assert candidate['publication'] is None with runtime.store.connection() as connection: for table in ('agent_skill_publications', 'agent_skill_completions', 'agent_task_handoff_revocations'): assert connection.execute(f'SELECT COUNT(*) FROM {table}').fetchone()[0] == 0 diff --git a/test/unit_test/headless/test_agent_task_handoff_security.py b/test/unit_test/headless/test_agent_task_handoff_security.py index 1e1ff850c..db68af72b 100644 --- a/test/unit_test/headless/test_agent_task_handoff_security.py +++ b/test/unit_test/headless/test_agent_task_handoff_security.py @@ -66,8 +66,9 @@ def test_revocation_during_draft_sql_rolls_back_the_draft_and_tombstone(recordin connection.execute('CREATE TRIGGER owned_draft_revoke AFTER INSERT ON agent_skill_candidates ' "BEGIN UPDATE agent_task_handoffs SET state='revoked'; " "INSERT INTO agent_task_handoff_revocations SELECT id,'{}',0 FROM agent_task_handoffs; END") + request = request_for(recording, handoff_id=grant['id']) with pytest.raises(AgentTaskError, match='revoked'): - runtime.skills.control('record', request_for(recording, handoff_id=grant['id'])) + runtime.skills.control('record', request) no_draft(recording) with runtime.store.connection() as connection: assert connection.execute('SELECT COUNT(*) FROM agent_task_handoff_revocations').fetchone()[0] == 0 @@ -94,8 +95,9 @@ def test_matching_private_digest_cannot_supply_different_completion_authority(re proof[field] = {'input_authorized': True} connection.execute('UPDATE agent_task_handoffs SET details=?,review=? WHERE id=?', (json.dumps(proof), _digest(proof), grant['id'])) + request = request_for(recording, handoff_id=grant['id']) with pytest.raises(AgentTaskError): - runtime.skills.control('record', request_for(recording, handoff_id=grant['id'])) + runtime.skills.control('record', request) no_draft(recording) diff --git a/test/unit_test/headless/test_agent_task_handoff_surfaces.py b/test/unit_test/headless/test_agent_task_handoff_surfaces.py index 1ff823a3c..540d35757 100644 --- a/test/unit_test/headless/test_agent_task_handoff_surfaces.py +++ b/test/unit_test/headless/test_agent_task_handoff_surfaces.py @@ -50,8 +50,9 @@ def exercise(value, surface, operation, adoption, monkeypatch, temporary): connection = temporary / 'connection.json' connection.write_text(json.dumps({'url': server.base_url, 'token': server.token}), encoding='utf-8') monkeypatch.setattr('je_auto_control.utils.agent_runtime.client.default_connection_path', lambda: connection) + client = AgentRuntimeClient(connection) with pytest.raises(AgentTaskError): - AgentRuntimeClient(connection).call('get', {'task_id': value.child_id}) + client.call('get', {'task_id': value.child_id}) request = {'source_task': value.task_id, 'task_id': value.child_id} with authorization_scope(value.reviewer): preview = invoke(surface, 'recovery', 'review_handoff', request, connection, temporary) @@ -69,11 +70,13 @@ def exercise(value, surface, operation, adoption, monkeypatch, temporary): result = invoke(surface, 'skill', operation, {**adoption, 'handoff_id': grant['id']}, connection, temporary) reviewed = (result['provenance']['completion_review'] if operation == 'record' else result['publication']['completion_review']) - assert reviewed['evidence_handoff']['id'] == grant['id'] and result['owner']['user_id'] == 'owner' + assert reviewed['evidence_handoff']['id'] == grant['id'] + assert result['owner']['user_id'] == 'owner' assert runtime.get(value.task_id).state == 'paused' + client = AgentRuntimeClient(connection) with pytest.raises(AgentTaskError): - AgentRuntimeClient(connection).call('get', {'task_id': value.child_id}) - revoked = invoke(surface, 'recovery', 'revoke_handoff', {'handoff_id': grant['id']}, connection, temporary) + client.call('get', {'task_id': value.child_id}) + revoked =invoke(surface, 'recovery', 'revoke_handoff', {'handoff_id': grant['id']}, connection, temporary) assert revoked['handoff']['state'] == 'revoked' finally: server.stop() diff --git a/test/unit_test/headless/test_agent_task_recovery.py b/test/unit_test/headless/test_agent_task_recovery.py index 23fe58845..7e10e05df 100644 --- a/test/unit_test/headless/test_agent_task_recovery.py +++ b/test/unit_test/headless/test_agent_task_recovery.py @@ -46,12 +46,15 @@ def test_preview_is_passive_reports_progress_and_does_not_reuse_receipts(stopped monkeypatch.setattr(target, "ready", Mock(side_effect=AssertionError("target probe"))) first = preview(runtime, source) assert first == preview(runtime, source) - assert first["completed_steps"] == 1 and first["issues"] == [] + assert first["completed_steps"] == 1 + assert first["issues"] == [] assert first["draft"]["actions"] == [{"tool": "AC_increment", "arguments": {"amount": 2}, "expect_result": {"value": 3}}] - assert first["approval_required"] is True and first["source_completion_inherited"] is False + assert first["approval_required"] is True + assert first["source_completion_inherited"] is False assert first["source_receipts"][0]["status"] == "confirmed" - assert (runtime.store.get(source), runtime.store.actions(source)) == before + after = runtime.store.get(source), runtime.store.actions(source) + assert after == before assert target.calls == ["AC_increment"] @@ -64,7 +67,8 @@ def test_new_successor_executes_only_reviewed_remaining_steps_and_keeps_parent_s assert child["recovery"]["source_task"] == source assert run_task(runtime, child["task_id"]).state == "succeeded" assert len(target.calls) == 2 - assert runtime.get(source).state == "paused" and runtime.receipts(source) == before + assert runtime.get(source).state == "paused" + assert runtime.receipts(source) == before with pytest.raises(AgentTaskError, match="reviewed successor"): runtime.resume(source) assert runtime.get(child["task_id"]).metadata["recovery"]["snapshot"] == request["snapshot"] @@ -117,7 +121,8 @@ def test_unknown_effects_must_be_reconciled_and_are_never_discarded(stopped): with runtime.store.connection(write=True) as connection: connection.execute("UPDATE agent_actions SET status='uncertain' WHERE id=?", (action["id"],)) view = preview(runtime, source) - assert view["draft"] is None and "unknown_source_effects" in view["issues"] + assert view["draft"] is None + assert "unknown_source_effects" in view["issues"] request = {"task_id": source, "snapshot": view["snapshot"], "spec": deepcopy(runtime.store.get(source)["spec"]), "note": "Must not erase unknown input"} with pytest.raises(AgentTaskError, match="reconcile unknown"): @@ -142,7 +147,8 @@ def test_old_capture_values_and_observation_indexes_are_not_transplanted(stopped import json connection.execute("UPDATE agent_tasks SET spec=? WHERE id=?", (json.dumps(data), source)) view = preview(runtime, source) - assert issue in view["issues"] and view["draft"] is None + assert issue in view["issues"] + assert view["draft"] is None request = {"task_id": source, "snapshot": view["snapshot"], "spec": data, "note": "review"} with pytest.raises(AgentTaskError, match="fresh semantic actions"): runtime.recovery.control("create", request) @@ -225,8 +231,9 @@ def test_retention_preserves_parent_evidence_until_the_successor_is_retired(stop with runtime.store.connection(write=True) as connection: connection.execute("UPDATE agent_tasks SET state='cancelled',updated=? WHERE id=?", (old, child)) assert runtime.prune()["deleted"] == [child] + request = approval(runtime, source) with pytest.raises(AgentTaskError): - runtime.recovery.control("create", approval(runtime, source)) + runtime.recovery.control("create", request) assert runtime.prune()["deleted"] == [source] @@ -270,5 +277,6 @@ def test_caller_request_keys_cannot_preclaim_or_supply_a_recovery_successor(stop verification=[{"kind": "actions_complete", "status": "passed"}]) child = runtime.recovery.control("create", request) assert child["task_id"] != preclaimed.task_id - assert child["state"] == "queued" and runtime.store.actions(child["task_id"]) == [] + assert child["state"] == "queued" + assert runtime.store.actions(child["task_id"]) == [] assert child["recovery"]["source_task"] == source diff --git a/test/unit_test/headless/test_agent_task_triggers.py b/test/unit_test/headless/test_agent_task_triggers.py index f2c1dbd3a..689bf6cdb 100644 --- a/test/unit_test/headless/test_agent_task_triggers.py +++ b/test/unit_test/headless/test_agent_task_triggers.py @@ -31,8 +31,9 @@ def test_request_key_is_atomic_owner_scoped_and_rejects_conflicting_specs(runtim with authorization_scope(AuthorizationContext("alice", "operator")): first = runtime.submit(spec(), idempotency_key="request") assert runtime.submit(spec(), idempotency_key="request").task_id == first.task_id + different = spec(goal="different") with pytest.raises(AgentTaskError, match="different task"): - runtime.submit(spec(goal="different"), idempotency_key="request") + runtime.submit(different, idempotency_key="request") with authorization_scope(AuthorizationContext("bob", "operator")): second = runtime.submit(spec(), idempotency_key="request") assert second.task_id != first.task_id @@ -192,7 +193,8 @@ def counted(**kwargs): monkeypatch.setattr(runtime.store, "connection", counted) result = runtime.stop_all() assert len(reads) <= 6 - assert result["requested"] == 5 and all(item["state"] == "cancelled" for item in result["tasks"]) + assert result["requested"] == 5 + assert all(item["state"] == "cancelled" for item in result["tasks"]) def test_canonical_root_replacement_requires_scope_review_before_target_acquisition(runtime, tmp_path): diff --git a/test/unit_test/headless/test_agent_tasks_gui.py b/test/unit_test/headless/test_agent_tasks_gui.py index 4c4fdea54..a46c02d2d 100644 --- a/test/unit_test/headless/test_agent_tasks_gui.py +++ b/test/unit_test/headless/test_agent_tasks_gui.py @@ -113,8 +113,9 @@ def call(self, operation, *_args, **_kwargs): monkeypatch.setattr("je_auto_control.gui.agent_tasks_tab.AgentRuntimeClient", Client) monkeypatch.setattr("je_auto_control.utils.agent_runtime.api.AgentRuntimeClient", Client) + token = CancellationToken(1) with pytest.raises(CancelledError): - _request("observe", {"task_id": "fixture"}, CancellationToken(1)) + _request("observe", {"task_id": "fixture"}, token) assert calls == ["observe"] @@ -138,12 +139,14 @@ def test_remainder_preview_is_passive_bound_to_selected_task_and_requires_explic tab._task_id.setText("other-task") tab._completed(TaskOutcome(42, "ok", {"source_task": "source-task", "snapshot": "a" * 64, "draft": {"goal": "review remaining actions"}})) - assert len(requests) == 1 and tab._scope.toPlainText() == original_scope + assert len(requests) == 1 + assert tab._scope.toPlainText() == original_scope review = json.loads(tab._remainder_request.toPlainText()) if change_task: assert review == {} return - assert review["task_id"] == "source-task" and review["note"] == "" + assert review["task_id"] == "source-task" + assert review["note"] == "" handle.outcome = "finished" review["note"] = "Reviewed remaining actions in the new context" tab._remainder_request.setPlainText(json.dumps(review)) @@ -170,9 +173,11 @@ def test_dynamic_review_template_requires_manual_goal_and_note_without_submittin template = {"goal": "", "mode": "ollama", "options": {"model": "fixture-tools"}, "actions": []} tab._present_result({"source_task": "source-task", "source_mode": "ollama", "snapshot": "a" * 64, "draft": None, "review_template": template, "issues": ["new_plan_required"]}) - assert submitted == [] and tab._scope.toPlainText() == original_scope + assert submitted == [] + assert tab._scope.toPlainText() == original_scope request = json.loads(tab._remainder_request.toPlainText()) - assert request["spec"]["goal"] == "" and request["note"] == "" + assert request["spec"]["goal"] == "" + assert request["note"] == "" request["spec"]["goal"] = "Read only the remaining field" request["note"] = "Reviewed the new plan" tab._remainder_request.setPlainText(json.dumps(request)) @@ -198,7 +203,8 @@ def test_authorization_review_and_creation_are_distinct_explicit_actions(monkeyp "draft": {"goal": "reviewed remaining work", "roots": ["D:/reports"]}, "issues": []}) assert len(submitted) == 1 request = json.loads(tab._remainder_request.toPlainText()) - assert request["authorization_review"] == "b" * 64 and request["note"] == "" + assert request["authorization_review"] == "b" * 64 + assert request["note"] == "" request["note"] = "Reviewed the new report root" tab._remainder_request.setPlainText(json.dumps(request)) dict(tab.menu_actions())["agent_reauthorize_create"]() @@ -257,7 +263,8 @@ def test_source_owner_recovery_requires_its_preview_and_cannot_reuse_another_own result["authorization_review"] = "c" * 64 tab._present_remainder(result) request = json.loads(tab._remainder_request.toPlainText()) - assert request["owner_policy"] == "source_owner" and request["ownership_review"] == "b" * 64 + assert request["owner_policy"] == "source_owner" + assert request["ownership_review"] == "b" * 64 request["note"] = "Reviewed original ownership and remainder" tab._remainder_request.setPlainText(json.dumps(request)) create = "create_authorized" if reauthorize else "create" @@ -265,7 +272,8 @@ def test_source_owner_recovery_requires_its_preview_and_cannot_reuse_another_own assert submitted[-1] == ("recovery", {"operation": create, "request": request}) tab._owner_policy.setCurrentIndex(0) tab._create_remainder(create) - assert len(submitted) == 2 and "selection changed" in tab._status.text() + assert len(submitted) == 2 + assert "selection changed" in tab._status.text() finally: tab.close() shiboken6.delete(tab) diff --git a/test/unit_test/headless/test_agent_transfer_adoption.py b/test/unit_test/headless/test_agent_transfer_adoption.py index a4704d03c..fd57a0fb5 100644 --- a/test/unit_test/headless/test_agent_transfer_adoption.py +++ b/test/unit_test/headless/test_agent_transfer_adoption.py @@ -58,7 +58,8 @@ def test_actual_coordinator_adopts_another_remote_owners_leaf_without_read_or_in running = start(value, reviewed, approved) result = value.coordinator.batches.control('validate_node', {'batch_id': reviewed['batch_id'], 'node_id': 'remote-file', 'attempt': running['attempt']}) - assert result['outcome'] == 'completed' and result['task_id'] == value.child + assert result['outcome'] == 'completed' + assert result['task_id'] == value.child with authorization_scope(value.owner), pytest.raises(AgentTaskError, match='another caller'): value.runtime.get(value.child) assert value.runtime.store.actions(value.child) == before @@ -98,11 +99,12 @@ def finish(checks, approval_id, pending, commit): value.server.stop() return original(checks, approval_id, pending, commit) monkeypatch.setattr(DagTransferChecks, 'finish', finish) + if phase == 'provisional': + consent, arguments = approve, (value, reviewed) + else: + consent, arguments = start, (value, reviewed, approved) with pytest.raises(AgentTaskError): - if phase == 'provisional': - approve(value, reviewed) - else: - start(value, reviewed, approved) + consent(*arguments) with value.coordinator.store.connection() as connection: assert connection.execute('SELECT state FROM agent_dag_approvals').fetchone()[0] == 'cancelled' assert connection.execute('SELECT state FROM agent_dag_batches').fetchone()[0] == 'waiting_review' @@ -126,7 +128,8 @@ def claim(connection, batch, request): status, absent = post(value.server, '/agent/lookup', {'idempotency_key': next( node['task']['idempotency_key'] for node in running['definition']['nodes'] if node['id'] == 'downstream')}, token=value.owner_token) - assert status == 200 and absent['found'] is False + assert status == 200 + assert absent['found'] is False @pytest.mark.parametrize('lost_ack', [False, True]) @@ -225,7 +228,8 @@ def interrupted(*_args): assert original['state'] == 'provisional' assert connection.execute('SELECT count(*) FROM agent_dag_adoptions').fetchone()[0] == 0 approved = approve(value, reviewed) - assert approved['approval_id'] == original['id'] and approved['approval_state'] == 'ready' + assert approved['approval_id'] == original['id'] + assert approved['approval_state'] == 'ready' with value.coordinator.store.connection() as connection: assert connection.execute('SELECT count(*) FROM agent_dag_approvals').fetchone()[0] == 1 @@ -240,11 +244,12 @@ def finish(checks, identifier, pending, commit): value.coordinator.batches.stop_all() return original(checks, identifier, pending, commit) monkeypatch.setattr(DagTransferChecks, 'finish', finish) + if phase == 'provisional': + consent, arguments = approve, (value, reviewed) + else: + consent, arguments = start, (value, reviewed, approved) with pytest.raises(AgentTaskError): - if phase == 'provisional': - approve(value, reviewed) - else: - start(value, reviewed, approved) + consent(*arguments) with value.coordinator.store.connection() as connection: assert connection.execute('SELECT state FROM agent_dag_approvals').fetchone()[0] == 'cancelled' assert connection.execute('SELECT state FROM agent_dag_batches').fetchone()[0] == 'waiting_review' diff --git a/test/unit_test/headless/test_agent_transfer_adoption_active_restart.py b/test/unit_test/headless/test_agent_transfer_adoption_active_restart.py index 11e4d623c..0c6cbe93f 100644 --- a/test/unit_test/headless/test_agent_transfer_adoption_active_restart.py +++ b/test/unit_test/headless/test_agent_transfer_adoption_active_restart.py @@ -79,15 +79,18 @@ def response(operation, body, timeout): value.coordinator.close() value.coordinator = AgentRuntime(path, targets={}) current = value.coordinator.batches.control('get', {'batch_id': reviewed['batch_id']}) - assert current['state'] == 'waiting_review' and not future.done() + assert current['state'] == 'waiting_review' + assert not future.done() node = next(row for row in current['nodes'] if row['id'] == 'downstream') assert node['phase'] == 'unknown' assert node['receipt'] == {'outcome': 'requires_review', 'task_id': dependent} with authorization_scope(value.owner): stopped = value.runtime.get(dependent) - assert stopped.state == 'pausing' and stopped.metadata['input_released'] is False + assert stopped.state == 'pausing' + assert stopped.metadata['input_released'] is False assert value.runtime.store.get(independent)['state'] == previous - assert effect.read_bytes() == original_bytes and calls == ['AC_workflow_manifest'] + assert effect.read_bytes() == original_bytes + assert calls == ['AC_workflow_manifest'] finally: release.set() try: diff --git a/test/unit_test/headless/test_agent_transfer_adoption_restart.py b/test/unit_test/headless/test_agent_transfer_adoption_restart.py index 76dfbe230..f5bfe6ca8 100644 --- a/test/unit_test/headless/test_agent_transfer_adoption_restart.py +++ b/test/unit_test/headless/test_agent_transfer_adoption_restart.py @@ -63,23 +63,27 @@ def test_actual_coordinator_reopen_preserves_consent_identity_and_revalidates_re if change == 'none': if phase == 'provisional': approved = approve(value, reviewed) - assert approved['approval_id'] == stored['id'] and approved['approval_state'] == 'ready' + assert approved['approval_id'] == stored['id'] + assert approved['approval_state'] == 'ready' running = start(value, reviewed, approved) result = value.coordinator.batches.control('validate_node', {'batch_id': reviewed['batch_id'], 'node_id': 'remote-file', 'attempt': running['attempt']}) - assert result['outcome'] == 'completed' and result['task_id'] == value.child + assert result['outcome'] == 'completed' + assert result['task_id'] == value.child with value.runtime.store.connection() as database: record = value.runtime.transfers.source.records.find(database, identifier) assert record['expires'] == expires, 'Coordinator restart cannot renew the source evidence deadline' else: + if phase == 'provisional': + step, arguments = approve, (value, reviewed) + else: + step, arguments = start, (value, reviewed, approved) with pytest.raises(AgentTaskError): - if phase == 'provisional': - approve(value, reviewed) - else: - start(value, reviewed, approved) + step(*arguments) with value.coordinator.store.connection() as database: assert database.execute('SELECT state FROM agent_dag_batches').fetchone()[0] != 'running' with value.coordinator.store.connection() as database: assert database.execute('SELECT count(*) FROM agent_dag_approvals').fetchone()[0] == 1 - assert value.coordinator.list() == [] and value.runtime.store.actions(value.child) == actions + assert value.coordinator.list() == [] + assert value.runtime.store.actions(value.child) == actions assert value.output.read_bytes() == value.body diff --git a/test/unit_test/headless/test_agent_transfer_cold_lifecycle.py b/test/unit_test/headless/test_agent_transfer_cold_lifecycle.py index d6356f5a1..fff2f2201 100644 --- a/test/unit_test/headless/test_agent_transfer_cold_lifecycle.py +++ b/test/unit_test/headless/test_agent_transfer_cold_lifecycle.py @@ -36,8 +36,9 @@ def constructing(controller, *args, **kwargs): assert entered.wait(5) with pytest.raises(AgentTaskError, match='construction|mutation|active'): reopened.close() + targets = {'local-files': FileTarget()} with pytest.raises(AgentTaskError, match='still owns'): - AgentRuntime(reopened.store.path, targets={'local-files': FileTarget()}) + AgentRuntime(reopened.store.path, targets=targets) finally: release.set() maintaining.result(timeout=5) @@ -73,8 +74,9 @@ def cleaning(cache, account): close_release.set() with pytest.raises(AgentTaskError, match='active transfer'): retiring.result(timeout=5) + targets = {'local-files': FileTarget()} with pytest.raises(AgentTaskError, match='still owns'): - AgentRuntime(reopened.store.path, targets={'local-files': FileTarget()}) + AgentRuntime(reopened.store.path, targets=targets) finally: close_release.set() cleanup_release.set() @@ -116,7 +118,8 @@ def confined(database): reopened.maintain() with reopened.store.connection() as connection: retained = reopened.transfers.source.records.find(connection, record['id']) - assert retained['state'] == 'revoked' and retained['held'] is True + assert retained['state'] == 'revoked' + assert retained['held'] is True assert retained['expires'] == record['expires'] assert foreign.path.read_bytes() == before assert foreign.store.read_chunk(record['account'], digest) == foreign_body diff --git a/test/unit_test/headless/test_agent_transfer_diagnostics.py b/test/unit_test/headless/test_agent_transfer_diagnostics.py index 9ade75a05..92cd73837 100644 --- a/test/unit_test/headless/test_agent_transfer_diagnostics.py +++ b/test/unit_test/headless/test_agent_transfer_diagnostics.py @@ -18,7 +18,9 @@ def forbidden(*_args, **_kwargs): monkeypatch.setattr('je_auto_control.utils.agent_runtime.transport.request_json', forbidden) before = set(tmp_path.rglob('*')) result = check() - assert result.name == 'agent_transfer_contract' and result.ok and result.severity == 'info' + assert result.name == 'agent_transfer_contract' + assert result.ok + assert result.severity == 'info' assert result.extra == {'contract': TransferManifest.contract, 'operations': list(OPERATIONS), 'max_manifest_bytes': MAX_MANIFEST_BYTES, 'max_transfer_bytes': MAX_TRANSFER_BYTES, 'max_files': 500, 'state': 'contract_only', 'authority_granted': False, @@ -41,5 +43,7 @@ def test_existing_shared_diagnostics_surface_reports_only_contract_availability( report = run_tool_call('AC_diagnose', {}) assert report['count'] == 1 extra = report['checks'][0]['extra'] - assert extra['state'] == 'contract_only' and extra['authority_granted'] is False - assert extra['remote_verified'] is False and extra['native_verified'] is False + assert extra['state'] == 'contract_only' + assert extra['authority_granted'] is False + assert extra['remote_verified'] is False + assert extra['native_verified'] is False diff --git a/test/unit_test/headless/test_agent_transfer_evidence.py b/test/unit_test/headless/test_agent_transfer_evidence.py index acd620a66..0c4c2d4f8 100644 --- a/test/unit_test/headless/test_agent_transfer_evidence.py +++ b/test/unit_test/headless/test_agent_transfer_evidence.py @@ -66,18 +66,24 @@ def test_actual_local_dag_recovery_resolves_only_the_reviewed_portable_proof(tmp binding = actual_binding(runtime, source) status, proof = evidence_read(server, grant, binding) assert status == 200, proof - assert proof['source_task'] == source and proof['task_id'] == child - assert proof['grant_id'] == grant and proof['evidence_only'] is True and proof['input_authorized'] is False + assert proof['source_task'] == source + assert proof['task_id'] == child + assert proof['grant_id'] == grant + assert proof['evidence_only'] is True + assert proof['input_authorized'] is False assert proof['submitted_spec_digest'] == binding['submitted_spec_digest'] expected = {'key_hash': binding['request_key_hash'], 'submitted_spec_digest': binding['submitted_spec_digest']} digest = sha256(json.dumps(expected, sort_keys=True, separators=(',', ':')).encode()).hexdigest() assert proof['node']['request_digest'] == digest - assert len(proof['chain']) == 1 and proof['chain'][0]['source_task'] == source + assert len(proof['chain']) == 1 + assert proof['chain'][0]['source_task'] == source assert str(tmp_path) not in json.dumps(proof) - assert runtime.store.actions(child) == before and output.read_bytes() == body + assert runtime.store.actions(child) == before + assert output.read_bytes() == body status, again = evidence_create(server, reviewed) - assert status == 200 and again == created + assert status == 200 + assert again == created @pytest.mark.parametrize('boundary', ['create', 'resolve']) diff --git a/test/unit_test/headless/test_agent_transfer_evidence_expiry.py b/test/unit_test/headless/test_agent_transfer_evidence_expiry.py index 920648dd9..de4c60d88 100644 --- a/test/unit_test/headless/test_agent_transfer_evidence_expiry.py +++ b/test/unit_test/headless/test_agent_transfer_evidence_expiry.py @@ -25,7 +25,8 @@ def test_evidence_expiry_preserves_later_artifact_grant_and_all_user_output(tmp_ expiry = created['evidence']['summary']['expires'] with runtime.store.connection() as connection: original = runtime.store.read_task(connection, source) - assert original['state'] == 'cancelled' and expiry == original['updated'] + 30 * 86400 + assert original['state'] == 'cancelled' + assert expiry == original['updated'] + 30 * 86400 intent = dict(connection.execute('SELECT * FROM agent_transfer_intents WHERE id=?', (grant,)).fetchone()) assert exported['manifest']['expires'] > expiry + 100 binding = actual_binding(runtime, source) @@ -55,7 +56,8 @@ def test_evidence_expiry_preserves_later_artifact_grant_and_all_user_output(tmp_ assert dict(retained) == intent assert connection.execute('SELECT 1 FROM agent_transfer_revocations WHERE id=?', (grant,)).fetchone() artifact = runtime.transfers.source.records.find(connection, exported['export_id']) - assert artifact['state'] == 'active' and artifact['held'] is True + assert artifact['state'] == 'active' + assert artifact['held'] is True assert runtime.transfers.peer('export', {'export_id': exported['export_id']}) == exported['manifest'] assert output.read_bytes() == body finally: diff --git a/test/unit_test/headless/test_agent_transfer_evidence_management.py b/test/unit_test/headless/test_agent_transfer_evidence_management.py index f0b4b49ee..7ba91ae61 100644 --- a/test/unit_test/headless/test_agent_transfer_evidence_management.py +++ b/test/unit_test/headless/test_agent_transfer_evidence_management.py @@ -63,8 +63,9 @@ def test_review_refuses_misbound_expired_private_or_nonconsensual_evidence(chang summary['node'] = {'batch_id': '1' * 32, 'graph_hash': '2' * 64, 'node_id': 'node', 'request_digest': '3' * 64} reply['digest'] = sha256(_canonical(summary)).hexdigest() + peer = TransferPeer(lambda *_args: reply) with pytest.raises(AgentTaskError): - TransferPeer(lambda *_args: reply).manage('source', 'f' * 64, 'review_evidence', request, 3) + peer.manage('source', 'f' * 64, 'review_evidence', request, 3) @pytest.mark.parametrize('operation,change', [('get_evidence', 'identity'), ('get_evidence', 'summary-id'), @@ -85,5 +86,6 @@ def test_management_records_refuse_wrong_identity_state_quota_and_private_fields reply['evidence'] *= 201 else: value['issuer'] = 'private' + peer = TransferPeer(lambda *_args: reply) with pytest.raises(AgentTaskError): - TransferPeer(lambda *_args: reply).manage('source', 'f' * 64, operation, request, 3) + peer.manage('source', 'f' * 64, operation, request, 3) diff --git a/test/unit_test/headless/test_agent_transfer_evidence_remote.py b/test/unit_test/headless/test_agent_transfer_evidence_remote.py index fc66495ee..631db3696 100644 --- a/test/unit_test/headless/test_agent_transfer_evidence_remote.py +++ b/test/unit_test/headless/test_agent_transfer_evidence_remote.py @@ -83,7 +83,8 @@ def test_registered_http_peer_resolves_other_owner_completion_without_general_ta binding = actual_binding(value.runtime, value.source) host_binding = value.coordinator.batches.transfer_binding('source') proof = resolve_remote_evidence(value.coordinator.transfers.remote, 'source', host_binding, identifier, binding) - assert proof['task_id'] == value.child and proof['node']['node_id'] == 'remote-file' + assert proof['task_id'] == value.child + assert proof['node']['node_id'] == 'remote-file' assert value.runtime.store.get(value.child)['owner']['user_id'] == 'reviewer' assert value.output.read_bytes() == value.body status, denied = post(value.server, '/agent/transfer/peer', {'operation': 'evidence', 'request': { @@ -100,9 +101,10 @@ def test_fresh_remote_resolution_refuses_a_different_original_coordinator_witnes identifier = grant(value) binding = actual_binding(value.runtime, value.source) binding[change] = 'different-node' if change == 'node_id' else ('f' * (32 if change == 'batch_id' else 64)) + coordinator_binding = value.coordinator.batches.transfer_binding('source') with pytest.raises(AgentTaskError): resolve_remote_evidence(value.coordinator.transfers.remote, 'source', - value.coordinator.batches.transfer_binding('source'), identifier, binding) + coordinator_binding, identifier, binding) @pytest.mark.parametrize('boundary', ['reserve', 'consume_review']) @@ -143,7 +145,9 @@ def test_keyed_standalone_recovery_resolves_without_a_fabricated_dag_admission(t binding = actual_binding(value.runtime, source) proof = resolve_remote_evidence(value.coordinator.transfers.remote, 'source', value.coordinator.batches.transfer_binding('source'), created['evidence']['grant_id'], binding) - assert proof['source_task'] == source and proof['task_id'] == child and 'node' not in proof + assert proof['source_task'] == source + assert proof['task_id'] == child + assert 'node' not in proof with value.runtime.store.connection() as connection: assert connection.execute('SELECT count(*) FROM agent_dag_local_tasks').fetchone()[0] == 0 submitted = connection.execute('SELECT 1 FROM agent_task_submissions WHERE task_id=?', (source,)) @@ -159,13 +163,16 @@ def test_revoked_wire_consent_never_recreates_wire_authority(tmp_path, monkeypat identifier = created['evidence']['grant_id'] status, revoked = post(value.server, '/agent/transfer', {'operation': 'revoke_evidence', 'request': {'grant_id': identifier}}, token=value.owner_token) - assert status == 200 and revoked['evidence']['state'] == 'revoked' + assert status == 200 + assert revoked['evidence']['state'] == 'revoked' status, denied = evidence_create(value.server, reviewed, token=value.reviewer_token) assert status == 409, denied + coordinator_binding = value.coordinator.batches.transfer_binding('source') + binding = actual_binding(value.runtime, value.source) with pytest.raises(AgentTaskError): resolve_remote_evidence(value.coordinator.transfers.remote, 'source', - value.coordinator.batches.transfer_binding('source'), identifier, - actual_binding(value.runtime, value.source)) + coordinator_binding, identifier, binding) with value.runtime.store.connection() as connection: record = value.runtime.transfers.source.records.find(connection, identifier) - assert record['state'] == 'revoked' and record['held'] is False + assert record['state'] == 'revoked' + assert record['held'] is False diff --git a/test/unit_test/headless/test_agent_transfer_gui.py b/test/unit_test/headless/test_agent_transfer_gui.py index 29943a582..ea46198ca 100644 --- a/test/unit_test/headless/test_agent_transfer_gui.py +++ b/test/unit_test/headless/test_agent_transfer_gui.py @@ -41,8 +41,10 @@ def test_examples_prepare_explicit_source_destination_and_grant_selection(tab, m actions['agent_transfer_example_' + example]() assert view._operation.currentData() == operation request = json.loads(view._request.toPlainText()) - assert set(request) == fields and request['idempotency_key'] == '' - assert tab._pages.currentWidget() is view and tab.findChildren(QPushButton) == [] + assert set(request) == fields + assert request['idempotency_key'] == '' + assert tab._pages.currentWidget() is view + assert tab.findChildren(QPushButton) == [] tab.retranslate() assert json.loads(view._request.toPlainText()) == request @@ -91,7 +93,8 @@ def test_review_only_prepares_approval_after_an_explicit_action_preserving_key_a actions = dict(tab.menu_actions()) actions['agent_transfer_apply']() view._completed(TaskOutcome(41, 'ok', {'review': 'a' * 64, 'approval_required': True})) - assert view._operation.currentData() == 'review_export' and len(calls) == 1 + assert view._operation.currentData() == 'review_export' + assert len(calls) == 1 assert json.loads(view._request.toPlainText()) == request actions['agent_transfer_prepare_approval']() assert view._operation.currentData() == 'create_export' @@ -103,7 +106,8 @@ def test_review_only_prepares_approval_after_an_explicit_action_preserving_key_a view._request.setPlainText(json.dumps(prepared)) handle.outcome = 'finished' actions['agent_transfer_apply']() - assert len(calls) == 2 and calls[1].args == ('create_export', prepared) + assert len(calls) == 2 + assert calls[1].args == ('create_export', prepared) @pytest.mark.parametrize('language', ['english', 'traditional_chinese', 'simplified_chinese', 'japanese']) @@ -129,7 +133,8 @@ def test_create_rejects_missing_blank_or_nontext_approval_before_dispatch(tab, m view._operation.setCurrentIndex(view._operation.findData('create_evidence')) view._request.setPlainText(json.dumps({'review': 'a' * 64, 'idempotency_key': 'key', 'note': note})) dict(tab.menu_actions())['agent_transfer_apply']() - assert calls == [] and view._status.text() + assert calls == [] + assert view._status.text() def test_closed_transfer_gui_leaves_the_actual_accepted_import_owned_by_the_service(tab, tmp_path, monkeypatch): @@ -181,8 +186,10 @@ def paused(*args): assert (Path(current['files_directory']) / 'report.txt').read_bytes() == body with host.runtime.store.connection() as database: after = host.runtime.transfers.source.records.find(database, identifier) - assert before['scope'] == after['scope'] and before['expires'] == after['expires'] - assert view._result.toPlainText() == '' and host.runtime.list() == [] + assert before['scope'] == after['scope'] + assert before['expires'] == after['expires'] + assert view._result.toPlainText() == '' + assert host.runtime.list() == [] finally: release.set() deadline = time.monotonic() + 5 diff --git a/test/unit_test/headless/test_agent_transfer_import_contract.py b/test/unit_test/headless/test_agent_transfer_import_contract.py index d5dcc5a25..882635384 100644 --- a/test/unit_test/headless/test_agent_transfer_import_contract.py +++ b/test/unit_test/headless/test_agent_transfer_import_contract.py @@ -30,8 +30,9 @@ def test_unknown_destination_operations_are_typed_boundary_errors(operation): def test_invalid_destination_request_never_reaches_an_adapter(field, value): calls = [] peer = TransferPeer(lambda *_args: calls.append(True)) + invalid = {**request(), field: value} with pytest.raises(AgentTaskError): - peer.manage('destination', 'f' * 64, 'review_import', {**request(), field: value}, 3) + peer.manage('destination', 'f' * 64, 'review_import', invalid, 3) assert calls == [] @@ -51,8 +52,9 @@ def test_destination_review_reply_is_closed_and_binds_the_requested_export(chang else: reply['review_expires'] = False peer = TransferPeer(lambda *_args: deepcopy(reply)) + reviewed = request() with pytest.raises(AgentTaskError): - peer.manage('destination', 'f' * 64, 'review_import', request(), 3) + peer.manage('destination', 'f' * 64, 'review_import', reviewed, 3) @pytest.mark.parametrize('field,value', [('import_id', 'wrong'), ('state', True), ('expires', True), diff --git a/test/unit_test/headless/test_agent_transfer_import_integrity.py b/test/unit_test/headless/test_agent_transfer_import_integrity.py index 66baa7092..c000936b7 100644 --- a/test/unit_test/headless/test_agent_transfer_import_integrity.py +++ b/test/unit_test/headless/test_agent_transfer_import_integrity.py @@ -61,7 +61,8 @@ def source_reply(importer, *args): status, accepted = operation(server, token, 'create_import', review(server, exported, token, root)) assert status == 200, accepted current = wait_import(server, token, accepted['import']['import_id']) - assert len(publications) == 1 and changed == [True] + assert len(publications) == 1 + assert changed == [True] if fault == 'none': assert current['state'] == 'published', current assert sha256((publications[0] / 'report.txt').read_bytes()).hexdigest() == ( diff --git a/test/unit_test/headless/test_agent_transfer_import_lifecycle.py b/test/unit_test/headless/test_agent_transfer_import_lifecycle.py index 73fe4e8c4..6a572c481 100644 --- a/test/unit_test/headless/test_agent_transfer_import_lifecycle.py +++ b/test/unit_test/headless/test_agent_transfer_import_lifecycle.py @@ -169,7 +169,8 @@ def observed(importer, identifier, *args): delivered = host.runtime.transfers._life.importer._state.jobs.ledger.read(connection, identifier) assert delivered['generation'] == before['generation'] assert delivered['queue_id'] == before['queue_id'] - assert after['scope'] == original['scope'] and after['expires'] == original['expires'] + assert after['scope'] == original['scope'] + assert after['expires'] == original['expires'] assert host.runtime.list() == [] finally: release.set() diff --git a/test/unit_test/headless/test_agent_transfer_import_restart.py b/test/unit_test/headless/test_agent_transfer_import_restart.py index 01725e0cd..cc260e009 100644 --- a/test/unit_test/headless/test_agent_transfer_import_restart.py +++ b/test/unit_test/headless/test_agent_transfer_import_restart.py @@ -244,7 +244,8 @@ def observed(peer, source_host, binding, name, request, timeout): current = wait_import(named, token, identifier) assert current['state'] == 'published', current assert (Path(current['files_directory']) / 'report.txt').read_bytes() == body - assert 'export' in calls and 'chunk' not in calls, 'resume rechecks the real grant and owned cached bytes' + assert 'export' in calls, 'resume rechecks the real grant and owned cached bytes' + assert 'chunk' not in calls, 'resume rechecks the real grant and owned cached bytes' with restarted.runtime.store.connection() as connection: after = restarted.runtime.transfers.source.records.find(connection, identifier) delivery = connection.execute('SELECT queue_id,generation FROM agent_transfer_deliveries WHERE id=?', diff --git a/test/unit_test/headless/test_agent_transfer_import_retention.py b/test/unit_test/headless/test_agent_transfer_import_retention.py index 43d102737..971f0115d 100644 --- a/test/unit_test/headless/test_agent_transfer_import_retention.py +++ b/test/unit_test/headless/test_agent_transfer_import_retention.py @@ -75,9 +75,11 @@ def test_expired_import_metadata_and_delivery_are_pruned_without_recreating_cons status, denied = operation(server, token, 'create_import', request) assert status == 409, denied else: + records = TransferRecords(runtime.store) + retry = {name: value for name, value in record.items() + if name not in {'state', 'held', 'note_hash', 'key_hash'}} | {'key': 'lifecycle-import'} with runtime.store.connection(write=True) as connection, pytest.raises(AgentTaskError) as failure: - TransferRecords(runtime.store).reserve(connection, {name: value for name, value in record.items() - if name not in {'state', 'held', 'note_hash', 'key_hash'}} | {'key': 'lifecycle-import'}) + records.reserve(connection, retry) assert 'transfer' in str(failure.value) finally: if restart: @@ -132,10 +134,14 @@ def test_import_metadata_pruning_requires_a_writer_and_rolls_back_all_three_fami monkeypatch.setattr(transfer_records.time, 'time', lambda: 2100.0) with records.store.connection() as connection, pytest.raises(AgentTaskError, match='writer'): ledger.prune(connection, 2100.0) - with pytest.raises(RuntimeError, match='rollback after metadata deletion'): + + def prune_then_fail(): with records.store.connection(write=True) as connection: assert ledger.prune(connection, 2100.0) == 1 raise RuntimeError('rollback after metadata deletion') + + with pytest.raises(RuntimeError, match='rollback after metadata deletion'): + prune_then_fail() with records.store.connection() as connection: assert records.find(connection, item['id'])['held'] is False assert ledger.read(connection, item['id'])['state'] == 'expired' diff --git a/test/unit_test/headless/test_agent_transfer_management.py b/test/unit_test/headless/test_agent_transfer_management.py index 041aa8552..470a98970 100644 --- a/test/unit_test/headless/test_agent_transfer_management.py +++ b/test/unit_test/headless/test_agent_transfer_management.py @@ -49,8 +49,9 @@ def test_foreign_review_reply_is_exact_typed_and_binds_the_requested_source(chan peer = TransferPeer(lambda *_args: deepcopy(reply)) manage = getattr(peer, 'manage', None) assert callable(manage), 'a bounded closed management method must validate replies before routing' + request = source_request() with pytest.raises(AgentTaskError): - manage('source', 'f' * 64, 'review_export', source_request(), 3) + manage('source', 'f' * 64, 'review_export', request, 3) def test_valid_closed_management_review_preserves_only_the_exact_public_envelope(): diff --git a/test/unit_test/headless/test_agent_transfer_manifest.py b/test/unit_test/headless/test_agent_transfer_manifest.py index 8dfe0cda1..a5320dd53 100644 --- a/test/unit_test/headless/test_agent_transfer_manifest.py +++ b/test/unit_test/headless/test_agent_transfer_manifest.py @@ -62,14 +62,16 @@ def test_manifest_round_trip_detaches_empty_file_and_hashes_exact_canonical_enve def test_manifest_rejects_malformed_identity_deadline_shape_and_authority_fields(field, value): payload = manifest_value() payload[field] = value + parse = manifest_type().from_dict with pytest.raises(AgentTaskError): - manifest_type().from_dict(payload) + parse(payload) @pytest.mark.parametrize('value', [None, [], True, {'contract': 1}]) def test_manifest_requires_the_exact_complete_wire_object(value): + parse = manifest_type().from_dict with pytest.raises(AgentTaskError): - manifest_type().from_dict(value) + parse(value) @pytest.mark.parametrize(('field', 'value'), [ @@ -86,8 +88,9 @@ def test_manifest_requires_the_exact_complete_wire_object(value): def test_manifest_rejects_nonportable_or_inconsistent_file_envelopes(field, value): payload = manifest_value() payload['files'][0][field] = value + parse = manifest_type().from_dict with pytest.raises(AgentTaskError): - manifest_type().from_dict(payload) + parse(payload) @pytest.mark.parametrize('names', [ @@ -96,15 +99,17 @@ def test_manifest_rejects_nonportable_or_inconsistent_file_envelopes(field, valu def test_manifest_rejects_output_names_that_collide_on_supported_filesystems(names): payload = manifest_value() payload['files'] = [{**payload['files'][0], 'name': name} for name in names] + parse = manifest_type().from_dict with pytest.raises(AgentTaskError): - manifest_type().from_dict(payload) + parse(payload) def test_manifest_rejects_501_distinct_files_before_publication(): payload = manifest_value() payload['files'] = [{**payload['files'][0], 'name': f'{number}.bin'} for number in range(501)] + parse = manifest_type().from_dict with pytest.raises(AgentTaskError): - manifest_type().from_dict(payload) + parse(payload) def file_descriptor(name, size): @@ -115,8 +120,9 @@ def file_descriptor(name, size): def test_manifest_rejects_combined_file_bytes_above_64_mib(): payload = manifest_value() payload['files'] = [file_descriptor('full.bin', 64 * 1024 * 1024), file_descriptor('extra.bin', 1)] + parse = manifest_type().from_dict with pytest.raises(AgentTaskError): - manifest_type().from_dict(payload) + parse(payload) @pytest.mark.parametrize(('field', 'value'), [ diff --git a/test/unit_test/headless/test_agent_transfer_models.py b/test/unit_test/headless/test_agent_transfer_models.py index a11fbd273..4c03f061e 100644 --- a/test/unit_test/headless/test_agent_transfer_models.py +++ b/test/unit_test/headless/test_agent_transfer_models.py @@ -62,16 +62,18 @@ def test_selection_round_trips_an_immutable_detached_criterion_selection(): None, [], True, ]) def test_selection_rejects_ambiguous_or_nonportable_wire_objects(value): + selection = selection_type() with pytest.raises(AgentTaskError): - selection_type().from_dict(value) + selection.from_dict(value) @pytest.mark.parametrize(('criterion', 'names'), [ (True, ()), (-1, ()), (0, ['report.csv']), (0, ('../report.csv',)), ]) def test_selection_constructor_keeps_the_same_immutable_value_bounds(criterion, names): + selection = selection_type() with pytest.raises(AgentTaskError): - selection_type()(criterion, names) + selection(criterion, names) @pytest.mark.parametrize('names', [[], ['報告.csv'], ['e\u0301.csv']]) diff --git a/test/unit_test/headless/test_agent_transfer_payloads.py b/test/unit_test/headless/test_agent_transfer_payloads.py index 9d1b1254a..be9584008 100644 --- a/test/unit_test/headless/test_agent_transfer_payloads.py +++ b/test/unit_test/headless/test_agent_transfer_payloads.py @@ -75,7 +75,8 @@ def transport(batches, host, binding, path, body, timeout): with import_hosts(tmp_path, monkeypatch, source_context=manifest_source, selection=selection) as values: host, server, exported, bodies, token, root, _actor = values manifests = {item['name']: item for item in exported['manifest']['files']} - assert manifests['empty.bin']['size'] == 0 and manifests['empty.bin']['chunks'] == [] + assert manifests['empty.bin']['size'] == 0 + assert manifests['empty.bin']['chunks'] == [] if 'large.bin' in manifests: assert len(manifests['large.bin']['chunks']) == 3 status, reply = operation(server, token, 'create_import', review(server, exported, token, root)) diff --git a/test/unit_test/headless/test_agent_transfer_private_sources.py b/test/unit_test/headless/test_agent_transfer_private_sources.py index d78140830..bc593ecb8 100644 --- a/test/unit_test/headless/test_agent_transfer_private_sources.py +++ b/test/unit_test/headless/test_agent_transfer_private_sources.py @@ -36,7 +36,8 @@ def test_actual_private_file_is_refused_before_opening_selected_bytes(tmp_path, 'evidence': runtime.evidence.directory(initial) / 'report.json', 'cache': runtime.transfers.source._cache.path} private = paths[kind] - assert private.is_file() and private.stat().st_size > 0 + assert private.is_file() + assert private.stat().st_size > 0 task = completed_file(runtime, tmp_path, private) actual_open = transfer_files.os.open @@ -71,16 +72,19 @@ def test_current_private_root_prevents_reading_an_older_approved_snapshot(tmp_pa with completed_source(tmp_path) as (runtime, task, output, _body, _users): review = review_for(runtime, task) exported = create_for(runtime, review) + + def attempt(): + if boundary == 'retry': + return create_for(runtime, review) + request = {'export_id': exported['export_id']} + if boundary == 'chunk': + request['sha256'] = exported['manifest']['files'][0]['chunks'][0] + return runtime.transfers.peer(boundary, request) + with monkeypatch.context() as scoped: scoped.setattr(type(runtime.evidence), 'root', property(lambda _self: output.parent)) with pytest.raises(AgentTaskError, match='private'): - if boundary == 'retry': - create_for(runtime, review) - else: - request = {'export_id': exported['export_id']} - if boundary == 'chunk': - request['sha256'] = exported['manifest']['files'][0]['chunks'][0] - runtime.transfers.peer(boundary, request) + attempt() def test_path_authority_deadline_is_checked_before_reading_a_cached_chunk(tmp_path, monkeypatch): diff --git a/test/unit_test/headless/test_agent_transfer_reader_evidence.py b/test/unit_test/headless/test_agent_transfer_reader_evidence.py index 6d9aecd12..54af01e76 100644 --- a/test/unit_test/headless/test_agent_transfer_reader_evidence.py +++ b/test/unit_test/headless/test_agent_transfer_reader_evidence.py @@ -41,10 +41,14 @@ def test_actual_renewed_reader_exports_only_evidence_without_reviving_old_input( binding = actual_binding(value.runtime, value.source) status, proof = evidence_read(server, identifier, binding, token=owner_token) assert status == 200, proof - assert proof['task_id'] == child and proof['source_task'] == value.source - assert proof['evidence_only'] is True and proof['input_authorized'] is False - assert 'owner' not in proof and str(value.reports) not in json.dumps(proof) - assert value.runtime.store.actions(value.source) == actions and value.manifest.read_bytes() == output + assert proof['task_id'] == child + assert proof['source_task'] == value.source + assert proof['evidence_only'] is True + assert proof['input_authorized'] is False + assert 'owner' not in proof + assert str(value.reports) not in json.dumps(proof) + assert value.runtime.store.actions(value.source) == actions + assert value.manifest.read_bytes() == output assert value.runtime.store.actions(child) == [] with authorization_scope(value.owner), pytest.raises(AgentTaskError, match='another caller'): value.runtime.get(child) @@ -92,7 +96,8 @@ def test_lost_actual_reader_authority_cannot_create_or_resolve_a_portable_grant( status, result = evidence_read(server, identifier, binding, token=owner_token) assert status == 409, result assert value.runtime.store.actions(value.source) == before - assert value.runtime.store.actions(child) == [] and value.manifest.read_bytes() == output + assert value.runtime.store.actions(child) == [] + assert value.manifest.read_bytes() == output def test_retained_readonly_consent_still_requires_the_original_batch_issuance_state(renewal, monkeypatch): diff --git a/test/unit_test/headless/test_agent_transfer_records.py b/test/unit_test/headless/test_agent_transfer_records.py index 2fa5e3966..a408b7876 100644 --- a/test/unit_test/headless/test_agent_transfer_records.py +++ b/test/unit_test/headless/test_agent_transfer_records.py @@ -61,8 +61,9 @@ def test_kind_pruning_reclaims_source_metadata_quota_without_erasing_consent_tom assert connection.execute('SELECT count(*) FROM agent_transfer_intents').fetchone()[0] == 128 assert connection.execute('SELECT count(*) FROM agent_transfer_revocations').fetchone()[0] == 128 assert reserve(records, fresh)['id'] == fresh['id'] + pruned = record(1) with pytest.raises(AgentTaskError): - reserve(records, record(1)) + reserve(records, pruned) def transfer_records_time(records): @@ -211,8 +212,9 @@ def test_global_revocation_tombstone_rejects_even_a_previously_absent_identity(t records = make_records(tmp_path, monkeypatch) with records.store.connection(write=True) as connection: records.revoke(connection, record()['id']) + absent = record() with pytest.raises(AgentTaskError): - reserve(records, record()) + reserve(records, absent) def test_private_blob_account_is_unique_across_families_even_after_cleanup(tmp_path, monkeypatch): @@ -220,15 +222,17 @@ def test_private_blob_account_is_unique_across_families_even_after_cleanup(tmp_p item = record() activate(records, item) for kind in ('export', 'import'): + sharing = record(2, kind=kind, account=item['account']) with pytest.raises(AgentTaskError): - reserve(records, record(2, kind=kind, account=item['account'])) + reserve(records, sharing) with records.store.connection(write=True) as connection: records.revoke(connection, item['id']) records.release(connection, item['id']) monkeypatch.setattr(importlib.import_module(MODULE).time, 'time', lambda: 2100.0) assert records.prune(2100.0) == {'removed': 1, 'held': 0} + reused = record(3, account=item['account'], created=2100.0, review_expires=2400.0, expires=3100.0) with pytest.raises(AgentTaskError): - reserve(records, record(3, account=item['account'], created=2100.0, review_expires=2400.0, expires=3100.0)) + reserve(records, reused) def test_expiry_blocks_exact_retry_and_active_metadata_without_clearing_bytes(tmp_path, monkeypatch): @@ -247,11 +251,15 @@ def test_expiry_blocks_exact_retry_and_active_metadata_without_clearing_bytes(tm def test_late_failure_rolls_back_review_key_and_reservation_together(tmp_path, monkeypatch): records = make_records(tmp_path, monkeypatch) item = record() - with pytest.raises(RuntimeError, match='crash'): + + def crash(): with records.store.connection(write=True) as connection: records.reserve(connection, item) records.consume_review(connection, item['review'], item['key'], 'approved') raise RuntimeError('crash after consent before commit') + + with pytest.raises(RuntimeError, match='crash'): + crash() assert records.prune(2100.0) == {'removed': 0, 'held': 0} assert activate(records, item)['id'] == item['id'] @@ -270,8 +278,9 @@ def test_global_capacity_remains_held_after_revoke(tmp_path, monkeypatch, kind, extra = dict(changes) if extra.get('chunks') == 500: extra.update(size=193, chunks=193) + overflow = record(999, kind=kind, **extra) with pytest.raises(AgentTaskError): - reserve(records, record(999, kind=kind, **extra)) + reserve(records, overflow) def test_two_writers_cannot_overbook_last_retained_grant_slot(tmp_path, monkeypatch): @@ -302,8 +311,9 @@ def test_cleaned_but_retained_source_grants_still_count_against_metadata_limit(t records.reserve(connection, item) records.revoke(connection, item['id']) records.release(connection, item['id']) + overflow = record(999) with pytest.raises(AgentTaskError): - reserve(records, record(999)) + reserve(records, overflow) def test_route_review_uses_own_family_and_exact_private_peer_binding(tmp_path, monkeypatch): @@ -331,8 +341,9 @@ def test_review_deadline_is_not_refreshed_but_exact_consumed_retry_is_allowed(tm with records.store.connection(write=True) as connection: with pytest.raises(AgentTaskError): records.consume_review(connection, second['review'], second['key'], 'approved') + third = record(3) with pytest.raises(AgentTaskError): - reserve(records, record(3)) + reserve(records, third) @pytest.mark.parametrize('change', [ @@ -344,8 +355,9 @@ def test_review_deadline_is_not_refreshed_but_exact_consumed_retry_is_allowed(tm ]) def test_invalid_private_reservation_never_commits(tmp_path, monkeypatch, change): records = make_records(tmp_path, monkeypatch) + invalid = record(**change) with pytest.raises(AgentTaskError): - reserve(records, record(**change)) + reserve(records, invalid) assert records.prune(2100.0) == {'removed': 0, 'held': 0} @@ -362,11 +374,15 @@ def test_invalid_note_does_not_consume_review(tmp_path, monkeypatch, note): def test_initialize_inside_writer_does_not_implicitly_commit_unrelated_changes(tmp_path, monkeypatch): records = make_records(tmp_path, monkeypatch) - with pytest.raises(RuntimeError): + + def initialize_then_fail(): with records.store.connection(write=True) as connection: connection.execute('CREATE TABLE owned_marker(value TEXT)') connection.execute("INSERT INTO owned_marker VALUES ('must roll back')") records.initialize(connection) raise RuntimeError('rollback initialization too') + + with pytest.raises(RuntimeError): + initialize_then_fail() with records.store.connection() as connection: assert not connection.execute("SELECT name FROM sqlite_master WHERE name='owned_marker'").fetchall() diff --git a/test/unit_test/headless/test_agent_transfer_registered.py b/test/unit_test/headless/test_agent_transfer_registered.py index ee6a72a12..1c928a60d 100644 --- a/test/unit_test/headless/test_agent_transfer_registered.py +++ b/test/unit_test/headless/test_agent_transfer_registered.py @@ -67,13 +67,15 @@ def test_another_real_clients_persisted_replacement_refuses_the_old_binding_befo else: other.add_host('source', server.base_url + '/changed' if change == 'endpoint' else server.base_url, 'replacement-credential' if change == 'token' else token) - assert book.list_hosts()[0].token == token and book.list_hosts()[0].base_url == server.base_url + assert book.list_hosts()[0].token == token + assert book.list_hosts()[0].base_url == server.base_url def forbidden(*_args, **_kwargs): raise AssertionError('replaced registration must fail before network dispatch') monkeypatch.setattr(transport, 'request_json', forbidden) + transfer = adapter() with pytest.raises(AgentTaskError): - adapter()('source', pinned, '/agent/transfer/peer', - {'operation': 'export', 'request': {'export_id': exported['export_id']}}, 3) + transfer('source', pinned, '/agent/transfer/peer', + {'operation': 'export', 'request': {'export_id': exported['export_id']}}, 3) @pytest.mark.parametrize('change', ['registration', 'caller']) @@ -91,9 +93,10 @@ def changed(*args, **kwargs): users.remove_user('coordinator') return result monkeypatch.setattr(transport, 'request_json', changed) + transfer = adapter() with pytest.raises((AgentTaskError, AuthorizationError)): - adapter()('source', pinned, '/agent/transfer/peer', - {'operation': 'export', 'request': {'export_id': exported['export_id']}}, 3) + transfer('source', pinned, '/agent/transfer/peer', + {'operation': 'export', 'request': {'export_id': exported['export_id']}}, 3) @pytest.mark.parametrize('payload', [b'not json', b'[]', b'{"hosts":null}', b'{"hosts":[]}']) @@ -107,9 +110,10 @@ def forbidden(*_args, **_kwargs): raise AssertionError('invalid book must not load/quarantine or dispatch') monkeypatch.setattr(admin_client.AdminConsoleClient, '_load', forbidden) monkeypatch.setattr(transport, 'request_json', forbidden) + transfer = adapter() with pytest.raises(AgentTaskError): - adapter()('source', pinned, '/agent/transfer/peer', - {'operation': 'export', 'request': {'export_id': exported['export_id']}}, 3) + transfer('source', pinned, '/agent/transfer/peer', + {'operation': 'export', 'request': {'export_id': exported['export_id']}}, 3) assert book.persist_path.read_bytes() == payload @@ -121,9 +125,10 @@ def test_transfer_adapter_refuses_other_routes_before_dispatch(tmp_path, monkeyp def forbidden(*_args, **_kwargs): raise AssertionError('unapproved transfer route was dispatched') monkeypatch.setattr(transport, 'request_json', forbidden) + transfer = adapter() with pytest.raises(AgentTaskError): - adapter()('source', pinned, path, - {'operation': 'export', 'request': {'export_id': exported['export_id']}}, 3) + transfer('source', pinned, path, + {'operation': 'export', 'request': {'export_id': exported['export_id']}}, 3) @pytest.mark.parametrize('change', ['empty_query', 'empty_fragment', 'credential_control']) @@ -166,9 +171,10 @@ def test_a_separate_real_process_cannot_leave_a_cached_transfer_binding_authorit def forbidden(*_args, **_kwargs): raise AssertionError('the stale registration was dispatched after a real process changed its book') monkeypatch.setattr(transport, 'request_json', forbidden) + transfer = adapter() with pytest.raises(AgentTaskError): - adapter()('source', pinned, '/agent/transfer/peer', - {'operation': 'export', 'request': {'export_id': exported['export_id']}}, 3) + transfer('source', pinned, '/agent/transfer/peer', + {'operation': 'export', 'request': {'export_id': exported['export_id']}}, 3) assert book.list_hosts()[0].token == token assert book.list_hosts()[0].base_url == server.base_url @@ -177,6 +183,7 @@ def test_huge_transfer_timeout_is_a_typed_pre_dispatch_failure(monkeypatch): def forbidden(*_args, **_kwargs): raise AssertionError('huge transfer timeout reached a network request') monkeypatch.setattr(transport, 'request_json', forbidden) + transfer = adapter() with pytest.raises(AgentTaskError): - adapter()('source', 'f' * 64, '/agent/transfer/peer', - {'operation': 'export', 'request': {'export_id': 'a' * 32}}, 1 << 4096) + transfer('source', 'f' * 64, '/agent/transfer/peer', + {'operation': 'export', 'request': {'export_id': 'a' * 32}}, 1 << 4096) diff --git a/test/unit_test/headless/test_agent_transfer_registry.py b/test/unit_test/headless/test_agent_transfer_registry.py index d29a867c9..78a680735 100644 --- a/test/unit_test/headless/test_agent_transfer_registry.py +++ b/test/unit_test/headless/test_agent_transfer_registry.py @@ -51,14 +51,18 @@ def test_current_registry_reclassification_invalidates_an_older_approved_artifac review = review_for(runtime, task) exported = create_for(runtime, review) monkeypatch.setattr(admin_client, '_default_console', second) + if boundary == 'retry': + def cross(): + return create_for(runtime, review) + else: + request = {'export_id': exported['export_id']} + if boundary == 'chunk': + request['sha256'] = exported['manifest']['files'][0]['chunks'][0] + + def cross(): + return runtime.transfers.peer(boundary, request) with pytest.raises(AgentTaskError, match='private'): - if boundary == 'retry': - create_for(runtime, review) - else: - request = {'export_id': exported['export_id']} - if boundary == 'chunk': - request['sha256'] = exported['manifest']['files'][0]['chunks'][0] - runtime.transfers.peer(boundary, request) + cross() def test_ordinary_output_with_the_actual_books_basename_is_not_reserved(tmp_path, monkeypatch): diff --git a/test/unit_test/headless/test_agent_transfer_route_records.py b/test/unit_test/headless/test_agent_transfer_route_records.py index 6667f3eda..bb7b61f46 100644 --- a/test/unit_test/headless/test_agent_transfer_route_records.py +++ b/test/unit_test/headless/test_agent_transfer_route_records.py @@ -12,8 +12,9 @@ def test_only_128_durable_routes_can_be_retained(tmp_path, monkeypatch): with records.store.connection(write=True) as connection: for number in range(128): records.reserve(connection, record(number, kind='route', account=None)) + extra = record(129, kind='route', account=None) with records.store.connection(write=True) as connection, pytest.raises(AgentTaskError, match='quota'): - records.reserve(connection, record(129, kind='route', account=None)) + records.reserve(connection, extra) def test_route_expiry_frees_only_its_metadata_and_keeps_key_revocation_fences(tmp_path, monkeypatch): diff --git a/test/unit_test/headless/test_agent_transfer_routed_evidence.py b/test/unit_test/headless/test_agent_transfer_routed_evidence.py index 04d79913c..8a4742812 100644 --- a/test/unit_test/headless/test_agent_transfer_routed_evidence.py +++ b/test/unit_test/headless/test_agent_transfer_routed_evidence.py @@ -23,7 +23,8 @@ def routed(tmp_path, monkeypatch): wire, invoke = [], transport.request_json def observe(url, **kwargs): if url == value.server.base_url + '/agent/transfer': - assert kwargs['token'] == value.reviewer_token and kwargs.get('delegate_local') is False + assert kwargs['token'] == value.reviewer_token + assert kwargs.get('delegate_local') is False record = deepcopy(kwargs['body']) wire.append(record) result = invoke(url, **kwargs) @@ -62,23 +63,28 @@ def test_named_coordinator_reviews_creates_lists_and_revokes_one_actual_source_e control = value.coordinator.transfers.control reviewed = review(value) assert reviewed['review'] != value.wire[-1]['reply']['review'] - assert reviewed['evidence']['task_id'] == value.child and count_grants(value) == 0 + assert reviewed['evidence']['task_id'] == value.child + assert count_grants(value) == 0 request = approval(reviewed) created = control('create_evidence', request)['evidence'] - assert created['summary'] == reviewed['evidence'] and created['state'] == 'active' + assert created['summary'] == reviewed['evidence'] + assert created['state'] == 'active' assert control('create_evidence', request)['evidence'] == created assert control('get_evidence', {'host': 'source', 'grant_id': created['grant_id']})['evidence'] == created listed = control('list_evidence', {'host': 'source', 'source_task': value.source})['evidence'] assert listed == [created] revoked = control('revoke_evidence', {'host': 'source', 'grant_id': created['grant_id']})['evidence'] - assert revoked['state'] == 'revoked' and revoked['summary'] == created['summary'] + assert revoked['state'] == 'revoked' + assert revoked['summary'] == created['summary'] with pytest.raises(AgentTaskError): control('create_evidence', request) - assert count_grants(value) == 1 and value.coordinator.list() == [] + assert count_grants(value) == 1 + assert value.coordinator.list() == [] with authorization_scope(value.owner), pytest.raises(AgentTaskError, match='another caller'): value.runtime.get(value.child) encoded = json.dumps(value.wire) - assert str(value.coordinator_users.path) not in encoded and value.owner_token not in encoded + assert str(value.coordinator_users.path) not in encoded + assert value.owner_token not in encoded assert all('host' not in item['request'] and 'owner' not in item['request'] for item in value.wire) assert value.output.read_bytes() == value.body @@ -110,13 +116,17 @@ def ambiguous(url, **kwargs): value.coordinator.close() value.coordinator = AgentRuntime(path, targets={}) current = value.coordinator.transfers.control('create_evidence', request)['evidence'] - assert current['summary'] == reviewed['evidence'] and current['state'] == 'active' - assert count_grants(value) == 1 and len(dropped) == 1 + assert current['summary'] == reviewed['evidence'] + assert current['state'] == 'active' + assert count_grants(value) == 1 + assert len(dropped) == 1 with value.coordinator.store.connection() as database: records = value.coordinator.transfers.source.records routes = records.entries(database, 'route') - assert len(routes) == 1 and routes[0]['scope']['evidence'] == reviewed['evidence'] - assert 'manifest' not in routes[0]['scope'] and routes[0]['account'] is None + assert len(routes) == 1 + assert routes[0]['scope']['evidence'] == reviewed['evidence'] + assert 'manifest' not in routes[0]['scope'] + assert routes[0]['account'] is None @pytest.mark.parametrize('change', ['host', 'actor', 'key', 'issuer-role']) @@ -134,4 +144,5 @@ def test_changed_host_identity_key_or_source_role_cannot_mint_a_routed_grant(rou if change == 'actor' else value.actor) with authorization_scope(actor), pytest.raises(AgentTaskError): value.coordinator.transfers.control('create_evidence', request) - assert count_grants(value) == 0 and value.output.read_bytes() == value.body + assert count_grants(value) == 0 + assert value.output.read_bytes() == value.body diff --git a/test/unit_test/headless/test_agent_transfer_routing.py b/test/unit_test/headless/test_agent_transfer_routing.py index a311690ac..87d7ae49e 100644 --- a/test/unit_test/headless/test_agent_transfer_routing.py +++ b/test/unit_test/headless/test_agent_transfer_routing.py @@ -74,11 +74,13 @@ def create(routed, reviewed, **changes): def test_real_routed_consent_returns_a_local_token_and_exact_source_bytes(routed): reviewed = review(routed) peer_review = routed.wire[-1]['reply']['review'] - assert reviewed['review'] != peer_review and reviewed['approval_required'] is True + assert reviewed['review'] != peer_review + assert reviewed['approval_required'] is True assert reviewed['digest'] == routed.wire[-1]['reply']['digest'] assert 'host' not in routed.wire[-1]['request'] created = create(routed, reviewed)['export'] - assert created['manifest'] == reviewed['manifest'] and created['state'] == 'active' + assert created['manifest'] == reviewed['manifest'] + assert created['state'] == 'active' assert routed.wire[-1]['request']['review'] == peer_review pin = routed.runtime.batches.transfer_binding('source') digest = created['manifest']['files'][0]['chunks'][0] @@ -89,7 +91,8 @@ def test_real_routed_consent_returns_a_local_token_and_exact_source_bytes(routed with pytest.raises(AgentTaskError): routed.runtime.get(routed.task) encoded = json.dumps(reviewed) - assert str(routed.users.path) not in encoded and routed.actor.user_id not in encoded + assert str(routed.users.path) not in encoded + assert routed.actor.user_id not in encoded with routed.source.store.connection() as connection: scope = json.loads(connection.execute('SELECT payload FROM agent_transfer_grants').fetchone()[0])['scope'] assert scope['issuer']['user_id'] == scope['recipient']['user_id'] == 'owner' @@ -102,7 +105,8 @@ def test_routed_management_get_list_and_revoke_use_the_actual_source(routed): assert controller.control('get_export', {'host': 'source', 'export_id': created['export_id']})['export'] == created assert controller.control('list_exports', {'host': 'source', 'source_task': routed.task})['exports'] == [created] revoked = controller.control('revoke_export', {'host': 'source', 'export_id': created['export_id']})['export'] - assert revoked['state'] == 'revoked' and revoked['cleanup_pending'] is False + assert revoked['state'] == 'revoked' + assert revoked['cleanup_pending'] is False assert routed.runtime.list() == [] @@ -116,7 +120,9 @@ def lose_ack(url, **kwargs): if envelope.get('operation') == 'create_export': with routed.runtime.store.connection() as connection: rows = connection.execute('SELECT state,note_hash FROM agent_transfer_routed_reviews').fetchall() - assert len(rows) == 1 and rows[0]['state'] == 'active' and rows[0]['note_hash'] + assert len(rows) == 1 + assert rows[0]['state'] == 'active' + assert rows[0]['note_hash'] sent.append(deepcopy(envelope)) reply = original(url, **kwargs) if len(sent) == 1: @@ -133,7 +139,8 @@ def lose_ack(url, **kwargs): routed.runtime.close() routed.runtime = AgentRuntime(path, targets={}) created = create(routed, reviewed)['export'] - assert created['export_id'] == identifier and sent[0] == sent[1] + assert created['export_id'] == identifier + assert sent[0] == sent[1] with routed.source.store.connection() as connection: assert connection.execute('SELECT count(*) FROM agent_transfer_grants').fetchone()[0] == 1 @@ -143,7 +150,8 @@ def test_routed_recipient_policy_derives_only_the_sources_actual_identities(rout created = create(routed, review(routed, recipient_policy='source_owner'))['export'] with routed.source.store.connection() as connection: scope = json.loads(connection.execute('SELECT payload FROM agent_transfer_grants').fetchone()[0])['scope'] - assert scope['issuer']['user_id'] == 'reviewer' and scope['recipient']['user_id'] == 'owner' + assert scope['issuer']['user_id'] == 'reviewer' + assert scope['recipient']['user_id'] == 'owner' pin = routed.runtime.batches.transfer_binding('source') with pytest.raises(AgentTaskError): routed.runtime.transfers.remote.call('source', pin, 'export', {'export_id': created['export_id']}, 3) @@ -331,7 +339,8 @@ def limited(url, **kwargs): monkeypatch.setattr(controller._routing, '_prepare', elapsed) monkeypatch.setattr(transport, 'request_json', limited) - assert review(routed)['approval_required'] is True and len(observed) == 1 + assert review(routed)['approval_required'] is True + assert len(observed) == 1 def test_pending_route_cap_refuses_more_source_requests(routed, monkeypatch): diff --git a/test/unit_test/headless/test_agent_transfer_skill_evidence.py b/test/unit_test/headless/test_agent_transfer_skill_evidence.py index 4b9f2c984..59ab2e542 100644 --- a/test/unit_test/headless/test_agent_transfer_skill_evidence.py +++ b/test/unit_test/headless/test_agent_transfer_skill_evidence.py @@ -36,13 +36,17 @@ def test_portable_consent_records_only_real_original_actions_without_fabricating with pytest.raises(AgentTaskError, match='another caller'): runtime.get(value.child_id) candidate = runtime.skills.control('record', request_for(value, handoff_id=identifier)) - assert candidate['state'] == 'draft' and candidate['version'] == 0 and candidate['publication'] is None + assert candidate['state'] == 'draft' + assert candidate['version'] == 0 + assert candidate['publication'] is None assert candidate['spec']['actions'] == [value.action] assert candidate['provenance']['source_task'] == value.task_id proof = candidate['provenance']['completion_review'] - assert proof['task_id'] == value.child_id and proof['effect_task'] == value.task_id + assert proof['task_id'] == value.child_id + assert proof['effect_task'] == value.task_id assert proof['evidence_handoff']['id'] == identifier - assert runtime.store.actions(value.task_id) == receipts and value.manifest.read_bytes() == manifest + assert runtime.store.actions(value.task_id) == receipts + assert value.manifest.read_bytes() == manifest assert runtime.store.actions(value.child_id) == [] with runtime.store.connection() as connection: assert connection.execute('SELECT count(*) FROM agent_skill_trials').fetchone()[0] == 0 @@ -61,12 +65,15 @@ def test_portable_completion_publishes_the_real_trial_and_preserves_its_original receipts = runtime.store.actions(trial.task_id) manifest = trial.manifest.read_bytes() accepted = runtime.skills.control('accept', {**acceptance(trial), 'handoff_id': identifier}) - assert accepted['state'] == 'accepted' and accepted['version'] == 1 + assert accepted['state'] == 'accepted' + assert accepted['version'] == 1 assert accepted['publication']['task_id'] == trial.task_id proof = accepted['publication']['completion_review'] - assert proof['task_id'] == trial.child_id and proof['effect_task'] == trial.task_id + assert proof['task_id'] == trial.child_id + assert proof['effect_task'] == trial.task_id assert proof['evidence_handoff']['id'] == identifier - assert runtime.store.actions(trial.task_id) == receipts and trial.manifest.read_bytes() == manifest + assert runtime.store.actions(trial.task_id) == receipts + assert trial.manifest.read_bytes() == manifest assert runtime.store.actions(trial.child_id) == [] with runtime.store.connection() as connection: tasks = connection.execute('SELECT task_id,purpose FROM agent_skill_trials').fetchall() @@ -83,8 +90,9 @@ def test_portable_revocation_inside_candidate_insert_rolls_back_the_entire_recor with runtime.store.connection(write=True) as connection: connection.execute('CREATE TRIGGER revoke_skill_grant AFTER INSERT ON agent_skill_candidates BEGIN ' "UPDATE agent_transfer_grants SET state='revoked' WHERE id='" + identifier + "'; END") + request = request_for(recording, handoff_id=identifier) with pytest.raises(AgentTaskError): - runtime.skills.control('record', request_for(recording, handoff_id=identifier)) + runtime.skills.control('record', request) assert runtime.skills.control('list')['total'] == 0 with runtime.store.connection() as connection: assert connection.execute('SELECT count(*) FROM agent_skill_trials').fetchone()[0] == 0 @@ -96,8 +104,9 @@ def test_a_revoked_portable_grant_cannot_be_reinterpreted_as_an_old_local_grant( runtime = recording.runtime identifier = portable_grant(recording) runtime.transfers.control('revoke_evidence', {'grant_id': identifier}) + request = request_for(recording, handoff_id=identifier) with pytest.raises(AgentTaskError): - runtime.skills.control('record', request_for(recording, handoff_id=identifier)) + runtime.skills.control('record', request) assert runtime.skills.control('list')['total'] == 0 @@ -108,10 +117,13 @@ def test_revocation_inside_publication_rolls_back_version_and_completion_adoptio with runtime.store.connection(write=True) as connection: connection.execute('CREATE TRIGGER revoke_published_grant AFTER INSERT ON agent_skill_publications BEGIN ' "UPDATE agent_transfer_grants SET state='revoked' WHERE id='" + identifier + "'; END") + request = {**acceptance(trial), 'handoff_id': identifier} with pytest.raises(AgentTaskError): - runtime.skills.control('accept', {**acceptance(trial), 'handoff_id': identifier}) + runtime.skills.control('accept', request) candidate = runtime.skills.control('get', {'candidate_id': trial.candidate['id']}) - assert candidate['state'] == 'draft' and candidate['version'] == 0 and candidate['publication'] is None + assert candidate['state'] == 'draft' + assert candidate['version'] == 0 + assert candidate['publication'] is None with runtime.store.connection() as connection: assert connection.execute('SELECT count(*) FROM agent_skill_completions').fetchone()[0] == 0 assert connection.execute('SELECT count(*) FROM agent_skill_publications').fetchone()[0] == 0 @@ -131,8 +143,9 @@ def test_changed_portable_authority_or_selection_cannot_record_a_skill(recording connection.execute('UPDATE agent_transfer_grants SET expires=0 WHERE id=?', (identifier,)) else: selected = value.task_id + request = request_for(value, completion_task_id=selected, handoff_id=identifier) with pytest.raises(AgentTaskError): - runtime.skills.control('record', request_for(value, completion_task_id=selected, handoff_id=identifier)) + runtime.skills.control('record', request) assert runtime.skills.control('list')['total'] == 0 @@ -145,7 +158,8 @@ def test_authenticated_source_service_records_portable_completion_and_rejects_re request = {'operation': 'record', 'request': request_for(value, handoff_id=identifier)} status, draft = post(server, '/agent/skill', request, token=token) assert status == 200, draft - assert draft['state'] == 'draft' and draft['spec']['actions'] == [value.action] + assert draft['state'] == 'draft' + assert draft['spec']['actions'] == [value.action] assert draft['provenance']['completion_review']['task_id'] == value.child_id status, revoked = post(server, '/agent/transfer', {'operation': 'revoke_evidence', 'request': {'grant_id': identifier}}, token=token) @@ -165,9 +179,11 @@ def test_portable_executable_chain_retains_original_trial_effects_and_explicit_r reviewed = runtime.skills.control('review_execution', request) accepted = runtime.skills.control('accept', {**request, 'execution_review': reviewed['execution_review'], 'expected_version': 0, 'note': 'Review exact original effects and the granted completed successor'}) - assert accepted['version'] == 1 and accepted['publication']['task_id'] == value.task_id + assert accepted['version'] == 1 + assert accepted['publication']['task_id'] == value.task_id proof = accepted['publication']['execution_review'] - assert proof['source_task'] == value.task_id and proof['task_id'] == value.child_id + assert proof['source_task'] == value.task_id + assert proof['task_id'] == value.child_id assert proof['evidence_handoff']['id'] == identifier assert value.target.calls == calls with runtime.store.connection() as connection: @@ -195,7 +211,8 @@ def during_write(*args, **kwargs): monkeypatch.setattr(runtime.skills, '_record_publication', during_write) result = runtime.skills.control('accept', {**request, 'execution_review': reviewed['execution_review'], 'expected_version': 0, 'note': 'Retain the original owner through publication'}) - assert result['version'] == 1 and result['publication']['task_id'] == value.task_id + assert result['version'] == 1 + assert result['publication']['task_id'] == value.task_id with runtime.store.connection() as connection: assert connection.execute('SELECT count(*) FROM agent_skill_publications').fetchone()[0] == 1 runtime.close() @@ -220,7 +237,8 @@ def during_write(*args, **kwargs): monkeypatch.setattr(runtime.skills, '_insert_candidate', during_write) result = runtime.skills.control('record', request_for(value, handoff_id=identifier)) - assert result['state'] == 'draft' and result['publication'] is None + assert result['state'] == 'draft' + assert result['publication'] is None runtime.close() reopened = AgentRuntime(runtime.store.path, targets={}) reopened.close() diff --git a/test/unit_test/headless/test_agent_transfer_source.py b/test/unit_test/headless/test_agent_transfer_source.py index 174507f8d..5c0a15eb1 100644 --- a/test/unit_test/headless/test_agent_transfer_source.py +++ b/test/unit_test/headless/test_agent_transfer_source.py @@ -180,7 +180,9 @@ def test_source_bytes_changed_after_review_never_publish_and_user_file_is_preser assert output.read_bytes() == b'changed after review' with runtime.store.connection() as connection: rows = runtime.transfers.source.records.entries(connection, 'export') - assert len(rows) == 1 and rows[0]['state'] == 'revoked' and rows[0]['held'] is False + assert len(rows) == 1 + assert rows[0]['state'] == 'revoked' + assert rows[0]['held'] is False @pytest.mark.parametrize('operation', [[], {}]) diff --git a/test/unit_test/headless/test_agent_transfer_source_expiry.py b/test/unit_test/headless/test_agent_transfer_source_expiry.py index 354d34077..67cdf1cad 100644 --- a/test/unit_test/headless/test_agent_transfer_source_expiry.py +++ b/test/unit_test/headless/test_agent_transfer_source_expiry.py @@ -34,15 +34,17 @@ def test_source_expiry_cleans_owned_cache_and_preserves_user_output_and_foreign_ try: source = runtime.transfers.source if boundary == 'cleanup-failure': - with monkeypatch.context() as scoped, pytest.raises(OSError, match='owned expiry cleanup failure'): - def failed(account): - assert account == original['account'] - raise OSError('owned expiry cleanup failure') + def failed(account): + assert account == original['account'] + raise OSError('owned expiry cleanup failure') + with monkeypatch.context() as scoped: scoped.setattr(source._cache, 'delete_account', failed) - runtime.transfers.maintain() + with pytest.raises(OSError, match='owned expiry cleanup failure'): + runtime.transfers.maintain() with runtime.store.connection() as connection: retained = source.records.find(connection, exported['export_id']) - assert retained['state'] == 'revoked' and retained['held'] is True + assert retained['state'] == 'revoked' + assert retained['held'] is True assert source._cache.store.status(original['account'])['chunks'] > 0 runtime.maintain() with runtime.store.connection() as connection: @@ -51,7 +53,8 @@ def failed(account): (exported['export_id'],)).fetchone() is not None assert connection.execute('SELECT 1 FROM agent_transfer_revocations WHERE id=?', (exported['export_id'],)).fetchone() is not None - assert retained['state'] == 'revoked' and retained['held'] is False + assert retained['state'] == 'revoked' + assert retained['held'] is False assert retained['expires'] == original['expires'] assert retained['scope'] == original['scope'] assert source._cache.store.status(original['account']) == {'bytes': 0, 'chunks': 0, 'blobs': 0} @@ -88,8 +91,9 @@ def blocked(account): assert entered.wait(5) with pytest.raises(AgentTaskError, match='active transfer'): runtime.close() + targets = {'local-files': FileTarget()} with pytest.raises(AgentTaskError, match='still owns'): - AgentRuntime(runtime.store.path, targets={'local-files': FileTarget()}) + AgentRuntime(runtime.store.path, targets=targets) finally: release.set() cleaning.result(timeout=5) @@ -145,7 +149,8 @@ def test_source_expiry_does_not_retire_another_live_export_of_the_same_bytes(tmp assert runtime.transfers.peer('export', {'export_id': second['export_id']}) == second['manifest'] with runtime.store.connection() as connection: retained = source.records.find(connection, second['export_id']) - assert retained['held'] is True and retained['state'] == 'active' + assert retained['held'] is True + assert retained['state'] == 'active' assert retained['expires'] == live['expires'] assert output.read_bytes() == body diff --git a/test/unit_test/headless/test_agent_transfer_source_lifecycle.py b/test/unit_test/headless/test_agent_transfer_source_lifecycle.py index 43b388c49..3b30b0418 100644 --- a/test/unit_test/headless/test_agent_transfer_source_lifecycle.py +++ b/test/unit_test/headless/test_agent_transfer_source_lifecycle.py @@ -55,16 +55,19 @@ def test_actual_ancestor_owner_and_explicit_reviewer_revocation_invalidates_expo review = review_for(runtime, child) if boundary != 'review' else None exported = create_for(runtime, review) if boundary in {'export', 'chunk'} else None (owners if lost == 'owner' else reviewers).remove_user(lost) - with pytest.raises(AgentTaskError): + + def attempt(): if boundary == 'review': - review_for(runtime, child) - elif boundary == 'create': - create_for(runtime, review) - else: - request = {'export_id': exported['export_id']} - if boundary == 'chunk': - request['sha256'] = exported['manifest']['files'][0]['chunks'][0] - runtime.transfers.peer(boundary, request) + return review_for(runtime, child) + if boundary == 'create': + return create_for(runtime, review) + request = {'export_id': exported['export_id']} + if boundary == 'chunk': + request['sha256'] = exported['manifest']['files'][0]['chunks'][0] + return runtime.transfers.peer(boundary, request) + + with pytest.raises(AgentTaskError): + attempt() @pytest.mark.parametrize('depth', [16, 17]) @@ -88,14 +91,17 @@ def test_cancelled_source_operation_does_not_continue_to_read_or_issue(tmp_path, exported = create_for(runtime, review) if boundary == 'chunk' else None token = CancellationToken(1) token.cancel() - with cancellation_scope(token), pytest.raises(CancelledError): + + def attempt(): if boundary == 'review': - review_for(runtime, task) - elif boundary == 'create': - create_for(runtime, review) - else: - runtime.transfers.peer('chunk', {'export_id': exported['export_id'], - 'sha256': exported['manifest']['files'][0]['chunks'][0]}) + return review_for(runtime, task) + if boundary == 'create': + return create_for(runtime, review) + return runtime.transfers.peer('chunk', {'export_id': exported['export_id'], + 'sha256': exported['manifest']['files'][0]['chunks'][0]}) + + with cancellation_scope(token), pytest.raises(CancelledError): + attempt() @pytest.mark.parametrize('boundary', ['reserve', 'consume_review']) @@ -168,12 +174,14 @@ def failed(_account): runtime.transfers.control('revoke_export', request) with runtime.store.connection() as connection: retained = source.records.find(connection, exported['export_id']) - assert retained['state'] == 'revoked' and retained['held'] + assert retained['state'] == 'revoked' + assert retained['held'] with pytest.raises(AgentTaskError): runtime.transfers.peer('export', request) monkeypatch.setattr(source._cache, 'delete_account', original) revoked = runtime.transfers.control('revoke_export', request)['export'] - assert revoked['state'] == 'revoked' and not revoked['cleanup_pending'] + assert revoked['state'] == 'revoked' + assert not revoked['cleanup_pending'] assert output.read_bytes() == body @@ -201,7 +209,8 @@ def test_artifact_expiry_uses_its_actual_completed_source_not_an_older_terminal_ with runtime.store.connection() as connection: ancestor = runtime.store.read_task(connection, source) leaf = runtime.store.read_task(connection, child) - assert ancestor['state'] == 'cancelled' and leaf['updated'] - ancestor['updated'] >= 120 + assert ancestor['state'] == 'cancelled' + assert leaf['updated'] - ancestor['updated'] >= 120 exported = create_for(runtime, review_for(runtime, child)) assert exported['manifest']['expires'] == leaf['updated'] + 30 * 86400 @@ -212,11 +221,14 @@ def test_actual_cyclic_recovery_relation_is_refused_without_unbounded_traversal( exported = create_for(runtime, review_for(runtime, child)) if boundary == 'export' else None with runtime.store.connection(write=True) as connection: connection.execute('UPDATE agent_task_recoveries SET source_task=? WHERE task_id=?', (child, child)) - with pytest.raises(AgentTaskError, match='cyclic'): + + def attempt(): if boundary == 'review': - review_for(runtime, child) - else: - runtime.transfers.peer('export', {'export_id': exported['export_id']}) + return review_for(runtime, child) + return runtime.transfers.peer('export', {'export_id': exported['export_id']}) + + with pytest.raises(AgentTaskError, match='cyclic'): + attempt() @pytest.mark.parametrize('boundary', ['create', 'export']) @@ -235,11 +247,14 @@ def test_valid_replacement_of_actual_private_reviewer_still_requires_new_export_ if key not in {'review', 'source_snapshot'}}, details['source_snapshot']) connection.execute('UPDATE agent_recovery_owners SET details=? WHERE task_id=?', (json.dumps(replacement), child)) - with pytest.raises(AgentTaskError, match='changed'): + + def attempt(): if boundary == 'create': - create_for(runtime, review) - else: - runtime.transfers.peer('export', {'export_id': exported['export_id']}) + return create_for(runtime, review) + return runtime.transfers.peer('export', {'export_id': exported['export_id']}) + + with pytest.raises(AgentTaskError, match='changed'): + attempt() def test_expired_grant_never_reads_surviving_private_cached_bytes(tmp_path, monkeypatch): @@ -272,7 +287,8 @@ def lost_reply(*args, **kwargs): create_for(runtime, review) with runtime.store.connection() as connection: committed = source.records.entries(connection, 'export') - assert len(committed) == 1 and committed[0]['state'] == 'active' + assert len(committed) == 1 + assert committed[0]['state'] == 'active' recovered = create_for(runtime, review) assert recovered['export_id'] == committed[0]['id'] with runtime.store.connection() as connection: @@ -310,12 +326,13 @@ def confined(value, *args, **kwargs): raise AssertionError('the unrelated target descriptor must never be opened') return original(value, *args, **kwargs) monkeypatch.setattr(transfer_files.os, 'open', confined) + def attempt(): + if boundary == 'create': + return create_for(runtime, review) + return runtime.transfers.peer('export', {'export_id': exported['export_id']}) try: with pytest.raises(AgentTaskError): - if boundary == 'create': - create_for(runtime, review) - else: - runtime.transfers.peer('export', {'export_id': exported['export_id']}) + attempt() assert outside.read_bytes() == body finally: if os.name == 'nt': diff --git a/test/unit_test/headless/test_agent_transfer_source_lineage.py b/test/unit_test/headless/test_agent_transfer_source_lineage.py index 084278e12..f6c09d36d 100644 --- a/test/unit_test/headless/test_agent_transfer_source_lineage.py +++ b/test/unit_test/headless/test_agent_transfer_source_lineage.py @@ -120,16 +120,17 @@ def test_actual_ancestor_damage_invalidates_source_at_every_boundary(tmp_path, b exported = create_for(runtime, review) if boundary in {'export', 'chunk'} else None with runtime.store.connection(write=True) as connection: damage_lineage(connection, runtime, source, child, change) + if boundary == 'review': + attempt, arguments = review_for, (runtime, child) + elif boundary == 'create': + attempt, arguments = create_for, (runtime, review) + else: + request = {'export_id': exported['export_id']} + if boundary == 'chunk': + request['sha256'] = exported['manifest']['files'][0]['chunks'][0] + attempt, arguments = runtime.transfers.peer, (boundary, request) with pytest.raises(AgentTaskError): - if boundary == 'review': - review_for(runtime, child) - elif boundary == 'create': - create_for(runtime, review) - else: - request = {'export_id': exported['export_id']} - if boundary == 'chunk': - request['sha256'] = exported['manifest']['files'][0]['chunks'][0] - runtime.transfers.peer(boundary, request) + attempt(*arguments) @pytest.mark.parametrize('boundary', ['review', 'export']) @@ -143,11 +144,12 @@ def test_completed_original_node_rechecks_its_actual_admitting_admin(tmp_path, b runtime, _source, child, _output, _body, _batch = values exported = create_for(runtime, review_for(runtime, child)) if boundary == 'export' else None users.remove_user('admitting-admin') + if boundary == 'review': + attempt, arguments = review_for, (runtime, child) + else: + attempt, arguments = runtime.transfers.peer, ('export', {'export_id': exported['export_id']}) with pytest.raises(AgentTaskError): - if boundary == 'review': - review_for(runtime, child) - else: - runtime.transfers.peer('export', {'export_id': exported['export_id']}) + attempt(*arguments) def test_actual_admitting_admin_store_is_private_even_when_its_path_differs_from_the_owner(tmp_path): diff --git a/test/unit_test/headless/test_agent_transfer_source_readonly.py b/test/unit_test/headless/test_agent_transfer_source_readonly.py index 65e07db81..1e7d2a217 100644 --- a/test/unit_test/headless/test_agent_transfer_source_readonly.py +++ b/test/unit_test/headless/test_agent_transfer_source_readonly.py @@ -34,9 +34,12 @@ def test_actual_renewed_reader_exports_only_the_selected_file_with_old_input_sti child, exported = export_reader(renewal, monkeypatch) with authorization_scope(renewal.reviewer): manifest = renewal.runtime.transfers.peer('export', {'export_id': exported['export_id']}) - assert manifest['source_task'] == child and manifest['files'][0]['name'] == renewal.output.name + assert manifest['source_task'] == child + assert manifest['files'][0]['name'] == renewal.output.name public = json.dumps(exported) - assert str(renewal.reports) not in public and 'store_path' not in public and 'origin_policies' not in public + assert str(renewal.reports) not in public + assert 'store_path' not in public + assert 'origin_policies' not in public assert renewal.runtime.store.actions(renewal.source) == before assert renewal.old.identifier not in renewal.runtime.store.get(child)['spec']['options']['origin_policies'] diff --git a/test/unit_test/headless/test_agent_transfer_source_security.py b/test/unit_test/headless/test_agent_transfer_source_security.py index 4241b50ac..a968e087a 100644 --- a/test/unit_test/headless/test_agent_transfer_source_security.py +++ b/test/unit_test/headless/test_agent_transfer_source_security.py @@ -34,16 +34,19 @@ def test_tagged_source_cannot_replace_a_missing_real_submission_witness_with_a_c exported = create_for(runtime, review) if boundary in {'export', 'chunk'} else None with runtime.store.connection(write=True) as connection: connection.execute('DELETE FROM agent_task_submissions WHERE task_id=?', (task.task_id,)) - with pytest.raises(AgentTaskError): + + def attempt(): if boundary == 'review': - review_for(runtime, task.task_id) - elif boundary == 'create': - create_for(runtime, review) - else: - request = {'export_id': exported['export_id']} - if boundary == 'chunk': - request['sha256'] = exported['manifest']['files'][0]['chunks'][0] - runtime.transfers.peer(boundary, request) + return review_for(runtime, task.task_id) + if boundary == 'create': + return create_for(runtime, review) + request = {'export_id': exported['export_id']} + if boundary == 'chunk': + request['sha256'] = exported['manifest']['files'][0]['chunks'][0] + return runtime.transfers.peer(boundary, request) + + with pytest.raises(AgentTaskError): + attempt() @pytest.mark.parametrize('boundary', ['reserve', 'consume_review']) @@ -73,8 +76,9 @@ def test_off_manifest_hash_and_corrupted_actual_chunk_are_refused(tmp_path): with completed_source(tmp_path) as (runtime, task, _output, _body, _users): created = create_for(runtime, review_for(runtime, task)) source = runtime.transfers.source + other = sha256(b'other').hexdigest() with pytest.raises(AgentTaskError): - runtime.transfers.peer('chunk', {'export_id': created['export_id'], 'sha256': sha256(b'other').hexdigest()}) + runtime.transfers.peer('chunk', {'export_id': created['export_id'], 'sha256': other}) with runtime.store.connection() as connection: record = source.records.find(connection, created['export_id']) digest = created['manifest']['files'][0]['chunks'][0] @@ -102,8 +106,9 @@ def blocked(*args, **kwargs): assert entered.wait(10) with pytest.raises(AgentTaskError, match='active transfer'): runtime.close() + targets = {'local-files': FileTarget()} with pytest.raises(AgentTaskError, match='another runtime'): - AgentRuntime(runtime.store.path, targets={'local-files': FileTarget()}) + AgentRuntime(runtime.store.path, targets=targets) finally: release.set() assert future.result(timeout=10)['state'] == 'active' diff --git a/test/unit_test/headless/test_agent_transfer_staging.py b/test/unit_test/headless/test_agent_transfer_staging.py index e5b4fd3b3..be318c3a2 100644 --- a/test/unit_test/headless/test_agent_transfer_staging.py +++ b/test/unit_test/headless/test_agent_transfer_staging.py @@ -129,16 +129,24 @@ def test_staging_cleanup_refuses_unrecognized_or_replaced_objects(tmp_path, chan journal, retired = [], [] staging = ImportStaging(root, [status.st_dev, status.st_ino], journal.append, retired.append) manifest = AssetManifest((AssetFile('report.txt', sha256(b'x').hexdigest(), 1),)) - with pytest.raises(AgentTaskError, match='identity|unrecognized'): + created = [] + + def leave_an_unrecognized_object(): with staging.directory(root, manifest) as directory: if change == 'replacement-directory': directory.rename(root / 'preserved-original') directory.mkdir() unexpected = directory / 'user-journal.json' unexpected.write_bytes(b'preserve unrelated data') + created.append(unexpected) + + with pytest.raises(AgentTaskError, match='identity|unrecognized'): + leave_an_unrecognized_object() + [unexpected] = created assert unexpected.read_bytes() == b'preserve unrelated data' assert retired == [] - assert journal and journal[0]['cleaned'] is False + assert journal + assert journal[0]['cleaned'] is False @pytest.mark.parametrize('cancelled', [False, True]) @@ -160,8 +168,9 @@ def test_shared_publisher_uses_journaled_nested_empty_and_multichunk_files(tmp_p def check(): return cancelled and bool(staging._state.used) if cancelled: + transport = DirectoryAssetTransport(source, root) with pytest.raises(CancelledError): - _sync_assets(manifest, DirectoryAssetTransport(source, root), cancelled=check, staging=staging) + _sync_assets(manifest, transport, cancelled=check, staging=staging) assert list(root.iterdir()) == [] else: result = _sync_assets(manifest, DirectoryAssetTransport(source, root), staging=staging) diff --git a/test/unit_test/headless/test_agent_transfer_surfaces.py b/test/unit_test/headless/test_agent_transfer_surfaces.py index fb975ff31..2150bd032 100644 --- a/test/unit_test/headless/test_agent_transfer_surfaces.py +++ b/test/unit_test/headless/test_agent_transfer_surfaces.py @@ -55,7 +55,8 @@ def test_shared_transfer_surfaces_list_the_actual_source_without_creating_a_gran result = AgentRuntimeClient(value.connection).call('transfer', { 'operation': 'list_exports', 'request': request}) assert result == {'exports': []} - assert value.runtime.get(value.source).state == 'succeeded' and value.runtime.store.actions(value.source) == [] + assert value.runtime.get(value.source).state == 'succeeded' + assert value.runtime.store.actions(value.source) == [] assert value.output.read_bytes() == value.body with value.runtime.store.connection() as database: assert database.execute('SELECT count(*) FROM agent_transfer_grants').fetchone()[0] == 0 @@ -75,8 +76,10 @@ def test_public_api_names_and_builder_fields_share_the_same_management_contract( for module in (ac, api, core, agent_runtime): assert 'manage_agent_transfer' in module.__all__ function, fields, required, readonly, management = CATALOG['AC_agent_transfer'] - assert function is ac.manage_agent_transfer and required == ('operation',) - assert readonly is False and management is False + assert function is ac.manage_agent_transfer + assert required == ('operation',) + assert readonly is False + assert management is False assert fields['operation']['enum'] == list(OPERATIONS) assert not {'export', 'chunk', 'evidence'} & set(fields['operation']['enum']) specification = COMMAND_SPECS['AC_agent_transfer'] @@ -110,7 +113,8 @@ def test_public_creation_requires_its_review_and_keeps_peer_reads_on_the_separat 'idempotency_key': 'public-consent-flow'} first = ac.manage_agent_transfer('create_export', request) second = ac.manage_agent_transfer('create_export', request) - assert first == second and first['export']['state'] == 'active' + assert first == second + assert first['export']['state'] == 'active' manifest = first['export']['manifest'] assert manifest['files'][0]['sha256'] == sha256(value.body).hexdigest() identifier = first['export']['export_id'] @@ -119,4 +123,5 @@ def test_public_creation_requires_its_review_and_keeps_peer_reads_on_the_separat assert value.runtime.transfers.peer('export', {'export_id': identifier}) == manifest with value.runtime.store.connection() as database: assert database.execute('SELECT count(*) FROM agent_transfer_grants').fetchone()[0] == 1 - assert value.output.read_bytes() == value.body and value.runtime.store.actions(value.source) == [] + assert value.output.read_bytes() == value.body + assert value.runtime.store.actions(value.source) == [] diff --git a/test/unit_test/headless/test_agent_transfer_transport.py b/test/unit_test/headless/test_agent_transfer_transport.py index 70eee900e..6536e3b75 100644 --- a/test/unit_test/headless/test_agent_transfer_transport.py +++ b/test/unit_test/headless/test_agent_transfer_transport.py @@ -89,8 +89,9 @@ def test_chunk_reply_requires_exact_hash_bounded_bytes_and_canonical_base64(chan def test_unavailable_or_invalid_peer_request_fails_before_the_host_adapter(operation, payload): def forbidden(*_args): raise AssertionError('invalid peer request reached the host adapter') + transfer = peer_type()(forbidden) with pytest.raises(AgentTaskError): - peer_type()(forbidden).call('source', 'f' * 64, operation, payload, 3) + transfer.call('source', 'f' * 64, operation, payload, 3) def test_late_injected_wire_reply_is_discarded_even_when_its_manifest_is_well_formed(monkeypatch): @@ -102,8 +103,9 @@ def test_late_injected_wire_reply_is_discarded_even_when_its_manifest_is_well_fo def late(*_args): clock[0] = 4.0 return reply + transfer = peer(late) with pytest.raises(AgentTaskError, match='deadline'): - peer(late).call('source', 'f' * 64, 'export', {'export_id': 'a' * 32}, 3) + transfer.call('source', 'f' * 64, 'export', {'export_id': 'a' * 32}, 3) def test_a_rounded_remaining_time_never_exceeds_the_routes_thirty_second_bound(monkeypatch): @@ -159,5 +161,6 @@ def forbidden(*_args, **_kwargs): def test_invalid_peer_timeout_fails_with_typed_error_before_the_adapter(timeout): def forbidden(*_args): raise AssertionError('invalid timeout reached the adapter') + transfer = peer_type()(forbidden) with pytest.raises(AgentTaskError): - peer_type()(forbidden).call('source', 'f' * 64, 'export', {'export_id': 'a' * 32}, timeout) + transfer.call('source', 'f' * 64, 'export', {'export_id': 'a' * 32}, timeout) diff --git a/test/unit_test/headless/test_agent_transport_boundary.py b/test/unit_test/headless/test_agent_transport_boundary.py index eebf48511..787059e94 100644 --- a/test/unit_test/headless/test_agent_transport_boundary.py +++ b/test/unit_test/headless/test_agent_transport_boundary.py @@ -52,7 +52,8 @@ def test_explicit_local_service_delegation_preserves_principal_and_live_roots(ca request_json("http://127.0.0.1:1234/agent/tasks", token="fixture-token", delegate_local=True) headers = calls[0]["headers"] principal = verify_principal(headers[PRINCIPAL_HEADER], "fixture-token", "/agent/tasks") - assert principal.user_id == "fixture-user" and principal.role == "operator" + assert principal.user_id == "fixture-user" + assert principal.role == "operator" assert headers[ORIGIN_HEADER] == identifier finally: policy.revoke_managed_origin() diff --git a/test/unit_test/headless/test_agent_trigger_authorization_surfaces.py b/test/unit_test/headless/test_agent_trigger_authorization_surfaces.py index 7d630eeab..f8f09baef 100644 --- a/test/unit_test/headless/test_agent_trigger_authorization_surfaces.py +++ b/test/unit_test/headless/test_agent_trigger_authorization_surfaces.py @@ -18,7 +18,8 @@ def test_cli_exposes_review_and_explicit_creation(): for operation in ("review_authorization", "create_authorized"): parsed = build_parser().parse_args(["agent-task", "trigger", operation, "review.json"]) - assert parsed.trigger_operation == operation and parsed.request == "review.json" + assert parsed.trigger_operation == operation + assert parsed.request == "review.json" def test_authenticated_http_preserves_owner_and_does_not_dispatch_inherited_queued_tasks(retired_trigger, monkeypatch): @@ -42,7 +43,8 @@ def test_authenticated_http_preserves_owner_and_does_not_dispatch_inherited_queu start.assert_not_called() duplicate = client.call("trigger", {"operation": "fire", "request": {"trigger_id": child, "event_key": "old-event", "payload": {"row": 1}}}) - assert duplicate["duplicate"] and duplicate["task_id"] == old_task + assert duplicate["duplicate"] + assert duplicate["task_id"] == old_task start.assert_not_called() proof = client.call("trigger", {"operation": "get", "request": {"trigger_id": child}}) assert proof["trigger"]["reauthorization"]["actor"] == "alice" @@ -72,14 +74,17 @@ def test_gui_review_prepares_separate_explicit_request_and_refuses_wrong_selecti tab._present_result({"trigger_id": "source-a", "snapshot": "a" * 64, "authorization_review": "b" * 64, "spec": {"goal": "future only"}, "approval_required": True}) request = json.loads(tab._trigger_config.toPlainText()) - assert request["note"] == "" and request["authorization_review"] == "b" * 64 - assert tab._scope.toPlainText() == original_scope and calls == [] + assert request["note"] == "" + assert request["authorization_review"] == "b" * 64 + assert tab._scope.toPlainText() == original_scope + assert calls == [] tab._trigger_operation.setCurrentIndex(tab._trigger_operation.findData("create_authorized")) dict(tab.menu_actions())["agent_trigger_apply"]() assert calls.pop() == ("trigger", {"operation": "create_authorized", "request": request}) tab._trigger_id.setText("source-b") dict(tab.menu_actions())["agent_trigger_apply"]() - assert calls == [] and "selected registration" in tab._status.text() + assert calls == [] + assert "selected registration" in tab._status.text() tab._view_task = "trigger:source-a" tab._handle = None tab._completed(TaskOutcome(100, "ok", {"trigger_id": "late-created"})) diff --git a/test/unit_test/headless/test_agent_trigger_reauthorization.py b/test/unit_test/headless/test_agent_trigger_reauthorization.py index dac1ee591..d74ffd470 100644 --- a/test/unit_test/headless/test_agent_trigger_reauthorization.py +++ b/test/unit_test/headless/test_agent_trigger_reauthorization.py @@ -65,7 +65,8 @@ def test_renewal_preserves_event_dedup_and_pending_limit_without_dispatching_old start.assert_not_called() assert runtime.triggers.control("create_authorized", request)["trigger_id"] == child duplicate = fire(runtime, child, "old-event", {"row": 1}) - assert duplicate["duplicate"] and duplicate["task_id"] == old_task + assert duplicate["duplicate"] + assert duplicate["task_id"] == old_task start.assert_not_called() with pytest.raises(AgentTaskError, match="different payload"): fire(runtime, child, "old-event", {"row": 2}) @@ -75,13 +76,15 @@ def test_renewal_preserves_event_dedup_and_pending_limit_without_dispatching_old assert fresh["task_id"] != old_task start.assert_called_once_with(fresh["task_id"]) assert fire(runtime, child, "future-event")["duplicate"] - assert start.call_count == 2 and start.call_args.args == (fresh["task_id"],) + assert start.call_count == 2 + assert start.call_args.args == (fresh["task_id"],) assert runtime.store.get(fresh["task_id"])["spec"]["roots"] == [str(narrow)] with pytest.raises(AgentTaskError, match="replacement"): runtime.triggers.control("enable", {"trigger_id": source}) after = runtime.triggers._row(source) assert not after["enabled"] - assert after["spec"] == original["spec"] and after["spec"]["roots"] == [str(allowed)] + assert after["spec"] == original["spec"] + assert after["spec"]["roots"] == [str(allowed)] @pytest.mark.parametrize("change", ["root", "actions", "tools", "criteria", "options", "snapshot", "digest"]) @@ -151,7 +154,9 @@ def test_three_generations_share_dedup_pending_and_do_not_resurrect_a_removed_ch assert fire(runtime, grandchild, "future-event")["deferred"] runtime.triggers.control("remove", {"trigger_id": grandchild}) repeat = runtime.triggers.control("create_authorized", second) - assert repeat["trigger_id"] == grandchild and repeat["retired"] and not repeat["enabled"] + assert repeat["trigger_id"] == grandchild + assert repeat["retired"] + assert not repeat["enabled"] assert runtime.triggers.control("review_authorization", {"trigger_id": child})["spec"] is None with pytest.raises(AgentTaskError, match="replacement"): runtime.triggers.control("enable", {"trigger_id": child}) @@ -219,7 +224,8 @@ def test_schedule_renewal_starts_in_the_future_without_replaying_missed_slots(re with origin_scope(current_id): child = runtime.triggers.control("create_authorized", approval(runtime, source))["trigger_id"] row = runtime.triggers._row(child) - assert row["next_run"] > time.time() + 50 and row["config"] == {"seconds": 60} + assert row["next_run"] > time.time() + 50 + assert row["config"] == {"seconds": 60} runtime.triggers.poll() assert len(runtime.store.list()) == 1 @@ -256,7 +262,8 @@ def create(_index): with ThreadPoolExecutor(max_workers=2) as workers: results = list(workers.map(create, (0, 1))) - assert results[0] == results[1] and len(runtime.triggers.list()["triggers"]) == 2 + assert results[0] == results[1] + assert len(runtime.triggers.list()["triggers"]) == 2 def test_new_event_history_failure_rolls_back_task_and_local_event_together(retired_trigger): @@ -290,4 +297,5 @@ def test_unavailable_fixed_watch_path_withholds_runnable_review(retired_trigger) path.write_text(json.dumps(data)) with origin_scope(current_id): view = runtime.triggers.control("review_authorization", {"trigger_id": source}) - assert view["spec"] is None and view["issues"] == ["authorization_scope_unavailable"] + assert view["spec"] is None + assert view["issues"] == ["authorization_scope_unavailable"] diff --git a/test/unit_test/headless/test_agent_worker_shutdown.py b/test/unit_test/headless/test_agent_worker_shutdown.py index 17e02e17d..f81e96aab 100644 --- a/test/unit_test/headless/test_agent_worker_shutdown.py +++ b/test/unit_test/headless/test_agent_worker_shutdown.py @@ -25,11 +25,13 @@ def __init__(self, _spec): try: task = runtime.submit(specification(mode="ollama")) jobs.start(task.task_id) - assert entered.wait(3) and not runtime._leases + assert entered.wait(3) + assert not runtime._leases assert runtime.get(task.task_id).state == "queued" jobs.close() assert runtime.get(task.task_id).state == "cancelled" - assert not jobs._running and all(not thread.is_alive() for thread in jobs._workers) + assert not jobs._running + assert all(not thread.is_alive() for thread in jobs._workers) finally: jobs.close() runtime.close() diff --git a/test/unit_test/headless/test_agent_workspace_lifetime.py b/test/unit_test/headless/test_agent_workspace_lifetime.py index c8c4f5237..146b731dd 100644 --- a/test/unit_test/headless/test_agent_workspace_lifetime.py +++ b/test/unit_test/headless/test_agent_workspace_lifetime.py @@ -86,4 +86,5 @@ def check(arguments, scope): assert workspace_scope.focus_approved(spec, {"title_substring": "report"}) == 42 with pytest.raises(AgentTaskError, match="approved plan"): workspace_scope.focus_approved(spec, {"title_substring": "other"}) - assert len(probes) == 2 and len(calls) == 1 + assert len(probes) == 2 + assert len(calls) == 1 diff --git a/test/unit_test/headless/test_arm64_dependency_markers.py b/test/unit_test/headless/test_arm64_dependency_markers.py index 97f5b9abe..5662b494e 100644 --- a/test/unit_test/headless/test_arm64_dependency_markers.py +++ b/test/unit_test/headless/test_arm64_dependency_markers.py @@ -71,10 +71,8 @@ def _dependencies() -> List[str]: """Return the project's required dependencies, verbatim. - Read with a small regex rather than `tomllib`, which is 3.11+ while this - project supports 3.10 back to the floor. A guard that cannot run on the - oldest supported interpreter is not a guard, and using `tomllib` here - took the whole 3.10 collection down with it. + Read with a small regex rather than `tomllib`: the dependency list is all + this needs. """ text = (ROOT / "pyproject.toml").read_text(encoding="utf-8") block = re.search(r"^dependencies = \[(.*?)^\]", text, re.S | re.M) @@ -125,8 +123,8 @@ def test_the_unavailable_wheels_are_marked_off_windows_arm64(name: str) -> None: def test_the_available_wheels_are_not_marked_off_anything(name: str) -> None: """Pillow, mss and defusedxml ship for arm64 and carry no platform marker. - A requirement may be split by Python version (Pillow 13 needs 3.11, so - 3.10 keeps 12.3.0), but no line may depend on the platform. + A requirement may be split by Python version, but no line may depend on + the platform. """ requirements = _requirements(name) assert requirements, f"{name} is missing from the dependencies" diff --git a/test/unit_test/headless/test_background_restart_keeps_old_run_stopped.py b/test/unit_test/headless/test_background_restart_keeps_old_run_stopped.py index de84364ca..e22c937a6 100644 --- a/test/unit_test/headless/test_background_restart_keeps_old_run_stopped.py +++ b/test/unit_test/headless/test_background_restart_keeps_old_run_stopped.py @@ -168,7 +168,8 @@ def test_restart_does_not_revive_the_stuck_run(monkeypatch, tmp_path, build): # Folder transfers now retain the blocked sender until it actually exits. try: assert len(_StuckThread.created) == 1 - assert service.is_running() and first.is_set() + assert service.is_running() + assert first.is_set() assert not service.wait_until_ready(0) finally: service.stop() diff --git a/test/unit_test/headless/test_cancellable_diagnostics_example.py b/test/unit_test/headless/test_cancellable_diagnostics_example.py index a15097295..2254cae4b 100644 --- a/test/unit_test/headless/test_cancellable_diagnostics_example.py +++ b/test/unit_test/headless/test_cancellable_diagnostics_example.py @@ -15,7 +15,8 @@ def forbidden(): monkeypatch.setattr(example, 'run_diagnostics', forbidden) assert example.main([]) == 0 data = json.loads(capsys.readouterr().out) - assert data['mode'] == 'demo' and data['status'] == 'cancelled' + assert data['mode'] == 'demo' + assert data['status'] == 'cancelled' assert [value['completed'] for value in data['progress']] == [0] assert current_cancellation_token() is None @@ -28,7 +29,9 @@ def controlled(): monkeypatch.setattr(example, 'run_diagnostics', controlled) assert example.main(['--active']) == 0 data = json.loads(capsys.readouterr().out) - assert data['mode'] == 'active' and data['status'] == 'completed' and data['report']['count'] == 1 + assert data['mode'] == 'active' + assert data['status'] == 'completed' + assert data['report']['count'] == 1 assert current_cancellation_token() is None diff --git a/test/unit_test/headless/test_cancellable_wayland_io.py b/test/unit_test/headless/test_cancellable_wayland_io.py index 717f72aaa..77f3f5033 100644 --- a/test/unit_test/headless/test_cancellable_wayland_io.py +++ b/test/unit_test/headless/test_cancellable_wayland_io.py @@ -57,8 +57,10 @@ def request(): token.cancel() worker.join(1) assert not worker.is_alive(), 'the cancelled portal still waits for its 30-second response deadline' - assert len(failures) == 1 and isinstance(failures[0], CancelledError) - assert len(closes) == 1 and closes[0][1:] == (portal.REQUEST_INTERFACE, 'Close', 0.25, None) + assert len(failures) == 1 + assert isinstance(failures[0], CancelledError) + assert len(closes) == 1 + assert closes[0][1:] == (portal.REQUEST_INTERFACE, 'Close', 0.25, None) assert owned.fileno() == -1 finally: peer.close() @@ -93,8 +95,10 @@ def tracked(*args, **kwargs): token.cancel() worker.join(2) assert not worker.is_alive(), 'capture cancellation still waits for the helper deadline' - assert len(failures) == 1 and isinstance(failures[0], CancelledError) - assert len(processes) == 1 and processes[0].poll() is not None + assert len(failures) == 1 + assert isinstance(failures[0], CancelledError) + assert len(processes) == 1 + assert processes[0].poll() is not None finally: for process in processes: if process.poll() is None: @@ -127,7 +131,8 @@ def poll(_read, _write, _error, timeout): monkeypatch.setattr(oeffis, 'select', SimpleNamespace(select=poll)) with cancellation_scope(token), pytest.raises(CancelledError): oeffis.connect_eis_fd(timeout=30, symbols=fake) - assert waits and max(waits) <= 0.1 + assert waits + assert max(waits) <= 0.1 assert fake.calls.count(('unref', HANDLE)) == 1 diff --git a/test/unit_test/headless/test_cancellation_token.py b/test/unit_test/headless/test_cancellation_token.py index ffea89b25..da7205762 100644 --- a/test/unit_test/headless/test_cancellation_token.py +++ b/test/unit_test/headless/test_cancellation_token.py @@ -32,40 +32,47 @@ def waiter(): assert entered.wait(1) token.cancel() thread.join(1) - assert not thread.is_alive() and woke.is_set() and token.is_cancelled + assert not thread.is_alive() + assert woke.is_set() + assert token.is_cancelled with pytest.raises(CancelledError): token.checkpoint() with pytest.raises(CancelledError): token.report(2, 3) - assert observations == [progress] and token.wait(0) + assert observations == [progress] + assert token.wait(0) @pytest.mark.parametrize('timeout', [-1, float('inf'), float('-inf'), float('nan'), 1e308]) def test_wait_refuses_unbounded_or_negative_intervals(timeout): + token = CancellationToken(1) with pytest.raises(ValueError, match='finite nonnegative'): - CancellationToken(1).wait(timeout) + token.wait(timeout) @pytest.mark.parametrize('completed', [-1, True, 1.5, '1']) def test_invalid_completed_counts_are_rejected_before_notifying(completed): observations = [] + token = CancellationToken(1, observations.append) with pytest.raises(ValueError, match='nonnegative integer'): - CancellationToken(1, observations.append).report(completed) + token.report(completed) assert observations == [] @pytest.mark.parametrize('total', [-1, True, 0, 1.5, '3']) def test_invalid_totals_are_rejected_before_notifying(total): observations = [] + token = CancellationToken(1, observations.append) with pytest.raises(ValueError, match='at least as large'): - CancellationToken(1, observations.append).report(1, total) + token.report(1, total) assert observations == [] def test_progress_details_need_text_and_a_callback_is_optional(): token = CancellationToken(1) token.report(0) - assert not token.wait(0) and not token.is_cancelled + assert not token.wait(0) + assert not token.is_cancelled with pytest.raises(TypeError, match='must be text'): token.report(1, detail=7) diff --git a/test/unit_test/headless/test_capability_diagnostics_gui.py b/test/unit_test/headless/test_capability_diagnostics_gui.py index 5e3d1ead1..427e4d667 100644 --- a/test/unit_test/headless/test_capability_diagnostics_gui.py +++ b/test/unit_test/headless/test_capability_diagnostics_gui.py @@ -59,7 +59,8 @@ def test_capability_inspection_is_explicit_and_matches_the_headless_snapshot(pan widget, snapshot, calls = panel assert calls == [] dict(widget.menu_actions())['diag_probe']() - assert calls == ['passive'] and widget._table.rowCount() == 0 + assert calls == ['passive'] + assert widget._table.rowCount() == 0 table = widget._capability_view._table assert table.rowCount() == 2 assert table.item(0, 1).text() == snapshot.input.backend @@ -91,6 +92,9 @@ def canceled(_context): raise original monkeypatch.setattr(diagnostics_tab, 'probe_capabilities', canceled) + probe = dict(widget.menu_actions())['diag_probe'] with pytest.raises(OperationCancelledError) as caught: - dict(widget.menu_actions())['diag_probe']() - assert caught.value is original and calls == [] and widget._table.rowCount() == 0 + probe() + assert caught.value is original + assert calls == [] + assert widget._table.rowCount() == 0 diff --git a/test/unit_test/headless/test_capability_surfaces.py b/test/unit_test/headless/test_capability_surfaces.py index 6fea522b4..f8fe183d1 100644 --- a/test/unit_test/headless/test_capability_surfaces.py +++ b/test/unit_test/headless/test_capability_surfaces.py @@ -43,7 +43,8 @@ def test_executor_and_read_only_mcp_share_the_sdk_snapshot_without_accepting_sup tools = {tool.name: tool for tool in build_default_tool_registry(read_only=True)} tool = tools['ac_probe_capabilities'] assert command() == tool.invoke({}) == expected - assert tool.annotations.read_only and tool.output_schema['type'] == 'object' + assert tool.annotations.read_only + assert tool.output_schema['type'] == 'object' assert tool.input_schema.get('additionalProperties') is False with pytest.raises(TypeError): tool.invoke({'context': {'input_state': 'available'}}) diff --git a/test/unit_test/headless/test_ci_native_diagnostics.py b/test/unit_test/headless/test_ci_native_diagnostics.py index 7ab3f3482..d0cf68b1a 100644 --- a/test/unit_test/headless/test_ci_native_diagnostics.py +++ b/test/unit_test/headless/test_ci_native_diagnostics.py @@ -34,8 +34,10 @@ def test_child_result_output_and_runtime_versions_survive_the_wrapper(tmp_path, raw = (directory / 'runtime.json').read_text(encoding='utf-8') record = json.loads(raw) assert 'controlled child evidence' in log - assert record['returncode'] == exit_code and record['elapsed_seconds'] >= 0 - assert record['packages']['pytest'] and record['python'] + assert record['returncode'] == exit_code + assert record['elapsed_seconds'] >= 0 + assert record['packages']['pytest'] + assert record['python'] assert 'private-native-runner-sentinel' not in log + raw assert 'AC_CONTROLLED_PRIVATE_TEST_VALUE' not in log + raw @@ -47,9 +49,12 @@ def test_real_native_abort_retains_a_backtrace_and_fails(tmp_path): assert result.returncode != 0 log = (directory / 'process.log').read_text(encoding='utf-8') record = json.loads((directory / 'runtime.json').read_text(encoding='utf-8')) - assert 'before native abort' in log and 'SIGABRT' in log - assert '#0 ' in log and ('abort' in log or 'raise' in log) - assert record['returncode'] != 0 and record['debugger'] + assert 'before native abort' in log + assert 'SIGABRT' in log + assert '#0 ' in log + assert 'abort' in log or 'raise' in log + assert record['returncode'] != 0 + assert record['debugger'] @pytest.mark.skipif(sys.platform != 'linux' or shutil.which('gdb') is None, reason='requires Linux GDB') @@ -90,7 +95,8 @@ def test_unwritable_evidence_destination_prevents_launching_the_child(tmp_path): destination.write_text('preserve this file', encoding='utf-8') marker = tmp_path / 'must-not-launch' result = run_child(destination, f'from pathlib import Path;Path({str(marker)!r}).touch()') - assert result.returncode != 0 and not marker.exists() + assert result.returncode != 0 + assert not marker.exists() assert destination.read_text(encoding='utf-8') == 'preserve this file' @@ -99,14 +105,17 @@ def test_ci_workflows_keep_native_diagnostics_after_failure(name): workflow = yaml.safe_load((WORKFLOWS / name).read_text(encoding='utf-8')) steps = workflow['jobs']['pytest-headless']['steps'] upload = next(step for step in steps if step.get('with', {}).get('name', '').startswith('native-')) - assert upload['if'] == 'always()' and upload['uses'].startswith('actions/upload-artifact@') + assert upload['if'] == 'always()' + assert upload['uses'].startswith('actions/upload-artifact@') runner = next(step for step in steps if step.get('name', '').startswith('Run headless pytest')) preflight = next(step for step in steps if step.get('name') == 'Verify native diagnostics runner (Linux)') assert steps.index(preflight) < steps.index(runner) assert preflight['if'] == "runner.os == 'Linux'" assert 'test/unit_test/headless/test_ci_native_diagnostics.py' in preflight['run'] - assert '--timeout=30' in preflight['run'] and '/preflight' in preflight['run'] - assert '--native' in runner['run'] and '--artifact-dir' in runner['run'] + assert '--timeout=30' in preflight['run'] + assert '/preflight' in preflight['run'] + assert '--native' in runner['run'] + assert '--artifact-dir' in runner['run'] assert upload['with']['path'] in runner['run'] assert ('python -m coverage run -m pytest' if name == 'quality.yml' else 'python -m pytest') in runner['run'] assert any('gdb' in step.get('run', '') and step.get('if') == "runner.os == 'Linux'" for step in steps) diff --git a/test/unit_test/headless/test_codegen_from_journal.py b/test/unit_test/headless/test_codegen_from_journal.py index 2aecf6601..74a4d41d7 100644 --- a/test/unit_test/headless/test_codegen_from_journal.py +++ b/test/unit_test/headless/test_codegen_from_journal.py @@ -152,7 +152,8 @@ def test_generation_does_not_append_to_enclosing_journal(journal, tmp_path): def test_all_targets_are_generated_without_dispatch(journal, target): append_step(journal, "kept") generated = candidate(journal, target=target) - assert generated.code and generated.manifest["target"] == target + assert generated.code + assert generated.manifest["target"] == target assert generated.manifest["validation"]["static"] == "passed" diff --git a/test/unit_test/headless/test_codegen_journal_gui.py b/test/unit_test/headless/test_codegen_journal_gui.py index fefab3327..bae0c1080 100644 --- a/test/unit_test/headless/test_codegen_journal_gui.py +++ b/test/unit_test/headless/test_codegen_journal_gui.py @@ -59,7 +59,8 @@ def test_changing_source_clears_review_and_prevents_stale_import(journal, panel, panel._start_generate() idle(app, panel) panel._run_id.setText("other") - assert panel._candidate is None and not panel._code.toPlainText() + assert panel._candidate is None + assert not panel._code.toPlainText() def test_builder_import_is_explicit_and_never_dispatches(journal, app): @@ -133,7 +134,8 @@ def forbidden(*_args, **_kwargs): outcomes = [] worker.finished.connect(outcomes.append) worker.run() - assert outcomes[0].status == "error" and outcomes[0].candidate is None + assert outcomes[0].status == "error" + assert outcomes[0].candidate is None worker.deleteLater() app.processEvents() @@ -158,7 +160,8 @@ def export(*args, **kwargs): outcomes = [] worker.finished.connect(outcomes.append) worker.run() - assert outcomes[0].status == "ok" and output.exists() + assert outcomes[0].status == "ok" + assert output.exists() worker.deleteLater() app.processEvents() @@ -185,7 +188,8 @@ def blocked(*args, **kwargs): finally: release.set() idle(app, panel) - assert panel._candidate is None and not panel._code.toPlainText() + assert panel._candidate is None + assert not panel._code.toPlainText() def test_owned_dialog_retranslates_and_releases_language_listener(app): diff --git a/test/unit_test/headless/test_codegen_journal_surfaces.py b/test/unit_test/headless/test_codegen_journal_surfaces.py index 9a884d11d..9f7710a15 100644 --- a/test/unit_test/headless/test_codegen_journal_surfaces.py +++ b/test/unit_test/headless/test_codegen_journal_surfaces.py @@ -65,8 +65,9 @@ def forbidden(*_args, **_kwargs): monkeypatch.setattr("je_auto_control.utils.codegen.journal_source.Path.open", forbidden) context = ToolCallContext("candidate") context.cancelled_event.set() + generator = tool() with pytest.raises(OperationCancelledError): - tool().handler("private.jsonl", "selected", ctx=context) + generator.handler("private.jsonl", "selected", ctx=context) def test_archive_checks_cached_review_and_leaves_existing_file_on_replace_failure(journal, tmp_path, monkeypatch): diff --git a/test/unit_test/headless/test_config_sync.py b/test/unit_test/headless/test_config_sync.py index 9ad70a170..3445459e3 100644 --- a/test/unit_test/headless/test_config_sync.py +++ b/test/unit_test/headless/test_config_sync.py @@ -179,7 +179,8 @@ def test_push_round_trip_uses_put(): assert len(calls) == 1 method, body = calls[0] assert method == "PUT" - assert body["version"] == 2 and body["base_revision"] == 0 + assert body["version"] == 2 + assert body["base_revision"] == 0 assert body["bucket"]["user_id"] == "alice" assert "hk1" in body["bucket"]["sections"]["hotkeys"] diff --git a/test/unit_test/headless/test_config_sync_causal_client.py b/test/unit_test/headless/test_config_sync_causal_client.py index e0e33c3eb..9bd794bd1 100644 --- a/test/unit_test/headless/test_config_sync_causal_client.py +++ b/test/unit_test/headless/test_config_sync_causal_client.py @@ -35,7 +35,8 @@ def perform(call): def test_causal_client_flush_confirms_snapshot_atomically_with_delivery(tmp_path, monkeypatch): client, queue, operation, server, calls, _perform = connected(tmp_path, monkeypatch) report = client.flush(queue) - assert report.delivered == 1 and report.pending == 0 + assert report.delivered == 1 + assert report.pending == 0 assert [call['method'] for call in calls] == ['GET', 'PUT'] assert all(call['url'].endswith('/config/alice/causal') for call in calls) assert queue.snapshot(endpoint=operation.endpoint, user_id='alice') == server.get('alice') @@ -56,17 +57,20 @@ def uncertain(call): monkeypatch.setattr(http_client, 'perform_call', uncertain) policy = ac.SyncRetryPolicy(max_attempts=1, backoff=0) report = client.flush(queue, policy=policy) - assert report.delivered == 0 and report.pending == 1 + assert report.delivered == 0 + assert report.pending == 1 prepared = queue.pending(endpoint=operation.endpoint, user_id='alice')[0].prepared accepted_revision = server.get('alice').revision register(server, 'laptop') calls.clear() reopened = ac.SyncOutbox(tmp_path / 'outbox.sqlite3') report = client.flush(reopened, policy=policy) - assert report.delivered == 1 and report.pending == 0 + assert report.delivered == 1 + assert report.pending == 0 assert [call['method'] for call in calls] == ['PUT'] request = json.loads(calls[0]['body']) - assert request['bucket'] == prepared['bucket'] and request['base_revision'] == prepared['base_revision'] + assert request['bucket'] == prepared['bucket'] + assert request['base_revision'] == prepared['base_revision'] assert reopened.status(endpoint=operation.endpoint, user_id='alice')['confirmed_revision'] == accepted_revision assert reopened.snapshot(endpoint=operation.endpoint, user_id='alice').revision == accepted_revision + 1 @@ -84,7 +88,8 @@ def competing(call): report = client.flush(queue, max_rebases=2) assert report.delivered == 1 requests = [json.loads(call['body']) for call in calls if call['method'] == 'PUT'] - assert len(requests) == 2 and requests[0]['operation_id'] == requests[1]['operation_id'] == operation.delivery_id + assert len(requests) == 2 + assert requests[0]['operation_id'] == requests[1]['operation_id'] == operation.delivery_id assert requests[0]['base_revision'] + 1 == requests[1]['base_revision'] @@ -97,7 +102,8 @@ def test_operation_id_conflict_keeps_frozen_request_and_never_rebases(tmp_path, client.flush(queue, max_rebases=2) assert [call['method'] for call in calls] == ['GET', 'PUT'] state = queue.status(endpoint=operation.endpoint, user_id='alice') - assert state['conflict'] == 1 and state['pending'] == 1 + assert state['conflict'] == 1 + assert state['pending'] == 1 assert queue.pending(endpoint=operation.endpoint, user_id='alice')[0].prepared is not None assert queue.snapshot(endpoint=operation.endpoint, user_id='alice') is None assert server.get('alice').live_entries == (other,) @@ -113,7 +119,8 @@ def test_obsolete_peer_is_blocked_without_a_write_and_cancellation_keeps_pending assert client.flush(queue, cancelled=lambda: True).cancelled is True assert calls == [] result = client.flush(queue) - assert result.pending == 1 and [call['method'] for call in calls] == ['GET'] + assert result.pending == 1 + assert [call['method'] for call in calls] == ['GET'] assert queue.status(endpoint=operation.endpoint, user_id='alice')['blocked'] == 1 @@ -133,11 +140,12 @@ def old_server(call): def test_snapshot_receipt_rejects_uncommitted_or_mismatched_evidence(tmp_path, monkeypatch): _client, queue, operation, server, _calls, _perform = connected(tmp_path, monkeypatch) + snapshot = server.get('alice') with pytest.raises(ac.ConfigSyncError): - ac.CausalReceipt(99, server.get('alice')) + ac.CausalReceipt(99, snapshot) receipt = ac.CausalReceipt(server.get('alice').revision, server.get('alice')) with pytest.raises(ac.ConfigSyncError): - ac.CausalReceipt(receipt.revision, replace(receipt.snapshot, committed={'unknown': 1})) + replace(receipt.snapshot, committed={'unknown': 1}) assert queue.status(endpoint=operation.endpoint, user_id='alice')['pending'] == 1 @@ -151,7 +159,8 @@ def busy(call): return perform(call) monkeypatch.setattr(http_client, 'perform_call', busy) result = client.flush(queue, policy=ac.SyncRetryPolicy(max_attempts=1, backoff=0), max_rebases=2) - assert result.failed == 1 and result.pending == 1 + assert result.failed == 1 + assert result.pending == 1 assert len([call for call in calls if call['method'] == 'PUT']) == 3 assert queue.status(endpoint=operation.endpoint, user_id='alice')['offline'] == 1 assert queue.pending(endpoint=operation.endpoint, user_id='alice')[0].prepared is None @@ -167,7 +176,8 @@ def test_equal_revision_snapshot_fork_cannot_replace_cached_evidence(tmp_path, m queue.enqueue(replace(operation, operation_id='second', entries=(other,))) result = queue.flush(lambda _operation: ac.CausalReceipt(1, fork), endpoint=operation.endpoint, user_id='alice', policy=ac.SyncRetryPolicy(max_attempts=1)) - assert result.delivered == 0 and result.pending == 1 + assert result.delivered == 0 + assert result.pending == 1 assert queue.snapshot(endpoint=operation.endpoint, user_id='alice') == first @@ -209,13 +219,15 @@ def unavailable(*_args): policy = ac.SyncRetryPolicy(max_attempts=1, backoff=0) assert client.flush(queue, policy=policy).pending == 1 state = queue.status(endpoint=operation.endpoint, user_id='alice') - assert state['confirmed_revision'] == 0 and state['delivered'] == 0 + assert state['confirmed_revision'] == 0 + assert state['delivered'] == 0 assert queue.snapshot(endpoint=operation.endpoint, user_id='alice') is None accepted = server.get('alice').revision calls.clear() monkeypatch.setattr(queue, '_save_snapshot', original) assert client.flush(queue, policy=policy).delivered == 1 - assert [call['method'] for call in calls] == ['PUT'] and server.get('alice').revision == accepted + assert [call['method'] for call in calls] == ['PUT'] + assert server.get('alice').revision == accepted def test_prepared_request_cannot_smuggle_changes_outside_its_logical_operation(tmp_path, monkeypatch): @@ -226,7 +238,9 @@ def test_prepared_request_cannot_smuggle_changes_outside_its_logical_operation(t body['_actor'] = ac.SyncActor('desktop', 0).to_dict() queue.prepare(operation, base_revision=current.revision, bucket=body) result = client.flush(queue, policy=ac.SyncRetryPolicy(max_attempts=1)) - assert result.pending == 1 and result.delivered == 0 and calls == [] + assert result.pending == 1 + assert result.delivered == 0 + assert calls == [] assert server.get('alice') == current @@ -242,7 +256,8 @@ def echoed(call): return perform(call) monkeypatch.setattr(http_client, 'perform_call', echoed) result = client.flush(queue, policy=ac.SyncRetryPolicy(max_attempts=1)) - assert result.delivered == 0 and result.pending == 1 + assert result.delivered == 0 + assert result.pending == 1 assert queue.snapshot(endpoint=operation.endpoint, user_id='alice') is None assert 'synthetic-only' not in str(queue.status(endpoint=operation.endpoint, user_id='alice')['errors']) assert b'synthetic-only' not in (tmp_path / 'outbox.sqlite3').read_bytes() @@ -262,7 +277,8 @@ def test_shared_control_signals_preserve_mcp_and_history_import_contracts(): assert run_outcome.is_run_interruption is control_signals.is_run_interruption assert run_outcome.raise_if_run_interrupted is control_signals.raise_if_run_interrupted signal = OperationCancelledError('controlled') - assert isinstance(signal, RuntimeError) and not isinstance(signal, ac.AutoControlException) + assert isinstance(signal, RuntimeError) + assert not isinstance(signal, ac.AutoControlException) try: raise signal except OperationCancelledError as cause: diff --git a/test/unit_test/headless/test_config_sync_causal_guards.py b/test/unit_test/headless/test_config_sync_causal_guards.py index 1ab063e51..92e59042f 100644 --- a/test/unit_test/headless/test_config_sync_causal_guards.py +++ b/test/unit_test/headless/test_config_sync_causal_guards.py @@ -83,9 +83,14 @@ def test_entry_changes_require_an_honest_origin_and_causal_floor(tmp_path, chang else: args.update(version=entry.version, operation_id=entry.operation_id) proposed = ac.SyncEntry(**args) - with pytest.raises(ac.ConfigSyncError): + claim = actor('desktop') + + def commit_proposal(): candidate = current.with_entries((proposed,)) - store.commit('alice', candidate, base_revision=current.revision, operation_id=change, actor=actor('desktop')) + store.commit('alice', candidate, base_revision=current.revision, operation_id=change, actor=claim) + + with pytest.raises(ac.ConfigSyncError): + commit_proposal() assert store.get('alice') == current @@ -122,19 +127,23 @@ def test_collection_reclaims_payload_but_retains_floor_across_restart_and_restor operation_id='collect', actor=actor('desktop', 'collect')) with sqlite3.connect(tmp_path / 'config.sqlite3') as connection: raw = json.loads(connection.execute('SELECT body FROM config_buckets').fetchone()[0]) - assert raw['entries'] == [] and 'value' not in raw['fences'][0] + assert raw['entries'] == [] + assert 'value' not in raw['fences'][0] current = ac.CausalConfigStore(tmp_path / 'config.sqlite3').get('alice') assert current.with_entries((entry, deleted)).entries == () concurrent = ac.SyncEntry('settings', 'theme', {'value': 'offline'}, {'laptop': 1}, 'offline', 'laptop') conflict = current.with_entries((concurrent,)) store.commit('alice', conflict, base_revision=current.revision, operation_id='offline', actor=actor('laptop')) current = store.get('alice') - assert len(current.conflicts) == 1 and current.collect() == current + assert len(current.conflicts) == 1 + assert current.collect() == current resolved = concurrent.evolve({'value': 'restored'}, origin='laptop', operation_id='restore', observed=(deleted,)) store.commit('alice', current.with_entries((resolved,)), base_revision=current.revision, operation_id='restore', actor=actor('laptop')) final = store.get('alice') - assert final.live_entries == (resolved,) and final.fences == () and final.conflicts == () + assert final.live_entries == (resolved,) + assert final.fences == () + assert final.conflicts == () def test_concurrent_store_instances_share_one_atomic_cas_boundary(tmp_path): @@ -149,7 +158,8 @@ def write(origin): return 'conflict' with ThreadPoolExecutor(max_workers=2) as pool: outcomes = list(pool.map(write, ('desktop', 'laptop'))) - assert outcomes.count('conflict') == 1 and outcomes.count(current.revision + 1) == 1 + assert outcomes.count('conflict') == 1 + assert outcomes.count(current.revision + 1) == 1 assert store.get('alice').revision == current.revision + 1 @@ -159,11 +169,13 @@ def test_legacy_tombstones_never_expire_during_upgrade_and_opaque_data_is_not_lo legacy.put_legacy('alice', ac.ConfigBucket('alice', {'settings': { 'old': {'deleted': True, 'last_modified': 0, 'value': 'previous'}}}).to_dict()) state = ac.CausalConfigStore(path).get('alice') - assert state.entries[0].is_deleted and state.live_entries == () + assert state.entries[0].is_deleted + assert state.live_entries == () assert state.entries[0].value == {'value': 'previous'} legacy.put_legacy('bob', {'user_id': 'bob', 'opaque': {'important': True}}) + upgraded = ac.CausalConfigStore(path) with pytest.raises(ac.ConfigSyncError): - ac.CausalConfigStore(path).get('bob') + upgraded.get('bob') assert legacy.get_document('bob')['opaque'] == {'important': True} @@ -184,7 +196,8 @@ def test_causal_commands_share_existing_mcp_builder_and_private_path_guards(tmp_ tools = {tool.name: tool for tool in build_default_tool_registry()} assert tools['ac_config_store_commit'].handler(**args) == {'version': 3, 'revision': 1, 'cas': True} read = tools['ac_config_store_get'].handler(path=args['path'], user_id='alice') - assert read['version'] == 3 and read['bucket']['revision'] == 1 + assert read['version'] == 3 + assert read['bucket']['revision'] == 1 @pytest.mark.parametrize('bad', [[], {}, {'schema_version': 1}, {'schema_version': True}]) diff --git a/test/unit_test/headless/test_config_sync_causal_http_surfaces.py b/test/unit_test/headless/test_config_sync_causal_http_surfaces.py index c0ca3d200..480377ba2 100644 --- a/test/unit_test/headless/test_config_sync_causal_http_surfaces.py +++ b/test/unit_test/headless/test_config_sync_causal_http_surfaces.py @@ -30,10 +30,12 @@ def test_causal_http_commands_and_builder_use_explicit_json_and_live_environment commit = dict(common, bucket=state.with_entries(operation.entries).to_dict(), actor=ac.SyncActor('desktop', 0).to_dict(), operation_id='explicit-commit') result = tools()['ac_config_causal_commit'].handler(**commit) - assert result['revision'] == 2 and result['bucket']['revision'] == 2 + assert result['revision'] == 2 + assert result['bucket']['revision'] == 2 flush = dict(common, path=str(tmp_path / 'outbox.sqlite3')) result = tools()['ac_sync_outbox_flush'].handler(**flush) - assert result['report']['delivered'] == 1 and result['snapshot'] == server.get('alice').to_dict() + assert result['report']['delivered'] == 1 + assert result['snapshot'] == server.get('alice').to_dict() for command, args in [('AC_config_causal_fetch', common), ('AC_config_causal_commit', commit), ('AC_sync_outbox_flush', flush)]: action = [command, args] @@ -53,12 +55,13 @@ def test_causal_http_tools_require_drive_even_when_reading_and_cancel_before_sto ctx = ToolCallContext('cancelled') ctx.cancelled_event.set() args = {'path': str(tmp_path / 'outbox.sqlite3'), 'endpoint': 'https://controlled.invalid', 'user_id': 'alice'} + flush_tool = tools()['ac_sync_outbox_flush'] with pytest.raises(OperationCancelledError): - tools()['ac_sync_outbox_flush'].handler(**args, ctx=ctx) + flush_tool.handler(**args, ctx=ctx) assert not (tmp_path / 'outbox.sqlite3').exists() ctx = ToolCallContext('confined', path_policy=PathPolicy([tmp_path / 'elsewhere'])) with pytest.raises(PathNotAllowedError): - tools()['ac_sync_outbox_flush'].handler(**args, ctx=ctx) + flush_tool.handler(**args, ctx=ctx) assert not (tmp_path / 'outbox.sqlite3').exists() @@ -74,9 +77,12 @@ def cancelled_after_fetch(call): ctx.cancelled_event.set() return response monkeypatch.setattr(http_client, 'perform_call', cancelled_after_fetch) + flush_tool = tools()['ac_sync_outbox_flush'] + path = str(tmp_path / 'outbox.sqlite3') with pytest.raises(OperationCancelledError): - tools()['ac_sync_outbox_flush'].handler(path=str(tmp_path / 'outbox.sqlite3'), endpoint=operation.endpoint, + flush_tool.handler(path=path, endpoint=operation.endpoint, user_id='alice', options={'secret_env': 'AC_TEST_CAUSAL_SHARED_SECRET'}, ctx=ctx) assert [call['method'] for call in calls] == ['GET'] state = queue.status(endpoint=operation.endpoint, user_id='alice') - assert state['pending'] == 1 and state['offline'] == 0 + assert state['pending'] == 1 + assert state['offline'] == 0 diff --git a/test/unit_test/headless/test_config_sync_causal_store.py b/test/unit_test/headless/test_config_sync_causal_store.py index 687b77272..71bc4cca0 100644 --- a/test/unit_test/headless/test_config_sync_causal_store.py +++ b/test/unit_test/headless/test_config_sync_causal_store.py @@ -29,13 +29,16 @@ def test_causal_upgrade_preserves_plain_data_and_guards_old_writers(tmp_path): plain.commit('alice',bucket,base_revision=0,operation_id='plain') store=CausalConfigStore(path) before=store.get('alice') - assert before.revision==1 and before.live_entries[0].value=={'value':'dark'} + assert before.revision==1 + assert before.live_entries[0].value=={'value':'dark'} state=register(store,'desktop') - assert state.revision==2 and state.live_entries==before.live_entries + assert state.revision==2 + assert state.live_entries==before.live_entries with pytest.raises(ConfigRevisionConflict): plain.commit('alice',bucket,base_revision=2,operation_id='old-writer') + legacy=bucket.to_dict() with pytest.raises(ConfigRevisionConflict): - plain.put_legacy('alice',bucket.to_dict()) + plain.put_legacy('alice',legacy) assert store.get('alice').revision==2 @@ -47,18 +50,21 @@ def test_concurrent_edits_are_retained_and_missing_alternatives_are_rejected(tmp left=SyncEntry('settings','theme',{'value':'dark'},{'desktop':1},'left','desktop') right=SyncEntry('settings','theme',{'value':'light'},{'laptop':1},'right','laptop') store.commit('alice',base.with_entries((left,)),base_revision=base.revision,operation_id='left',actor=actor('desktop')) + stale=base.with_entries((right,)) + laptop=actor('laptop') with pytest.raises(ConfigRevisionConflict): - store.commit('alice',base.with_entries((right,)),base_revision=base.revision, - operation_id='stale-right',actor=actor('laptop')) + store.commit('alice',stale,base_revision=base.revision,operation_id='stale-right',actor=laptop) current=store.get('alice') store.commit('alice',current.with_entries((right,)),base_revision=current.revision, operation_id='right',actor=actor('laptop')) current=store.get('alice') - assert len(current.conflicts)==1 and set(current.conflicts[0].origins)=={'desktop','laptop'} + assert len(current.conflicts)==1 + assert set(current.conflicts[0].origins)=={'desktop','laptop'} assert current.live_entries==() bad=replace(current,entries=(left,),committed={left.change_id:current.committed[left.change_id]}) + desktop=actor('desktop') with pytest.raises(ConfigSyncError): - store.commit('alice',bad,base_revision=current.revision,operation_id='drop-right',actor=actor('desktop')) + store.commit('alice',bad,base_revision=current.revision,operation_id='drop-right',actor=desktop) assert store.get('alice').revision==current.revision @@ -83,10 +89,13 @@ def test_collection_waits_for_acknowledgements_and_keeps_a_deletion_fence(tmp_pa operation_id=f'ack-{peer_id}',actor=actor(peer_id,'ack',revision=deletion_revision)) state=store.get('alice') collected=state.collect() - assert collected.entries==() and collected.fences[0].version==deleted.version + assert collected.entries==() + assert collected.fences[0].version==deleted.version store.commit('alice',collected,base_revision=state.revision,operation_id='collect',actor=actor('desktop','collect')) restored=store.get('alice').with_entries((live,)) - assert restored.live_entries==() and restored.entries==() and len(restored.fences)==1 + assert restored.live_entries==() + assert restored.entries==() + assert len(restored.fences)==1 concurrent=SyncEntry('settings','theme',{'value':'light'},{'laptop':1},'offline-edit','laptop') assert len(restored.with_entries((concurrent,)).conflicts)==1 @@ -102,16 +111,18 @@ def test_retired_peer_needs_explicit_rejoin_and_old_epoch_stays_invalid(tmp_path store.commit('alice',state.with_peers(peers),base_revision=state.revision, operation_id='retire',actor=actor('desktop','retire',peer_id='laptop')) state=store.get('alice') + laptop=actor('laptop') with pytest.raises(FullResyncRequired): - store.commit('alice',state,base_revision=state.revision,operation_id='old-epoch',actor=actor('laptop')) + store.commit('alice',state,base_revision=state.revision,operation_id='old-epoch',actor=laptop) peers=state.peers peers.register('laptop',full_revision=state.revision,full_resync=True) store.commit('alice',state.with_peers(peers),base_revision=state.revision,operation_id='rejoin', actor=actor('laptop','full_sync',epoch=1,revision=state.revision)) current=store.get('alice') assert current.peers.require_active('laptop',epoch=1).epoch==1 + laptop=actor('laptop') with pytest.raises(FullResyncRequired): - store.commit('alice',current,base_revision=current.revision,operation_id='stale-callback',actor=actor('laptop')) + store.commit('alice',current,base_revision=current.revision,operation_id='stale-callback',actor=laptop) def test_dedicated_causal_http_protocol_is_private_and_retries_are_idempotent(tmp_path): @@ -121,10 +132,12 @@ def test_dedicated_causal_http_protocol_is_private_and_retries_are_idempotent(tm from je_auto_control.utils.config_sync.causal_state import CausalBucket path=tmp_path/'config.sqlite3' client=TestClient(create_app(config_store_path=path,shared_secret='synthetic-only',serve_web_viewer=False)) - assert client.get('/config/alice/causal').status_code==401 and not path.exists() + assert client.get('/config/alice/causal').status_code==401 + assert not path.exists() headers={'X-Signaling-Secret':'synthetic-only'} response=client.get('/config/alice/causal',headers=headers) - assert response.status_code==200 and response.json()['version']==3 + assert response.status_code==200 + assert response.json()['version']==3 state=CausalBucket.from_dict(response.json()['bucket']) peers=state.peers peers.register('desktop',full_revision=0) @@ -132,7 +145,9 @@ def test_dedicated_causal_http_protocol_is_private_and_retries_are_idempotent(tm body['_actor']=actor('desktop','full_sync',revision=0).to_dict() envelope={'version':3,'base_revision':0,'operation_id':'register','bucket':body} first=client.put('/config/alice/causal',json=envelope,headers=headers) - assert first.status_code==200 and first.json()['revision']==1 + assert first.status_code==200 + assert first.json()['revision']==1 repeated=client.put('/config/alice/causal',json=envelope,headers=headers) - assert repeated.json()['revision']==1 and repeated.json()['bucket']['revision']==1 + assert repeated.json()['revision']==1 + assert repeated.json()['bucket']['revision']==1 assert client.get('/config/alice',headers=headers).status_code==409 diff --git a/test/unit_test/headless/test_config_sync_causal_surfaces.py b/test/unit_test/headless/test_config_sync_causal_surfaces.py index 385a85461..d50709539 100644 --- a/test/unit_test/headless/test_config_sync_causal_surfaces.py +++ b/test/unit_test/headless/test_config_sync_causal_surfaces.py @@ -25,7 +25,8 @@ def forbidden(*_args,**_kwargs): args={'left':left,'right':right} command=executor.event_dict['AC_sync_entry_merge'](**args) assert command==tools()['ac_sync_entry_merge'].handler(**args) - assert command['conflict']['local']==left and command['conflict']['remote']==right + assert command['conflict']['local']==left + assert command['conflict']['remote']==right assert 'AC_sync_entry_merge' in COMMAND_SPECS @@ -37,10 +38,12 @@ def forbidden(*_args,**_kwargs): item=operation() args={'path':str(tmp_path/'queue.sqlite3'),'operation':item.to_dict()} accepted=executor.event_dict['AC_sync_outbox_enqueue'](**args) - assert accepted['accepted'] is True and accepted['state']['pending']==1 + assert accepted['accepted'] is True + assert accepted['state']['pending']==1 result=tools()['ac_sync_outbox_status'].handler(path=args['path'],endpoint=item.endpoint,user_id='alice', include_pending=True,limit=1) - assert result['operations']==[item.to_dict()] and result['truncated'] is False + assert result['operations']==[item.to_dict()] + assert result['truncated'] is False assert {'AC_sync_outbox_enqueue','AC_sync_outbox_status'}<=COMMAND_SPECS.keys() @@ -58,13 +61,14 @@ def test_private_queue_access_is_drive_but_supplied_data_transforms_are_observab def test_cancelled_or_confined_queue_tools_do_not_create_a_database(tmp_path,monkeypatch): monkeypatch.setenv('AUTOCONTROL_ALLOWED_PATH_ROOTS',str(tmp_path)) args={'path':str(tmp_path/'queue.sqlite3'),'operation':operation().to_dict()} + enqueue=tools()['ac_sync_outbox_enqueue'].handler ctx=ToolCallContext('cancelled') ctx.cancelled_event.set() with pytest.raises(OperationCancelledError): - tools()['ac_sync_outbox_enqueue'].handler(**args,ctx=ctx) + enqueue(**args,ctx=ctx) ctx=ToolCallContext('confined',path_policy=PathPolicy([tmp_path/'elsewhere'])) with pytest.raises(PathNotAllowedError): - tools()['ac_sync_outbox_enqueue'].handler(**args,ctx=ctx) + enqueue(**args,ctx=ctx) assert not (tmp_path/'queue.sqlite3').exists() diff --git a/test/unit_test/headless/test_config_sync_convergence.py b/test/unit_test/headless/test_config_sync_convergence.py index e3c45d508..d5ebec998 100644 --- a/test/unit_test/headless/test_config_sync_convergence.py +++ b/test/unit_test/headless/test_config_sync_convergence.py @@ -15,7 +15,8 @@ def test_parallel_edits_preserve_conflict(): left=entry('desktop','dark') right=entry('laptop','light') conflict=merge_entries(left,right).conflict - assert conflict.local==left and conflict.remote==right + assert conflict.local==left + assert conflict.remote==right assert merge_entries(right,left).conflict.local==right @@ -52,7 +53,8 @@ def test_retired_peer_requires_full_sync(): with pytest.raises(FullResyncRequired): peers.register('laptop',full_revision=10) record=peers.register('laptop',full_revision=10,full_resync=True) - assert record.acknowledged_revision==10 and record.epoch==1 + assert record.acknowledged_revision==10 + assert record.epoch==1 assert peers.require_active('laptop',epoch=1)==record diff --git a/test/unit_test/headless/test_config_sync_gui.py b/test/unit_test/headless/test_config_sync_gui.py index 5bda9b00c..1b5c92765 100644 --- a/test/unit_test/headless/test_config_sync_gui.py +++ b/test/unit_test/headless/test_config_sync_gui.py @@ -57,7 +57,8 @@ def run(worker): def test_constructing_sync_is_inert_and_local_actions_run_off_the_gui_thread(panel, app, tmp_path, monkeypatch): - assert not list(tmp_path.iterdir()) and panel.findChildren(QPushButton) == [] + assert not list(tmp_path.iterdir()) + assert panel.findChildren(QPushButton) == [] assert len(panel.menu_actions()) == 17 config = configuration(tmp_path) panel._configuration.setPlainText(json.dumps(config)) @@ -74,8 +75,10 @@ def invoke(*args, **kwargs): idle(app, panel) panel.begin('status') idle(app, panel) - assert threads and all(thread != gui_thread for thread in threads) - assert panel._status_key == 'done' and panel._last_sync is None + assert threads + assert all(thread != gui_thread for thread in threads) + assert panel._status_key == 'done' + assert panel._last_sync is None assert panel._reviewed is None @@ -87,7 +90,8 @@ def test_review_selection_resolves_all_heads_and_configuration_change_discards_r panel._configuration.setPlainText(json.dumps(config)) panel._kind.setCurrentText('workspace') panel.begin('preferences') - assert panel._handle is None and panel._status_key == 'error' + assert panel._handle is None + assert panel._status_key == 'error' panel.begin('review') idle(app, panel) assert panel._heads.rowCount() == 2 @@ -95,12 +99,14 @@ def test_review_selection_resolves_all_heads_and_configuration_change_discards_r panel._value.setPlainText('{"value":"light"}') panel.begin('resolve') idle(app, panel) - assert panel._status_key == 'done' and panel._reviewed is None + assert panel._status_key == 'done' + assert panel._reviewed is None assert len(ac.SyncWorkspace(config).review('workspace')) == 1 panel.begin('review') idle(app, panel) panel._configuration.setPlainText('{}') - assert panel._reviewed is None and panel._heads.rowCount() == 0 + assert panel._reviewed is None + assert panel._heads.rowCount() == 0 assert not panel._value.toPlainText() @@ -129,17 +135,21 @@ def test_loopback_sync_does_not_apply_preferences_until_explicit_reviewed_action assert view._status_key == 'done', view._status.text() assert view._last_sync is not None successful_revision = view._last_sync - assert window._theme_name == 'dark' and window._user_font_pt == 0 + assert window._theme_name == 'dark' + assert window._user_font_pt == 0 view._kind.setCurrentText('workspace') assert view._last_sync == successful_revision view.begin('review') idle(app, view) - assert view._heads.rowCount() == 4 and window._theme_name == 'dark' + assert view._heads.rowCount() == 4 + assert window._theme_name == 'dark' view.begin('preferences') idle(app, view) assert view._status_key == 'applied', view._status.text() - assert window._theme_name == 'light' and window._user_font_pt == 16 - assert window._navigation_dock.isHidden() and window._navigation_width == 440 + assert window._theme_name == 'light' + assert window._user_font_pt == 16 + assert window._navigation_dock.isHidden() + assert window._navigation_width == 440 assert settings.value('private/token') == 'keep-local' assert settings.value('workspace/theme') is None finally: @@ -159,7 +169,8 @@ def test_live_preferences_validate_every_value_before_mutating_the_window(app, t with pytest.raises(ac.ConfigSyncError): window.apply_workspace_preferences({'theme': 'light', 'font_pt': True, 'navigation_visible': False, 'navigation_width': 440}) - assert window._theme_name == 'dark' and window._user_font_pt == 0 + assert window._theme_name == 'dark' + assert window._user_font_pt == 0 finally: window.close() window.deleteLater() @@ -185,7 +196,8 @@ def delayed(*_args, **_kwargs): panel.cancel() release.set() idle(app, panel) - assert applied == [] and panel._status_key == 'cancelled' + assert applied == [] + assert panel._status_key == 'cancelled' release.clear() entered.clear() panel.begin('preferences') @@ -253,10 +265,13 @@ def failure(*_args, **_kwargs): local = ConfigSyncWorker(SyncUIRequest(2, 'status', '{}', '{"secret_env":"SYNC_UI_TEST_SECRET"}')) try: outcome = run(network) - assert outcome.status == 'error' and secret not in outcome.error and '***' in outcome.error + assert outcome.status == 'error' + assert secret not in outcome.error + assert '***' in outcome.error assert accesses == ['SYNC_UI_TEST_SECRET'] accesses.clear() - assert run(local).status == 'error' and accesses == [] + assert run(local).status == 'error' + assert accesses == [] finally: network.deleteLater() local.deleteLater() diff --git a/test/unit_test/headless/test_config_sync_outbox.py b/test/unit_test/headless/test_config_sync_outbox.py index c2e9025d6..5259437a5 100644 --- a/test/unit_test/headless/test_config_sync_outbox.py +++ b/test/unit_test/headless/test_config_sync_outbox.py @@ -43,7 +43,8 @@ def test_bounded_views_and_drains_report_the_full_remaining_count(tmp_path): assert queue.pending(endpoint=first.endpoint,user_id='alice',limit=1)==(first,) assert queue.status(endpoint=first.endpoint,user_id='alice')['pending']==2 report=queue.flush(lambda _item:1,endpoint=first.endpoint,user_id='alice',policy=SyncRetryPolicy(max_batch=1)) - assert report.delivered==1 and report.pending==1 + assert report.delivered==1 + assert report.pending==1 def test_prepared_request_survives_restart_and_cannot_change_without_rejection(tmp_path): @@ -55,7 +56,8 @@ def test_prepared_request_survives_restart_and_cannot_change_without_rejection(t prepared=queue.prepare(original,base_revision=0,bucket=bucket) bucket['revision']=99 recovered=SyncOutbox(path).pending(endpoint=original.endpoint,user_id='alice')[0] - assert recovered==prepared and recovered.prepared['bucket']['revision']==0 + assert recovered==prepared + assert recovered.prepared['bucket']['revision']==0 with pytest.raises(ConfigSyncError): queue.prepare(original,base_revision=1,bucket={'user_id':'alice','revision':1,'sections':{}}) wrong=replace(original,entries=(replace(original.entries[0],value={'value':'wrong'}),)) @@ -75,14 +77,16 @@ def test_pre_cancel_and_cancel_after_commit_keep_exact_pending_work(tmp_path): cancelled.set() report=queue.flush(lambda item:calls.append(item) or 1,endpoint=first.endpoint,user_id='alice', cancelled=cancelled.is_set) - assert report.cancelled and calls==[] + assert report.cancelled + assert calls==[] cancelled.clear() def send(item): calls.append(item) cancelled.set() return 1 report=queue.flush(send,endpoint=first.endpoint,user_id='alice',cancelled=cancelled.is_set) - assert report.delivered==1 and report.cancelled + assert report.delivered==1 + assert report.cancelled assert queue.pending(endpoint=first.endpoint,user_id='alice')==(second,) @@ -98,7 +102,8 @@ def interrupted(_item): with pytest.raises(CancelledError): queue.flush(interrupted,endpoint=item.endpoint,user_id='alice') state=queue.status(endpoint=item.endpoint,user_id='alice') - assert state['queued']==1 and state['offline']==0 + assert state['queued']==1 + assert state['offline']==0 def test_backoff_survives_reopen_without_sleeping(tmp_path): @@ -113,11 +118,13 @@ def offline(received): raise OSError('controlled offline') policy=SyncRetryPolicy(backoff=2,max_delay=10) report=queue.flush(offline,endpoint=item.endpoint,user_id='alice',policy=policy) - assert report.failed==1 and len(calls)==1 + assert report.failed==1 + assert len(calls)==1 reopened=SyncOutbox(path,clock=lambda:now[0]) assert reopened.status(endpoint=item.endpoint,user_id='alice')['next_attempt']==102 report=reopened.flush(offline,endpoint=item.endpoint,user_id='alice',policy=policy) - assert report.failed==0 and len(calls)==1 + assert report.failed==0 + assert len(calls)==1 now[0]=102 assert reopened.flush(lambda _item:1,endpoint=item.endpoint,user_id='alice').delivered==1 @@ -173,8 +180,9 @@ def test_foreign_database_is_not_modified(tmp_path): path=tmp_path/'foreign.sqlite3' store=ConfigStore(path) store.commit('alice',ConfigBucket('alice'),base_revision=0,operation_id='original') + outbox=SyncOutbox(path) with pytest.raises(ConfigStoreError): - SyncOutbox(path).pending(endpoint='https://sync.invalid',user_id='alice') + outbox.pending(endpoint='https://sync.invalid',user_id='alice') assert store.get('alice').revision==1 @@ -182,8 +190,9 @@ def test_foreign_database_is_not_modified(tmp_path): 'file:///local','https://sync.invalid/#fragment']) def test_credential_urls_are_rejected_before_queue_io(tmp_path,endpoint): path=tmp_path/'queue.sqlite3' + outbox=SyncOutbox(path) with pytest.raises(ConfigSyncError): - SyncOutbox(path).identity(endpoint,'alice') + outbox.identity(endpoint,'alice') assert not path.exists() @@ -221,8 +230,9 @@ def test_namespace_epoch_is_stable_and_obsolete_edits_are_refused(tmp_path): current=replace(operation(),epoch=1) queue.enqueue(current) assert queue.identity(current.endpoint,'alice').epoch==1 + obsolete=replace(current,operation_id='obsolete',epoch=0) with pytest.raises(FullResyncRequired): - queue.enqueue(replace(current,operation_id='obsolete',epoch=0)) + queue.enqueue(obsolete) assert queue.pending(endpoint=current.endpoint,user_id='alice')==(current,) diff --git a/test/unit_test/headless/test_config_sync_peer_control.py b/test/unit_test/headless/test_config_sync_peer_control.py index 338836e9f..66bf5f773 100644 --- a/test/unit_test/headless/test_config_sync_peer_control.py +++ b/test/unit_test/headless/test_config_sync_peer_control.py @@ -35,7 +35,9 @@ def test_register_preserves_initial_offline_edits_then_flushes_them(tmp_path, mo client, queue, server, _calls, _perform = peer(tmp_path, monkeypatch) operation = edit(queue) result = client.control(queue, 'register') - assert result.delivered == 1 and result.pending == 0 and result.revision == 1 + assert result.delivered == 1 + assert result.pending == 0 + assert result.revision == 1 assert queue.pending(endpoint=operation.endpoint, user_id='alice') == (operation,) assert queue.snapshot(endpoint=operation.endpoint, user_id='alice') == server.get('alice') assert client.flush(queue).delivered == 1 @@ -71,7 +73,8 @@ def test_ack_and_collection_wait_for_every_active_peer(tmp_path, monkeypatch): client.control(other, 'ack') client.control(queue, 'collect') final = server.get('alice') - assert final.entries == () and len(final.fences) == 1 + assert final.entries == () + assert len(final.fences) == 1 assert final.fences[0].deletion_revision == deletion_revision @@ -90,7 +93,8 @@ def test_full_resync_requires_explicit_pending_retirement_and_adopts_new_epoch(t client.control(queue, 'full_resync') assert len(calls) == before result = client.control(queue, 'full_resync', discard_pending=True) - assert result.delivered == 1 and result.pending == 0 + assert result.delivered == 1 + assert result.pending == 0 assert queue.identity(operation.endpoint, 'alice').epoch == 1 assert queue.status(endpoint=operation.endpoint, user_id='alice')['retired'] == 1 assert queue.pending(endpoint=operation.endpoint, user_id='alice') == () @@ -98,7 +102,8 @@ def test_full_resync_requires_explicit_pending_retirement_and_adopts_new_epoch(t with pytest.raises(ac.FullResyncRequired): edit(queue, operation_id='obsolete') fresh = edit(queue, epoch=1, operation_id='fresh') - assert client.flush(queue).delivered == 1 and server.get('alice').live_entries == fresh.entries + assert client.flush(queue).delivered == 1 + assert server.get('alice').live_entries == fresh.entries def test_uncertain_register_replays_exact_control_before_fetch_after_restart(tmp_path, monkeypatch): @@ -113,13 +118,15 @@ def uncertain(call): return response monkeypatch.setattr(http_client, 'perform_call', uncertain) result = client.control(queue, 'register', policy=ac.SyncRetryPolicy(max_attempts=1, backoff=0)) - assert result.pending == 1 and result.delivered == 0 + assert result.pending == 1 + assert result.delivered == 0 assert server.get('alice').revision == 1 original_body = next(call['body'] for call in calls if call['method'] == 'PUT') calls.clear() reopened = ac.SyncOutbox(tmp_path / 'outbox.sqlite3') client.flush(reopened, policy=ac.SyncRetryPolicy(max_attempts=1, backoff=0)) - assert [call['method'] for call in calls] == ['PUT'] and calls[0]['body'] == original_body + assert [call['method'] for call in calls] == ['PUT'] + assert calls[0]['body'] == original_body assert reopened.control_status(endpoint='https://controlled.invalid', user_id='alice')['state'] == 'delivered' @@ -138,7 +145,8 @@ def uncertain(call): monkeypatch.setattr(http_client, 'perform_call', uncertain) result = client.control(queue, 'full_resync', discard_pending=True, policy=ac.SyncRetryPolicy(max_attempts=1, backoff=0)) - assert result.pending == 1 and queue.identity(operation.endpoint, 'alice').epoch == 0 + assert result.pending == 1 + assert queue.identity(operation.endpoint, 'alice').epoch == 0 assert queue.status(endpoint=operation.endpoint, user_id='alice')['retired'] == 0 queue.enqueue(operation) with pytest.raises(ac.FullResyncRequired): @@ -195,7 +203,8 @@ def held_reply(call): try: assert entered.wait(3) second = client.control(ac.SyncOutbox(tmp_path / 'outbox.sqlite3'), 'register') - assert second.delivered == 0 and second.pending == 1 + assert second.delivered == 0 + assert second.pending == 1 assert [call['method'] for call in calls] == ['GET', 'PUT'] finally: release.set() @@ -205,8 +214,9 @@ def held_reply(call): def test_pre_cancelled_peer_control_never_opens_an_outbox(tmp_path): path = tmp_path / 'never-opened.sqlite3' client = ac.CausalSyncClient('https://controlled.invalid', user_id='alice') + queue = ac.SyncOutbox(path) with pytest.raises(CancelledError): - client.control(ac.SyncOutbox(path), 'register', cancelled=lambda: True) + client.control(queue, 'register', cancelled=lambda: True) assert not path.exists() @@ -255,9 +265,12 @@ def busy(call): monkeypatch.setattr(http_client, 'perform_call', busy) policy = ac.SyncRetryPolicy(max_attempts=1, backoff=0) result = client.control(queue, 'ack', policy=policy) - assert result.pending == 1 and result.failed == 1 and result.delivered == 0 + assert result.pending == 1 + assert result.failed == 1 + assert result.delivered == 0 requests = [json.loads(call['body']) for call in calls if call['method'] == 'PUT'] - assert len(requests) == 4 and len({request['operation_id'] for request in requests}) == 1 + assert len(requests) == 4 + assert len({request['operation_id'] for request in requests}) == 1 assert queue.control_status(endpoint='https://controlled.invalid', user_id='alice')['state'] == 'offline' monkeypatch.setattr(http_client, 'perform_call', perform) assert client.flush(queue, policy=policy).pending == 0 @@ -273,9 +286,12 @@ def test_full_resync_after_server_reset_rotates_local_origin_and_adopts_authorit connection.execute('DELETE FROM config_operations') connection.execute('DELETE FROM config_buckets') result = client.control(queue, 'full_resync', discard_pending=True) - assert result.delivered == 1 and result.revision == 1 + assert result.delivered == 1 + assert result.revision == 1 identity = queue.identity(operation.endpoint, 'alice') - assert identity.origin != 'desktop' and identity.epoch == 0 and identity.confirmed_revision == 1 + assert identity.origin != 'desktop' + assert identity.epoch == 0 + assert identity.confirmed_revision == 1 assert queue.snapshot(endpoint=operation.endpoint, user_id='alice') == server.get('alice') assert server.get('alice').live_entries == () @@ -297,7 +313,8 @@ def conflicting(call): return perform(call) monkeypatch.setattr(http_client, 'perform_call', conflicting) result = client.control(queue, 'register') - assert result.pending == 1 and result.delivered == 0 + assert result.pending == 1 + assert result.delivered == 0 assert [call['method'] for call in calls] == ['GET', 'PUT'] assert queue.control_status(endpoint='https://controlled.invalid', user_id='alice')['state'] == 'conflict' assert client.control(queue, 'full_resync').delivered == 1 @@ -315,11 +332,14 @@ def test_historical_control_request_id_never_retires_work_queued_after_its_first queue = ac.SyncOutbox(tmp_path / 'outbox.sqlite3') before_calls, before_revision = len(calls), server.get('alice').revision repeated = client.control(queue, request) - assert repeated.revision == original.revision and repeated.pending == 0 - assert len(calls) == before_calls and server.get('alice').revision == before_revision + assert repeated.revision == original.revision + assert repeated.pending == 0 + assert len(calls) == before_calls + assert server.get('alice').revision == before_revision assert queue.pending(endpoint=fresh.endpoint, user_id='alice') == (fresh,) + reused = ac.SyncControlRequest('ack', operation_id='reviewed-resync') with pytest.raises(ac.ConfigSyncError): - client.control(queue, ac.SyncControlRequest('ack', operation_id='reviewed-resync')) + client.control(queue, reused) def test_adoption_and_pending_retirement_roll_back_when_local_receipt_storage_fails(tmp_path, monkeypatch): @@ -332,7 +352,8 @@ def test_adoption_and_pending_retirement_roll_back_when_local_receipt_storage_fa "BEGIN SELECT RAISE(ABORT,'controlled receipt failure'); END") result = client.control(queue, 'full_resync', discard_pending=True, policy=ac.SyncRetryPolicy(max_attempts=1, backoff=0)) - assert result.pending == 1 and result.delivered == 0 + assert result.pending == 1 + assert result.delivered == 0 assert queue.identity(operation.endpoint, 'alice').epoch == 0 assert queue.status(endpoint=operation.endpoint, user_id='alice')['retired'] == 0 assert queue.pending(endpoint=operation.endpoint, user_id='alice') == (operation,) @@ -354,8 +375,9 @@ def test_invalid_owned_version_one_schema_is_refused_before_migration_writes(tmp connection.execute('PRAGMA user_version=1') connection.execute('CREATE TABLE important (value TEXT)') connection.execute("INSERT INTO important VALUES ('preserve')") + queue = ac.SyncOutbox(path) with pytest.raises(ac.ConfigStoreError): - ac.SyncOutbox(path).status(endpoint='https://controlled.invalid', user_id='alice') + queue.status(endpoint='https://controlled.invalid', user_id='alice') with sqlite3.connect(path) as connection: assert connection.execute('PRAGMA user_version').fetchone()[0] == 1 assert connection.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() == [('important',)] diff --git a/test/unit_test/headless/test_config_sync_peer_control_surfaces.py b/test/unit_test/headless/test_config_sync_peer_control_surfaces.py index 18227f684..bf2a8acea 100644 --- a/test/unit_test/headless/test_config_sync_peer_control_surfaces.py +++ b/test/unit_test/headless/test_config_sync_peer_control_surfaces.py @@ -45,14 +45,15 @@ def test_peer_control_is_private_and_cancellation_or_path_refusal_precedes_stora assert capability_for_tool('ac_sync_peer_control', True) == 'drive_input' arguments = {'path': str(tmp_path / 'unused.sqlite3'), 'endpoint': 'https://controlled.invalid', 'user_id': 'alice', 'operation': 'register'} + handler = tool().handler ctx = ToolCallContext('cancelled') ctx.cancelled_event.set() with pytest.raises(OperationCancelledError): - tool().handler(**arguments, ctx=ctx) + handler(**arguments, ctx=ctx) assert not (tmp_path / 'unused.sqlite3').exists() ctx = ToolCallContext('confined', path_policy=PathPolicy([tmp_path / 'elsewhere'])) with pytest.raises(PathNotAllowedError): - tool().handler(**arguments, ctx=ctx) + handler(**arguments, ctx=ctx) assert not (tmp_path / 'unused.sqlite3').exists() @@ -66,9 +67,11 @@ def cancel(call): ctx.cancelled_event.set() return response monkeypatch.setattr(http_client, 'perform_call', cancel) + handler, path = tool().handler, str(tmp_path / 'outbox.sqlite3') with pytest.raises(OperationCancelledError): - tool().handler(path=str(tmp_path / 'outbox.sqlite3'), endpoint='https://controlled.invalid', - user_id='alice', operation='register', ctx=ctx) + handler(path=path, endpoint='https://controlled.invalid', user_id='alice', operation='register', ctx=ctx) assert [call['method'] for call in calls] == ['GET'] status = queue.control_status(endpoint='https://controlled.invalid', user_id='alice') - assert status['pending'] == 1 and status['state'] == 'pending' and status['error'] == '' + assert status['pending'] == 1 + assert status['state'] == 'pending' + assert status['error'] == '' diff --git a/test/unit_test/headless/test_config_sync_persistence.py b/test/unit_test/headless/test_config_sync_persistence.py index 4234cee13..8db50ac9c 100644 --- a/test/unit_test/headless/test_config_sync_persistence.py +++ b/test/unit_test/headless/test_config_sync_persistence.py @@ -60,8 +60,9 @@ def test_retry_id_is_bound_to_account_and_content(tmp_path): from je_auto_control.utils.config_sync.store import ConfigRevisionConflict,ConfigStore store=ConfigStore(tmp_path/'config.sqlite3') assert store.commit('alice',bucket(),base_revision=0,operation_id='same')==1 + changed=bucket(value='changed') with pytest.raises(ConfigRevisionConflict): - store.commit('alice',bucket(value='changed'),base_revision=0,operation_id='same') + store.commit('alice',changed,base_revision=0,operation_id='same') assert store.commit('bob',bucket('bob'),base_revision=0,operation_id='same')==1 assert store.get('nobody') is None @@ -75,7 +76,8 @@ def test_default_store_path_is_resolved_only_on_use(tmp_path,monkeypatch): assert not first.exists() monkeypatch.setenv('AUTOCONTROL_CONFIG_STORE',str(second)) store.commit('alice',bucket(),base_revision=0,operation_id='first') - assert second.is_file() and not first.exists() + assert second.is_file() + assert not first.exists() def test_server_reopen_and_retry_do_not_overwrite_a_later_commit(tmp_path): @@ -142,8 +144,10 @@ def test_capacity_and_independent_reads_survive_reopen(tmp_path): assert read.sections read.sections.clear() assert store.get('alice').sections + bounded=ConfigStore(path,max_users=1) + second=bucket('bob') with pytest.raises(ConfigStoreFull): - ConfigStore(path,max_users=1).commit('bob',bucket('bob'),base_revision=0,operation_id='first') + bounded.commit('bob',second,base_revision=0,operation_id='first') assert store.get('bob') is None @@ -152,9 +156,10 @@ def test_client_rejects_unconfirmed_conditional_commits(reply): from unittest.mock import patch from je_auto_control.utils.config_sync import ConfigSyncClient,ConfigSyncError client=ConfigSyncClient('https://synthetic.invalid',user_id='alice') + local=bucket() with patch.object(client,'_request',return_value=reply): with pytest.raises(ConfigSyncError): - client.push(bucket(),operation_id='first') + client.push(local,operation_id='first') def test_conditional_client_never_writes_an_unsupported_legacy_server(monkeypatch): @@ -175,6 +180,8 @@ def perform(call): content=call['body'],headers=call['headers']) return {'status':response.status_code,'text':response.text,'headers':dict(response.headers)} monkeypatch.setattr('je_auto_control.utils.http_client.http_client.perform_call',perform) + client=ConfigSyncClient('https://synthetic.invalid',user_id='alice') + local=bucket() with pytest.raises(ConfigSyncError): - ConfigSyncClient('https://synthetic.invalid',user_id='alice').push(bucket(),operation_id='first') + client.push(local,operation_id='first') assert writes==[] diff --git a/test/unit_test/headless/test_config_sync_store_surfaces.py b/test/unit_test/headless/test_config_sync_store_surfaces.py index e28749252..6a0da447a 100644 --- a/test/unit_test/headless/test_config_sync_store_surfaces.py +++ b/test/unit_test/headless/test_config_sync_store_surfaces.py @@ -29,7 +29,8 @@ def test_executor_mcp_and_builder_share_store_and_journal_evidence(tmp_path,monk reply=tools()['ac_config_store_commit'].handler(**args) assert reply=={'version':2,'revision':1,'cas':True} read=tools()['ac_config_store_get'].handler(path=args['path'],user_id='alice') - assert read['bucket']['revision']==1 and read['cas'] is True + assert read['bucket']['revision']==1 + assert read['cas'] is True assert {'AC_config_store_commit','AC_config_store_get'}<=COMMAND_SPECS.keys() candidate=ac.generate_candidate_from_log(source.path,run_id='config-commit') assert candidate.actions==[['AC_config_store_commit',args]] @@ -49,11 +50,12 @@ def test_cancelled_or_confined_mcp_does_not_open_a_database(tmp_path,monkeypatch args=arguments(tmp_path) ctx=ToolCallContext('cancelled') ctx.cancelled_event.set() + commit_tool=tools()['ac_config_store_commit'] with pytest.raises(OperationCancelledError): - tools()['ac_config_store_commit'].handler(**args,ctx=ctx) + commit_tool.handler(**args,ctx=ctx) ctx=ToolCallContext('confined',path_policy=PathPolicy([tmp_path/'elsewhere'])) with pytest.raises(PathNotAllowedError): - tools()['ac_config_store_commit'].handler(**args,ctx=ctx) + commit_tool.handler(**args,ctx=ctx) assert not (tmp_path/'config.sqlite3').exists() diff --git a/test/unit_test/headless/test_dag_authority.py b/test/unit_test/headless/test_dag_authority.py index cc7001dc5..dd101a064 100644 --- a/test/unit_test/headless/test_dag_authority.py +++ b/test/unit_test/headless/test_dag_authority.py @@ -45,8 +45,9 @@ def runner(*_args): token.cancel() cancellation_checkpoint() + graph = definition() with cancellation_scope(token), pytest.raises(CancelledError): - run_dag(definition(), local_runner=runner) + run_dag(graph, local_runner=runner) @pytest.mark.parametrize("change", ["remove", "demote"]) @@ -98,7 +99,9 @@ def test_default_remote_dispatch_keeps_admin_and_trusted_python_support(role, mo class Console: def broadcast_execute(self, actions, *, labels, raise_on_error): - assert actions == [] and labels == ["other-host"] and raise_on_error + assert actions == [] + assert labels == ["other-host"] + assert raise_on_error return [{"ok": True, "result": {"verified": True}}] monkeypatch.setattr(admin_client, "default_admin_console", Console) diff --git a/test/unit_test/headless/test_dag_host_registration.py b/test/unit_test/headless/test_dag_host_registration.py index 5e81b81e4..f7bd9f147 100644 --- a/test/unit_test/headless/test_dag_host_registration.py +++ b/test/unit_test/headless/test_dag_host_registration.py @@ -43,7 +43,8 @@ def test_another_process_invalidates_existing_dag_host_authority(tmp_path, monke connection = managed_tasks.bound_task_connection('source', pinned) old_url = book.list_hosts()[0].base_url replace_from_process(book, change) - assert book.list_hosts()[0].token == token and book.list_hosts()[0].base_url == old_url + assert book.list_hosts()[0].token == token + assert book.list_hosts()[0].base_url == old_url def forbidden(*_args, **_kwargs): raise AssertionError('stale DAG registration reached network dispatch') @@ -52,13 +53,14 @@ def forbidden(*_args, **_kwargs): if boundary == 'binding' and change != 'removed': assert managed_tasks.task_host_binding('source') != pinned return + if boundary == 'binding': + attempt, arguments = managed_tasks.task_host_binding, ('source',) + elif boundary == 'connection': + attempt, arguments = managed_tasks.bound_task_connection, ('source', pinned) + else: + attempt, arguments = connection, ('get', {'task_id': exported['manifest']['source_task']}, 3) with pytest.raises(AgentTaskError): - if boundary == 'binding': - managed_tasks.task_host_binding('source') - elif boundary == 'connection': - managed_tasks.bound_task_connection('source', pinned) - else: - connection('get', {'task_id': exported['manifest']['source_task']}, 3) + attempt(*arguments) @pytest.mark.parametrize('change', ['registration', 'caller']) @@ -82,7 +84,8 @@ def changed(*args, **kwargs): monkeypatch.setattr(transport, 'request_json', changed) with pytest.raises((AgentTaskError, AuthorizationError)): connection('get', {'task_id': exported['manifest']['source_task']}, 3) - assert len(calls) == 1 and calls[0]['state'] == 'succeeded' + assert len(calls) == 1 + assert calls[0]['state'] == 'succeeded' def test_unbound_dag_connection_also_rejects_a_later_registration_change(tmp_path, monkeypatch): diff --git a/test/unit_test/headless/test_dag_managed_gui.py b/test/unit_test/headless/test_dag_managed_gui.py index 96d5d36bc..0f1ab0ee9 100644 --- a/test/unit_test/headless/test_dag_managed_gui.py +++ b/test/unit_test/headless/test_dag_managed_gui.py @@ -48,7 +48,8 @@ def test_dag_gui_worker_does_not_borrow_ambient_admin_authority(monkeypatch): with authorization_scope(admin): worker.run() assert current_authorization() is admin - assert len(outcomes) == 1 and not outcomes[0].succeeded + assert len(outcomes) == 1 + assert not outcomes[0].succeeded assert "manage_hosts" in outcomes[0].nodes["one"].error worker.deleteLater() app.processEvents() @@ -66,7 +67,8 @@ def test_dag_inspection_worker_calls_only_inspection(monkeypatch): results = [] worker.finished.connect(results.append) worker.run() - assert calls == [graph] and results == [{"inspection_only": True}] + assert calls == [graph] + assert results == [{"inspection_only": True}] worker.deleteLater() app.processEvents() diff --git a/test/unit_test/headless/test_dag_managed_loopback.py b/test/unit_test/headless/test_dag_managed_loopback.py index 14066c291..f05bee1a6 100644 --- a/test/unit_test/headless/test_dag_managed_loopback.py +++ b/test/unit_test/headless/test_dag_managed_loopback.py @@ -77,7 +77,8 @@ def test_two_host_dag_deduplicates_and_keeps_local_principal_and_witnesses_local assert "origin_policies" not in remote_row["spec"]["options"] assert str(store.path) not in str(remote_row) assert witness.identifier not in str(remote_row) - assert not local.runtime._leases and not remote.runtime._leases + assert not local.runtime._leases + assert not remote.runtime._leases finally: local.close() remote.close() diff --git a/test/unit_test/headless/test_dag_managed_tasks.py b/test/unit_test/headless/test_dag_managed_tasks.py index ba258d970..3bd87cacc 100644 --- a/test/unit_test/headless/test_dag_managed_tasks.py +++ b/test/unit_test/headless/test_dag_managed_tasks.py @@ -31,7 +31,8 @@ def test_versioned_dag_exports_match_historical_facade(): for name in ("DagDefinition", "DagDefinitionError", "DagNode", "DagRunResult", "NodeResult", "parse_dag", "run_dag"): assert getattr(je_auto_control, name) is getattr(core, name) is getattr(api, name) - assert name in core.__all__ and name in api.__all__ + assert name in core.__all__ + assert name in api.__all__ def definition(payload=None, host="local"): @@ -75,15 +76,17 @@ def test_task_payload_is_detached_frozen_and_does_not_resolve_destination_roots( {"wait_seconds": 0}, {"wait_seconds": float("inf")}, {"wait_seconds": 86_401}, {"url": "http://evil"}, ]) def test_malformed_envelope_is_refused_before_dispatch(change): + graph = definition(task(**change)) with pytest.raises(DagDefinitionError): - parse_dag(definition(task(**change))) + parse_dag(graph) def test_external_or_ambiguous_payloads_are_refused(): payload = task() payload["spec"]["mode"] = "external" + external = definition(payload) with pytest.raises(DagDefinitionError): - parse_dag(definition(payload)) + parse_dag(external) graph = definition() graph["nodes"][0]["actions"] = [] with pytest.raises(DagDefinitionError): @@ -163,8 +166,9 @@ def endpoint(operation, _body, _timeout): monkeypatch.setattr(managed_tasks, "_connection", lambda _host: endpoint) with cancellation_scope(token): if reason == "cancel": + graph = definition() with pytest.raises(CancelledError): - run_dag(definition(), stop_event=stop) + run_dag(graph, stop_event=stop) else: payload = task(wait_seconds=0.01) if reason == "timeout" else task() result = run_dag(definition(payload), stop_event=stop) diff --git a/test/unit_test/headless/test_diagnostics_cancellable.py b/test/unit_test/headless/test_diagnostics_cancellable.py index 8656e1828..526902906 100644 --- a/test/unit_test/headless/test_diagnostics_cancellable.py +++ b/test/unit_test/headless/test_diagnostics_cancellable.py @@ -22,7 +22,8 @@ def _scope(token): def test_cancellation_primitives_are_shared_by_both_public_sdk_namespaces(): for name in ('CancellationToken', 'TaskProgress', 'cancellation_scope'): - assert name in ac.__all__ and name in api.__all__ + assert name in ac.__all__ + assert name in api.__all__ assert getattr(ac, name) is getattr(api, name) is getattr(cancellation, name) @@ -54,10 +55,14 @@ def test_scope_does_not_appear_in_an_independent_context(): @pytest.mark.parametrize('invalid', [1, True, 'token', object()]) def test_invalid_scope_values_are_rejected_before_changing_the_current_owner(invalid): owner = cancellation.CancellationToken(1) + + def bind_invalid(): + with _scope(invalid): + pytest.fail('invalid cancellation binding was accepted') + with _scope(owner): with pytest.raises(TypeError, match='CancellationToken or None'): - with _scope(invalid): - pytest.fail('invalid cancellation binding was accepted') + bind_invalid() assert cancellation.current_cancellation_token() is owner @@ -93,7 +98,8 @@ def later(): monkeypatch.setattr(diagnostics, '_ALL_CHECKS', (stopping, later)) with _scope(token), pytest.raises(CancelledError): diagnostics.run_diagnostics() - assert calls == ['first'] and [item.completed for item in progress] == [0] + assert calls == ['first'] + assert [item.completed for item in progress] == [0] def test_progress_and_report_stay_shared_with_read_only_executor_and_active_mcp(monkeypatch, tmp_path): @@ -116,7 +122,8 @@ def test_progress_and_report_stay_shared_with_read_only_executor_and_active_mcp( request = {'jsonrpc': '2.0', 'id': 1, 'method': 'tools/call', 'params': {'name': 'ac_diagnose', 'arguments': {}}} reply = json.loads(server.handle_line(json.dumps(request))) - assert not reply['result']['isError'] and reply['result']['structuredContent'] == expected + assert not reply['result']['isError'] + assert reply['result']['structuredContent'] == expected assert [item['progress'] for item in progress] == [0, 1] request['params']['arguments'] = {'token': 'caller-supplied-authority'} refused = json.loads(server.handle_line(json.dumps(request))) @@ -132,7 +139,8 @@ def test_mcp_request_cancellation_stops_providers_and_keeps_its_established_sign ctx.cancelled_event.set() with pytest.raises(OperationCancelledError) as caught: tool.invoke({}, ctx=ctx) - assert caught.value.request_id == 'diagnostic-request' and calls == [] + assert caught.value.request_id == 'diagnostic-request' + assert calls == [] def test_real_jsonrpc_cancel_is_connection_scoped_and_never_becomes_an_error_screenshot(monkeypatch, tmp_path): @@ -180,7 +188,8 @@ def call(connection): assert server.handle_line(json.dumps({'jsonrpc': '2.0', 'method': 'notifications/cancelled', 'params': {'requestId': 1}})) is None workers['first'].join(1) - assert not workers['first'].is_alive() and workers['second'].is_alive() + assert not workers['first'].is_alive() + assert workers['second'].is_alive() assert replies['first']['error']['code'] == -32800 assert 'finish first' not in calls finally: diff --git a/test/unit_test/headless/test_diagnostics_interruptions.py b/test/unit_test/headless/test_diagnostics_interruptions.py index 5eafb3f92..1c5aaeb02 100644 --- a/test/unit_test/headless/test_diagnostics_interruptions.py +++ b/test/unit_test/headless/test_diagnostics_interruptions.py @@ -52,7 +52,8 @@ def later(): with pytest.raises(type(original)) as caught: entry() assert caught.value is original - assert events == ['before', 'cancelled'] and warnings == [] + assert events == ['before', 'cancelled'] + assert warnings == [] @pytest.mark.parametrize('error_type', [RuntimeError, ValueError, OSError]) @@ -74,7 +75,8 @@ def later(): monkeypatch.setattr(diagnostics, '_ALL_CHECKS', (broken, later)) monkeypatch.setattr(diagnostics, 'autocontrol_logger', SimpleNamespace(warning=lambda *args: warnings.append(args))) report = diagnostics.run_diagnostics() - assert events == ['broken', 'later'] and len(warnings) == 1 + assert events == ['broken', 'later'] + assert len(warnings) == 1 assert report.to_dict() == { 'ok': False, 'checks': [ diff --git a/test/unit_test/headless/test_diagnostics_tab_idle.py b/test/unit_test/headless/test_diagnostics_tab_idle.py index f7373cdb2..5dd2375c4 100644 --- a/test/unit_test/headless/test_diagnostics_tab_idle.py +++ b/test/unit_test/headless/test_diagnostics_tab_idle.py @@ -58,9 +58,11 @@ def test_opening_and_translating_diagnostics_keeps_an_honest_localized_idle_stat assert calls == [], 'merely opening Diagnostics started its active runner' wrapper.reset_language(language) panel.retranslate() - assert calls == [] and panel._table.rowCount() == 0 + assert calls == [] + assert panel._table.rowCount() == 0 message = wrapper.translate('diag_summary_idle') - assert message != 'diag_summary_idle' and panel._summary_label.text() == message + assert message != 'diag_summary_idle' + assert panel._summary_label.text() == message assert panel._table.horizontalHeaderItem(3).text() == wrapper.translate('diag_col_detail') @@ -69,13 +71,15 @@ def test_actions_menu_runs_the_existing_diagnostic_runner_once_and_displays_its_ assert calls == [] dict(panel.menu_actions())['diag_run']() assert _finished(panel) - assert calls == ['explicit run'] and panel._table.rowCount() == 1 + assert calls == ['explicit run'] + assert panel._table.rowCount() == 1 assert panel._table.item(0, 0).text() == 'controlled' assert panel._table.item(0, 3).text() == 'controlled subsystem' expected = wrapper.translate('diag_summary_failed').format(failed=1, count=1) assert panel._summary_label.text() == expected panel.retranslate() - assert calls == ['explicit run'] and panel._summary_label.text() == expected + assert calls == ['explicit run'] + assert panel._summary_label.text() == expected def test_async_cancellation_is_displayed_without_an_ordinary_error_and_allows_deliberate_retry(tab, monkeypatch): @@ -91,8 +95,10 @@ def canceled(): action() handle = panel._active_task assert _finished(panel) - assert handle.outcome.status == 'cancelled' and handle.outcome.control == type(original).__name__ - assert calls == [] and panel._table.rowCount() == 0 + assert handle.outcome.status == 'cancelled' + assert handle.outcome.control == type(original).__name__ + assert calls == [] + assert panel._table.rowCount() == 0 panel.retranslate() assert panel._summary_label.text() == wrapper.translate('diag_summary_cancelled') monkeypatch.setattr(diagnostics_tab, 'run_diagnostics', lambda: DiagnosticsReport([])) @@ -121,12 +127,15 @@ def later(): action() handle = panel._active_task assert _finished(panel) - assert handle.outcome.status == 'cancelled' and handle.outcome.control == type(original).__name__ - assert events == ['cancelled'] and panel._table.rowCount() == 0 + assert handle.outcome.status == 'cancelled' + assert handle.outcome.control == type(original).__name__ + assert events == ['cancelled'] + assert panel._table.rowCount() == 0 panel.retranslate() assert panel._summary_label.text() == wrapper.translate('diag_summary_cancelled') monkeypatch.setattr(diagnostics, '_ALL_CHECKS', (later,)) action() assert _finished(panel) - assert events == ['cancelled', 'later'] and panel._table.rowCount() == 1 + assert events == ['cancelled', 'later'] + assert panel._table.rowCount() == 1 assert panel._summary_label.text() == wrapper.translate('diag_summary_ok').format(count=1) diff --git a/test/unit_test/headless/test_diagnostics_task_lifecycle.py b/test/unit_test/headless/test_diagnostics_task_lifecycle.py index 65ed66be7..9d79dae24 100644 --- a/test/unit_test/headless/test_diagnostics_task_lifecycle.py +++ b/test/unit_test/headless/test_diagnostics_task_lifecycle.py @@ -64,11 +64,13 @@ def controlled(): try: run() assert entered.wait(1) - assert threads == [threads[0]] and threads[0] != threading.get_ident() + assert threads == [threads[0]] + assert threads[0] != threading.get_ident() run() QTimer.singleShot(0, lambda: ticks.append('tick')) assert _pump(app, lambda: ticks) - assert len(threads) == 1 and widget._table.rowCount() == 0 + assert len(threads) == 1 + assert widget._table.rowCount() == 0 finally: release.set() assert _pump(app, lambda: widget._active_task is None and widget._table.rowCount() == 1) @@ -100,7 +102,8 @@ def later(): assert entered.wait(1) actions['diag_stop']() assert _pump(app, lambda: widget._active_task is None) - assert calls == ['waiting'] and widget._table.rowCount() == 0 + assert calls == ['waiting'] + assert widget._table.rowCount() == 0 assert widget._summary_label.text() == language.translate('diag_summary_cancelled') monkeypatch.setattr(diagnostics, '_ALL_CHECKS', (later,)) actions['diag_run']() @@ -126,7 +129,8 @@ def uncooperative(): finally: release.set() assert _pump(app, lambda: widget._task_controller.active_count == 0) - assert widget._table.rowCount() == 0 and widget._active_task is None + assert widget._table.rowCount() == 0 + assert widget._active_task is None @pytest.mark.parametrize('language', ['English', 'Traditional_Chinese', 'Simplified_Chinese', 'Japanese']) @@ -198,7 +202,8 @@ def broken(): run = dict(widget.menu_actions())['diag_run'] run() assert _pump(app, lambda: widget._active_task is None) - assert 'controlled error' in widget._summary_label.text() and widget._table.rowCount() == 0 + assert 'controlled error' in widget._summary_label.text() + assert widget._table.rowCount() == 0 assert widget._summary_label.textFormat().name == 'PlainText' monkeypatch.setattr(diagnostics_tab, 'run_diagnostics', lambda: diagnostics.DiagnosticsReport([])) run() @@ -214,7 +219,8 @@ def failed_start(*_args, **_kwargs): monkeypatch.setattr(widget._task_controller, 'submit', failed_start) dict(widget.menu_actions())['diag_run']() - assert widget._active_task is None and 'controlled startup failure' in widget._summary_label.text() + assert widget._active_task is None + assert 'controlled startup failure' in widget._summary_label.text() def test_synchronous_submission_cancellation_keeps_the_original_control_and_restores_ui(panel, monkeypatch): @@ -225,13 +231,11 @@ def failed_start(*_args, **_kwargs): raise original monkeypatch.setattr(widget._task_controller, 'submit', failed_start) + run = dict(widget.menu_actions())['diag_run'] try: - try: - dict(widget.menu_actions())['diag_run']() - except OperationCancelledError as caught: - assert caught is original - else: - pytest.fail('submission cancellation was converted into an ordinary failure') + with pytest.raises(OperationCancelledError) as caught: + run() + assert caught.value is original finally: original.__traceback__ = None assert widget._active_task is None diff --git a/test/unit_test/headless/test_ei_native_verification.py b/test/unit_test/headless/test_ei_native_verification.py index 83bcd817d..dc1d3991a 100644 --- a/test/unit_test/headless/test_ei_native_verification.py +++ b/test/unit_test/headless/test_ei_native_verification.py @@ -52,7 +52,8 @@ def test_isolated_expected_teardown_fault_retains_its_native_trace(verifier, mon trace = 'Fatal Python error: Segmentation fault\n controlled native frame' monkeypatch.setattr(subprocess, 'run', lambda *_args, **_kwargs: completed(-11, READY, trace)) result = verifier._check_unref_sentinel('/controlled/eis') - assert 'still segfaults' in result and 'rc=-11' in result + assert 'still segfaults' in result + assert 'rc=-11' in result assert trace in capsys.readouterr().out @@ -67,7 +68,8 @@ def test_completed_cleanup_reports_exact_descriptor_counts(verifier, monkeypatch output = READY+DONE+'EI_UNREF_RESULT='+json.dumps({'fd_before': 4, 'fd_after': 4})+'\n' monkeypatch.setattr(subprocess, 'run', lambda *_args, **_kwargs: completed(0, output)) result = verifier._check_unref_sentinel('/controlled/eis') - assert 'no longer crashes' in result and '4 -> 4' in result + assert 'no longer crashes' in result + assert '4 -> 4' in result @pytest.mark.parametrize('before,after', [(4, 5), (4, 4.0), (1, True), (4, None)]) @@ -81,7 +83,8 @@ def test_successful_unref_with_invalid_descriptor_evidence_remains_a_failure(ver def test_child_has_a_finite_timeout_and_native_fault_handler(verifier, monkeypatch): def run(argv, **kwargs): assert 0 < kwargs.get('timeout', 0) <= 30 - assert argv[1:3] == ['-X', 'faulthandler'] and kwargs['check'] is False + assert argv[1:3] == ['-X', 'faulthandler'] + assert kwargs['check'] is False assert kwargs['capture_output'] is True compile(argv[-1], '', 'exec') raise subprocess.TimeoutExpired(argv, kwargs['timeout']) @@ -135,9 +138,11 @@ def test_dev_native_workflow_runs_both_probes_and_retains_failure_logs(): assert all('always()' in step['if'] and "steps.build.outcome == 'success'" in step['if'] for step in (probe, peer)) assert not any(step.get('continue-on-error') == 'true' for step in (probe, peer)) upload = next(step for step in steps if step.get('uses', '').startswith('actions/upload-artifact@')) - assert upload['if'] == 'always()' and upload['with']['path'] == 'native-ei/' + assert upload['if'] == 'always()' + assert upload['with']['path'] == 'native-ei/' cleanup = next(step for step in steps if step.get('name') == 'Remove owned verification containers') - assert cleanup['if'] == 'always()' and 'docker rm -f' in cleanup['run'] + assert cleanup['if'] == 'always()' + assert 'docker rm -f' in cleanup['run'] assert not any('--privileged' in step.get('run', '') or '--device' in step.get('run', '') for step in steps) dockerfile = (ROOT/'docker/Dockerfile.eis').read_text(encoding='utf-8') assert 'COPY docker/libei_verify.py /opt/verify/libei_verify.py' in dockerfile @@ -168,7 +173,8 @@ def test_live_native_boundary_and_trace_are_retained(live_verifier, monkeypatch, with pytest.raises(AssertionError, match='ei_unref.*SIGSEGV'): live_verifier._live_teardown_sentinel('/controlled/eis') captured = capsys.readouterr().out - assert output in captured and trace in captured + assert output in captured + assert trace in captured def test_live_fault_before_unref_does_not_misidentify_the_native_root(live_verifier, monkeypatch): @@ -179,7 +185,8 @@ def test_live_fault_before_unref_does_not_misidentify_the_native_root(live_verif def test_live_child_is_bounded_and_releases_owned_devices_before_context(live_verifier, monkeypatch): def run(argv, **kwargs): - assert argv[1:3] == ['-X', 'faulthandler'] and kwargs['check'] is False + assert argv[1:3] == ['-X', 'faulthandler'] + assert kwargs['check'] is False assert 0 < kwargs.get('timeout', 0) <= 60 assert kwargs['capture_output'] is True program = argv[-1] diff --git a/test/unit_test/headless/test_file_receive_receipts.py b/test/unit_test/headless/test_file_receive_receipts.py index 7588c3c63..5600b0c41 100644 --- a/test/unit_test/headless/test_file_receive_receipts.py +++ b/test/unit_test/headless/test_file_receive_receipts.py @@ -30,8 +30,10 @@ def test_receipt_is_frozen_and_describes_received_content_after_atomic_publicati receive(receiver, content, receipts, done=lambda path: done.append(path.read_bytes())) receipt, = receipts assert receipt.path == tmp_path / 'shared.bin' - assert receipt.sha256 == hashlib.sha256(content).hexdigest() and receipt.size == len(content) - assert done == [content] and receipt.path.read_bytes() == content + assert receipt.sha256 == hashlib.sha256(content).hexdigest() + assert receipt.size == len(content) + assert done == [content] + assert receipt.path.read_bytes() == content with pytest.raises(FrozenInstanceError): receipt.sha256 = '0' * 64 @@ -89,7 +91,9 @@ def failed(*_args): receiver.handle_message(b'part' if boundary != 'truncated' else b'x', on_error=errors.append) kind = 'file_abort' if boundary == 'abort' else 'file_end' receiver.handle_message(json.dumps({'type': kind}), on_receipt=receipts.append, on_error=errors.append) - assert errors and receipts == [] and target.read_bytes() == b'previous' + assert errors + assert receipts == [] + assert target.read_bytes() == b'previous' assert sorted(p.name for p in tmp_path.iterdir()) == ['shared.bin'] @@ -111,7 +115,8 @@ def test_receipt_cannot_register_an_unselected_or_missing_path(tmp_path, placeme target.write_bytes(b'received') engine = FolderSyncEngine(watch_dir=watch, sender=lambda *_args: None) receipt = FileReceiveReceipt(target, hashlib.sha256(b'received').hexdigest(), 8) - assert not engine.record_receipt(receipt) and engine._content.snapshot == {} + assert not engine.record_receipt(receipt) + assert engine._content.snapshot == {} @pytest.mark.parametrize('digest,size', [('bad', 1), ('G' * 64, 1), ('0' * 64, -1), ('0' * 64, True), @@ -150,8 +155,10 @@ def test_relative_inbox_remains_bound_when_another_operation_changes_cwd(tmp_pat receipts = [] receiver.handle_message(b'data') receiver.handle_message(json.dumps({'type': 'file_end'}), on_receipt=receipts.append) - assert len(receipts) == 1 and receipts[0].path == tmp_path / 'inbox' / 'shared.bin' - assert receipts[0].path.read_bytes() == b'data' and list(changed.iterdir()) == [] + assert len(receipts) == 1 + assert receipts[0].path == tmp_path / 'inbox' / 'shared.bin' + assert receipts[0].path.read_bytes() == b'data' + assert list(changed.iterdir()) == [] def test_relative_watch_keeps_its_original_selected_root_when_cwd_changes(tmp_path, monkeypatch): @@ -171,7 +178,8 @@ def test_relative_watch_keeps_its_original_selected_root_when_cwd_changes(tmp_pa assert engine.record_receipt(receipts[0]) (watch / 'shared.bin').write_bytes(b'original watch edit') loop(engine, lambda: None) - assert sent == [b'original watch edit'] and sentinel.read_bytes() == b'unselected content' + assert sent == [b'original watch edit'] + assert sentinel.read_bytes() == b'unselected content' @pytest.fixture(params=['host', 'viewer']) @@ -207,7 +215,8 @@ def test_native_receipt_listener_can_be_replaced_and_cleared_without_changing_pa native_deliver(channel, b'second') owner.set_file_receipt_callback(None) native_deliver(channel, b'third') - assert len(first) == len(second) == 1 and len(paths) == 3 + assert len(first) == len(second) == 1 + assert len(paths) == 3 assert first[0].sha256 == hashlib.sha256(b'first').hexdigest() assert second[0].sha256 == hashlib.sha256(b'second').hexdigest() assert (inbox / 'shared.bin').read_bytes() == b'third' @@ -223,7 +232,8 @@ def failed(_receipt): owner.set_file_receipt_callback(failed) owner.set_file_received_callback(paths.append) native_deliver(channel) - assert paths == [inbox / 'shared.bin'] and paths[0].read_bytes() == b'bytes' + assert paths == [inbox / 'shared.bin'] + assert paths[0].read_bytes() == b'bytes' @pytest.mark.parametrize('signal', [KeyboardInterrupt, SystemExit, asyncio.CancelledError, @@ -238,5 +248,6 @@ def failed(_receipt): owner.set_file_receipt_callback(failed) with pytest.raises(signal) as caught: native_deliver(channel) - assert caught.value is original and (inbox / 'shared.bin').read_bytes() == b'bytes' + assert caught.value is original + assert (inbox / 'shared.bin').read_bytes() == b'bytes' assert list(inbox.glob('*.part')) == [] diff --git a/test/unit_test/headless/test_folder_sync_checkpoint.py b/test/unit_test/headless/test_folder_sync_checkpoint.py index 093f406dd..d4eecbe39 100644 --- a/test/unit_test/headless/test_folder_sync_checkpoint.py +++ b/test/unit_test/headless/test_folder_sync_checkpoint.py @@ -29,7 +29,8 @@ def failed(*_args): guard.setattr(Path, 'stat', lambda *_args, **_kwargs: pytest.fail('checkpoint accessed the filesystem')) checkpoint = old.checkpoint() replacement = FolderSyncEngine(watch_dir=tmp_path, sender=lambda *_args: None, checkpoint=checkpoint) - assert checkpoint.baselined and dict(checkpoint.entries) == old._content.snapshot + assert checkpoint.baselined + assert dict(checkpoint.entries) == old._content.snapshot assert 'pending.bin' not in dict(checkpoint.entries) existing.write_bytes(b'offline edit') sent = [] @@ -60,13 +61,15 @@ def blocked(*_args): assert entered.wait(5) worker = engine._thread engine.stop() - assert engine.is_running() and engine.stop_requested + assert engine.is_running() + assert engine.stop_requested with pytest.raises(FolderSyncStateError, match='retired'): engine.checkpoint() finally: release.set() engine.stop() - assert worker is not None and not worker.is_alive() + assert worker is not None + assert not worker.is_alive() assert 'pending.bin' in dict(engine.checkpoint().entries) diff --git a/test/unit_test/headless/test_folder_sync_content.py b/test/unit_test/headless/test_folder_sync_content.py index d09f50740..22656c46f 100644 --- a/test/unit_test/headless/test_folder_sync_content.py +++ b/test/unit_test/headless/test_folder_sync_content.py @@ -112,7 +112,7 @@ def send(_path, _name): assert engine.wait_until_ready() (tmp_path / 'new.txt').write_bytes(b'data') assert entered.wait(5) - with patch.object(worker, 'join', lambda timeout: None): + with patch.object(worker, 'join', return_value=None): engine.stop() assert engine.is_running(), 'the stopped but blocked worker must remain owned' engine.start() diff --git a/test/unit_test/headless/test_folder_sync_receipts.py b/test/unit_test/headless/test_folder_sync_receipts.py index 95a4da594..9fc72991e 100644 --- a/test/unit_test/headless/test_folder_sync_receipts.py +++ b/test/unit_test/headless/test_folder_sync_receipts.py @@ -32,7 +32,8 @@ def send(path, _name): engine = FolderSyncEngine(watch_dir=tmp_path, sender=send) loop(engine, lambda: target.write_bytes(b'old local'), lambda: None) - assert sent == [b'old local'] and target.read_bytes() == b'newer remote' + assert sent == [b'old local'] + assert target.read_bytes() == b'newer remote' def test_receive_during_initial_scan_is_not_overwritten_by_the_older_baseline(tmp_path, monkeypatch): @@ -123,13 +124,12 @@ def interrupted(*_args, **_kwargs): if boundary == 'receipt': target.write_bytes(b'data') monkeypatch.setattr(engine, '_digest', interrupted) + attempt, arguments = engine.record_received, (target,) else: monkeypatch.setattr(engine, '_scan' if boundary == 'scan' else '_sender', interrupted) + attempt, arguments = loop, (engine, lambda: target.write_bytes(b'data')) with pytest.raises(type(signal)) as caught: - if boundary == 'receipt': - engine.record_received(target) - else: - loop(engine, lambda: target.write_bytes(b'data')) + attempt(*arguments) assert caught.value is signal diff --git a/test/unit_test/headless/test_gui_lazy_registry.py b/test/unit_test/headless/test_gui_lazy_registry.py index f711236c0..8d299506e 100644 --- a/test/unit_test/headless/test_gui_lazy_registry.py +++ b/test/unit_test/headless/test_gui_lazy_registry.py @@ -41,7 +41,8 @@ def test_importing_the_registry_loads_no_qt(): def test_every_lazy_tab_names_a_class_that_exists(spec): """Read, not imported: a typo here would otherwise surface when a user opens the tab.""" found = importlib.util.find_spec(spec.module) - assert found is not None and found.origin, spec.module + assert found is not None, spec.module + assert found.origin, spec.module source = pathlib.Path(found.origin).read_text(encoding="utf-8") assert (f"class {spec.class_name}(" in source or f"import {spec.class_name}" in source or f"{spec.class_name}," in source or f'"{spec.class_name}"' in source), (spec.module, spec.class_name) @@ -55,17 +56,21 @@ def factory(): return built[-1] entry = TabEntry("k", "tab_k", factory, on_build=adopted.append) - assert not entry.built and built == [] + assert not entry.built + assert built == [] first = entry.widget - assert entry.built and entry.widget is first - assert built == [first] and adopted == [first] + assert entry.built + assert entry.widget is first + assert built == [first] + assert adopted == [first] def test_a_lazy_factory_imports_at_call_time(): factory = lazy_factory("collections", "OrderedDict") assert type(factory()).__name__ == "OrderedDict" + missing = lazy_factory("je_auto_control.gui.no_such_tab", "Missing") with pytest.raises(ModuleNotFoundError): - lazy_factory("je_auto_control.gui.no_such_tab", "Missing")() + missing() _PROBE = r""" @@ -143,7 +148,8 @@ def test_opening_builds_the_tab_and_reopening_reuses_it(report): assert report["variables_imported_after_open"] assert report["current_after_open"] == "variables" assert report["hidden_is_owned"] - assert report["activated"] and report["same_widget_on_reopen"] + assert report["activated"] + assert report["same_widget_on_reopen"] # Registration order, not opening order: Variables sits between Script Builder and Remote Desktop. assert report["tab_order"] == ["record", "script_builder", "variables", "remote_desktop"] diff --git a/test/unit_test/headless/test_gui_map_measurements.py b/test/unit_test/headless/test_gui_map_measurements.py index eae2a3b59..24e0fa90c 100644 --- a/test/unit_test/headless/test_gui_map_measurements.py +++ b/test/unit_test/headless/test_gui_map_measurements.py @@ -28,8 +28,9 @@ def test_gui_package_caption_and_total_use_the_same_tree(): def test_unknown_gui_source_cannot_silently_escape_measurement(): + document = _document("| missing | `no_such_gui_file.py` | 1 | missing |") with pytest.raises(ValueError, match="no_such_gui_file"): - measure.rewrite(_document("| missing | `no_such_gui_file.py` | 1 | missing |")) + measure.rewrite(document) def test_unresolved_rows_reports_gui_sources(tmp_path, monkeypatch): diff --git a/test/unit_test/headless/test_gui_registry_close.py b/test/unit_test/headless/test_gui_registry_close.py index 9247240cd..9272c2548 100644 --- a/test/unit_test/headless/test_gui_registry_close.py +++ b/test/unit_test/headless/test_gui_registry_close.py @@ -32,14 +32,17 @@ def factory(): entry = TabEntry("feature", "feature", factory) registry = TabRegistry([entry], on_close=closed.append) registry.close("feature") - assert not entry.built and built == [] + assert not entry.built + assert built == [] first = registry.open("feature") assert registry.open("feature") is first registry.close("feature") - assert not entry.built and closed == [first] + assert not entry.built + assert closed == [first] _flush(qapp) second = registry.open("feature") - assert second is not first and second.tab_key == first.tab_key + assert second is not first + assert second.tab_key == first.tab_key registry.close("feature") _flush(qapp) @@ -57,7 +60,8 @@ def closeEvent(self, event): registry = TabRegistry([entry], on_close=closed.append) page = registry.open("unsaved") registry.close("unsaved") - assert entry.built and registry.open("unsaved") is page + assert entry.built + assert registry.open("unsaved") is page assert closed == [] page.deleteLater() @@ -90,7 +94,8 @@ def test_close_button_releases_presence_listener_and_reopen_resubscribes(workspa _flush(qapp) assert len(registry._listeners) == baseline workspace.show_tab("presence") - assert entry.widget is not first and len(registry._listeners) == baseline + 1 + assert entry.widget is not first + assert len(registry._listeners) == baseline + 1 def test_close_detaches_hud_log_tail(workspace, qapp): @@ -102,7 +107,8 @@ def test_close_detaches_hud_log_tail(workspace, qapp): assert tail in autocontrol_logger.handlers workspace.close_tab("live_hud") _flush(qapp) - assert not entry.built and tail not in autocontrol_logger.handlers + assert not entry.built + assert tail not in autocontrol_logger.handlers def test_hide_still_reuses_a_panel_and_explicit_close_can_release_it(workspace, qapp): @@ -133,6 +139,7 @@ def test_legacy_form_and_remote_session_owner_are_retained_on_close(workspace, k entry = workspace._find_entry(key) first = entry.widget workspace.close_tab(key) - assert entry.built and workspace.tabs.indexOf(first) == -1 + assert entry.built + assert workspace.tabs.indexOf(first) == -1 workspace.show_tab(key) assert entry.widget is first diff --git a/test/unit_test/headless/test_gui_slots_menu_audit.py b/test/unit_test/headless/test_gui_slots_menu_audit.py index 17a5c65e1..b504b4918 100644 --- a/test/unit_test/headless/test_gui_slots_menu_audit.py +++ b/test/unit_test/headless/test_gui_slots_menu_audit.py @@ -54,7 +54,8 @@ def test_a_zero_interval_is_refused(monkeypatch): stub = types.SimpleNamespace(interval_input=_Text("0"), timer=types.SimpleNamespace(start=lambda: started.append(1))) tab.AutoClickTabMixin._start_auto_click(stub) - assert warned and not started + assert warned + assert not started def test_an_assertion_backend_error_is_shown_once_the_run_is_delivered(monkeypatch): @@ -156,7 +157,8 @@ def test_a_recording_that_does_not_start_is_reported(monkeypatch): monkeypatch.setattr(tab, "record", lambda: False) stub = types.SimpleNamespace(_record_status_key="record_idle", _apply_record_status_label=lambda: None) tab.RecordTabMixin._start_record(stub) - assert warned and stub._record_status_key == "record_idle" + assert warned + assert stub._record_status_key == "record_idle" def test_a_template_is_cropped_into_a_non_ascii_folder(monkeypatch, tmp_path): diff --git a/test/unit_test/headless/test_gui_task_lifecycle.py b/test/unit_test/headless/test_gui_task_lifecycle.py index 56633ae08..e22cc1258 100644 --- a/test/unit_test/headless/test_gui_task_lifecycle.py +++ b/test/unit_test/headless/test_gui_task_lifecycle.py @@ -70,13 +70,16 @@ def work(_token): assert entered.wait(1) QTimer.singleShot(0, lambda: ticks.append('tick')) assert _pump(app, lambda: ticks) - assert not results and handle.is_running + assert not results + assert handle.is_running finally: release.set() assert _pump(app, lambda: results and controller.active_count == 0) outcome, callback_thread = results[0] - assert outcome.status == 'ok' and outcome.task_id == handle.task_id - assert outcome.value != threading.get_ident() and callback_thread == threading.get_ident() + assert outcome.status == 'ok' + assert outcome.task_id == handle.task_id + assert outcome.value != threading.get_ident() + assert callback_thread == threading.get_ident() def test_cancel_releases_session(tasks): @@ -99,7 +102,9 @@ def work(token): handle.cancel() assert _pump(app, lambda: handle.outcome is not None and controller.active_count == 0) assert handle.outcome.status == 'cancelled' - assert released.is_set() and sessions == [] and not handle.is_running + assert released.is_set() + assert sessions == [] + assert not handle.is_running def test_owner_death_drops_result(tasks): @@ -121,7 +126,9 @@ def ignores_cancel(_token): finally: release.set() assert _pump(app, lambda: controller.active_count == 0) - assert received == [] and handle.outcome is None and handle.is_cancelled + assert received == [] + assert handle.outcome is None + assert handle.is_cancelled def test_no_worker_touches_widget(tasks): @@ -155,8 +162,11 @@ def test_submission_captures_context_and_viewer_authority_without_drive_privileg assert _pump(app, lambda: handle.outcome is not None and controller.active_count == 0) assert handle.outcome.status == 'ok' value, authority = handle.outcome.value - assert value == 'registrant' and authority.user_id == 'reader' and authority.role == 'viewer' - assert current_authorization() is None and marker.get() == 'ambient' + assert value == 'registrant' + assert authority.user_id == 'reader' + assert authority.role == 'viewer' + assert current_authorization() is None + assert marker.get() == 'ambient' @pytest.mark.parametrize('kind', ['keyboard', 'exit', 'generator', 'async', 'future', 'operation']) @@ -174,7 +184,8 @@ def work(_token): handle = controller.submit(work, owner=owner) assert _pump(app, lambda: handle.outcome is not None and controller.active_count == 0) - assert handle.outcome.status == 'cancelled' and handle.outcome.error == '' + assert handle.outcome.status == 'cancelled' + assert handle.outcome.error == '' def test_an_ordinary_error_finishes_once_and_does_not_block_a_later_task(tasks): @@ -187,11 +198,14 @@ def broken(_token): handle = controller.submit(broken, owner=owner) handle.finished.connect(outcomes.append) assert _pump(app, lambda: handle.outcome is not None and controller.active_count == 0) - assert handle.outcome.status == 'error' and 'controlled failure' in handle.outcome.error + assert handle.outcome.status == 'error' + assert 'controlled failure' in handle.outcome.error assert len(outcomes) == 1 later = controller.submit(lambda _token: 42, owner=owner) assert _pump(app, lambda: later.outcome is not None and controller.active_count == 0) - assert later.outcome.status == 'ok' and later.outcome.value == 42 and later.task_id != handle.task_id + assert later.outcome.status == 'ok' + assert later.outcome.value == 42 + assert later.task_id != handle.task_id def test_start_failure_does_not_leak_worker_or_controller_job(tasks, monkeypatch): @@ -208,7 +222,8 @@ def failed_start(_thread): with pytest.raises(RuntimeError) as caught: controller.submit(lambda _token: None, owner=owner) assert caught.value is original - assert controller.active_count == 0 and set(_worker_thread._RUNNING) == previous + assert controller.active_count == 0 + assert set(_worker_thread._RUNNING) == previous finally: # A failed pre-fix run must not leave an unstarted thread for atexit to join. for handle in set(_worker_thread._RUNNING)-previous: @@ -253,7 +268,8 @@ def work(_token): finally: release.set() assert _pump(app, lambda: handle.outcome is not None and controller.active_count == 0) - assert handle.outcome.status == 'error' and calls == [] + assert handle.outcome.status == 'error' + assert calls == [] def test_destroying_one_owner_does_not_cancel_another_owners_work(tasks): @@ -274,14 +290,17 @@ def second_work(_token): first = controller.submit(first_work, owner=first_owner) second = controller.submit(second_work, owner=second_owner) try: - assert first_entered.wait(1) and second_entered.wait(1) + assert first_entered.wait(1) + assert second_entered.wait(1) first_owner.deleteLater() QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete) - assert first.is_cancelled and not second.is_cancelled + assert first.is_cancelled + assert not second.is_cancelled finally: release.set() assert _pump(app, lambda: second.outcome is not None and controller.active_count == 0) - assert first.outcome is None and second.outcome.value == 'second owner' + assert first.outcome is None + assert second.outcome.value == 'second owner' second_owner.deleteLater() @@ -301,7 +320,9 @@ def work(token): controller.deleteLater() QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete) assert _pump(app, lambda: controller.active_count == 0) - assert handle.is_cancelled and handle.outcome is None and outcomes == [] + assert handle.is_cancelled + assert handle.outcome is None + assert outcomes == [] def test_discarding_a_handle_does_not_retain_it_through_worker_callbacks(tasks): @@ -339,7 +360,8 @@ def work(token): gc.collect() assert owner_ref() is None assert _pump(app, lambda: controller.active_count == 0) - assert handle.outcome is None and handle.is_cancelled + assert handle.outcome is None + assert handle.is_cancelled def test_foreign_and_late_progress_or_results_cannot_replace_this_tasks_outcome(tasks): @@ -357,13 +379,15 @@ def work(_token): try: handle.complete(TaskOutcome(handle.task_id+1, 'ok', 'foreign')) handle.forward_progress(TaskProgress(handle.task_id+1, 1)) - assert handle.outcome is None and progress == [] + assert handle.outcome is None + assert progress == [] finally: release.set() assert _pump(app, lambda: handle.outcome is not None and controller.active_count == 0) handle.complete(TaskOutcome(handle.task_id, 'error', error='late')) handle.forward_progress(TaskProgress(handle.task_id, 2)) - assert handle.outcome.value == 'correct' and progress == [] + assert handle.outcome.value == 'correct' + assert progress == [] def test_late_cooperative_stop_does_not_relabel_successful_committed_work(tasks): @@ -379,7 +403,9 @@ def work(token): assert entered.wait(1) handle.cancel() assert _pump(app, lambda: handle.outcome is not None and controller.active_count == 0) - assert handle.is_cancelled and handle.outcome.status == 'ok' and handle.outcome.value == 'committed' + assert handle.is_cancelled + assert handle.outcome.status == 'ok' + assert handle.outcome.value == 'committed' @pytest.mark.parametrize('operation', ['submit', 'close']) @@ -399,7 +425,9 @@ def off_thread(): thread = threading.Thread(target=off_thread) thread.start() thread.join(1) - assert not thread.is_alive() and len(errors) == 1 and controller.active_count == 0 + assert not thread.is_alive() + assert len(errors) == 1 + assert controller.active_count == 0 def test_failure_cause_tracebacks_are_released_before_outcome_delivery(tasks): @@ -415,7 +443,8 @@ def work(_token): handle = controller.submit(work, owner=owner) assert _pump(app, lambda: handle.outcome is not None and controller.active_count == 0) assert handle.outcome.status == 'error' - assert original.__traceback__ is None and wrapper.__traceback__ is None + assert original.__traceback__ is None + assert wrapper.__traceback__ is None def test_worker_progress_and_error_text_use_the_captured_secret_scope(tasks): @@ -435,8 +464,12 @@ def work(token): handle.progress.connect(progress.append) release.set() assert _pump(app, lambda: handle.outcome is not None and controller.active_count == 0) - assert len(progress) == 1 and secret not in progress[0].detail and '***' in progress[0].detail - assert handle.outcome.status == 'error' and secret not in handle.outcome.error and '***' in handle.outcome.error + assert len(progress) == 1 + assert secret not in progress[0].detail + assert '***' in progress[0].detail + assert handle.outcome.status == 'error' + assert secret not in handle.outcome.error + assert '***' in handle.outcome.error @pytest.mark.parametrize('kind', ['ordinary', 'keyboard', 'exit', 'generator', 'async', 'future', 'operation']) @@ -456,9 +489,11 @@ def work(_token): handle = controller.submit(work, owner=owner) assert _pump(app, lambda: handle.outcome is not None and controller.active_count == 0) if kind == 'ordinary': - assert handle.outcome.status == 'error' and 'detail unavailable' in handle.outcome.error + assert handle.outcome.status == 'error' + assert 'detail unavailable' in handle.outcome.error else: - assert handle.outcome.status == 'cancelled' and handle.outcome.error == '' + assert handle.outcome.status == 'cancelled' + assert handle.outcome.error == '' def test_retiring_tasks_disconnects_owner_notifications_instead_of_accumulating_them(tasks): diff --git a/test/unit_test/headless/test_gui_workspace_layout.py b/test/unit_test/headless/test_gui_workspace_layout.py index af601c156..3aafc4cb1 100644 --- a/test/unit_test/headless/test_gui_workspace_layout.py +++ b/test/unit_test/headless/test_gui_workspace_layout.py @@ -56,7 +56,8 @@ def test_narrow_content_scrolls_without_changing_the_tab_widget_contract(qapp): qapp.processEvents() try: assert tabs.indexOf(page) == 0 - assert tabs.widget(0) is page and tabs.currentWidget() is page + assert tabs.widget(0) is page + assert tabs.currentWidget() is page scroll = tabs.scroll_area(0) assert scroll.widget() is page assert scroll.horizontalScrollBar().maximum() > 0 @@ -69,7 +70,8 @@ def test_narrow_content_scrolls_without_changing_the_tab_widget_contract(qapp): tabs.removeTab(0) assert tabs.indexOf(page) == -1 tabs.insertTab(0, page, "Wide again") - assert tabs.widget(0) is page and tabs.count() == 2 + assert tabs.widget(0) is page + assert tabs.count() == 2 assert len(tabs.findChildren(type(scroll))) == 2 finally: tabs.close() diff --git a/test/unit_test/headless/test_history_interruption.py b/test/unit_test/headless/test_history_interruption.py index 9a646c86c..cc1df0834 100644 --- a/test/unit_test/headless/test_history_interruption.py +++ b/test/unit_test/headless/test_history_interruption.py @@ -91,8 +91,10 @@ def test_interruption_records_failure_and_never_captures_or_runs_the_next_action else: fire() row = history.list_runs()[0] - assert row.status == STATUS_ERROR and type(failure).__name__ in row.error_text - assert snapshots == [] and reached == [] + assert row.status == STATUS_ERROR + assert type(failure).__name__ in row.error_text + assert snapshots == [] + assert reached == [] assert len(history.journals_for_run(row.id)) == 1 assert read_events(path)[-1].status == "cancelled" @@ -105,11 +107,14 @@ def test_history_outage_cannot_replace_a_pending_hard_interruption(kind, environ def unavailable(*_args, **_kwargs): raise HistoryStoreError("controlled history outage") monkeypatch.setattr(history, "finish_run", unavailable) + fire = _fire(kind, script, execute) with pytest.raises(KeyboardInterrupt) as raised: - _fire(kind, script, execute)() + fire() assert raised.value is failure assert history.list_runs()[0].status == STATUS_RUNNING - assert snapshots == [] and reached == [] and read_events(path)[-1].status == "cancelled" + assert snapshots == [] + assert reached == [] + assert read_events(path)[-1].status == "cancelled" @pytest.mark.parametrize("kind", KINDS) @@ -124,12 +129,14 @@ def test_ordinary_soft_failures_still_capture_and_keep_their_existing_containmen else: fire() assert history.list_runs()[0].status == STATUS_ERROR - assert len(snapshots) == 1 and reached == [True] + assert len(snapshots) == 1 + assert reached == [True] def test_mcp_cancellation_keeps_its_deliberate_control_signal_contract(): failure = OperationCancelledError("controlled") - assert isinstance(failure, RuntimeError) and not isinstance(failure, AutoControlException) + assert isinstance(failure, RuntimeError) + assert not isinstance(failure, AutoControlException) @pytest.mark.parametrize("block", ["parallel", "dag"]) @@ -152,7 +159,8 @@ def test_worker_interruptions_reach_the_owner_without_becoming_success_or_an_ord else: fire() assert history.list_runs()[0].status == STATUS_ERROR - assert snapshots == [] and reached == [] + assert snapshots == [] + assert reached == [] assert read_events(path)[-1].status == "cancelled" @@ -200,5 +208,6 @@ def invoke(): script.write_text(json.dumps(actions), encoding="utf-8") _fire("scheduler", script, execute)() assert history.list_runs()[0].status == STATUS_ERROR - assert snapshots == [] and reached == [] + assert snapshots == [] + assert reached == [] assert read_events(path)[-1].status == "cancelled" diff --git a/test/unit_test/headless/test_journal_browser_gui.py b/test/unit_test/headless/test_journal_browser_gui.py index efb4766ca..5fd85d340 100644 --- a/test/unit_test/headless/test_journal_browser_gui.py +++ b/test/unit_test/headless/test_journal_browser_gui.py @@ -70,15 +70,18 @@ def inspect(path, **_kwargs): finally: release.set() _until(app, lambda: browser._handle is None) - assert calls == ["old.jsonl"] and browser._events == [] + assert calls == ["old.jsonl"] + assert browser._events == [] if next_action == "viewer": assert browser._status_key == "journal_error" def test_browser_has_only_inputs_and_views_until_an_action(browser): assert browser.findChildren(QPushButton) == [] - assert browser._handle is None and browser._table.rowCount() == 0 - assert browser._limit.minimum() == 1 and browser._limit.maximum() == 10000 + assert browser._handle is None + assert browser._table.rowCount() == 0 + assert browser._limit.minimum() == 1 + assert browser._limit.maximum() == 10000 assert browser._limit.value() == 100 @@ -101,13 +104,15 @@ def test_links_select_run_without_automatic_file_io(browser, app, tmp_path): _journal(path, "one") _journal(path, "two") browser.set_links([JournalLink(1, str(path), "one"), JournalLink(1, str(path), "two")]) - assert browser._handle is None and browser._table.rowCount() == 0 + assert browser._handle is None + assert browser._table.rowCount() == 0 browser._links.setCurrentIndex(2) assert browser._run_id.text() == "two" browser._limit.setValue(1) browser.read() _until(app, lambda: browser._handle is None) - assert len(browser._events) == 1 and browser._events[0]["run_id"] == "two" + assert len(browser._events) == 1 + assert browser._events[0]["run_id"] == "two" assert browser._events[0]["status"] == "incomplete" @@ -120,7 +125,8 @@ def forbidden(*args, **kwargs): worker.finished.connect(outcomes.append) worker.request_stop() worker.run() - assert outcomes[0].status == "cancelled" and outcomes[0].events == [] + assert outcomes[0].status == "cancelled" + assert outcomes[0].events == [] worker.deleteLater() @@ -132,7 +138,8 @@ def test_worker_rechecks_captured_viewer_authority(app, monkeypatch): outcomes = [] worker.finished.connect(outcomes.append) worker.run() - assert outcomes[0].status == "error" and calls == [] + assert outcomes[0].status == "error" + assert calls == [] worker.deleteLater() @@ -144,7 +151,8 @@ def test_worker_retains_enclosing_path_policy(app, tmp_path): outcomes = [] worker.finished.connect(outcomes.append) worker.run() - assert outcomes[0].status == "error" and "PathNotAllowedError" in outcomes[0].error + assert outcomes[0].status == "error" + assert "PathNotAllowedError" in outcomes[0].error worker.deleteLater() @@ -161,7 +169,8 @@ def inspect(path, **kwargs): outcomes = [] worker.finished.connect(outcomes.append) worker.run() - assert outcomes[0].status == "cancelled" and max(scanned) <= 101 + assert outcomes[0].status == "cancelled" + assert max(scanned) <= 101 worker.deleteLater() @@ -187,7 +196,8 @@ def test_cancel_does_not_block_gui(browser, app, monkeypatch): finally: release.set() _until(app, lambda: browser._handle is None) - assert browser._events == [] and browser._status_key == "journal_cancelled" + assert browser._events == [] + assert browser._status_key == "journal_cancelled" def test_changing_target_discards_stale_completion(browser, app, monkeypatch): @@ -200,8 +210,10 @@ def test_changing_target_discards_stale_completion(browser, app, monkeypatch): finally: release.set() _until(app, lambda: browser._handle is None) - assert browser._path.text() == "new" and browser._run_id.text() == "new-run" - assert browser._events == [] and browser._status_key == "journal_idle" + assert browser._path.text() == "new" + assert browser._run_id.text() == "new-run" + assert browser._events == [] + assert browser._status_key == "journal_idle" def test_destroying_owner_requests_stop_and_drops_late_results(app, monkeypatch): @@ -225,7 +237,8 @@ def test_viewer_is_refused_before_worker_creation(browser): browser._path.setText("private.jsonl") with authorization_scope(AuthorizationContext("viewer", Role.VIEWER)): browser.read() - assert browser._handle is None and browser._status_key == "journal_error" + assert browser._handle is None + assert browser._status_key == "journal_error" def test_corrupt_read_clears_previous_snapshots(browser, app, tmp_path): @@ -238,7 +251,8 @@ def test_corrupt_read_clears_previous_snapshots(browser, app, tmp_path): path.write_text("invalid\n", encoding="utf-8") browser.read() _until(app, lambda: browser._handle is None) - assert browser._events == [] and browser._table.rowCount() == 0 + assert browser._events == [] + assert browser._table.rowCount() == 0 assert browser._status_key == "journal_error" @@ -253,10 +267,13 @@ def test_history_menu_loads_links_without_reading(app, monkeypatch, tmp_path): tab._timer.stop() tab._table.selectRow(0) actions = dict(tab.menu_actions()) - assert "journal_selected_run" in actions and "journal_read" in actions and "journal_cancel" in actions + assert "journal_selected_run" in actions + assert "journal_read" in actions + assert "journal_cancel" in actions actions["journal_selected_run"]() assert tab._journal._path.text() == str(tmp_path / "events.jsonl") - assert tab._journal._run_id.text() == "chosen" and tab._journal._handle is None + assert tab._journal._run_id.text() == "chosen" + assert tab._journal._handle is None finally: tab.deleteLater() store.close() @@ -294,7 +311,8 @@ def forbidden(*args, **kwargs): outcomes = [] worker.finished.connect(outcomes.append) worker.run() - assert outcomes[0].status == "error" and outcomes[0].events == [] + assert outcomes[0].status == "error" + assert outcomes[0].events == [] worker.deleteLater() @@ -345,7 +363,8 @@ def forbidden(*args, **kwargs): monkeypatch.setattr("je_auto_control.gui.journal_browser.QFileDialog.getOpenFileName", forbidden) with authorization_scope(AuthorizationContext("viewer", Role.VIEWER)): browser.browse() - assert browser._status_key == "journal_error" and browser._handle is None + assert browser._status_key == "journal_error" + assert browser._handle is None def test_worker_masks_errors_in_active_captured_context(app, monkeypatch): @@ -360,7 +379,8 @@ def fail(*args, **kwargs): worker = JournalReadWorker(JournalReadRequest(1, "unused", None, 100)) worker.finished.connect(outcomes.append) worker.run() - assert outcomes[0].status == "error" and secret not in outcomes[0].error + assert outcomes[0].status == "error" + assert secret not in outcomes[0].error assert "***" in outcomes[0].error worker.deleteLater() @@ -387,7 +407,8 @@ def inspect(path, **kwargs): browser._path.setText("unused") browser.read() _until(app, lambda: browser._handle is None) - assert calls and set(calls) == {gui_ident} + assert calls + assert set(calls) == {gui_ident} def test_history_close_cancels_its_child_reader(app, monkeypatch, tmp_path): @@ -402,7 +423,8 @@ def test_history_close_cancels_its_child_reader(app, monkeypatch, tmp_path): try: assert entered.wait(2) tab.close() - assert tab._journal._worker._stop.is_set() and not tab._timer.isActive() + assert tab._journal._worker._stop.is_set() + assert not tab._timer.isActive() finally: release.set() assert handle.wait(5) @@ -421,5 +443,6 @@ def test_lookup_error_invalidates_an_older_read(browser, app, monkeypatch): finally: release.set() _until(app, lambda: browser._handle is None) - assert browser._events == [] and browser._status_key == "journal_error" + assert browser._events == [] + assert browser._status_key == "journal_error" assert "new history lookup failed" in browser._status.text() diff --git a/test/unit_test/headless/test_journal_commands.py b/test/unit_test/headless/test_journal_commands.py index 674985718..a4268cea9 100644 --- a/test/unit_test/headless/test_journal_commands.py +++ b/test/unit_test/headless/test_journal_commands.py @@ -27,9 +27,11 @@ def test_capture_defers_body_variables_and_preserves_owner_scope(tmp_path): result = executor.execute_action([_capture(path, body, run_id="nested")], raise_on_error=True) assert executor.variables.get("message") == "after" summary = next(iter(result.values())) - assert summary["run_id"] == "nested" and summary["failed_actions"] == 0 + assert summary["run_id"] == "nested" + assert summary["failed_actions"] == 0 assert "after" in summary["record"].values() - assert body[-1][1]["name"] == "${variable_name}" and current_journal() is None + assert body[-1][1]["name"] == "${variable_name}" + assert current_journal() is None assert [event.command for event in read_events(path) if event.status == "ok"] == ["AC_set_var", "AC_get_var"] @@ -63,7 +65,8 @@ def test_nested_validation_rejects_unknown_body_before_prior_actions(tmp_path, m actions = [["AC_journal_command_probe"], _capture(tmp_path / "invalid.jsonl", [["AC_unknown_journal_body"]])] with pytest.raises(AutoControlActionException, match="unknown command"): executor.execute_action(actions, raise_on_error=True) - assert seen == [] and not (tmp_path / "invalid.jsonl").exists() + assert seen == [] + assert not (tmp_path / "invalid.jsonl").exists() @pytest.mark.parametrize("strict", [False, True]) @@ -95,16 +98,19 @@ def cancel(): raise signal monkeypatch.setitem(executor.event_dict, "AC_journal_command_probe", cancel) cancelled = tmp_path / "cancelled.jsonl" + actions = [_capture(cancelled, [["AC_journal_command_probe"]])] with pytest.raises(asyncio.CancelledError) as caught: - executor.execute_action([_capture(cancelled, [["AC_journal_command_probe"]])]) - assert caught.value is signal and current_journal() is None + executor.execute_action(actions) + assert caught.value is signal + assert current_journal() is None assert [event.status for event in read_events(cancelled)] == ["incomplete", "cancelled"] def test_capture_is_inert_during_outer_dry_run(tmp_path): path = tmp_path / "dry.jsonl" result = executor.execute_action([_capture(path, [["AC_screen_size"]])], dry_run=True) - assert any("dry-run" in key for key in result) and not path.exists() + assert any("dry-run" in key for key in result) + assert not path.exists() @pytest.mark.parametrize("name", ["AC_journal_capture", "AC_journal_read"]) @@ -124,8 +130,10 @@ def test_read_command_returns_sanitized_bounded_snapshots(tmp_path): result = executor.execute_action([["AC_journal_read", {"path": str(path), "run_id": "first", "limit": 1}]], raise_on_error=True) payload = next(iter(result.values())) - assert len(payload) == 1 and payload[0]["run_id"] == "first" - assert payload[0]["status"] == "incomplete" and path.read_bytes() == before + assert len(payload) == 1 + assert payload[0]["run_id"] == "first" + assert payload[0]["status"] == "incomplete" + assert path.read_bytes() == before def test_schema_exposes_capture_body_and_linter_checks_it(tmp_path): @@ -137,8 +145,9 @@ def test_schema_exposes_capture_body_and_linter_checks_it(tmp_path): actions = [_capture(tmp_path / "bad.jsonl", [["AC_unknown_journal_body"]])] issues = lint_actions(actions) assert any(issue.code == "unknown-command" for issue in issues) + known = executor.known_commands() with pytest.raises(AutoControlActionException): - validate_actions(actions, executor.known_commands()) + validate_actions(actions, known) @pytest.mark.parametrize("name", ["AC_journal_capture", "AC_journal_read"]) @@ -202,10 +211,10 @@ def test_resolved_secrets_cannot_enter_capture_identifiers_or_destination(tmp_pa monkeypatch.setattr(default_secret_manager, "get", lambda name: "fake-private-metadata") path = tmp_path / ("leaked-${secrets.PRIVATE}.jsonl" if field == "path" else "metadata.jsonl") metadata = {} if field == "path" else {field: "${secrets.PRIVATE}"} + actions = [_capture(path, [["AC_set_var", {"name": "effect", "value": 1}]], **metadata)] with execution_scope(): with pytest.raises(ActionJournalError, match="resolved secret"): - executor.execute_action([_capture(path, [["AC_set_var", {"name": "effect", "value": 1}]], **metadata)], - raise_on_error=True) + executor.execute_action(actions, raise_on_error=True) assert "effect" not in executor.variables assert list(tmp_path.glob("*.jsonl")) == [] diff --git a/test/unit_test/headless/test_journal_history_links.py b/test/unit_test/headless/test_journal_history_links.py index 4637e6b6b..c2515529f 100644 --- a/test/unit_test/headless/test_journal_history_links.py +++ b/test/unit_test/headless/test_journal_history_links.py @@ -30,7 +30,8 @@ def test_multiple_journals_attach_to_one_history_run_idempotently(store, tmp_pat assert [(item.run_id, item.journal_path, item.journal_run_id) for item in links] == [ (run, first, "one"), (run, second, "two"), ] - assert not Path(first).exists() and not Path(second).exists() + assert not Path(first).exists() + assert not Path(second).exists() assert store.journals_for_run(run + 1) == [] assert not store.attach_journal(run + 1, first, "unknown") @@ -107,7 +108,8 @@ def close(self): try: with pytest.raises(HistoryStoreError, match="controlled schema failure"): store.attach_journal(1, "unused.jsonl", "unused") - assert connection.closed and store._conn is None + assert connection.closed + assert store._conn is None finally: connection.close() @@ -135,9 +137,9 @@ def test_unknown_history_owner_prevents_the_action_before_any_effect(store, tmp_ from je_auto_control.utils.run_history.context import HistoryBinding, bound_history reached = [] monkeypatch.setitem(executor.event_dict, "AC_history_probe", lambda: reached.append(True)) - with bound_history(HistoryBinding(store, 999)), pytest.raises(ActionJournalError, match="history"): - with journal_scope(ActionJournal(tmp_path / "no-effect.jsonl")): - executor.execute_action([["AC_history_probe"]]) + scope = journal_scope(ActionJournal(tmp_path / "no-effect.jsonl")) + with bound_history(HistoryBinding(store, 999)), pytest.raises(ActionJournalError, match="history"), scope: + executor.execute_action([["AC_history_probe"]]) assert reached == [] @@ -146,10 +148,10 @@ def test_history_bindings_restore_on_nested_failure_and_explicitly_clear_ambient first = HistoryBinding(store, 1) second = HistoryBinding(store, 2) with bound_history(first): - with pytest.raises(RuntimeError): - with bound_history(second): - assert current_history() is second - raise RuntimeError("controlled") + nested, failure = bound_history(second), RuntimeError("controlled") + with pytest.raises(RuntimeError), nested: + assert current_history() is second + raise failure assert current_history() is first with bound_history(None): assert current_history() is None @@ -213,6 +215,7 @@ def capture(): trigger = owner.add("imap.invalid", "user", "unused", str(script)) owner._execute_with_history(trigger, {}) rows = store.list_runs() - assert len(rows) == 1 and rows[0].status == STATUS_OK + assert len(rows) == 1 + assert rows[0].status == STATUS_OK links = store.journals_for_run(rows[0].id) assert [(item.journal_path, item.journal_run_id) for item in links] == [(str(path.resolve()), "background")] diff --git a/test/unit_test/headless/test_journal_observation.py b/test/unit_test/headless/test_journal_observation.py index 313176bc4..56efb6ccc 100644 --- a/test/unit_test/headless/test_journal_observation.py +++ b/test/unit_test/headless/test_journal_observation.py @@ -21,8 +21,10 @@ def observe(status): options = ac.JournalRunOptions(variables=variables, run_id="observed", observer=observe) result = ac.execute_journal_run([["AC_set_var", {"name": "message", "value": "after"}]], path, options=options) - assert statuses == ["incomplete", "ok"] and result["run_id"] == "observed" - assert variables == {"message": "before"} and current_journal() is None + assert statuses == ["incomplete", "ok"] + assert result["run_id"] == "observed" + assert variables == {"message": "before"} + assert current_journal() is None @pytest.mark.parametrize("worker", ["loop", "parallel", "dag", "capture"]) @@ -52,10 +54,12 @@ def test_cancellation_before_dispatch_opens_no_file(tmp_path): signal = asyncio.CancelledError("controlled cancellation") def observe(_status): raise signal + options = ac.JournalRunOptions(observer=observe) with pytest.raises(asyncio.CancelledError) as caught: - ac.execute_journal_run([["AC_set_var", {"name": "x", "value": 1}]], path, - options=ac.JournalRunOptions(observer=observe)) - assert caught.value is signal and not path.exists() and current_journal() is None + ac.execute_journal_run([["AC_set_var", {"name": "x", "value": 1}]], path, options=options) + assert caught.value is signal + assert not path.exists() + assert current_journal() is None @pytest.mark.parametrize("signal", [ValueError("original"), KeyboardInterrupt(), asyncio.CancelledError()]) @@ -66,10 +70,11 @@ def observe(status): if status != "incomplete": raise RuntimeError("observer unavailable") monkeypatch.setitem(executor.event_dict, "AC_observed_failure", fail) - with pytest.raises(type(signal)) as caught: - with ac.journal_scope(ac.ActionJournal(tmp_path / "failure.jsonl"), observer=observe): - executor.execute_action([["AC_observed_failure"]], raise_on_error=True) - assert caught.value is signal and current_journal() is None + scope = ac.journal_scope(ac.ActionJournal(tmp_path / "failure.jsonl"), observer=observe) + with pytest.raises(type(signal)) as caught, scope: + executor.execute_action([["AC_observed_failure"]], raise_on_error=True) + assert caught.value is signal + assert current_journal() is None def test_observer_failure_after_success_leaves_a_durable_success(tmp_path): @@ -77,9 +82,9 @@ def test_observer_failure_after_success_leaves_a_durable_success(tmp_path): def observe(status): if status == "ok": raise RuntimeError("observer unavailable") + options = ac.JournalRunOptions(observer=observe) with pytest.raises(RuntimeError, match="observer unavailable"): - ac.execute_journal_run([["AC_set_var", {"name": "x", "value": 1}]], path, - options=ac.JournalRunOptions(observer=observe)) + ac.execute_journal_run([["AC_set_var", {"name": "x", "value": 1}]], path, options=options) assert [event.status for event in ac.read_events(path)] == ["incomplete", "ok"] @@ -95,12 +100,14 @@ def test_nested_controls_compose_without_relaxing_or_duplicating(tmp_path): path_policy=policy) as binding: assert len(binding.observers) == len(binding.path_policies) == 1 executor.execute_action([["AC_set_var", {"name": "x", "value": 1}]]) + refused = ac.journal_scope(ac.ActionJournal(outside / "refused.jsonl"), path_policy=PathPolicy()) + with pytest.raises(PathNotAllowedError), refused: + pytest.fail("nested policy relaxed its enclosing roots") + unrestricted = PathPolicy() with pytest.raises(PathNotAllowedError): - with ac.journal_scope(ac.ActionJournal(outside / "refused.jsonl"), path_policy=PathPolicy()): - pytest.fail("nested policy relaxed its enclosing roots") - with pytest.raises(PathNotAllowedError): - ac.inspect_journal(outside / "absent.jsonl", path_policy=PathPolicy()) - assert statuses == ["incomplete", "ok"] and list(outside.iterdir()) == [] + ac.inspect_journal(outside / "absent.jsonl", path_policy=unrestricted) + assert statuses == ["incomplete", "ok"] + assert list(outside.iterdir()) == [] @pytest.mark.parametrize("worker", ["direct", "parallel", "dag"]) @@ -117,7 +124,8 @@ def test_dynamic_nested_journal_paths_cannot_escape_captured_roots(tmp_path, wor assert not outside.exists() if worker == "dag": outcome = next(iter(result["record"].values())) - assert outcome["succeeded"] is False and outcome["nodes"]["one"]["status"] == "failed" + assert outcome["succeeded"] is False + assert outcome["nodes"]["one"]["status"] == "failed" assert "PathNotAllowedError" in outcome["nodes"]["one"]["error"] else: assert result["failed_actions"] > 0 @@ -133,9 +141,9 @@ def test_policy_revocation_stops_the_next_append(tmp_path): def observe(status): if status == "ok": policy.set_client_roots([other]) + options = ac.JournalRunOptions(path_policy=policy, observer=observe) with pytest.raises(PathNotAllowedError): - ac.execute_journal_run([["AC_set_var", {"name": "x", "value": 1}]] * 2, path, - options=ac.JournalRunOptions(path_policy=policy, observer=observe)) + ac.execute_journal_run([["AC_set_var", {"name": "x", "value": 1}]] * 2, path, options=options) assert [event.status for event in ac.read_events(path)] == ["incomplete", "ok"] @@ -151,8 +159,9 @@ def fail(): raise signal monkeypatch.setitem(executor.event_dict, "AC_policy_interruption", fail) path = allowed / "unfinished.jsonl" + options = ac.JournalRunOptions(path_policy=policy) with pytest.raises(KeyboardInterrupt) as caught: - ac.execute_journal_run([["AC_policy_interruption"]], path, options=ac.JournalRunOptions(path_policy=policy)) + ac.execute_journal_run([["AC_policy_interruption"]], path, options=options) assert caught.value is signal assert [event.status for event in ac.read_events(path)] == ["incomplete"] @@ -173,7 +182,9 @@ def observe(_count): monkeypatch.setattr(Path, "open", track) with pytest.raises(asyncio.CancelledError) as caught: ac.inspect_journal(path, observer=observe) - assert caught.value is signal and opened and all(handle.closed for handle in opened) + assert caught.value is signal + assert opened + assert all(handle.closed for handle in opened) def test_reader_observes_all_validated_records_before_filtering(tmp_path): @@ -182,4 +193,6 @@ def test_reader_observes_all_validated_records_before_filtering(tmp_path): ac.execute_action_with_journal([["AC_set_var", {"name": "x", "value": 1}]], path, run_id=run_id) seen = [] payload = ac.inspect_journal(path, run_id="two", limit=1, observer=seen.append) - assert seen == [1, 2, 3, 4] and len(payload) == 1 and payload[0]["run_id"] == "two" + assert seen == [1, 2, 3, 4] + assert len(payload) == 1 + assert payload[0]["run_id"] == "two" diff --git a/test/unit_test/headless/test_journal_public_cli.py b/test/unit_test/headless/test_journal_public_cli.py index 6a393bda5..8c8ffdb0a 100644 --- a/test/unit_test/headless/test_journal_public_cli.py +++ b/test/unit_test/headless/test_journal_public_cli.py @@ -22,14 +22,19 @@ def test_facade_capture_and_filtered_read_keep_original_results(tmp_path, monkey path = tmp_path / "public.jsonl" actions = [["AC_public_journal_probe", {"value": "${message}"}]] result = ac.execute_action_with_journal(actions, path, variables={"message": "hello"}, run_id="public") - assert seen == ["hello"] and result["failed_actions"] == 0 - assert result["record"] and result["run_id"] == "public" + assert seen == ["hello"] + assert result["failed_actions"] == 0 + assert result["record"] + assert result["run_id"] == "public" assert ac.read_events(path, run_id="other") == [] assert [event.status for event in ac.read_events(path, run_id="public")] == ["incomplete", "ok"] - assert actions[0][1]["value"] == "${message}" and current_journal() is None + assert actions[0][1]["value"] == "${message}" + assert current_journal() is None for name in action_journal.__all__: - assert name in ac.__all__ and getattr(ac, name) is getattr(action_journal, name) - assert ac.JournalLink is JournalLink and "JournalLink" in ac.__all__ + assert name in ac.__all__ + assert getattr(ac, name) is getattr(action_journal, name) + assert ac.JournalLink is JournalLink + assert "JournalLink" in ac.__all__ def _script(tmp_path, actions): @@ -46,7 +51,8 @@ def test_cli_capture_keeps_stdout_and_variable_execution(tmp_path, monkeypatch, assert main(["run", script, "--var", "message=hello", "--journal", str(path), "--journal-run-id", "cli"]) == 0 payload = json.loads(capsys.readouterr().out) - assert "journal_path" not in payload and seen == ["hello"] + assert "journal_path" not in payload + assert seen == ["hello"] events = action_journal.read_events(path) assert [event.status for event in events] == ["incomplete", "ok"] assert all(event.run_id == "cli" for event in events) @@ -82,8 +88,10 @@ def test_cli_foreign_destination_is_preserved_before_effects(tmp_path, monkeypat path = tmp_path / "foreign.txt" path.write_bytes(b"leave this file intact\n") assert main(["run", script, "--journal", str(path)]) == 1 - assert seen == [] and path.read_bytes() == b"leave this file intact\n" - assert "error:" in capsys.readouterr().err and current_journal() is None + assert seen == [] + assert path.read_bytes() == b"leave this file intact\n" + assert "error:" in capsys.readouterr().err + assert current_journal() is None def test_cli_run_id_requires_capture(tmp_path, capsys): @@ -123,7 +131,8 @@ def cancel(): path = tmp_path / "cancelled.jsonl" with pytest.raises(asyncio.CancelledError) as caught: main(["run", script, "--journal", str(path)]) - assert caught.value is signal and current_journal() is None + assert caught.value is signal + assert current_journal() is None assert [event.status for event in action_journal.read_events(path)] == ["incomplete", "cancelled"] @@ -139,13 +148,15 @@ def test_cli_journal_corruption_never_prints_raw_input(tmp_path, capsys): path.write_text('{"password":"fake-private-value"}\n', encoding="utf-8") assert main(["journal", str(path)]) == 1 captured = capsys.readouterr() - assert captured.out == "" and "fake-private-value" not in captured.err + assert captured.out == "" + assert "fake-private-value" not in captured.err def test_cli_journal_missing_path_is_empty_without_creation(tmp_path, capsys): path = tmp_path / "absent.jsonl" assert main(["journal", str(path)]) == 0 - assert json.loads(capsys.readouterr().out) == [] and not path.exists() + assert json.loads(capsys.readouterr().out) == [] + assert not path.exists() @pytest.mark.parametrize("command", ["run", "journal"]) @@ -158,7 +169,8 @@ def test_cli_journal_paths_use_allowed_roots(tmp_path, monkeypatch, capsys, comm args = ["journal", str(path)] if command == "journal" else [ "run", _script(tmp_path, [["AC_screen_size"]]), "--journal", str(path)] assert main(args) == 1 - assert "error:" in capsys.readouterr().err and not path.exists() + assert "error:" in capsys.readouterr().err + assert not path.exists() def test_public_and_cli_journals_work_without_qt_or_sqlite(tmp_path): diff --git a/test/unit_test/headless/test_manual_history.py b/test/unit_test/headless/test_manual_history.py index fccebec43..9f4d664ea 100644 --- a/test/unit_test/headless/test_manual_history.py +++ b/test/unit_test/headless/test_manual_history.py @@ -30,12 +30,18 @@ def test_scope_preserves_existing_variables_results_and_owner(store): assert ac.manual_history_scope is manual_module.manual_history_scope with execution_scope({"message": "before"}): with ac.manual_history_scope(store=store, source_id="editor", script_path="") as binding: - assert isinstance(binding, ac.HistoryBinding) and current_history() is binding + assert isinstance(binding, ac.HistoryBinding) + assert current_history() is binding result = executor.execute_action([["AC_set_var", {"name": "message", "value": "after"}]]) - assert executor.variables.get("message") == "after" and result + assert executor.variables.get("message") == "after" + assert result row = store.get_run(binding.run_id) - assert row.source_type == "manual" and row.source_id == "editor" and row.script_path == "" - assert row.status == "ok" and row.finished_at is not None and row.artifact_path is None + assert row.source_type == "manual" + assert row.source_id == "editor" + assert row.script_path == "" + assert row.status == "ok" + assert row.finished_at is not None + assert row.artifact_path is None assert current_history() is None @@ -47,7 +53,8 @@ def fail(): before = recorded_failures() with ac.manual_history_scope(store=store) as clean: executor.execute_action([["AC_set_var", {"name": "x", "value": 1}]]) - assert store.get_run(clean.run_id).status == "ok" and recorded_failures() == before + assert store.get_run(clean.run_id).status == "ok" + assert recorded_failures() == before with ac.manual_history_scope(store=store) as failed: result = executor.execute_action([["AC_manual_failure"]]) assert any("controlled failure" in str(value) for value in result.values()) @@ -64,7 +71,8 @@ def test_original_signal_is_retained_and_history_is_error(store, signal): with pytest.raises(type(signal)) as caught: with ac.manual_history_scope(store=store) as binding: raise signal - assert caught.value is signal and current_history() is None + assert caught.value is signal + assert current_history() is None assert store.get_run(binding.run_id).status == "error" @@ -76,7 +84,8 @@ def fail(*args, **kwargs): with pytest.raises(type(signal)) as caught: with ac.manual_history_scope(store=store) as binding: raise signal - assert caught.value is signal and store.get_run(binding.run_id).status == "running" + assert caught.value is signal + assert store.get_run(binding.run_id).status == "running" def test_failed_completion_without_original_signal_propagates(store, monkeypatch): @@ -92,8 +101,9 @@ def fail(*args, **kwargs): raise HistoryStoreError("start unavailable") monkeypatch.setattr(store, "start_run", fail) with bound_history(parent): + scope = ac.manual_history_scope(store=store) with pytest.raises(HistoryStoreError): - with ac.manual_history_scope(store=store): + with scope: pytest.fail("body executed after failed start") assert current_history() is parent @@ -137,18 +147,21 @@ def test_worker_captures_link_to_the_manual_owner(store, tmp_path, worker): actions = [["AC_run_dag", {"definition": {"nodes": [{"id": "one", "actions": body}]}}]] with ac.manual_history_scope(store=store) as owner: executor.execute_action(actions, raise_on_error=True) - assert len(store.journals_for_run(owner.run_id)) == 1 and path.exists() + assert len(store.journals_for_run(owner.run_id)) == 1 + assert path.exists() def test_metadata_and_failure_text_are_masked_before_sqlite(store): secret = "manual-private-marker" with secret_log_scope(): remember_secret(secret) + scope = ac.manual_history_scope(store=store, source_id=secret, script_path=secret) with pytest.raises(ValueError): - with ac.manual_history_scope(store=store, source_id=secret, script_path=secret) as owner: + with scope as owner: raise ValueError(secret) row = store.get_run(owner.run_id) - assert row.source_id == row.script_path == "***" and secret not in row.error_text + assert row.source_id == row.script_path == "***" + assert secret not in row.error_text def test_ambient_journal_policy_is_checked_before_linking(store, tmp_path): @@ -156,10 +169,12 @@ def test_ambient_journal_policy_is_checked_before_linking(store, tmp_path): policy.set_client_roots([tmp_path]) with ac.journal_scope(ac.ActionJournal(tmp_path / "events.jsonl"), path_policy=policy): policy.set_client_roots([tmp_path / "elsewhere"]) + scope = ac.manual_history_scope(store=store) with pytest.raises(ac.PathNotAllowedError): - with ac.manual_history_scope(store=store): + with scope: pytest.fail("body executed outside active roots") - assert store.list_runs()[0].status == "error" and store.journals_for_run(store.list_runs()[0].id) == [] + assert store.list_runs()[0].status == "error" + assert store.journals_for_run(store.list_runs()[0].id) == [] def test_viewer_is_refused_before_store_effects(store, monkeypatch): @@ -196,18 +211,21 @@ def test_existing_journal_secret_metadata_is_refused_before_link(store, tmp_path with secret_log_scope(): with ac.journal_scope(ac.ActionJournal(tmp_path / (secret + ".jsonl")), run_id="ambient"): remember_secret(secret) + scope = ac.manual_history_scope(store=store) with pytest.raises(HistoryStoreError, match="resolved secret"): - with ac.manual_history_scope(store=store): + with scope: pytest.fail("unsafe provenance reached body") row = store.list_runs()[0] - assert row.status == "error" and store.journals_for_run(row.id) == [] + assert row.status == "error" + assert store.journals_for_run(row.id) == [] assert secret not in row.error_text @pytest.mark.parametrize("field,value", [("source_id", ""), ("source_id", None), ("script_path", "")]) def test_invalid_labels_have_no_store_effects(store, field, value): + scope = ac.manual_history_scope(store=store, **{field: value}) with pytest.raises(HistoryStoreError): - with ac.manual_history_scope(store=store, **{field: value}): + with scope: pytest.fail("invalid labels executed") assert store._conn is None @@ -220,7 +238,8 @@ def __repr__(self): with pytest.raises(BrokenError) as caught: with ac.manual_history_scope(store=store) as owner: raise original - assert caught.value is original and store.get_run(owner.run_id).status == "error" + assert caught.value is original + assert store.get_run(owner.run_id).status == "error" def test_public_binding_annotations_resolve_at_runtime(): diff --git a/test/unit_test/headless/test_manual_history_entrypoints.py b/test/unit_test/headless/test_manual_history_entrypoints.py index b8900058a..c338ef83a 100644 --- a/test/unit_test/headless/test_manual_history_entrypoints.py +++ b/test/unit_test/headless/test_manual_history_entrypoints.py @@ -29,10 +29,14 @@ def test_cli_history_and_journal_preserve_stdout_and_link(tmp_path, store, capsy journal = tmp_path / "events.jsonl" assert main(["run", script, "--history", "--var", "seed=hello", "--journal", str(journal)]) == 0 payload = json.loads(capsys.readouterr().out) - assert "history_id" not in payload and "journal_path" not in payload + assert "history_id" not in payload + assert "journal_path" not in payload rows = store.list_runs() - assert len(rows) == 1 and rows[0].source_type == "manual" and rows[0].script_path == script - assert rows[0].status == "ok" and len(store.journals_for_run(rows[0].id)) == 1 + assert len(rows) == 1 + assert rows[0].source_type == "manual" + assert rows[0].script_path == script + assert rows[0].status == "ok" + assert len(store.journals_for_run(rows[0].id)) == 1 def test_cli_custom_history_database_is_explicit(tmp_path, store, capsys): @@ -42,7 +46,8 @@ def test_cli_custom_history_database_is_explicit(tmp_path, store, capsys): capsys.readouterr() check = ac.HistoryStore(custom) try: - assert check.list_runs()[0].status == "ok" and store.list_runs() == [] + assert check.list_runs()[0].status == "ok" + assert store.list_runs() == [] finally: check.close() @@ -64,7 +69,8 @@ def fail(): script = _script(tmp_path, [["AC_manual_cli_fail"]]) assert main(["run", script, "--history"]) == 1 output = capsys.readouterr() - assert json.loads(output.out) and "1 action(s) failed" in output.err + assert json.loads(output.out) + assert "1 action(s) failed" in output.err assert store.list_runs()[0].status == "error" @@ -74,7 +80,8 @@ def fail(): monkeypatch.setitem(executor.event_dict, "AC_manual_cli_preview", fail) script = _script(tmp_path, [["AC_manual_cli_preview"]]) assert main(["run", script, "--history", "--dry-run"]) == 0 - assert "dry-run" in capsys.readouterr().out and store.list_runs()[0].status == "ok" + assert "dry-run" in capsys.readouterr().out + assert store.list_runs()[0].status == "ok" assert store.list_runs()[0].source_id == "cli.preview" @@ -83,7 +90,9 @@ def test_cli_history_database_requires_opt_in(tmp_path, store, capsys, monkeypat monkeypatch.setitem(executor.event_dict, "AC_manual_cli_probe", lambda: effects.append(1)) script = _script(tmp_path, [["AC_manual_cli_probe"]]) assert main(["run", script, "--history-db", str(tmp_path / "unused.sqlite")]) == 1 - assert "requires --history" in capsys.readouterr().err and effects == [] and store._conn is None + assert "requires --history" in capsys.readouterr().err + assert effects == [] + assert store._conn is None def test_named_history_body_defers_values_and_keeps_private_executor(store): @@ -93,8 +102,10 @@ def test_named_history_body_defers_values_and_keeps_private_executor(store): result = private.execute_action([["AC_history_capture", {"body": body, "source_id": "nested"}]], raise_on_error=True) summary = next(iter(result.values())) - assert private.variables.get("message") == "after" and "after" in summary["record"].values() - assert store.get_run(summary["history_id"]).status == "ok" and summary["failed_actions"] == 0 + assert private.variables.get("message") == "after" + assert "after" in summary["record"].values() + assert store.get_run(summary["history_id"]).status == "ok" + assert summary["failed_actions"] == 0 def test_history_command_validation_is_before_any_prior_action(store, monkeypatch): @@ -103,7 +114,8 @@ def test_history_command_validation_is_before_any_prior_action(store, monkeypatc with pytest.raises(ac.AutoControlActionException, match="unknown command"): executor.execute_action([["AC_manual_command_probe"], ["AC_history_capture", {"body": [["AC_unknown_manual_body"]]}]], raise_on_error=True) - assert effects == [] and store._conn is None + assert effects == [] + assert store._conn is None def test_positional_capture_is_rejected_without_body_effects(store): @@ -134,7 +146,8 @@ def test_command_body_data_is_interpolated_once(store, monkeypatch): with execution_scope({"message": "${secrets.PRIVATE}"}): executor.execute_action([["AC_history_capture", {"body": [ ["AC_manual_data_probe", {"value": "${message}"}]]}]], raise_on_error=True) - assert seen == ["${secrets.PRIVATE}"] and store.list_runs()[0].status == "ok" + assert seen == ["${secrets.PRIVATE}"] + assert store.list_runs()[0].status == "ok" def test_mcp_generic_execute_rejects_admin_body_before_manual_history(store): @@ -146,7 +159,8 @@ def test_mcp_generic_execute_rejects_admin_body_before_manual_history(store): "name": tool.name, "arguments": {"actions": [["AC_history_capture", {"body": [["AC_admin_list_hosts"]]}]]}}} with authorization_scope(AuthorizationContext("operator", Role.OPERATOR)): reply = json.loads(server.handle_line(json.dumps(request))) - assert "error" in reply and store._conn is None + assert "error" in reply + assert store._conn is None @pytest.mark.parametrize("signal,rows", [("AC_break", 1), ("AC_continue", 3)]) @@ -155,7 +169,8 @@ def test_normal_loop_signals_do_not_mark_manual_history_failed(store, signal, ro ["AC_history_capture", {"body": [[signal]]}]]}]] executor.execute_action(actions, raise_on_error=True) history = store.list_runs() - assert len(history) == rows and all(row.status == "ok" for row in history) + assert len(history) == rows + assert all(row.status == "ok" for row in history) def test_loop_signal_keeps_preceding_recorded_failure(store, monkeypatch): @@ -164,7 +179,8 @@ def fail(): monkeypatch.setitem(executor.event_dict, "AC_manual_command_fail", fail) executor.execute_action([["AC_loop", {"times": 2, "body": [ ["AC_history_capture", {"body": [["AC_manual_command_fail"], ["AC_break"]]}]]}]]) - assert len(store.list_runs()) == 1 and store.list_runs()[0].status == "error" + assert len(store.list_runs()) == 1 + assert store.list_runs()[0].status == "error" def test_mcp_generic_execute_records_an_explicit_manual_owner(store): @@ -180,4 +196,5 @@ def test_mcp_generic_execute_records_an_explicit_manual_owner(store): assert reply["result"]["isError"] is False data = json.loads(reply["result"]["content"][0]["text"]) assert "history_id" in json.dumps(data) - assert store.list_runs()[0].status == "ok" and store.list_runs()[0].source_type == "manual" + assert store.list_runs()[0].status == "ok" + assert store.list_runs()[0].source_type == "manual" diff --git a/test/unit_test/headless/test_manual_history_gui.py b/test/unit_test/headless/test_manual_history_gui.py index 0fda5cf09..9795fa2e8 100644 --- a/test/unit_test/headless/test_manual_history_gui.py +++ b/test/unit_test/headless/test_manual_history_gui.py @@ -40,7 +40,8 @@ def store(tmp_path, monkeypatch): def test_controls_default_to_no_recording_or_database(app, store): controls = ManualRunControls() try: - assert not controls.record_history.isChecked() and controls.journal_path.text() == "" + assert not controls.record_history.isChecked() + assert controls.journal_path.text() == "" with controls.capture(source_id="test", script_path=""): pass assert store._conn is None @@ -59,11 +60,13 @@ def test_editor_capture_preserves_result_and_links_opted_in_owner(app, store, tm host = _host(output, script_editor=editor, script_history_controls=controls) try: ScriptTabMixin._execute_manual_script(host) - assert json.loads(output.toPlainText()) and len(ac.read_events(journal)) == 2 + assert json.loads(output.toPlainText()) + assert len(ac.read_events(journal)) == 2 rows = store.list_runs() assert len(rows) == int(enabled) if enabled: - assert rows[0].status == "ok" and len(store.journals_for_run(rows[0].id)) == 1 + assert rows[0].status == "ok" + assert len(store.journals_for_run(rows[0].id)) == 1 finally: controls.deleteLater() editor.deleteLater() @@ -96,9 +99,11 @@ def test_builder_records_with_optional_journal_without_result_wrapper(app, store builder._on_run() assert settle(builder._runs, "task") result = json.loads(builder._result.toPlainText()) - assert result and "history_id" not in result + assert result + assert "history_id" not in result row = store.list_runs()[0] - assert row.status == "ok" and len(store.journals_for_run(row.id)) == 1 + assert row.status == "ok" + assert len(store.journals_for_run(row.id)) == 1 finally: builder.deleteLater() @@ -113,7 +118,8 @@ def test_capture_command_form_keeps_body_when_metadata_changes(app): form.load_step(step) form._editors["source_id"].setText("after") result = step_to_action(step) - assert result[1]["source_id"] == "after" and result[1]["body"] == body + assert result[1]["source_id"] == "after" + assert result[1]["body"] == body finally: form.deleteLater() @@ -133,7 +139,8 @@ def test_directory_execution_records_one_batch_and_all_snapshots(app, store, tmp try: ScriptTabMixin._execute_dir(host) assert len(json.loads(output.toPlainText())) == 2 - assert len(store.list_runs()) == 1 and len(ac.read_events(journal)) == 4 + assert len(store.list_runs()) == 1 + assert len(ac.read_events(journal)) == 4 assert store.list_runs()[0].script_path == str(directory) finally: for widget in (controls, source, output): @@ -150,7 +157,8 @@ def test_manual_controls_retranslate_without_resetting_options(app): language_wrapper.reset_language("Japanese") controls.retranslate() assert controls.record_history.text() == "実行履歴に記録" - assert controls.record_history.isChecked() and controls.journal_path.text() == "events.jsonl" + assert controls.record_history.isChecked() + assert controls.journal_path.text() == "events.jsonl" finally: language_wrapper.reset_language(old) controls.deleteLater() diff --git a/test/unit_test/headless/test_mcp_cli.py b/test/unit_test/headless/test_mcp_cli.py index c45725378..ccfcb4946 100644 --- a/test/unit_test/headless/test_mcp_cli.py +++ b/test/unit_test/headless/test_mcp_cli.py @@ -32,10 +32,12 @@ def test_list_tools_with_read_only_drops_destructive(monkeypatch): def test_search_and_single_schema_flags_do_not_start_the_server(monkeypatch): results = json.loads(_capture(monkeypatch, ["--search-tools", "ac_mcp_tool_schema", "--limit", "1"])) - assert len(results) == 1 and results[0]["name"] == "ac_mcp_tool_schema" + assert len(results) == 1 + assert results[0]["name"] == "ac_mcp_tool_schema" assert "inputSchema" not in results[0] schema = json.loads(_capture(monkeypatch, ["--tool-schema", "ac_mcp_tool_schema"])) - assert schema["name"] == "ac_mcp_tool_schema" and "inputSchema" in schema + assert schema["name"] == "ac_mcp_tool_schema" + assert "inputSchema" in schema def test_list_resources_emits_json(monkeypatch): diff --git a/test/unit_test/headless/test_mcp_disclosure_policy.py b/test/unit_test/headless/test_mcp_disclosure_policy.py index edbfbf06e..fb8c80edf 100644 --- a/test/unit_test/headless/test_mcp_disclosure_policy.py +++ b/test/unit_test/headless/test_mcp_disclosure_policy.py @@ -64,7 +64,8 @@ def test_plugin_list_changes_reach_all_open_http_session_streams_once(): try: first, second = host.sessions.create(), host.sessions.create() left, right = [], [] - assert first.attach_stream(left.append) and second.attach_stream(right.append) + assert first.attach_stream(left.append) + assert second.attach_stream(right.append) bridge.register_tool(MCPTool("fixture", "Read", {}, lambda: None, MCPToolAnnotations(read_only=True))) assert len(left) == len(right) == 1 assert json.loads(left[0])["method"] == "notifications/tools/list_changed" @@ -84,7 +85,8 @@ def test_closed_async_acceptance_does_not_recreate_a_session_view(): with server.connection_scope(connection_id="second"): callback() assert server._connection_id == "second" - assert "error" in json.loads(replies[0]) and calls == [] + assert "error" in json.loads(replies[0]) + assert calls == [] assert "first" not in server._disclosure_views server.stop() @@ -109,13 +111,15 @@ def test_targeted_http_notifications_are_not_broadcast_or_duplicated(): try: first, second = host.sessions.create(), host.sessions.create() left, right = [], [] - assert first.attach_stream(left.append) and second.attach_stream(right.append) + assert first.attach_stream(left.append) + assert second.attach_stream(right.append) with bridge.connection_scope(connection_id=first.id, notifier=lambda method, params: left.append( json.dumps({"jsonrpc": "2.0", "method": method, "params": params}))): reply = bridge.handle_line(json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "ac_mcp_tools_enable", "arguments": {"names": ["fixture"]}}})) assert json.loads(reply)["result"]["isError"] is False - assert len(left) == 1 and right == [] + assert len(left) == 1 + assert right == [] finally: host.stop() assert bridge._tool_change_sinks == {} @@ -144,7 +148,8 @@ def test_core_discovery_shortcut_still_checks_the_actual_readonly_annotation(): server.configure_tool_disclosure("progressive", read_only=True) reply = server.handle_line(json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "ac_mcp_tool_search"}})) - assert "error" in json.loads(reply) and calls == [] + assert "error" in json.loads(reply) + assert calls == [] server.stop() @@ -219,7 +224,8 @@ def test_discovered_call_uses_existing_policy(tmp_path, monkeypatch): assert _call(server, "fixture", {"path": str(tmp_path / "denied.txt")})["result"]["isError"] is True assert _call(server, "fixture", {"reference": "env://FIXTURE_DENIED"})["result"]["isError"] is True assert calls == [{"path": str((root / "ok.txt").resolve())}] - assert confirmations == ["ac_mcp_tools_enable", "fixture"] and len(rate_checks) == 2 + assert confirmations == ["ac_mcp_tools_enable", "fixture"] + assert len(rate_checks) == 2 server.stop() @@ -244,7 +250,8 @@ def test_removed_tool_cannot_run(): server.configure_tool_disclosure("progressive") assert _call(server, "ac_mcp_tools_enable", {"names": ["fixture"]})["result"]["isError"] is False server.unregister_tool("fixture") - assert "error" in _call(server, "fixture") and calls == [] + assert "error" in _call(server, "fixture") + assert calls == [] server.stop() diff --git a/test/unit_test/headless/test_mcp_discovery_gui.py b/test/unit_test/headless/test_mcp_discovery_gui.py index dd647bb5c..d616349b5 100644 --- a/test/unit_test/headless/test_mcp_discovery_gui.py +++ b/test/unit_test/headless/test_mcp_discovery_gui.py @@ -21,7 +21,8 @@ def test_metadata_controls_only_scan_through_actions(monkeypatch): tab = DiagnosticsTab() try: view = tab._mcp_discovery - assert calls == [] and view.findChildren(QPushButton) == [] + assert calls == [] + assert view.findChildren(QPushButton) == [] view.query.setText("fixture") view.read_only.setChecked(True) actions = dict(tab.menu_actions()) @@ -35,7 +36,8 @@ def test_metadata_controls_only_scan_through_actions(monkeypatch): view.profile.setText("ac_screen_size") actions["mcp_disclosure_plan"]() plan = json.loads(view.output.toPlainText()) - assert plan["mode"] == "static" and plan["read_only"] is True + assert plan["mode"] == "static" + assert plan["read_only"] is True assert plan["names"] == ["ac_screen_size"] finally: tab.close() diff --git a/test/unit_test/headless/test_mcp_discovery_index.py b/test/unit_test/headless/test_mcp_discovery_index.py index fc61b4c6a..71899d52f 100644 --- a/test/unit_test/headless/test_mcp_discovery_index.py +++ b/test/unit_test/headless/test_mcp_discovery_index.py @@ -42,7 +42,8 @@ def test_schema_lookup_is_single_tool_and_detached(): index = ToolIndex(tools) tools[0].input_schema["changed"] = True descriptor = index.get_schema("ac_read") - assert descriptor["name"] == "ac_read" and "changed" not in descriptor["inputSchema"] + assert descriptor["name"] == "ac_read" + assert "changed" not in descriptor["inputSchema"] descriptor["inputSchema"]["corrupt"] = True assert "corrupt" not in index.get_schema("ac_read")["inputSchema"] assert index.snapshot_id == ToolIndex(catalog()).snapshot_id @@ -52,8 +53,9 @@ def test_schema_lookup_is_single_tool_and_detached(): @pytest.mark.parametrize("query,limit", [(None, 1), ("x" * 257, 1), ("", 0), ("", 51), ("", True)]) def test_search_rejects_unbounded_inputs(query, limit): from je_auto_control.utils.mcp_server.discovery import ToolIndex + index = ToolIndex(catalog()) with pytest.raises(ToolDiscoveryError, match="query|limit"): - ToolIndex(catalog()).search(query, limit=limit) + index.search(query, limit=limit) def test_mcp_discovery_uses_the_server_registry_including_plugins(): diff --git a/test/unit_test/headless/test_mcp_journal.py b/test/unit_test/headless/test_mcp_journal.py index 718c6daea..7362f185f 100644 --- a/test/unit_test/headless/test_mcp_journal.py +++ b/test/unit_test/headless/test_mcp_journal.py @@ -34,10 +34,13 @@ def _data(reply): def test_tools_expose_capture_and_sensitive_read_hints(): capture, reader = _tool("ac_journal_capture"), _tool("ac_journal_read") - assert capture.annotations.destructive and not capture.annotations.read_only - assert reader.annotations.read_only and reader.annotations.idempotent + assert capture.annotations.destructive + assert not capture.annotations.read_only + assert reader.annotations.read_only + assert reader.annotations.idempotent readonly = {tool.name for tool in build_default_tool_registry(read_only=True, aliases=False)} - assert reader.name in readonly and capture.name not in readonly + assert reader.name in readonly + assert capture.name not in readonly for tool in (capture, reader): assert tool.input_schema["properties"]["path"]["format"] == "path" @@ -50,13 +53,16 @@ def test_rpc_capture_read_and_metadata_round_trip(tmp_path): "actions": [["AC_get_var", {"name": "message"}]]}) assert captured["result"]["isError"] is False result = _data(captured) - assert result["run_id"] == "rpc" and result["failed_actions"] == 0 + assert result["run_id"] == "rpc" + assert result["failed_actions"] == 0 assert "hello" in result["record"].values() reply = _rpc(server, "ac_journal_read", {"path": str(path), "run_id": "rpc", "limit": 1}) assert reply["result"]["isError"] is False events = _data(reply)["events"] - assert len(events) == 1 and events[0]["status"] == "incomplete" - assert events[0]["device_id"] == "device" and events[0]["session_id"] == "session" + assert len(events) == 1 + assert events[0]["status"] == "incomplete" + assert events[0]["device_id"] == "device" + assert events[0]["session_id"] == "session" def test_recorded_failures_are_tool_errors_with_structured_run_metadata(tmp_path, monkeypatch): @@ -68,8 +74,10 @@ def fail(): reply = _rpc(server, "ac_journal_capture", {"path": str(path), "actions": [["AC_mcp_journal_failure"]]}) assert reply["result"]["isError"] is True result = reply["result"]["structuredContent"] - assert result["failed_actions"] == 1 and result["journal_path"] == str(path.resolve()) - assert result["run_id"] and _data(reply) == result + assert result["failed_actions"] == 1 + assert result["journal_path"] == str(path.resolve()) + assert result["run_id"] + assert _data(reply) == result assert [event.status for event in ac.read_events(path)] == ["incomplete", "error"] @@ -82,7 +90,8 @@ def test_viewers_cannot_list_or_call_private_journal_operations(tmp_path, name): with authorization_scope(AuthorizationContext("viewer", Role.VIEWER)): listed = json.loads(server.handle_line('{"jsonrpc":"2.0","id":2,"method":"tools/list"}')) reply = _rpc(server, name, args) - assert listed["result"]["tools"] == [] and "error" in reply + assert listed["result"]["tools"] == [] + assert "error" in reply assert not (tmp_path / "refused.jsonl").exists() @@ -108,7 +117,8 @@ def test_server_passes_its_policy_to_dynamic_nested_captures(tmp_path): reply = _rpc(server, "ac_journal_capture", {"path": str(allowed / "outer.jsonl"), "variables": {"destination": str(outside)}, "actions": [["AC_journal_capture", { "path": "${destination}", "body": [["AC_set_var", {"name": "x", "value": 1}]]}]]}) - assert reply["result"]["isError"] is True and not outside.exists() + assert reply["result"]["isError"] is True + assert not outside.exists() assert _data(reply)["failed_actions"] == 1 @@ -121,9 +131,11 @@ def notify(_method, params): ctx.cancelled_event.set() ctx.notifier = notify path = tmp_path / "cancelled.jsonl" + capture = _tool("ac_journal_capture") + arguments = {"path": str(path), "actions": [["AC_loop", {"times": 3, + "body": [["AC_set_var", {"name": "x", "value": 1}]]}]]} with pytest.raises(OperationCancelledError): - _tool("ac_journal_capture").invoke({"path": str(path), "actions": [["AC_loop", {"times": 3, - "body": [["AC_set_var", {"name": "x", "value": 1}]]}]]}, ctx=ctx) + capture.invoke(arguments, ctx=ctx) assert updates == [0, 1] assert [event.status for event in ac.read_events(path)] == ["incomplete", "incomplete", "ok", "cancelled"] @@ -132,8 +144,10 @@ def test_cancellation_before_capture_has_no_file_effect(tmp_path): path = tmp_path / "unused.jsonl" ctx = ToolCallContext(18) ctx.cancelled_event.set() + capture = _tool("ac_journal_capture") + arguments = {"path": str(path), "actions": [["AC_screen_size"]]} with pytest.raises(OperationCancelledError): - _tool("ac_journal_capture").invoke({"path": str(path), "actions": [["AC_screen_size"]]}, ctx=ctx) + capture.invoke(arguments, ctx=ctx) assert not path.exists() @@ -150,8 +164,10 @@ def cancel(): ctx.cancelled_event.set() check() monkeypatch.setattr(ctx, "check_cancelled", cancel) + reader = _tool("ac_journal_read") + arguments = {"path": str(path)} with pytest.raises(OperationCancelledError): - _tool("ac_journal_read").invoke({"path": str(path)}, ctx=ctx) + reader.invoke(arguments, ctx=ctx) assert path.read_bytes() == before @@ -163,7 +179,8 @@ def test_invalid_fields_are_rejected_before_effects(tmp_path, extra): if name.endswith("capture"): args["actions"] = [["AC_set_var", {"name": "x", "value": 1}]] reply = _rpc(MCPServer(tools=[_tool(name)]), name, args) - assert reply["result"]["isError"] is True and not path.exists() + assert reply["result"]["isError"] is True + assert not path.exists() def test_explicit_object_results_signal_errors_without_changing_json_data(): @@ -171,7 +188,8 @@ def test_explicit_object_results_signal_errors_without_changing_json_data(): assert json.loads(json.dumps(result)) == {"value": 3} tool = MCPTool(name="controlled", description="controlled result", input_schema=schema({}), handler=lambda: result) reply = _rpc(MCPServer(tools=[tool]), "controlled", {}) - assert reply["result"]["isError"] is True and _data(reply) == {"value": 3} + assert reply["result"]["isError"] is True + assert _data(reply) == {"value": 3} plain = MCPTool(name="ordinary", description="ordinary data", input_schema=schema({}), handler=lambda: {"is_error": True}) assert _rpc(MCPServer(tools=[plain]), "ordinary", {})["result"]["isError"] is False @@ -184,7 +202,9 @@ def test_operator_cannot_hide_administration_inside_capture(tmp_path, monkeypatc server = MCPServer(tools=[_tool("ac_journal_capture")]) with authorization_scope(AuthorizationContext("operator", Role.OPERATOR)): reply = _rpc(server, "ac_journal_capture", {"path": str(path), "actions": [["AC_secret_init", {}]]}) - assert "error" in reply and not effects and not path.exists() + assert "error" in reply + assert not effects + assert not path.exists() def test_stdio_capture_uses_the_same_typed_journal_result(tmp_path): @@ -196,5 +216,6 @@ def test_stdio_capture_uses_the_same_typed_journal_result(tmp_path): server = MCPServer(tools=[_tool("ac_journal_capture")]) server.serve_stdio(stdin=io.StringIO(json.dumps(request) + "\n"), stdout=output) reply = next(json.loads(line) for line in output.getvalue().splitlines() if json.loads(line).get("id") == 44) - assert reply["result"]["isError"] is False and reply["result"]["structuredContent"]["run_id"] == "stdio" + assert reply["result"]["isError"] is False + assert reply["result"]["structuredContent"]["run_id"] == "stdio" assert [event.status for event in ac.read_events(path)] == ["incomplete", "ok"] diff --git a/test/unit_test/headless/test_mcp_progressive_sessions.py b/test/unit_test/headless/test_mcp_progressive_sessions.py index bc43a109f..77795ca30 100644 --- a/test/unit_test/headless/test_mcp_progressive_sessions.py +++ b/test/unit_test/headless/test_mcp_progressive_sessions.py @@ -26,7 +26,8 @@ def test_enable_is_session_local_and_emits_one_change(): first.enable(["fixture_0"]) first.enable(["fixture_0"]) assert first.state()["enabled"] == ["fixture_0"] - assert second.state()["enabled"] == [] and events == ["changed"] + assert second.state()["enabled"] == [] + assert events == ["changed"] assert first.state()["snapshot_id"] != second.state()["snapshot_id"] @@ -128,7 +129,8 @@ def test_stateless_requests_use_a_fixed_profile_and_create_no_session_views(): page = request(server, "tools/list", {"_meta": meta})["result"] assert "fixture_0" in {item["name"] for item in page["tools"]} assert "fixture_1" not in {item["name"] for item in page["tools"]} - assert "nextCursor" not in page and server._disclosure_views == {} + assert "nextCursor" not in page + assert server._disclosure_views == {} result = request(server, "tools/call", {"_meta": meta, "name": "ac_mcp_tools_enable", "arguments": {"names": ["fixture_1"]}}) assert result["result"]["isError"] is True @@ -160,7 +162,9 @@ def rpc(method, params=None, session=None): try: first, _ = rpc("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}}) second, _ = rpc("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}}) - assert first and second and first != second + assert first + assert second + assert first != second _, enabled = rpc("tools/call", {"name": "ac_mcp_tools_enable", "arguments": {"names": ["fixture_0"]}}, first) assert enabled["result"]["isError"] is False @@ -168,7 +172,8 @@ def rpc(method, params=None, session=None): _, second_page = rpc("tools/list", session=second) assert "fixture_0" in {item["name"] for item in first_page["result"]["tools"]} assert "fixture_0" not in {item["name"] for item in second_page["result"]["tools"]} - assert first in bridge._disclosure_views and second in bridge._disclosure_views + assert first in bridge._disclosure_views + assert second in bridge._disclosure_views finally: server.stop() assert bridge._disclosure_views == {} @@ -182,5 +187,6 @@ def test_stdio_eof_retires_the_view_and_its_existing_callback(): callback = server._build_call_context(1, {}).tool_catalog server.serve_stdio(io.StringIO(""), io.StringIO()) assert server._disclosure_views == {} + view = callback() with pytest.raises(ToolDiscoveryError, match="closed"): - callback().state() + view.state() diff --git a/test/unit_test/headless/test_mobile_self_heal_frame.py b/test/unit_test/headless/test_mobile_self_heal_frame.py index 905cff89d..a4e5b38c4 100644 --- a/test/unit_test/headless/test_mobile_self_heal_frame.py +++ b/test/unit_test/headless/test_mobile_self_heal_frame.py @@ -82,5 +82,6 @@ def test_a_miss_on_both_reports_each_reason_and_taps_nothing(): def test_neither_a_template_nor_a_description_is_refused(): + session = _Session(_Frame()) with pytest.raises(ValueError): - mobile_commands._self_heal(_Session(_Frame())) + mobile_commands._self_heal(session) diff --git a/test/unit_test/headless/test_rbac_deferred_callbacks.py b/test/unit_test/headless/test_rbac_deferred_callbacks.py index d23d805ae..9f7a925ba 100644 --- a/test/unit_test/headless/test_rbac_deferred_callbacks.py +++ b/test/unit_test/headless/test_rbac_deferred_callbacks.py @@ -94,7 +94,8 @@ def test_callback_runs_have_fresh_variables_and_clear_unscoped_authority(kind, c fire() assert len(seen) == 1 assert seen[0][0] is None - assert seen[0][1] is not None and seen[0][1] is not outer + assert seen[0][1] is not None + assert seen[0][1] is not outer @pytest.mark.parametrize("kind", KINDS) diff --git a/test/unit_test/headless/test_rbac_sensitive_read_tools.py b/test/unit_test/headless/test_rbac_sensitive_read_tools.py index 2d1f745d0..d111e569d 100644 --- a/test/unit_test/headless/test_rbac_sensitive_read_tools.py +++ b/test/unit_test/headless/test_rbac_sensitive_read_tools.py @@ -66,7 +66,8 @@ def test_sensitive_read_call_is_refused_before_handler_invocation(name): assert reply["error"]["code"] == FORBIDDEN_CODE assert called == [] else: - assert names == [name] and "result" in reply + assert names == [name] + assert "result" in reply assert called == [True, True] @@ -109,7 +110,8 @@ def test_http_user_tokens_cannot_bypass_sensitive_tool_policy(tmp_path): assert response.status == 200 reply = json.loads(response.read()) if role == Role.VIEWER: - assert reply["error"]["code"] == FORBIDDEN_CODE and called == [] + assert reply["error"]["code"] == FORBIDDEN_CODE + assert called == [] else: assert "result" in reply finally: diff --git a/test/unit_test/headless/test_rbac_server_wiring.py b/test/unit_test/headless/test_rbac_server_wiring.py index 68f705422..03cd8907e 100644 --- a/test/unit_test/headless/test_rbac_server_wiring.py +++ b/test/unit_test/headless/test_rbac_server_wiring.py @@ -441,4 +441,5 @@ def test_default_registry_offers_a_viewer_only_read_only_tools(users): and capability_for_tool(tool.name, True) == Capability.READ_SCREEN} assert {"ac_get_mouse_position", "ac_screen_size"} <= names assert not {"ac_get_clipboard", "ac_sql_query", "ac_load_dotenv", "ac_jwt_encode"} & names - assert "ac_execute_actions" not in names and "ac_click_mouse" not in names + assert "ac_execute_actions" not in names + assert "ac_click_mouse" not in names diff --git a/test/unit_test/headless/test_remaining_execution_scopes.py b/test/unit_test/headless/test_remaining_execution_scopes.py index 76006b0e3..ac548abe3 100644 --- a/test/unit_test/headless/test_remaining_execution_scopes.py +++ b/test/unit_test/headless/test_remaining_execution_scopes.py @@ -97,8 +97,9 @@ def test_reusing_a_state_machine_starts_a_new_scope_each_time(seen): def test_fsm_error_restores_the_previous_scope(seen): executor.variables.set("owner", "process") + machine = _machine([SET, READ], finish=False) with pytest.raises(StateMachineError, match="no transition"): - _machine([SET, READ], finish=False).run() + machine.run() assert seen == ["child"] assert executor.variables.as_dict() == {"owner": "process"} assert current_scope() is None diff --git a/test/unit_test/headless/test_remote_clipboard_delivery_gui.py b/test/unit_test/headless/test_remote_clipboard_delivery_gui.py index 61c336832..b552f7362 100644 --- a/test/unit_test/headless/test_remote_clipboard_delivery_gui.py +++ b/test/unit_test/headless/test_remote_clipboard_delivery_gui.py @@ -122,7 +122,8 @@ def set(self, content): try: enqueue(peer, kind, value) qapp.processEvents() - assert applied == [(kind, value)] and sent == [] + assert applied == [(kind, value)] + assert sent == [] native.text_value = 'deliberate repeat' native.dataChanged.emit() assert sent == [] @@ -171,7 +172,8 @@ def interrupted(_value): delivery = ClipboardDelivery(panel._own_viewer(), 'text', 'controlled', DeferredAuthorization.capture()) with pytest.raises(type(original)) as caught: panel._on_clipboard_main(delivery) - assert caught.value is original and applied == [] + assert caught.value is original + assert applied == [] finally: panel._disconnect() panel.deleteLater() @@ -244,7 +246,8 @@ def registered(panel, *args, **kwargs): QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete) qapp.processEvents() gc.collect() - assert created and created[0]() is None, 'the parametrized exception retained a deleted native Qt panel' + assert created, 'the parametrized exception retained a deleted native Qt panel' + assert created[0]() is None, 'the parametrized exception retained a deleted native Qt panel' def test_disconnected_clipboard_callback_does_not_retain_its_own_viewer_until_gc(qapp): diff --git a/test/unit_test/headless/test_remote_clipboard_lifecycle.py b/test/unit_test/headless/test_remote_clipboard_lifecycle.py index cf11608e1..1322f9bc0 100644 --- a/test/unit_test/headless/test_remote_clipboard_lifecycle.py +++ b/test/unit_test/headless/test_remote_clipboard_lifecycle.py @@ -50,9 +50,11 @@ def test_buffered_old_viewer_clipboard_is_rejected_after_disconnect_or_reconnect finally: channel.release.set() worker.join(5) - assert not worker.is_alive() and channel.closed + assert not worker.is_alive() + assert channel.closed assert applied == [], 'old buffered clipboard reached the replacement/current callback' - assert results == [False] and viewer.connected is replacement + assert results == [False] + assert viewer.connected is replacement def host_handler(payload, applied): @@ -106,7 +108,8 @@ def test_clipboard_receiver_does_not_own_its_peer_without_a_queued_delivery(): receiver.bind(peer) reference = weakref.ref(peer) del peer - assert reference() is None and pending == [] + assert reference() is None + assert pending == [] receiver('text', 'retired') assert pending == [] @@ -123,9 +126,11 @@ def test_queued_clipboard_receiver_keeps_the_original_peer_and_authority_until_d receiver.bind(peer) reference = weakref.ref(peer) receiver('text', 'original') - assert pending[0].peer is peer and pending[0].authority is authority + assert pending[0].peer is peer + assert pending[0].authority is authority del peer - assert reference() is not None and pending[0].data == 'original' + assert reference() is not None + assert pending[0].data == 'original' pending.clear() assert reference() is None @@ -142,8 +147,9 @@ def test_clipboard_receiver_cannot_retarget_a_dead_original_peer_or_accept_unown peer = type('Peer', (), {})() receiver.bind(peer) del peer + replacement = type('Replacement', (), {})() with pytest.raises(ClipboardSyncError, match='already bound'): - receiver.bind(type('Replacement', (), {})()) + receiver.bind(replacement) receiver = ClipboardReceiver(DeferredAuthorization.capture(), pending.append) with pytest.raises(ClipboardSyncError, match='weak references'): receiver.bind(object()) @@ -165,8 +171,9 @@ def interrupted(*_args): else: receiver = host_handler(encode_text('text'), []) receiver._host._apply_clipboard = interrupted + payload = encode_text('text') with pytest.raises(type(original)) as caught: - receiver._handle_clipboard_payload(encode_text('text')) + receiver._handle_clipboard_payload(payload) assert caught.value is original @@ -184,9 +191,11 @@ def replaced(data): return encode(data) monkeypatch.setattr(viewer_module, 'encode_' + kind, replaced) + send = getattr(viewer, 'send_clipboard_' + kind) with pytest.raises(ConnectionError): - getattr(viewer, 'send_clipboard_' + kind)(value) - assert old == [] and new == [], 'a clipboard send adopted the replacement transport' + send(value) + assert old == [], 'a clipboard send adopted the replacement transport' + assert new == [], 'a clipboard send adopted the replacement transport' @pytest.mark.parametrize('kind,value', [('text', 'text'), ('image', b'image')]) @@ -263,7 +272,9 @@ def broadcast(kind, payload): for _ in range(2): assert getattr(host, 'broadcast_clipboard_' + kind)(value) == 2 assert first[1] == second[1] == [(kind, value)] * 2 - assert applied == [(kind, value)] * 2 and len(first[2]) == 2 and second[2] == [] + assert applied == [(kind, value)] * 2 + assert len(first[2]) == 2 + assert second[2] == [] assert len(first[3]) == len(second[3]) == 2 @@ -279,4 +290,5 @@ def failed(*_args): receiver = host_handler(encode_text('text'), []) receiver._host._apply_clipboard = failed receiver._handle_clipboard_payload(encode_text('text')) - assert 'clipboard' in caplog.text and str(error) in caplog.text + assert 'clipboard' in caplog.text + assert str(error) in caplog.text diff --git a/test/unit_test/headless/test_remote_desktop_optional_codec_ui.py b/test/unit_test/headless/test_remote_desktop_optional_codec_ui.py index 3e82cc359..ecf16cf0c 100644 --- a/test/unit_test/headless/test_remote_desktop_optional_codec_ui.py +++ b/test/unit_test/headless/test_remote_desktop_optional_codec_ui.py @@ -19,7 +19,7 @@ def qapp(): """Keep Qt entirely offscreen for these widget checks.""" app = QApplication.instance() or QApplication([]) - yield app + return app @pytest.mark.parametrize("missing", ["available_hardware_codecs", "active_hardware_codec", "both"]) diff --git a/test/unit_test/headless/test_remote_host_access_audit.py b/test/unit_test/headless/test_remote_host_access_audit.py index 8bb7bdb10..95ab51aee 100644 --- a/test/unit_test/headless/test_remote_host_access_audit.py +++ b/test/unit_test/headless/test_remote_host_access_audit.py @@ -90,7 +90,8 @@ def hold_after_terminal_reply(channel, kind, payload): kind, _payload = read_message(first) else: first, kind = _connect(host, token='wrong-token' if reason == 'bad_token' else TOKEN) - assert kind is MessageType.AUTH_FAIL and entered.wait(5) + assert kind is MessageType.AUTH_FAIL + assert entered.wait(5) host._on_pending_viewer = None second, kind = _connect(host) assert kind is MessageType.AUTH_OK diff --git a/test/unit_test/headless/test_rest_auth_compatibility.py b/test/unit_test/headless/test_rest_auth_compatibility.py index 0a0a2ee72..eb9321758 100644 --- a/test/unit_test/headless/test_rest_auth_compatibility.py +++ b/test/unit_test/headless/test_rest_auth_compatibility.py @@ -88,7 +88,8 @@ def test_registry_reports_environment_configured_rbac(tmp_path, monkeypatch): registry = _RestApiRegistry() try: status = registry.start(port=0, enable_audit=False) - assert status["rbac_enabled"] is True and status["token"] is None + assert status["rbac_enabled"] is True + assert status["token"] is None finally: registry.stop() @@ -130,7 +131,8 @@ def test_gui_restores_shared_token_controls_after_rbac_stops(qapp, monkeypatch): assert not tab._token_input.isEnabled() status.update(running=False, rbac_enabled=False) tab._refresh_status() - assert tab._token_input.isEnabled() and tab._token_value.text() == "-" + assert tab._token_input.isEnabled() + assert tab._token_value.text() == "-" status.update(running=True, token="new-shared") tab._refresh_status() assert tab._token_value.text() == "new-shared" diff --git a/test/unit_test/headless/test_runtime_secret_boundaries.py b/test/unit_test/headless/test_runtime_secret_boundaries.py index 461d7acd5..55cab7d31 100644 --- a/test/unit_test/headless/test_runtime_secret_boundaries.py +++ b/test/unit_test/headless/test_runtime_secret_boundaries.py @@ -71,7 +71,8 @@ def test_cli_errors_are_masked_through_the_outer_error_handler(legacy, output, s from je_auto_control.cli import main assert main(["run", str(script)]) == 1 error = capsys.readouterr().err - assert FAKE not in output.getvalue() and FAKE not in error + assert FAKE not in output.getvalue() + assert FAKE not in error assert "private failure: ***" in error @@ -88,7 +89,8 @@ def test_real_local_rest_request_keeps_the_mask_until_error_handling_finishes(ou payload = json.loads(response.read()) assert response.status == (200 if strict else 500) if strict: - assert payload["ok"] is False and FAKE in payload["error"] + assert payload["ok"] is False + assert FAKE in payload["error"] assert FAKE not in output.getvalue() finally: connection.close() @@ -171,7 +173,8 @@ def test_background_run_owner_masks_history_and_outer_error_logs(kind, output, s assert FAKE not in trigger.last_error row = history.list_runs()[0] assert row.status == STATUS_ERROR - assert FAKE not in row.error_text and "***" in row.error_text + assert FAKE not in row.error_text + assert "***" in row.error_text assert FAKE not in output.getvalue() @@ -195,7 +198,8 @@ def probe(value): monkeypatch.setattr(test_record_instance, "init_record", True) monkeypatch.setattr(test_record_instance, "test_record_list", []) result = executor.execute_action(ACTIONS) - assert list(result.values()) == [FAKE] and source == {"value": FAKE} + assert list(result.values()) == [FAKE] + assert source == {"value": FAKE} assert FAKE not in json.dumps(test_record_instance.test_record_list) assert test_record_instance.test_record_list[0]["local_param"]["value"] == "***" @@ -213,7 +217,8 @@ def __repr__(self): result = scrub_secret_data(source) assert result == {"value": "***", "opaque": "", "tuple": ("***", 1, True, None), "cycle": "", "": "visible"} - assert source["value"] == FAKE and source["cycle"] is source + assert source["value"] == FAKE + assert source["cycle"] is source assert FAKE not in json.dumps(result) diff --git a/test/unit_test/headless/test_runtime_secret_callbacks.py b/test/unit_test/headless/test_runtime_secret_callbacks.py index 5bb720c41..e47e0776c 100644 --- a/test/unit_test/headless/test_runtime_secret_callbacks.py +++ b/test/unit_test/headless/test_runtime_secret_callbacks.py @@ -42,7 +42,8 @@ def test_watchdog_masks_both_detection_and_action_failures(output, phase): owner = PopupWatchdog() owner.add_rule(WatchdogRule("private", _fail if phase == "matcher" else lambda: True, _fail)) assert owner.check_once() == 0 - assert FAKE not in output.getvalue() and "private callback failure: ***" in output.getvalue() + assert FAKE not in output.getvalue() + assert "private callback failure: ***" in output.getvalue() @pytest.mark.parametrize("phase", ["predicate", "handler"]) @@ -52,7 +53,8 @@ def test_observer_masks_predicate_and_handler_failures(output, phase): owner.add("private", _fail if phase == "predicate" else lambda: True, _fail) events = owner.poll_once() assert len(events) == (0 if phase == "predicate" else 1) - assert FAKE not in output.getvalue() and "private callback failure: ***" in output.getvalue() + assert FAKE not in output.getvalue() + assert "private callback failure: ***" in output.getvalue() @pytest.mark.parametrize("phase", ["trigger", "callback"]) @@ -64,7 +66,8 @@ def callback(): raise RuntimeError("private callback failure: " + FAKE) result = owner.callback_function("AC_probe", callback) assert result == (None if phase == "trigger" else FAKE) - assert FAKE not in output.getvalue() and "private callback failure: ***" in output.getvalue() + assert FAKE not in output.getvalue() + assert "private callback failure: ***" in output.getvalue() def test_callback_calls_do_not_retain_an_earlier_invocations_mask(output): @@ -103,7 +106,8 @@ def test_direct_observer_callback_boundary_also_masks_its_own_error(output): from je_auto_control.utils.observer.observer import ScreenObserver, WatchRule owner = ScreenObserver() owner._fire(WatchRule("private", lambda: True, _fail), "appear", True) - assert FAKE not in output.getvalue() and "private callback failure: ***" in output.getvalue() + assert FAKE not in output.getvalue() + assert "private callback failure: ***" in output.getvalue() @pytest.mark.parametrize("kind", ["watchdog", "observer"]) @@ -120,4 +124,5 @@ def later_failure(*_args): owner = ScreenObserver() owner.add("private", _resolve, later_failure) assert len(owner.poll_once()) == 1 - assert FAKE not in output.getvalue() and "later callback failure: ***" in output.getvalue() + assert FAKE not in output.getvalue() + assert "later callback failure: ***" in output.getvalue() diff --git a/test/unit_test/headless/test_runtime_secret_logs.py b/test/unit_test/headless/test_runtime_secret_logs.py index 08badd58e..543c8579e 100644 --- a/test/unit_test/headless/test_runtime_secret_logs.py +++ b/test/unit_test/headless/test_runtime_secret_logs.py @@ -56,7 +56,8 @@ def fail(value): else: assert FAKE in next(iter(executor.execute_action(actions).values())) assert FAKE not in output.getvalue() - assert "Traceback" in output.getvalue() and "ValueError" in output.getvalue() + assert "Traceback" in output.getvalue() + assert "ValueError" in output.getvalue() @pytest.mark.parametrize("block", ["loop", "parallel", "dag"]) @@ -138,7 +139,8 @@ def test_closed_mask_cannot_retain_values_via_an_inherited_binding(): scope = current_secret_scope() remember_secret(FAKE) assert scope.scrub(FAKE) == "***" - assert not scope.active and not scope.has_values + assert not scope.active + assert not scope.has_values with bound_secret_scope(scope): remember_secret("later-private-value") assert current_secret_scope() is None @@ -163,7 +165,8 @@ def test_filter_masks_cached_exception_stack_and_unformattable_records(): record.stack_info = "stack " + FAKE assert SecretLogFilter().filter(record) assert record.getMessage() == "" - assert record.exc_text == "cached ***" and record.stack_info == "stack ***" + assert record.exc_text == "cached ***" + assert record.stack_info == "stack ***" def test_an_unformattable_outcome_cannot_abort_execution(output, monkeypatch): diff --git a/test/unit_test/headless/test_scoped_transport_io.py b/test/unit_test/headless/test_scoped_transport_io.py index d1d43a15e..a0bff16b6 100644 --- a/test/unit_test/headless/test_scoped_transport_io.py +++ b/test/unit_test/headless/test_scoped_transport_io.py @@ -43,7 +43,8 @@ def test_scoped_read_retains_fragmented_message_bytes_across_poll_timeouts(): reader._socket = incoming with cancellation_scope(CancellationToken(1)): message = reader.read_message(time.monotonic()+1) - assert message.body == ['controlled payload'] and reader._buffer == b'' + assert message.body == ['controlled payload'] + assert reader._buffer == b'' assert all(0 < value <= 0.1 for value in incoming.timeouts) @@ -71,7 +72,8 @@ def send(self, data): with cancellation_scope(CancellationToken(1)): cancellable_io.send(sender, b'abcdef') assert b''.join(sent) == b'abcdef' - assert attempted == [b'abcdef', b'cdef', b'cdef'] and sender.timeout == 1.0 + assert attempted == [b'abcdef', b'cdef', b'cdef'] + assert sender.timeout == 1.0 def test_repeated_receive_polls_do_not_restart_the_total_deadline(monkeypatch): @@ -104,8 +106,9 @@ def pending(_read, _write, _error, timeout): return [], [], [] monkeypatch.setattr(cancellable_io, 'select', SimpleNamespace(select=pending)) + connection = PendingSocket() with cancellation_scope(token), pytest.raises(CancelledError): - cancellable_io.connect(PendingSocket(), '/controlled', 10) + cancellable_io.connect(connection, '/controlled', 10) assert calls[-1] == ('timeout', 10) assert len([item for item in calls if item[0] == 'connect']) == 1 assert 0 < next(value for kind, value in calls if kind == 'poll') <= 0.1 @@ -142,7 +145,8 @@ def communicate(self, timeout): with cancellation_scope(CancellationToken(1)), pytest.raises(CancelledError) as caught: cancellable_process.run_capture_helper(['controlled'], 10) assert caught.value is primary - assert calls == ['signal', 'reaped', 'close'] and process.stderr.closed + assert calls == ['signal', 'reaped', 'close'] + assert process.stderr.closed finally: primary.__traceback__ = None diff --git a/test/unit_test/headless/test_self_healing_boundaries.py b/test/unit_test/headless/test_self_healing_boundaries.py index 1738e6453..ba1868e59 100644 --- a/test/unit_test/headless/test_self_healing_boundaries.py +++ b/test/unit_test/headless/test_self_healing_boundaries.py @@ -86,12 +86,16 @@ def cancel(*_args, **_kwargs): monkeypatch.setattr("je_auto_control.utils.self_healing.locator._try_vlm", lambda *_: calls.append("vlm")) monkeypatch.setattr("je_auto_control.utils.self_healing.locator._click_at", lambda *_: calls.append("click")) arguments = {"template_path": "probe.png", "description": "probe"} - with pytest.raises(OperationCancelledError) as caught: + + def run_surface(): if surface == "command": executor.execute_action([[f"AC_self_heal_{operation}", arguments]]) else: tool = next(tool for tool in build_default_tool_registry() if tool.name == f"ac_self_heal_{operation}") tool.handler(**arguments) + + with pytest.raises(OperationCancelledError) as caught: + run_surface() assert caught.value is signal assert calls == [] assert not log.path.exists() @@ -108,8 +112,9 @@ def cancel(*_args, **_kwargs): raise signal monkeypatch.setattr("je_auto_control.wrapper.auto_control_image.locate_image_center", cancel) + log = HealEventLog(tmp_path / "events.jsonl") with pytest.raises(OperationCancelledError) as caught: - self_heal_locate(template_path="probe.png", log=HealEventLog(tmp_path / "events.jsonl")) + self_heal_locate(template_path="probe.png", log=log) assert caught.value is signal @@ -125,7 +130,8 @@ def image(*_args, **_kwargs): with secret_log_scope(): remember_secret(secret) outcome = self_heal_locate(template_path=secret, description=secret, log=log) - assert outcome.found and outcome.coordinates == (4, 5) + assert outcome.found + assert outcome.coordinates == (4, 5) assert outcome.template_path == outcome.description == secret assert secret in outcome.image_error stored = log.list_events()[0] diff --git a/test/unit_test/headless/test_self_healing_evaluation.py b/test/unit_test/headless/test_self_healing_evaluation.py index 864f1a7f8..61f70ea4d 100644 --- a/test/unit_test/headless/test_self_healing_evaluation.py +++ b/test/unit_test/headless/test_self_healing_evaluation.py @@ -87,7 +87,8 @@ def test_real_template_dataset_and_versioned_acceptance(): assert actual.recovery.value is None attempt = next(row for row in report.attempts if row.version == "new" and row.sample_id.startswith("negative")) assert attempt.candidate.coordinates == (-66, -29) - assert attempt.correct is True and attempt.step_id == "scaled-submit" + assert attempt.correct is True + assert attempt.step_id == "scaled-submit" def test_image_miss_and_vlm_use_same_crop_and_transform_without_capture(monkeypatch): @@ -114,8 +115,10 @@ def forbidden(*_args, **_kwargs): vlm = VLMFrameLocator(Backend(), "submit", model="fake") image = TemplateFrameLocator((DATASET / "fixtures/old.png").read_bytes(), .99, vlm) result = image.locate(sample) - assert result.coordinates == (-66, -29) and result.method == "vlm" - assert result.verified is None and result.fallback_used + assert result.coordinates == (-66, -29) + assert result.method == "vlm" + assert result.verified is None + assert result.fallback_used assert [name for name, _ in result.timings_ms] == ["image", "fallback.vlm"] with Image.open(io.BytesIO(frame)) as original, Image.open(io.BytesIO(seen[0])) as actual: assert actual.size == (24, 20) @@ -128,8 +131,10 @@ def test_measured_percentiles_costs_and_verified_recovery(monkeypatch): locator = FixedLocator(LocatorCandidate((4, 5), "vlm", fallback_used=True, verified=True, cost=.01)) samples = [EvaluationSample(str(index), png(), expected_box=(0, 0, 10, 10)) for index in range(2)] version = evaluate_locators(samples, {"v1": locator}).versions["v1"] - assert version.p50_ms == 1500 and version.p95_ms == 1950 - assert version.cost_total == .02 and version.cost_known == 2 + assert version.p50_ms == 1500 + assert version.p95_ms == 1950 + assert version.cost_total == .02 + assert version.cost_known == 2 assert version.recovery.to_dict() == {"numerator": 2, "denominator": 2, "value": 1} @@ -154,13 +159,19 @@ def locate(self, sample): seen.append(sample.sample_id) raise signal + def observer(_): + raise signal + samples = [EvaluationSample("first", png()), EvaluationSample("second", png())] + strategies = {"v1": Failure()} with pytest.raises(RuntimeError) as caught: - evaluate_locators(samples, {"v1": Failure()}) - assert caught.value is signal and seen == ["first"] + evaluate_locators(samples, strategies) + assert caught.value is signal + assert seen == ["first"] with pytest.raises(RuntimeError) as caught: - evaluate_locators(samples, {"v1": Failure()}, observer=lambda _: (_ for _ in ()).throw(signal)) - assert caught.value is signal and seen == ["first"] + evaluate_locators(samples, strategies, observer=observer) + assert caught.value is signal + assert seen == ["first"] def test_exports_escape_html_and_contain_provenance_without_images(tmp_path): @@ -169,9 +180,11 @@ def test_exports_escape_html_and_contain_provenance_without_images(tmp_path): json_path = write_healing_report(report, tmp_path / "report.json") html_path = write_healing_report(report, tmp_path / "report.html") payload = json.loads(json_path.read_text(encoding="utf-8")) - assert payload["attempts"][0]["run_id"] == "run" and "frame" not in payload["attempts"][0] + assert payload["attempts"][0]["run_id"] == "run" + assert "frame" not in payload["attempts"][0] html = html_path.read_text(encoding="utf-8") - assert "