From d18ad46eee2f398d2cece79bd2a2ea4b9536e649 Mon Sep 17 00:00:00 2001 From: yoniebans Date: Tue, 1 Sep 2026 14:48:46 +0200 Subject: [PATCH] feat(install-e2e): cover non-app update methods after a dmg install A dmg user can also update from the terminal (hermes update) or by re-running the install one-liner; the dmg arm only routed the two app-button methods, leaving three declared cells as permanent TODOs. Port the POSIX driver's method blocks into the macos driver's update phase (hermes-update with the --yes probe, installer re-run with per-ref flag probing, the +desktop built-app assert) and open the workflow gate. The dmg bootstrap driver also learns to recover instead of waiting out its bound when a stage fails: the bootstrap parks on an error screen with a Retry button (seen live: HTTP 429 downloading install.sh under full-matrix runner load), so the driver watches the bootstrap log for real failure shapes, requires two consecutive error probes before retargeting the click at Retry's measured position, unlatches when the log goes healthy, and gives up with the true cause after three retries. Driver rule learned three times in this suite (lsof +D, git show and find piped to grep -q): under set -euo pipefail, never feed grep -q from a pipe... grep exits at first match, the producer takes SIGPIPE, and a TRUE condition reads as failure. Capture to a variable or test paths directly. Verified end to end: run 33506177130, macos slice 13/13 green. --- .github/workflows/install-e2e-macos-run.yml | 2 +- tests/install/e2e-assets/drive-dmg-install.sh | 54 ++++++++++++++- tests/install/macos-desktop-e2e.sh | 68 ++++++++++++++++++- 3 files changed, 119 insertions(+), 5 deletions(-) diff --git a/.github/workflows/install-e2e-macos-run.yml b/.github/workflows/install-e2e-macos-run.yml index de90b863b2..1599e566c4 100644 --- a/.github/workflows/install-e2e-macos-run.yml +++ b/.github/workflows/install-e2e-macos-run.yml @@ -82,7 +82,7 @@ jobs: name: Hermes-Setup.dmg if: >- inputs.install-method == 'desktop-installer@latest' && inputs.tag-has-desktop - && contains(fromJSON('["open-app-update", "hermes-desktop-app-update"]'), inputs.update-method) + && contains(fromJSON('["open-app-update", "hermes-desktop-app-update", "hermes-update", "installer-script", "installer-script+desktop"]'), inputs.update-method) runs-on: macos-latest timeout-minutes: ${{ inputs.timeout-minutes }} diff --git a/tests/install/e2e-assets/drive-dmg-install.sh b/tests/install/e2e-assets/drive-dmg-install.sh index 60ddd1e35f..1335379bd6 100644 --- a/tests/install/e2e-assets/drive-dmg-install.sh +++ b/tests/install/e2e-assets/drive-dmg-install.sh @@ -76,8 +76,16 @@ click_install() { # 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). + if [ -n "$LAST_ERR" ]; then + # Error screen: Retry install sits left of center at ~59% height + # (measured: button x 359-492, y 402-441 in the 880x620 window). + local cx=$((x + wd * 48 / 100)) + local cy=$((y + ht * 59 / 100)) + cliclick "c:${cx},${cy}" 2>&1 || true + echo "clicked retry ${cx},${cy} (window ${x},${y} ${wd}x${ht})" + return 0 + fi + # First-run screen: button center sits at ~65% of window height. local cx=$((x + wd / 2)) local cy=$((y + ht * 65 / 100)) cliclick "c:${cx},${cy}" 2>&1 || true @@ -101,6 +109,26 @@ installed_app() { install_complete() { [ -d "$INSTALL_DIR/.git" ] && [ -x "$HERMES_BIN" ] && installed_app } +# The bootstrap parks on an error screen instead of exiting when a stage +# fails (e.g. a transient 429 downloading install.sh), with a Retry button +# in the same button zone the install click hits. Its log names the cause; +# watch for new failure lines, let the regular click drive the retry, and +# give up after a few so a persistent failure reports the real error +# instead of burning the whole install timeout. +BOOTSTRAP_LOG="$HOME/.hermes/logs/bootstrap-installer.log" +bootstrap_error() { + [ -f "$BOOTSTRAP_LOG" ] || return 0 + # Match REAL failure shapes only: the structured stage log's state=Failed, + # or the error screen's own message. Healthy INFO lines carry the literal + # field error=None, so a bare error/failed substring match false-positives + # on every stage transition. + tail -5 "$BOOTSTRAP_LOG" 2>/dev/null \ + | grep -E 'state=Failed|install script failed|didn.t finish|ERROR ' | tail -1 || true +} +RETRIES=0 +MAX_RETRIES=3 +LAST_ERR="" +PENDING_ERR_COUNT=0 DEADLINE=$((SECONDS + INSTALL_TIMEOUT_SECS)) FIRST_SHOT=0 CLICKS=0 @@ -119,6 +147,28 @@ while :; do log "Hermes-Setup exited (pid $SETUP_PID) before the install landed" exit 1 fi + err="$(bootstrap_error)" + if [ -n "$err" ]; then + PENDING_ERR_COUNT=$((PENDING_ERR_COUNT + 1)) + else + PENDING_ERR_COUNT=0 + fi + # Two consecutive error probes before switching click targets: a single + # log-parse glitch must never redirect the clicker. + if [ "$PENDING_ERR_COUNT" -ge 2 ] && [ "$err" != "$LAST_ERR" ]; then + LAST_ERR="$err" + RETRIES=$((RETRIES + 1)) + log "bootstrap error (retry $RETRIES/$MAX_RETRIES): $err" + shot "ERROR-bootstrap-attempt-$RETRIES" + if [ "$RETRIES" -gt "$MAX_RETRIES" ]; then + log "bootstrap failing persistently; giving up" + exit 1 + fi + fi + if [ -z "$err" ] && [ -n "$LAST_ERR" ]; then + log "bootstrap error cleared; resuming install-button targeting" + LAST_ERR="" + fi if [ "$FIRST_SHOT" -eq 0 ] && [ "$SECONDS" -gt 10 ]; then shot "00-setup-window" FIRST_SHOT=1 diff --git a/tests/install/macos-desktop-e2e.sh b/tests/install/macos-desktop-e2e.sh index c623431d35..3467b0b811 100755 --- a/tests/install/macos-desktop-e2e.sh +++ b/tests/install/macos-desktop-e2e.sh @@ -21,6 +21,8 @@ # hermes-desktop-app-update capture `hermes desktop`'s spawn, # launch the spec under Playwright, # click Update now +# hermes-update CLI update from the installed venv +# installer-script[+desktop] re-run the current install one-liner # # Usage: # tests/install/macos-desktop-e2e.sh --phase stage|install|update|all @@ -61,8 +63,8 @@ while [ "$#" -gt 0 ]; do esac done case "$UPDATE_METHOD" in - open-app-update|hermes-desktop-app-update) ;; - *) echo "error: --update-method must be open-app-update or hermes-desktop-app-update, got '$UPDATE_METHOD'" >&2; exit 1 ;; + open-app-update|hermes-desktop-app-update|hermes-update|installer-script|installer-script+desktop) ;; + *) echo "error: unsupported --update-method '$UPDATE_METHOD'" >&2; exit 1 ;; esac [ "$(uname -s)" = "Darwin" ] || { echo "error: this driver runs on macOS only" >&2; exit 1; } @@ -266,6 +268,39 @@ ensure_playwright() { printf '%s' "$pw_dir" } +installer_supports() { + # $1: ref; $2: flag. Installer flags must match the installer being run, + # not this checkout's: older releases reject options added later. + # Capture before grepping: a `git show | grep -q` pipe takes SIGPIPE + # under pipefail when grep exits at first match, so a supported flag + # would read as unsupported. + local text + text="$(git -C "$REPO_ROOT" show "$1:scripts/install.sh")" + grep -qF -- "$2" <<< "$text" +} + +run_installer() { + # $1: ref whose scripts/install.sh to run; $2: log name; $3: "desktop" to + # opt the desktop stage in (--include-desktop). Mirrors the POSIX driver. + local script="$WORK_ROOT/install-$2.sh" + git -C "$REPO_ROOT" show "$1:scripts/install.sh" > "$script" + chmod +x "$script" + local flags=(--skip-setup) + if installer_supports "$1" "--skip-browser"; then + flags+=(--skip-browser) + fi + if [ "${3:-}" = "desktop" ]; then + installer_supports "$1" "--include-desktop" \ + || fail "ref $1 does not support --include-desktop; this leg cannot mean what it claims" + flags+=(--include-desktop) + fi + # &1 | ts_prefix > "$LOG_DIR/install-$2.log" || rc=$? + log_group "installer ($2) transcript" "$LOG_DIR/install-$2.log" + [ "$rc" -eq 0 ] || fail "installer ($2) exited $rc; transcript above" +} + run_playwright_update() { # $1: spec file to launch from. local spec="$1" @@ -300,6 +335,35 @@ phase_update() { mock_start "$WORK_ROOT" trap mock_stop EXIT case "$UPDATE_METHOD" in + hermes-update) + # The CLI route a dmg user takes from a terminal. `--yes` reaches the + # update subcommand only in later releases; ask the installed hermes. + local hermes="$INSTALL_DIR/venv/bin/hermes" + local update_cmd=("$hermes" update) + if "$hermes" update --help 2>&1 | grep -qF -- --yes; then + update_cmd=("$hermes" update --yes) + fi + local rc=0 + (cd "$INSTALL_DIR" && "${update_cmd[@]}" < /dev/null 2>&1 | ts_prefix > "$LOG_DIR/update.log") || rc=$? + log_group "hermes update transcript" "$LOG_DIR/update.log" + [ "$rc" -eq 0 ] || fail "hermes update exited $rc; transcript above" + ;; + installer-script) + # A dmg user re-running today's install one-liner. + run_installer "$HEAD_SHA" head + ;; + installer-script+desktop) + run_installer "$HEAD_SHA" head desktop + # The desktop stage is this leg's claim: the rebuilt app must exist. + head_app="" + for cand in \ + "$INSTALL_DIR/apps/desktop/release/mac-arm64/Hermes.app" \ + "$INSTALL_DIR/apps/desktop/release/mac/Hermes.app"; do + [ -d "$cand" ] && { head_app="$cand"; break; } + done + [ -n "$head_app" ] || fail "no built Hermes.app under the checkout after the +desktop update" + ok "rebuilt app present: $head_app" + ;; open-app-update) # The installed app IS the user surface here (double-click the .app); # hand-build the spec Playwright launches from. Env: the redirect set,