agent-desktop/tests/e2e/electron-live.sh
Lahfir 3f322728b4
feat!: implement Playwright-grade foundation contract
Settle the Playwright-grade reliability contract in agent-desktop-core
before the Windows/Linux adapters are built, so they inherit it instead
of redesigning it. Every command now observes, waits, verifies, and
reports honestly instead of firing blindly.

Highlights: capability-supertrait split of PlatformAdapter with
not_supported() defaults; canonical role/state vocabulary with live
`is --property visible`; display enumeration (`list-displays`) and honest
`--screen` with scale factor; truthful Automation permission; `native_id`
identity spine; window-id-first resolution; serializable `LocatorQuery`
with live `find`; default-on auto-wait before every ref action; three-way
`hit_test` occlusion gate; `scroll_into_view` in core; core accessible-name
precedence; typed `ActionStep` delivery tier; `ProcessState` and
`APP_UNRESPONSIVE`; `LaunchOptions`; baseline-diff desktop signals
(`wait --event`); typed clipboard (`Text`/`Image`/`FileUrls`); mouse
modifier chords and `mouse-wheel`. Hardened through a 35-reviewer pass with
independent validation and a green live e2e gate (109/0), plus a
head-vs-main performance comparison harness.

BREAKING CHANGE: default-on auto-wait changes the timing of every
previously-untouched ref-action call (bounded 5000 ms default; `--timeout-ms 0`
restores single-shot). `ENVELOPE_VERSION` is now `2.1` (adds the
`APP_UNRESPONSIVE` code and process state in error details). FFI ABI major
is `3` (append-only struct evolution; `wait --event` is intentionally not
exposed over FFI). The legacy string clipboard API is removed in favor of
typed content. `key-down`/`key-up` fail closed until daemon-owned held input
exists. `close-app` verifies termination and the osascript fallback path is
removed. `--text` matching is subtree containment: `find --text X --first`
returns the outermost matching container.
2026-07-20 00:21:38 -07:00

141 lines
4.9 KiB
Bash
Executable file

