diff --git a/architecture_explore.md b/architecture_explore.md index c7567dbe6..65d065475 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 | @@ -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。 | @@ -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/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/docs/updates/2026-11.md b/docs/updates/2026-11.md index b154d74c4..f9bac54f2 100644 --- a/docs/updates/2026-11.md +++ b/docs/updates/2026-11.md @@ -588,3 +588,36 @@ 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. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 93049f040..c71cbfd5c 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `Progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| 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-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) | 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/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..fd71b2920 100644 --- a/test/unit_test/headless/test_agent_presence_reader.py +++ b/test/unit_test/headless/test_agent_presence_reader.py @@ -118,7 +118,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 +141,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 +159,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 +192,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 +214,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 +254,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 +265,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 +290,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 +309,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 +322,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 +332,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 +353,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_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 "