packages/cua-driver/docs/test-harnesses-guide.md
This contributor guide explains where the CUA Driver tests live, how to run
them, and where to add a new case. For current coverage and platform gaps, use
test-matrix.md and action-support.md.
There are two main test layers:
The Rust tests are the source of truth. Python tests, old shell runners, and historical recording scripts are not part of the canonical E2E path.
The canonical E2E command on every OS runs the complete matrix and takes no suite selector:
Linux: scripts/ci/linux/run-rust-e2e.sh
Windows: .\scripts\ci\windows\run-rust-e2e.ps1 -RequireGui
macOS: packages/cua-driver/tests/runners/macos-lume/run-all.sh
The OS workflow may fan the complete matrix out into independent jobs for reporting and failure isolation. That is an execution detail; contributors should think of it as one canonical suite.
cua/
|-- packages/cua-driver/
| |-- rust/
| | |-- crates/
| | | |-- cua-driver/ Rust driver and integration tests
| | | |-- cua-driver-core/ Shared driver logic and unit tests
| | | |-- cua-driver-testkit/ Shared Rust E2E helpers and evidence capture
| | | |-- platform-linux/ Linux backend
| | | |-- platform-macos/ macOS backend
| | | |-- platform-windows/ Windows backend
| | | `-- cursor-overlay/ Cursor evidence helper
| | |-- test-apps/ Ignored staged harness binaries
| | `-- Cargo.toml Rust workspace
| |-- tests/
| | |-- fixtures/
| | | |-- shared/web/ Shared web page and external markers
| | | |-- apps/ Repo-local fixture sources
| | | `-- build/ macOS/Linux/Windows fixture builders
| | `-- runners/
| | `-- macos-lume/ Maintainer Lume setup and guest entrypoint
| `-- docs/ Test matrix, reporting, and contributor docs
`-- scripts/ci/
|-- linux/run-rust-e2e.sh Linux canonical runner
|-- windows/run-rust-e2e.ps1 Windows canonical runner
`-- macos/run-rust-e2e.sh macOS canonical runner
The important separation is:
| Layer | Owns | Does not own |
|---|---|---|
| Rust integration test | Scenarios, driver calls, assertions, action metadata | OS setup and fixture compilation |
cua-driver-testkit | Session helpers, fixture launching, screenshots, recordings, trajectories | The scenario list |
| Fixture app | Visible controls and externally observable state markers | Driver correctness assertions |
| OS runner | Build environment, user session, test selection, artifact collection | Test behavior definitions |
There is deliberately no second Python E2E implementation that the Rust suite has to mirror.
Every canonical E2E cell follows this shape:
OS runner
-> builds the Rust driver and repo-local fixture
-> starts or connects to a real desktop session
-> Rust testkit starts one harness application
-> Rust test discovers the target window
-> get_window_state provides accessibility tree and screenshot
-> action addresses a target by AX element index or PX coordinates
-> delivery is foreground or background where the action supports it
-> fixture state, focus, pixels, or protocol response is checked
-> testkit writes video, screenshots, trajectory, logs, and result data
A tool returning ok is not enough to pass an E2E cell. The fixture must show
that the action happened, or the test must verify a documented structured
refusal and the absence of focus or input side effects.
These run without a repo-local GUI application and normally run without
--ignored:
| Location or prefix | What it proves |
|---|---|
rust/crates/*/src/** | Core driver, platform-independent logic, schemas, and helpers |
protocol_*_test.rs | MCP handshake, tool calls, sessions, media, and errors |
schema_*_test.rs | Shared schema and backend consistency |
transport_config_persistence_test.rs | CLI/MCP configuration persistence |
protocol_element_token_test.rs | Element-token protocol behavior |
These tests should be fast, deterministic, and safe to run on ordinary CI workers. They do not prove that a real click, key, scroll, or background input reached an application.
These are Rust integration tests under:
packages/cua-driver/rust/crates/cua-driver/tests/
Most are marked #[ignore] because they require a desktop, built fixtures, and
platform permissions. They are selected by the OS runner rather than the
ordinary unit command.
The canonical E2E suite has two behavior owners:
| Owner | Purpose |
|---|---|
| Shared app | Same web behavior tested through Electron and Tauri |
| Native harness | Toolkit-specific controls and native window behavior |
WebView, CDP, and page-tool integration stays inside the shared or native owner that exercises it. It is not a third public test family or command.
Delivery is not a test family. It is a dimension on each action row: an action is tested in foreground and background modes whenever the driver and OS support both. Capture and desktop scope are separate environment checks, while focus preservation is a cross-cutting oracle that can be attached to any action row. Focus, z-order, cursor, and desktop-state checks are cross-cutting invariants, not a separate family. These names describe responsibilities, not separate sources of truth or commands to run instead of the selector-free canonical invocation.
Runner: scripts/ci/windows/run-rust-e2e.ps1
| Runner area | Rust test | Real harness or app |
|---|---|---|
| Shared app matrix | cross_platform_behavior_test.rs | Electron and Tauri |
| Native controls | harness_wpf_test.rs | Repo-local WPF app |
| Native controls | harness_winui3_test.rs | Repo-local WinUI3 app |
| Web integration | harness_web_test.rs | WebView2 and Electron |
| Capture contract | capture_contract_test.rs | WPF plus driver tree/image output |
| Launch contract | launch_windows_test.rs | Repo-local Electron launch and focus behavior |
| Agent cursor | agent_cursor_windows_test.rs | Source-built cursor overlay and pixel evidence |
| Desktop scope | desktop_scope_windows_test.rs | Windowless desktop input and scope rejection |
Cross-cutting instrumentation used by these rows includes the testkit
DesktopObserver, capture validation, cursor evidence, and desktop-scope
checks. These invariants are attached to their owning action rows rather than
run as a separate test family.
Windows is currently the broadest native matrix. It covers UIA controls, web integration, background focus checks, and Windows-specific input routes. The desktop observer is attached to shared and native action rows wherever background delivery is tested.
Runner: packages/cua-driver/tests/runners/macos-lume/run-all.sh
| Runner area | Rust test | Real harness or app |
|---|---|---|
| Shared app matrix | cross_platform_behavior_test.rs | Electron and Tauri |
| Native web matrix | cross_platform_behavior_test.rs | Repo-local WKWebView host |
| Native controls | harness_appkit_test.rs | Repo-local AppKit app |
| Native controls | harness_swiftui_test.rs | Repo-local SwiftUI app |
| Installed app launch | installed_app_launch_macos_test.rs | Calculator and TextEdit |
| Installed app AX delivery | installed_app_textedit_macos_test.rs | TextEdit |
| Capture contract | capture_contract_test.rs | Installed driver and macOS capture APIs |
| Desktop scope | desktop_scope_macos_test.rs | macOS window and desktop scope |
The WKWebView host runs the same typed shared-web catalog as Electron and
Tauri. Calculator and TextEdit add typed supporting rows for built-in app
launch focus and native Cocoa background value delivery. They run in the
canonical logged-in macOS lane, but they do not replace repo-local fixtures.
The maintainer wrapper provisions the exact source build and verifies the
private Lume seed's TCC/signing contract before delegating the behavior matrix
to scripts/ci/macos/run-rust-e2e.sh.
Runner: scripts/ci/linux/run-rust-e2e.sh
| Runner area | Rust test | Real harness or app |
|---|---|---|
| Shared app matrix | cross_platform_behavior_test.rs | Electron and Tauri |
| Native controls | harness_gtk3_test.rs | Repo-local GTK3 app |
| Capture contract | capture_contract_test.rs | Linux capture backend |
| Desktop scope | desktop_scope_linux_test.rs | X11/Wayland desktop scope |
Linux has separate X11 and Wayland concerns. Nix supplies the reproducible build and desktop environment, but the E2E test still needs an actual X11 or Wayland session. Linux does not need GIF output; MP4, screenshots, accessibility trees, trajectories, and logs are the useful evidence.
Wayland results are compositor-specific. The hosted lane uses Sway to prove wlroots protocols. GNOME requires the optional WinRects Shell helper for authoritative frame and buffer geometry, observation, capture, and verified target activation. A portal/libei grant persists until the user revokes it, so subsequent driver processes do not reopen the consent dialog. KDE requires a future target-addressable KWin adapter; portal availability by itself is not evidence that input can be sent safely to a named window. Standard Wayland does not expose the physical pointer position, so canonical Wayland rows do not claim the real-cursor preservation oracle. Focus, full occlusion, sentinel input isolation, liveness, and fixture-state oracles remain mandatory. Issue #2194 tracks compositor, portal/libei, sentinel, and capture-based ways to add a proven cursor observer where the environment supports one.
See action-support.md for the current Windows, macOS, and Linux
delivery, refusal, and unproven-action ledger.
These terms describe different dimensions:
| Term | Meaning |
|---|---|
| AX | Address a target through its accessibility/UI automation element |
| PX | Address a target by screen coordinates or pointer geometry |
| Foreground | The target may be brought to the foreground for delivery |
| Background | The target should receive the action without being raised or stealing focus |
| Window scope | Capture or action is limited to one target window |
| Desktop scope | Capture or action covers the full desktop |
The shared and native action matrices should test left click, right click, double click, typing, keys, hotkeys, scroll, child windows, and drag across AX/PX and foreground/background combinations where the driver supports them. Unsupported background routes require an explicit refusal contract with an allowed structured code and desktop-side-effect oracles. A refusal fails a cell that requires delivery. There should not be a separate "delivery" family whose only purpose is to repeat those same actions in the background.
Native harness rows use the same typed case/result contract as the shared
matrix. Current native set_value rows declare background delivery because
their contract includes no-focus and no-raise observations; actions without a
delivery concept use not_applicable explicitly.
The desktop observer is cross-cutting test instrumentation. It answers the same question for any action, harness, or catalog area:
Did the driver perform or reject the operation without disturbing the user's foreground application or desktop?
cua-driver-testkit::DesktopObserver owns the shared interface. Native Windows,
macOS, and Linux backends snapshot foreground-window, target z-order, cursor,
and focus state before and after an action. A separate full-desktop Electron
sentinel journals keyboard, pointer, wheel, visibility, focus, and heartbeat
events while it fully covers the target. Background rows opt into both pieces
of instrumentation directly; there is no special guard suite.
| Invariant or scenario | What it checks |
|---|---|
| Background click/type/key | The target action does not move focus away from the user's foreground window |
| Minimized app launch | launch_app(start_minimized=true) does not raise the new app |
| Background hotkey | A keyboard chord does not steal focus |
| Child-window click | A target-created window does not unexpectedly become foreground |
| Background screenshot | Reading the target does not change focus or z-order |
| Agent cursor visibility | The cursor appears in the captured pixels when enabled and moved |
The sentinel contract fails closed when the target is only partly covered or
the heartbeat stops. Before any behavioral cells run, the strict environment
preflight deliberately sends input to the sentinel and deliberately raises the
background target. The lane proceeds only if the leaked input and transient
focus loss are observed, the sentinel is restored, and it once again fully
occludes the target. Windows, macOS, and X11 require the sentinel's live focus
journal to report the loss. Wayland uses the compositor-backed native focus
observer because Electron/Ozone does not reliably emit a DOM blur event for
an external surface focus transition. The sentinel heartbeat and leaked-input
journal remain mandatory on Wayland. This positive control prevents a broken
guard from making every background row look green.
A focus assertion can prove "no focus steal" while failing to prove that a click changed the target application state. An action row must therefore check both the target's external state and, when background delivery is under test, the cross-cutting desktop observer.
These tests require a real interactive Windows user desktop. They reject
Session 0, locked desktops, and disconnected RDP sessions. Without
CUA_REQUIRE_GUI=1, an unusable desktop can self-skip for local development;
the canonical Windows runner enables the hard-failure behavior.
Canonical GUI runs are expected to produce evidence per test cell:
artifacts/cua-driver/<os>/
|-- recordings/<cell-label>-pid<pid>-<sequence>/recording.mp4
|-- recordings/<cell-label>-pid<pid>-<sequence>/trajectory.json
|-- recordings/<cell-label>-pid<pid>-<sequence>/turn-*/before_state.json
|-- recordings/<cell-label>-pid<pid>-<sequence>/turn-*/before.png
|-- recordings/<cell-label>-pid<pid>-<sequence>/turn-*/after_state.json
|-- recordings/<cell-label>-pid<pid>-<sequence>/turn-*/after.png
|-- cases.jsonl
|-- environment.jsonl
|-- results.jsonl
|-- summary.md
`-- <rust-target>.log
The GitHub Actions summary contains one row per meaningful behavioral cell, including its OS, harness, action, AX/PX targeting, delivery mode, driver route, expected and observed behavior, oracles, and one evidence link. The link uses the exact video path as its label and opens the owning lane archive. Unit tests need normal test output and logs; they do not need desktop video.
bring_to_front restore whose minimized target has no
pre-action image or whose host capture remains unavailable afterward: it must
retain the successful action response, captured post-action accessibility
state, an explicit capture classification, trajectory, and MP4. Any other
missing expected evidence fails the report.trajectory.json must finish with
behavior_video.status = "finalized".agent_cursor_showcase_test for cursor review media. Shared behavior
matrix daemons deliberately use --no-overlay so synthetic cursor pixels do
not contaminate action oracles; their videos prove tool behavior, not cursor
rendering.GetConsoleWindow to select the inherited
HostedComputeAgent/runner console, verify its identity, and minimize it
through ShowWindow(SW_MINIMIZE) before fixture or sentinel posture is
established. The sentinel remains a separate test fixture and is reasserted
after console cleanup.CUA_E2E_FORBID_SKIPS=1. Unfiltered shared runs also
set CUA_E2E_EXPECTED_MIN_CELLS to 80 on Windows/Linux and 120 on macOS, so
a filtered, shortened, or accidentally emptied catalog cannot report green.
Explicit diagnostic cell or harness filters disable only the minimum-count
check; matching no cells still fails inside the Rust matrix.The remaining work is platform coverage and validation, not another test hierarchy:
action-support.md.#1922
tracks the grouped backend work.The goal is not to put every assertion into one enormous test file. The goal is to give each behavior one clear owner and make cross-cutting evidence reusable.
rust/crates/cua-driver-testkit/src/
`-- observer.rs Cross-OS desktop-side-effect interface
rust/crates/cua-driver/tests/
|-- cross_platform_behavior_test.rs Shared Electron/Tauri action matrix
|-- harness_wpf_test.rs Windows WPF action rows
|-- harness_winui3_test.rs Windows WinUI3 action rows
|-- harness_web_test.rs WebView2/Electron page and CDP rows
|-- harness_appkit_test.rs macOS AppKit action rows
|-- harness_swiftui_test.rs macOS SwiftUI action rows
|-- harness_gtk3_test.rs Linux GTK3 action rows
|-- capture_contract_test.rs Tree and screenshot read contract
|-- desktop_scope_<os>_test.rs Window/desktop scope invariants
`-- protocol_*_test.rs Protocol and schema tests
The desktop observer is a helper, not a test family. An action row invokes it when the row is testing background delivery. The row then records both outcomes:
Focus, z-order, cursor, and input-leak assertions belong to the typed action rows that exercise them; launch, capture, cursor, and desktop-scope contracts retain their narrowly owned scenarios. The canonical runner is the only user-facing command; lane selectors are internal diagnostics.
When adding a new scenario:
rust/crates/cua-driver/tests/.docs/test-matrix.md and this guide when it changes the
cross-OS structure.The goal is one understandable Rust E2E model across platforms, with platform-specific harnesses where the OS genuinely differs.