diff --git a/.github/workflows/install-e2e.yml b/.github/workflows/install-e2e.yml index 6320a848dc..8ca7875642 100644 --- a/.github/workflows/install-e2e.yml +++ b/.github/workflows/install-e2e.yml @@ -47,11 +47,11 @@ on: workflow_dispatch: inputs: route: - description: 'Which combinations to run. all = every OS; both/update/installer = the linux legs; windows-desktop = the windows legs.' + description: 'Which combinations to run. all = every OS; both/update/installer = the linux legs; windows-desktop = the windows legs; macos-desktop = the macos legs.' required: false type: choice default: all - options: [all, both, update, installer, windows-desktop] + options: [all, both, update, installer, windows-desktop, macos-desktop] tag-count: description: 'How many release tags to sample (newest, oldest, and a spread between).' required: false @@ -179,7 +179,7 @@ jobs: macos: name: ${{ matrix.name }} - if: github.event_name != 'workflow_dispatch' || inputs.route == 'all' + if: github.event_name != 'workflow_dispatch' || contains(fromJSON('["all", "macos-desktop"]'), inputs.route) needs: generate-matrix strategy: fail-fast: false diff --git a/tests/install/e2e-assets/drive-dmg-install.sh b/tests/install/e2e-assets/drive-dmg-install.sh new file mode 100644 index 0000000000..60ddd1e35f --- /dev/null +++ b/tests/install/e2e-assets/drive-dmg-install.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# Drive the Hermes-Setup dmg bootstrap through its first-run GUI. +# +# The Setup app is Tauri (Rust + system webview), so Playwright/Electron +# attach never works. Launch the binary bare in the background (it inherits +# the redirect env), click "Install Hermes ->" with native input, then watch +# the install land on disk: checkout + venv console script. +# +# Usage: +# drive-dmg-install.sh --app-bin --install-dir \ +# [--install-timeout-secs 2700] [--proof-dir ] +# +# Exit 0: install landed. Non-zero: click never registered or the install +# never landed; desktop screenshots in --proof-dir say which. +set -euo pipefail + +APP_BIN="" +INSTALL_DIR="" +INSTALL_TIMEOUT_SECS=2700 +PROOF_DIR="." +while [ "$#" -gt 0 ]; do + case "$1" in + --app-bin) APP_BIN="$2"; shift 2 ;; + --install-dir) INSTALL_DIR="$2"; shift 2 ;; + --install-timeout-secs) INSTALL_TIMEOUT_SECS="$2"; shift 2 ;; + --proof-dir) PROOF_DIR="$2"; shift 2 ;; + *) echo "error: unknown argument: $1" >&2; exit 1 ;; + esac +done +[ -n "$APP_BIN" ] && [ -n "$INSTALL_DIR" ] || { echo 'error: --app-bin and --install-dir are required' >&2; exit 1; } +mkdir -p "$PROOF_DIR" + +log() { echo "[drive-dmg-install] $*"; } +shot() { + # screencapture works regardless of app internals; -x mutes the shutter. + screencapture -x "$PROOF_DIR/$1.png" 2>/dev/null || true + log "screenshot: $PROOF_DIR/$1.png" +} + +"$APP_BIN" & +SETUP_PID=$! +log "launched $APP_BIN (pid $SETUP_PID)" +cleanup() { kill "$SETUP_PID" 2>/dev/null || true; } +trap cleanup EXIT + +# Wait for the installer window, then click the install button. The webview's +# button is not a native control and System Events is not permitted assistive +# access on the runners (error -25208), so post real CGEvents with cliclick. +# The AppleScript half only READS window geometry (position/size need no +# assistive grant). Re-click every probe until the install starts: a click +# that lands before the webview finishes painting is swallowed, and +# re-clicking a started install hits a progress screen (harmless). +command -v cliclick >/dev/null 2>&1 || brew install --quiet cliclick + +window_geometry() { + osascript <<'OSA' 2>/dev/null +tell application "System Events" + set procs to (every process whose name contains "Hermes") + if (count of procs) = 0 then return "no-process" + set p to item 1 of procs + if (count of windows of p) = 0 then return "no-window" + set w to window 1 of p + set {x, y} to position of w + set {wd, ht} to size of w + return (x as text) & " " & (y as text) & " " & (wd as text) & " " & (ht as text) +end tell +OSA +} + +click_install() { + local geo + geo="$(window_geometry)" + case "$geo" in + no-process|no-window|'') echo "$geo"; return 0 ;; + esac + # shellcheck disable=SC2086 + set -- $geo + local x=$1 y=$2 wd=$3 ht=$4 + # Button center sits at ~65% of window height (measured from the + # installer's first-run screen at its fixed 880x620 window size). + local cx=$((x + wd / 2)) + local cy=$((y + ht * 65 / 100)) + cliclick "c:${cx},${cy}" 2>&1 || true + echo "clicked ${cx},${cy} (window ${x},${y} ${wd}x${ht})" +} + +HERMES_BIN="$INSTALL_DIR/venv/bin/hermes" +# The bootstrap runs 11 stages; checkout + venv land in the first few and +# the desktop app build is near the end, so success requires all three or +# the EXIT trap kills the installer mid-build. +installed_app() { + local cand + for cand in \ + "$INSTALL_DIR/apps/desktop/release/mac-arm64/Hermes.app" \ + "$INSTALL_DIR/apps/desktop/release/mac/Hermes.app" \ + "/Applications/Hermes.app"; do + [ -d "$cand" ] && return 0 + done + return 1 +} +install_complete() { + [ -d "$INSTALL_DIR/.git" ] && [ -x "$HERMES_BIN" ] && installed_app +} +DEADLINE=$((SECONDS + INSTALL_TIMEOUT_SECS)) +FIRST_SHOT=0 +CLICKS=0 +while :; do + if install_complete; then + log "install landed: checkout + venv console script + Hermes.app present" + shot "02-install-landed" + break + fi + if ! kill -0 "$SETUP_PID" 2>/dev/null; then + # The bootstrap relaunches the desktop and exits on success; only fail + # if it died without the install landing (give the FS one last look). + sleep 5 + install_complete && continue + shot "ERROR-setup-exited" + log "Hermes-Setup exited (pid $SETUP_PID) before the install landed" + exit 1 + fi + if [ "$FIRST_SHOT" -eq 0 ] && [ "$SECONDS" -gt 10 ]; then + shot "00-setup-window" + FIRST_SHOT=1 + fi + # Keep clicking until the install shows up on disk; count for the log. + result="$(click_install || true)" + CLICKS=$((CLICKS + 1)) + [ $((CLICKS % 6)) -eq 1 ] && log "click attempt $CLICKS: $result" + if [ "$CLICKS" -eq 3 ]; then shot "01-after-first-clicks"; fi + if [ "$SECONDS" -ge "$DEADLINE" ]; then + shot "ERROR-install-timeout" + log "install did not land within ${INSTALL_TIMEOUT_SECS}s ($CLICKS clicks attempted)" + exit 1 + fi + sleep 10 +done + +# Success: the bootstrap owns its own exit (it relaunches the desktop). +# Leave it a grace window, then stop it if it lingers; the caller's asserts +# take over from here. +for _ in 1 2 3 4 5 6; do + kill -0 "$SETUP_PID" 2>/dev/null || break + sleep 5 +done +log "done" +exit 0 diff --git a/tests/install/macos-desktop-e2e.sh b/tests/install/macos-desktop-e2e.sh index ccf36a4f93..c623431d35 100755 --- a/tests/install/macos-desktop-e2e.sh +++ b/tests/install/macos-desktop-e2e.sh @@ -226,12 +226,17 @@ phase_install() { app_bin="$(find "$app/Contents/MacOS" -type f -perm +111 | head -1)" [ -n "$app_bin" ] || fail "no executable inside $app/Contents/MacOS" - # Run the installer binary DIRECTLY: `open` launches via launchd, which - # inherits NONE of the redirect env (GIT_CONFIG_GLOBAL, HOME) - the whole - # isolation would silently evaporate. Direct exec is the same binary and - # the same first-launch flow. + # The Setup app is Tauri (Rust + system webview): Playwright/Electron + # attach never works, and run bare it waits forever on its setup-choice + # screen. Launch it in the background with our env (direct exec, not + # `open`: launchd inherits NONE of the redirect env) and drive the + # "Install Hermes" button with native input. local rc=0 - "$app_bin" 2>&1 | ts_prefix > "$LOG_DIR/bootstrap-install.log" || rc=$? + bash "$ASSETS/drive-dmg-install.sh" \ + --app-bin "$app_bin" \ + --install-dir "$INSTALL_DIR" \ + --proof-dir "$LOG_DIR" 2>&1 \ + | ts_prefix > "$LOG_DIR/bootstrap-install.log" || rc=$? log_group "Hermes-Setup (dmg bootstrap) transcript" "$LOG_DIR/bootstrap-install.log" hdiutil detach "$mount" >/dev/null 2>&1 || true [ "$rc" -eq 0 ] || fail "dmg bootstrap exited $rc; transcript above" @@ -249,15 +254,23 @@ phase_install() { ok "installed app: $(find_installed_app)" } -run_playwright_update() { - # $1: spec file to launch from. Installs the driver's OWN pinned - # @playwright/test into a scratch dir (never the installed tree's copy). - local spec="$1" +ensure_playwright() { + # Install the driver's OWN pinned @playwright/test into a scratch dir + # (never the installed tree's copy). Idempotent across phases. local pw_dir="$WORK_ROOT/playwright" + [ -d "$pw_dir/node_modules/@playwright/test" ] && { printf '%s' "$pw_dir"; return 0; } mkdir -p "$pw_dir" (cd "$pw_dir" && npm install --no-save --no-audit --no-fund \ "@playwright/test@$PLAYWRIGHT_VERSION" 2>&1 | ts_prefix > "$LOG_DIR/playwright-install.log") \ || { log_group "playwright install transcript" "$LOG_DIR/playwright-install.log"; fail "playwright install failed"; } + printf '%s' "$pw_dir" +} + +run_playwright_update() { + # $1: spec file to launch from. + local spec="$1" + local pw_dir + pw_dir="$(ensure_playwright)" cp "$ASSETS/launch-from-spec.mjs" "$pw_dir/" local rc=0 (cd "$pw_dir" && node launch-from-spec.mjs \