#!/usr/bin/env bash
set -uo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -z "${AGENT_DESKTOP_INTERACTION_LEASE_FD:-}" ]; then
exec python3 "$here/interaction_lock.py" run "$0" "$@"
fi
# shellcheck source=tests/e2e/harness.sh
source "$here/harness.sh"
app=""
out=""
baseline_source=""
current_binary=""
current_sha=""
baseline_binary=""
baseline_sha=""
contaminated=0
report_started=0
cleanup() {
if [ -n "$current_binary" ] && ! verify_immutable_binary "$current_binary" "$current_sha"; then
contaminated=1
fi
if [ -n "$baseline_binary" ] && ! verify_immutable_binary "$baseline_binary" "$baseline_sha"; then
contaminated=1
fi
if [ "$contaminated" -ne 0 ] && [ "$report_started" -eq 1 ] && [ -n "$out" ]; then
if trash_recoverably "$out"; then
echo "measurement was contaminated; report moved to recoverable Trash" >&2
else
echo "measurement was contaminated; retained report must not be used: $out" >&2
fi
fi
cleanup_isolated_environment
release_exclusive_lock
}
trap cleanup EXIT
while [ "$#" -gt 0 ]; do
case "$1" in
--app) app="${2:-}"; shift 2 ;;
--out) out="${2:-}"; shift 2 ;;
--baseline-binary) baseline_source="${2:-}"; shift 2 ;;
*) echo "usage: $0 --app Slack [--baseline-binary PATH] [--out PATH]" >&2; exit 2 ;;
esac
done
if [ "$(uname -s)" != "Darwin" ]; then
echo "electron-live requires macOS" >&2
exit 2
fi
if [ -z "$app" ]; then
echo "--app is required (for example: --app Slack)" >&2
exit 2
fi
if [ ! -x "$release_bin" ]; then
echo "release binary missing at $release_bin" >&2
exit 2
fi
require_exclusive_acknowledgement || exit 2
acquire_exclusive_lock || exit 2
setup_isolated_environment electron || exit 2
copy_immutable_binary "$release_bin" current-agent-desktop || exit 2
current_binary="$prepared_binary"
current_sha="$prepared_binary_sha"
export AGENT_DESKTOP_E2E_BINARY="$current_binary"
bin="$command_guard"
if [ -n "$baseline_source" ]; then
copy_immutable_binary "$baseline_source" baseline-agent-desktop || exit 2
baseline_binary="$prepared_binary"
baseline_sha="$prepared_binary_sha"
fi
expected_version="$(awk -F'"' '/^version[[:space:]]*=/{print $2; exit}' "$repo/Cargo.toml")"
version_json="$("$bin" version 2>/dev/null)" || exit 2
current_version="$(printf '%s' "$version_json" | python3 "$json_tool" get data.version 2>/dev/null)"
if [ -z "$expected_version" ] || [ "$current_version" != "$expected_version" ]; then
echo "current binary version mismatch: Cargo='$expected_version' binary='$current_version'" >&2
exit 2
fi
installed_path=""
for applications_dir in /Applications "$host_home/Applications"; do
if [ -d "$applications_dir/$app.app" ]; then
installed_path="$applications_dir/$app.app"
break
fi
done
if [ -z "$installed_path" ] && ! guard_exec 10 1048576 open -Ra "$app" >/dev/null 2>&1; then
echo "installed application '$app' was not found by Launch Services" >&2
exit 2
fi
permission_json="$("$bin" permissions 2>/dev/null)"
permission_state="$(printf '%s' "$permission_json" | python3 "$json_tool" get data.accessibility.state 2>/dev/null)"
if [ "$permission_state" != "granted" ]; then
echo "Accessibility permission must be granted; current state is '${permission_state:-unknown}'" >&2
exit 2
fi
app_json="$("$bin" list-apps --app "$app" 2>/dev/null)"
app_running="$(printf '%s' "$app_json" | python3 -c '
import json, sys
data = json.load(sys.stdin)
print(1 if any(item.get("name", "").casefold() == sys.argv[1].casefold()
for item in data.get("data", {}).get("apps", [])) else 0)
' "$app" 2>/dev/null)"
if [ "$app_running" != "1" ]; then
echo "'$app' is installed but not running; start it without changing its UI state, then retry" >&2
exit 2
fi
window_json="$("$bin" list-windows --app "$app" 2>/dev/null)"
window_count="$(printf '%s' "$window_json" | python3 -c \
'import json,sys; print(len(json.load(sys.stdin).get("data",[])))' 2>/dev/null)"
if [ "${window_count:-0}" -lt 1 ]; then
echo "'$app' has no accessibility window to measure" >&2
exit 2
fi
if [ -z "$out" ]; then
slug="$(printf '%s' "$app" | tr '[:upper:] ' '[:lower:]-' | tr -cd 'a-z0-9._-')"
out="/tmp/agent-desktop-electron-${slug}-$(date -u +%Y%m%dT%H%M%SZ).json"
fi
baseline_args=()
if [ -n "$baseline_binary" ]; then
baseline_args=(--baseline-binary "$baseline_binary")
fi
report_started=1
guard_agent_wrapper_exec 1800 4194304 python3 "$repo/tests/e2e/electron_metrics.py" \
--binary "$current_binary" "${baseline_args[@]}" --app "$app" --out "$out" \
--work-root "$suite_root" --warmups 5 --samples 31 >/dev/null || exit 1
verify_immutable_binary "$current_binary" "$current_sha" || { contaminated=1; exit 1; }
if [ -n "$baseline_binary" ]; then
verify_immutable_binary "$baseline_binary" "$baseline_sha" || { contaminated=1; exit 1; }
fi
printf '%s\n' "$out"