qwen-code/packages/cua-driver/test-harness
顾盼 9605bcc25f
feat(cua-driver): sync vendored cua-driver 0.6.8 → 0.7.0 (#6212)
* feat(cua-driver): sync vendored cua-driver from 0.6.8 to 0.7.0

Upstream 0.7.0 brings action-time modality (get_window_state always
returns AX tree + screenshot, capture_mode retired), honest verification
(every action returns verified/path/effect/escalation), browser page
(CDP) tools, desktop-scope (capture_scope=window|desktop,
get_desktop_state, screen-absolute click/scroll), and Rust-only
consolidation (Swift backend retired).

Cherry-pick patch changes:
- #2025 (X11 synthetic click): retired — superseded by 0.7.0
  honest-verification
- #2035 (session revive): retired — superseded by 0.7.0
  revive_session/loud-reject/is_session_lifecycle_tool
- #2036 (EAGAIN socket write retry): re-injected — merged upstream
  post-0.7.0 tag, EAGAIN os error 35 is not acceptable
- #2021 (null/empty-title windows + resolve_uwp_host_window): retained,
  adapted to 0.7.0's new is_listable_top_level and window_title helpers

Shim extensions:
- coord_norm: desktop-scope support — denormalize_args falls back to
  screen basis when no window cache (desktop-scope click),
  normalize_result handles get_desktop_state, ingest_screen_size reads
  get_desktop_state's screen_width/height
- serve.rs: rewrite_coord_desc re-injected at both tools/list sites

Install scripts simplified to Rust-only (Swift backend retired upstream).

Fix platform-linux cross-compilation: cfg-gate wayland::PORTAL_LIBEI_ENABLED
reference in health_report.rs for non-linux hosts.

* fix(cua-driver): distinguish desktop-scope from window cache miss in denormalize_args

When screenshot_w == 0 (no cached window size), check whether the call
has a pid/window_id target before falling back to screen-size basis.
Window-scoped clicks with a cache miss now correctly leave coordinates
as-is instead of mapping them against the screen dimensions.

* fix(cua-driver): use screenshot pixels not logical points for desktop-scope coord basis

ingest_screen_size was reading screen_width/height (logical display
points from CGDisplayBounds) for get_desktop_state, but the model
operates on the screenshot PNG whose dimensions are physical pixels.
On Retina displays (2x) this caused every desktop-scope click to land
at half the intended position.

Read screenshot_width/height instead — the same basis normalize_result
rewrites to 1000. Updated test to use realistic Retina values
(screen 1920, screenshot 3840) to catch 1:1 masking.

* fix(cua-driver): split screen cache into logical (move_cursor) and physical (desktop-scope)

The single screen_cache was being written by both get_screen_size
(logical points, e.g. 1920x1080) and get_desktop_state (physical
pixels, e.g. 3840x2160). Since move_cursor operates in CGEvent screen
points (logical) while desktop-scope clicks operate in screenshot
pixels (physical), whichever tool ran last would corrupt the other's
denormalization basis — causing 2x overshoot or 1/2 offset on Retina.

Split into:
- SCREEN_SIZE: logical points, written by get_screen_size, consumed by
  move_cursor (screen_basis=true)
- DESKTOP_SCREENSHOT_SIZE: physical pixels, written by get_desktop_state,
  consumed by desktop-scope click fallback (screenshot_w==0, no pid)

Added cross-contamination isolation tests.

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-03 06:27:00 +00:00
..
apps feat(cua-driver): sync vendored cua-driver 0.6.8 → 0.7.0 (#6212) 2026-07-03 06:27:00 +00:00
build feat(cua-driver): sync vendored cua-driver 0.6.8 → 0.7.0 (#6212) 2026-07-03 06:27:00 +00:00
linux-container feat(cua-driver): sync vendored cua-driver 0.6.8 → 0.7.0 (#6212) 2026-07-03 06:27:00 +00:00
modality-recordings feat(cua-driver): sync vendored cua-driver 0.6.8 → 0.7.0 (#6212) 2026-07-03 06:27:00 +00:00
shared feat(cua-driver): sync vendored cua-driver 0.6.8 → 0.7.0 (#6212) 2026-07-03 06:27:00 +00:00
smoke feat(cua-driver): vendor qwen-cua-driver with opt-in 0–1000 relative coordinates (#5896) 2026-06-26 13:06:43 +00:00
vision-agent-test feat(cua-driver): sync vendored cua-driver 0.6.8 → 0.7.0 (#6212) 2026-07-03 06:27:00 +00:00
.gitignore feat(cua-driver): vendor qwen-cua-driver with opt-in 0–1000 relative coordinates (#5896) 2026-06-26 13:06:43 +00:00
CuaTestHarness.sln feat(cua-driver): vendor qwen-cua-driver with opt-in 0–1000 relative coordinates (#5896) 2026-06-26 13:06:43 +00:00
README.md feat(cua-driver): vendor qwen-cua-driver with opt-in 0–1000 relative coordinates (#5896) 2026-06-26 13:06:43 +00:00
TEST_HARNESS_STRUCTURE.md feat(cua-driver): sync vendored cua-driver 0.6.8 → 0.7.0 (#6212) 2026-07-03 06:27:00 +00:00
TEST_SUITE.md feat(cua-driver): sync vendored cua-driver 0.6.8 → 0.7.0 (#6212) 2026-07-03 06:27:00 +00:00

cua-driver test harness

Deterministic host apps that present a fixed scenario set for cua-driver to drive. One layout, three buckets: cross-OS reusable assets in shared/, OS-specific (or cross-platform) target apps in apps/, host-OS build scripts in build/, host-OS CLI smoke runners in smoke/.

Layout

test-harness/
├── shared/                  # cross-OS reusable
│   ├── scenarios.json       # single SoT for AutomationIds / window titles
│   └── web/index.html       # WebView2 + Electron load this same file
├── apps/                    # the test-target host apps
│   ├── cross-platform/
│   │   └── electron/        # CuaTestHarness.Electron (CDP host, runs on any OS)
│   ├── macos/
│   │   ├── appkit/          # CuaTestHarness.AppKit  (single-file Swift)
│   │   └── swiftui/         # CuaTestHarness.SwiftUI (single-file Swift)
│   └── windows/
│       ├── wpf/             # CuaTestHarness.Wpf      (.NET 8 WPF)
│       ├── winui3/          # CuaTestHarness.WinUI3   (.NET 8 WinUI3 unpackaged)
│       └── webview2/        # CuaTestHarness.WebView  (WPF + Microsoft.Edge.WebView2)
├── build/                   # host-OS build scripts (publish into ../rust/test-apps/)
│   ├── macos.sh             # builds appkit + swiftui
│   └── windows.ps1          # builds wpf + winui3 + webview2 + electron
└── smoke/                   # host-OS per-tool CLI smoke runners
    ├── macos.sh             # spawns appkit harness, iterates every tool
    └── results/macos.txt    # checked-in baseline for diff-based regression detection

shared/scenarios.json is the single source of truth — both the host apps and the Rust integration tests read it so AutomationIds, AX identifiers, and window titles never drift between the two halves.

Published build outputs land in ../rust/test-apps/harness-{wpf,winui3,webview,electron,appkit,swiftui}/ so the existing sandbox runner and Rust integration tests pick them up without further wiring.

What's exercised

WPF (apps/windows/wpf):

  • Plain XAML controls + AutomationIds (counter, text_body)
  • TextBox + mirrored label (text_input)
  • Right/double-click target (click_target)
  • Scroll body with VerticalOffset label + WM_VSCROLL hook (scroll_target)
  • Modal MessageBox (message_box)
  • Save/Cancel bottom strip — regression guard for PR #1696 (bottom_strip)
  • Native Win32 child HWNDs via HwndHost (child_hwnd)
  • Owned secondary Window (owned_popup)
  • Layered transparent window (layered_popup)
  • Keyboard accelerators — F5 + Ctrl+Shift+H (accelerator)

WinUI3 (apps/windows/winui3):

  • counter / text_body / exit (parity with WPF)
  • TextBox + mirror (text_input — UIA ValuePattern)
  • CommandBarFlyout — popup in same HWND, DComp-rendered
  • XAML Popup — primitive, not a separate HWND

WebView2 (apps/windows/webview2):

  • Loads shared/web/index.html — same page as the Electron harness.
  • Exposes CDP on default_cdp_port=9222 so cua-driver's page tool can drive it.

Electron (apps/cross-platform/electron):

  • Loads the same shared/web/index.html. CDP on default_cdp_port=9223.

AppKit (apps/macos/appkit):

  • Plain NSButton + NSTextField counter (counter)
  • NSTextField label with shared marker (text_body)
  • Editable NSTextField + mirror label (text_input)
  • Custom NSView click-target (click_target)
  • NSScrollView with tall NSTextView body + offset label (scroll_target)
  • Top NSMenu item with known title (ns_menubar)
  • Exit button

SwiftUI (apps/macos/swiftui):

  • Counter / text_body / text_input parity with AppKit
  • SwiftUI .popover() — Mac analogue of WinUI3 CommandBarFlyout / XAML Popup
  • Exit button

Coverage matrices (Rust integration tests)

Windows:

Capability WPF WinUI3
Smoke (UIA tree + AutomationIds) yes yes
UIA Invoke click yes (implicit)
right_click yes
double_click yes
type_text (PostMessage / UIA) yes yes
set_value (UIA ValuePattern) yes (covered)
scroll (WM_VSCROLL via hook) yes
press_key accelerator (F5) yes
Modal MessageBox open + dismiss yes
Owned popup open + parse yes
Layered popup capture yes
XAML Popup open + parse yes

macOS:

Capability AppKit SwiftUI
Smoke (AX tree + identifiers) yes yes
Counter click via element_index yes (implicit)
set_value (AXSetAttribute) yes (covered)
Popover open + body assert yes
Per-tool CLI smoke smoke/macos.sh

Build

macOS (requires Xcode CLT for xcrun swiftc):

libs/cua-driver/test-harness/build/macos.sh                # both apps
libs/cua-driver/test-harness/build/macos.sh --skip swiftui # AppKit only
libs/cua-driver/test-harness/build/macos.sh --clean        # nuke stage dirs first

Windows (requires .NET 8 SDK on PATH; Node.js + npm for the Electron app):

cd libs\cua-driver\test-harness
.\build\windows.ps1                  # all four apps
.\build\windows.ps1 -Skip winui3     # skip a specific one

Run the integration tests

# macOS
cd libs/cua-driver/rust
cargo test --release --test harness_appkit_test    -- --ignored --nocapture
cargo test --release --test harness_swiftui_test   -- --ignored --nocapture

# Windows (locally)
cd libs\cua-driver\rust
cargo test --test harness_wpf_test     -- --ignored --nocapture
cargo test --test harness_winui3_test  -- --ignored --nocapture

# Windows Sandbox (matches CI)
.\rust\sandbox\run-tests-in-sandbox.ps1 harness_wpf
.\rust\sandbox\run-tests-in-sandbox.ps1 harness_winui3

Tests are #[ignore] so they don't run in plain cargo test (they require the harness apps to be built and, on macOS, TCC Accessibility granted to the test runner).

Per-tool CLI smoke

Companion to the MCP integration tests. Spawns the AppKit harness as a victim, then iterates every cua-driver tool with sensible JSON args and reports PASS / FAIL / SKIP in a sorted table. Runs in ~25 seconds.

libs/cua-driver/test-harness/smoke/macos.sh

A baseline result file (smoke/results/macos.txt) is checked in for the current driver version — diff against it to catch regressions in tool coverage.

AppKit / SwiftUI AX quirks captured in the tests

  • accessibilityIdentifier on AXStaticText doesn't propagate. NSTextField label-mode + SwiftUI Text views render as AXStaticText leaves and the OS drops the identifier slot. Tests assert AX-id only on actionable controls and assert on text-content for labels.
  • SwiftUI popovers may live in a separate AXWindow. The popover test walks list_windows for any new window owned by the harness pid if the trigger window doesn't contain the marker.
  • AX element budget caps tree depth + width. The scroll-body is capped at 30 lines so later scenarios don't get truncated.