#!/usr/bin/env bash # # Unified WDIO E2E runner — one Appium session, one CEF app launch, all specs. # # Architecture: # 1. Build artefacts must exist (run `pnpm test:e2e:build` first). # 2. Clean cached app data + write a fresh E2E config.toml pointing at the # shared mock backend. # 3. Launch the built CEF binary directly (cross-platform). # 4. Wait for the CEF process to expose CDP on 127.0.0.1:19222. # 5. Start Appium with the `chromium` driver. # 6. Run wdio against `test/wdio.conf.ts`, which attaches to the running # CEF via Appium's Chromium driver. All specs share one session. # 7. Tear everything down (Appium → CEF → workspace). # # Usage: # ./app/scripts/e2e-run-session.sh # whole suite # ./app/scripts/e2e-run-session.sh test/e2e/specs/foo.spec.ts # single spec # set -euo pipefail # Accept either: # - Zero args → run the entire `specs` glob from wdio.conf.ts # - One spec path arg → legacy single-spec mode (e2e-run-spec.sh shim) # - One spec + log suffix → legacy two-arg mode used by debug runner / CI # - N>1 spec paths → multi-spec mode, one shared session # # To disambiguate "spec + suffix" from "two specs", we treat arg2 as a log # suffix only when it does NOT look like a spec path (i.e. doesn't end in # `.spec.ts` and doesn't start with `test/`). SPEC_ARGS=() LOG_SUFFIX="session" if [ "$#" -ge 1 ]; then SPEC_ARGS+=("$1") if [ "$#" -eq 2 ] && [[ "$2" != *.spec.ts && "$2" != test/* ]]; then LOG_SUFFIX="$2" else shift while [ "$#" -gt 0 ]; do SPEC_ARGS+=("$1") shift done fi fi # Back-compat: SPEC_ARG is the first spec (only used in stale log lines below). SPEC_ARG="${SPEC_ARGS[0]:-}" E2E_MOCK_PORT="${E2E_MOCK_PORT:-18473}" CEF_CDP_PORT="${CEF_CDP_PORT:-19222}" APPIUM_PORT="${APPIUM_PORT:-4723}" E2E_CDP_READY_TIMEOUT_SECONDS="${E2E_CDP_READY_TIMEOUT_SECONDS:-180}" OS="$(uname)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" REPO_ROOT="$(cd "$APP_DIR/.." && pwd)" cd "$APP_DIR" CREATED_TEMP_WORKSPACE="" APPIUM_PID="" APP_PID="" E2E_CONFIG_BACKUP="" E2E_CONFIG_FILE="" CREATED_TEMP_CEF_CACHE="" # ------------------------------------------------------------------------------ # Workspace + config # ------------------------------------------------------------------------------ if [ -z "${OPENHUMAN_WORKSPACE:-}" ]; then OPENHUMAN_WORKSPACE="$(mktemp -d)" CREATED_TEMP_WORKSPACE="$OPENHUMAN_WORKSPACE" export OPENHUMAN_WORKSPACE echo "[runner] Using temporary OPENHUMAN_WORKSPACE: $OPENHUMAN_WORKSPACE" else echo "[runner] Using OPENHUMAN_WORKSPACE from environment: $OPENHUMAN_WORKSPACE" fi # Headless Linux CI does not always have a usable Secret Service/keychain. # Keep E2E credentials under OPENHUMAN_WORKSPACE so auth state is deterministic # and gets cleaned up with the rest of the test workspace. : "${OPENHUMAN_KEYRING_BACKEND:=file}" export OPENHUMAN_KEYRING_BACKEND echo "[runner] Using OPENHUMAN_KEYRING_BACKEND: $OPENHUMAN_KEYRING_BACKEND" # Place the CEF cache directory OUTSIDE the workspace. By default the Tauri # shell roots it under `$OPENHUMAN_WORKSPACE/users//cef`, but our # `mega-flow` spec calls `openhuman.config_reset_local_data` between # sub-scenarios — that RPC does `remove_dir_all($OPENHUMAN_WORKSPACE)`, # which yanks CEF's cache out from under the running process and kills # the WebDriver session (every later sub-test then fails with # "invalid session id"). Pointing CEF at a sibling tmpdir via the # `OPENHUMAN_CEF_CACHE_PATH` escape hatch (`cef_profile.rs:7`) keeps it # unaffected by the reset. if [ -z "${OPENHUMAN_CEF_CACHE_PATH:-}" ]; then OPENHUMAN_CEF_CACHE_PATH="$(mktemp -d)" CREATED_TEMP_CEF_CACHE="$OPENHUMAN_CEF_CACHE_PATH" export OPENHUMAN_CEF_CACHE_PATH echo "[runner] Using temporary OPENHUMAN_CEF_CACHE_PATH: $OPENHUMAN_CEF_CACHE_PATH" fi if [ "${OPENHUMAN_SERVICE_MOCK:-0}" = "1" ] && [ -z "${OPENHUMAN_SERVICE_MOCK_STATE_FILE:-}" ]; then OPENHUMAN_SERVICE_MOCK_STATE_FILE="$OPENHUMAN_WORKSPACE/service-mock-state.json" export OPENHUMAN_SERVICE_MOCK_STATE_FILE fi cleanup() { local status=$? set +e if [ -n "$APPIUM_PID" ]; then echo "[runner] Stopping Appium (pid $APPIUM_PID)..." kill "$APPIUM_PID" 2>/dev/null || true wait "$APPIUM_PID" 2>/dev/null || true fi if [ -n "$APP_PID" ]; then echo "[runner] Stopping CEF app (pid $APP_PID)..." # CEF spawns helper child processes (zygote, GPU, renderers) that # the parent does not reap on SIGTERM. If we only `kill $APP_PID` # the parent exits but children keep writing into the temp # workspace, and the `rm -rf` below races them and fails with # "Directory not empty" on Linux runners — even though the WDIO # spec itself passed. Reap the whole process tree before cleanup. # # CRITICAL: capture child PIDs **before** killing the parent. # The instant the parent exits, the kernel reparents its children # to init (PID 1). After that, `pkill -P "$APP_PID"` matches # nothing because no process has the dying parent as its PPID # anymore. Snapshot the PIDs while the relationship still exists, # then signal them directly by PID. CHILD_PIDS="$(pgrep -P "$APP_PID" 2>/dev/null || true)" pkill -TERM -P "$APP_PID" 2>/dev/null || true kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true # Brief grace period so CEF helpers can flush their CEF/Default # files and exit on the SIGTERM we already sent. Anything that # ignored it gets SIGKILLed by the captured-PID sweep below. sleep 1 if [ -n "$CHILD_PIDS" ]; then for pid in $CHILD_PIDS; do kill -KILL "$pid" 2>/dev/null || true done fi fi if [ -n "$CREATED_TEMP_WORKSPACE" ]; then for attempt in 1 2 3; do rm -rf "$CREATED_TEMP_WORKSPACE" 2>/dev/null && break echo "[runner] Warning: temporary workspace cleanup failed (attempt $attempt): $CREATED_TEMP_WORKSPACE" >&2 sleep "$attempt" done if [ -e "$CREATED_TEMP_WORKSPACE" ]; then echo "[runner] Warning: leaving temporary workspace after cleanup retries: $CREATED_TEMP_WORKSPACE" >&2 fi fi if [ -n "$CREATED_TEMP_CEF_CACHE" ]; then rm -rf "$CREATED_TEMP_CEF_CACHE" 2>/dev/null || true fi if [ -n "$E2E_CONFIG_BACKUP" ] && [ -f "$E2E_CONFIG_BACKUP" ]; then mv "$E2E_CONFIG_BACKUP" "$E2E_CONFIG_FILE" \ || echo "[runner] Warning: failed to restore E2E config backup: $E2E_CONFIG_BACKUP" >&2 elif [ -n "$E2E_CONFIG_FILE" ] && [ -f "$E2E_CONFIG_FILE" ]; then rm -f "$E2E_CONFIG_FILE" \ || echo "[runner] Warning: failed to remove generated E2E config: $E2E_CONFIG_FILE" >&2 fi return "$status" } trap cleanup EXIT export VITE_BACKEND_URL="http://127.0.0.1:${E2E_MOCK_PORT}" export BACKEND_URL="http://127.0.0.1:${E2E_MOCK_PORT}" export OPENHUMAN_E2E_MODE="1" export APPIUM_PORT export CEF_CDP_PORT # Redirect Telegram Bot API calls to the mock server during E2E runs. # The mock server (WS-A) serves /bot/* routes on the same port as the # rest of the mock backend. The core reads this at TelegramChannel::new() time, # which runs after the config is fully loaded. export OPENHUMAN_TELEGRAM_BOT_API_BASE="http://127.0.0.1:${E2E_MOCK_PORT}" export OPENHUMAN_COMPOSIO_DIRECT_BASE_V2="http://127.0.0.1:${E2E_MOCK_PORT}" export OPENHUMAN_COMPOSIO_DIRECT_BASE_V3="http://127.0.0.1:${E2E_MOCK_PORT}" echo "[runner] Killing any running OpenHuman instances..." case "$OS" in Darwin) pkill -f "OpenHuman" 2>/dev/null || true ;; Linux) pkill -f "OpenHuman" 2>/dev/null || true ;; MINGW*|MSYS*|CYGWIN*|Windows_NT) taskkill //F //IM "OpenHuman.exe" 2>/dev/null || true ;; esac sleep 1 echo "[runner] Cleaning cached app data..." case "$OS" in Darwin) rm -rf ~/Library/WebKit/com.openhuman.app rm -rf ~/Library/Caches/com.openhuman.app rm -rf "$HOME/Library/Application Support/com.openhuman.app" rm -rf "$HOME/Library/Saved Application State/com.openhuman.app.savedState" ;; Linux) rm -rf "$HOME/.local/share/com.openhuman.app" 2>/dev/null || true rm -rf "$HOME/.cache/com.openhuman.app" 2>/dev/null || true rm -rf "$HOME/.config/com.openhuman.app" 2>/dev/null || true ;; MINGW*|MSYS*|CYGWIN*|Windows_NT) rm -rf "${APPDATA:-$HOME/AppData/Roaming}/com.openhuman.app" 2>/dev/null || true rm -rf "${LOCALAPPDATA:-$HOME/AppData/Local}/com.openhuman.app" 2>/dev/null || true ;; esac # Mock URL must reach the core sidecar — XCUITest doesn't inherit env, # and CEF child processes won't either. Pinning via config.toml works # on every platform. The runner always sets OPENHUMAN_WORKSPACE above; # Config::load_or_init gives that path precedence over $HOME/.openhuman. E2E_CONFIG_DIR="${OPENHUMAN_WORKSPACE:-$HOME/.openhuman}" E2E_CONFIG_FILE="$E2E_CONFIG_DIR/config.toml" mkdir -p "$E2E_CONFIG_DIR" if [ -f "$E2E_CONFIG_FILE" ]; then E2E_CONFIG_BACKUP="$E2E_CONFIG_FILE.e2e-backup.$$" cp "$E2E_CONFIG_FILE" "$E2E_CONFIG_BACKUP" fi # Write a complete E2E config that routes ALL LLM inference through the mock # server via OpenAiCompatibleProvider (supports_streaming=true). # # WHY pre-populate cloud_providers here: # The unify_ai_provider_settings migration runs on first startup. If # cloud_providers is empty it seeds an OpenHuman entry and sets primary_cloud # to that entry — which routes all inference to OpenHumanBackendProvider # (supports_streaming=false, always returns non-streaming responses, so the # mock server never receives /openai/v1/chat/completions). # # By pre-populating [[cloud_providers]] with a "none" auth mock entry and # setting primary_cloud to its id, the migration sees !is_empty() and skips # seeding entirely. provider_for_role() resolves unset workloads via # primary_cloud → slug "e2e" (non-openhuman) → returns "e2e:" → # make_cloud_provider_by_slug → auth_style=none → OpenAiCompatibleProvider # → supports_streaming=true → streams to mock at /openai/v1/chat/completions. cat > "$E2E_CONFIG_FILE" << TOMLEOF api_url = "http://127.0.0.1:${E2E_MOCK_PORT}" primary_cloud = "p_e2e_mock" default_model = "e2e-mock-model" chat_provider = "e2e:e2e-mock-model" reasoning_provider = "e2e:e2e-mock-model" agentic_provider = "e2e:e2e-mock-model" coding_provider = "e2e:e2e-mock-model" [[cloud_providers]] id = "p_e2e_mock" slug = "e2e" label = "E2E Mock" endpoint = "http://127.0.0.1:${E2E_MOCK_PORT}/openai/v1" auth_style = "none" default_model = "e2e-mock-model" TOMLEOF echo "[runner] Wrote E2E config.toml routing inference to mock at http://127.0.0.1:${E2E_MOCK_PORT}" DIST_JS="$(ls dist/assets/index-*.js 2>/dev/null | head -1)" if [ -z "$DIST_JS" ]; then echo "ERROR: No frontend bundle found at dist/assets/index-*.js." >&2 echo " Run 'pnpm test:e2e:build' first." >&2 exit 1 fi if ! grep -q "127.0.0.1:${E2E_MOCK_PORT}" "$DIST_JS"; then echo "ERROR: frontend bundle does NOT contain mock server URL (127.0.0.1:${E2E_MOCK_PORT})." >&2 echo " Run 'pnpm test:e2e:build' to rebuild." >&2 exit 1 fi # ------------------------------------------------------------------------------ # Resolve the built CEF binary for this platform # ------------------------------------------------------------------------------ resolve_app_binary() { case "$OS" in Darwin) for base in \ "$APP_DIR/src-tauri/target/debug/bundle/macos/OpenHuman.app/Contents/MacOS/OpenHuman" \ "$REPO_ROOT/target/debug/bundle/macos/OpenHuman.app/Contents/MacOS/OpenHuman"; do if [ -x "$base" ]; then echo "$base"; return; fi done ;; Linux) for candidate in \ "$APP_DIR/src-tauri/target/debug/OpenHuman" \ "$REPO_ROOT/target/debug/OpenHuman"; do if [ -x "$candidate" ]; then echo "$candidate"; return; fi done ;; MINGW*|MSYS*|CYGWIN*|Windows_NT) for candidate in \ "$APP_DIR/src-tauri/target/debug/OpenHuman.exe" \ "$REPO_ROOT/target/debug/OpenHuman.exe"; do if [ -x "$candidate" ]; then echo "$candidate"; return; fi done ;; esac } APP_BIN="$(resolve_app_binary)" if [ -z "${APP_BIN:-}" ] || [ ! -x "$APP_BIN" ]; then echo "ERROR: built OpenHuman binary not found. Run 'pnpm test:e2e:build' first." >&2 exit 1 fi # ------------------------------------------------------------------------------ # Ensure a dbus session bus exists on Linux. # # The Tauri `single-instance` plugin (used inside OpenHuman) talks to dbus # via zbus on Linux. If DBUS_SESSION_BUS_ADDRESS is missing or set to # `disabled:` (which is the openhuman_ci container default), the plugin # panics during plugin setup: # panicked at plugins/single-instance/src/platform_impl/linux.rs:57 # Result::unwrap() on Err(Address("unsupported transport 'disabled'")) # So start a real session bus with `dbus-launch` and inherit its # DBUS_SESSION_BUS_ADDRESS for the rest of the runner. # ------------------------------------------------------------------------------ if [ "$OS" = "Linux" ]; then if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] || \ printf '%s' "${DBUS_SESSION_BUS_ADDRESS:-}" | grep -q '^disabled'; then if command -v dbus-launch >/dev/null 2>&1; then DBUS_LAUNCH_OUT="$(dbus-launch --sh-syntax)" eval "$DBUS_LAUNCH_OUT" echo "[runner] Started dbus session bus: $DBUS_SESSION_BUS_ADDRESS" else echo "[runner] Warning: dbus-launch not available — single-instance plugin may panic." fi fi fi # ------------------------------------------------------------------------------ # Make CEF runtime libraries discoverable. # # macOS bundles the framework into `OpenHuman.app/Contents/Frameworks/` so the # OS resolves it automatically. On Linux + Windows we build with --no-bundle # (faster, no .deb / .msi staging needed for tests), which means the bare # binary has no co-located libcef.so / libcef.dll. CEF lives in the # vendored-tauri-cli cache that `ensure-tauri-cli.sh` pinned via CEF_PATH: # $CEF_PATH//cef__/{libcef.so,libcef.dll,Resources,…} # # We find the only versioned directory under CEF_PATH and prepend its CEF # dist dir to LD_LIBRARY_PATH (Linux) / PATH (Windows). On macOS this is a # no-op — the .app bundle already self-resolves. # ------------------------------------------------------------------------------ CEF_PATH="${CEF_PATH:-$HOME/Library/Caches/tauri-cef}" # Pick exactly one CEF distribution per platform. If two cached versions # coexist (e.g. after a CEF upgrade) `head -1` silently picks an arbitrary # one and the binary can load the wrong libcef — Linux/Windows then fail # only on warmed runners. Error out so the failure is loud. pick_one_cef_dist() { local pattern="$1" local matches mapfile -t matches < <(find "$CEF_PATH" -mindepth 2 -maxdepth 2 -type d -name "$pattern" 2>/dev/null | sort) if [ "${#matches[@]}" -gt 1 ]; then echo "ERROR: multiple CEF distributions found under $CEF_PATH matching $pattern:" >&2 printf ' %s\n' "${matches[@]}" >&2 echo "Set CEF_PATH to a directory containing exactly one cef_* version." >&2 return 1 fi printf '%s' "${matches[0]:-}" } case "$OS" in Linux) CEF_DIST_DIR="$(pick_one_cef_dist 'cef_linux_*')" || exit 1 if [ -n "$CEF_DIST_DIR" ] && [ -d "$CEF_DIST_DIR" ]; then export LD_LIBRARY_PATH="$CEF_DIST_DIR:${LD_LIBRARY_PATH:-}" echo "[runner] LD_LIBRARY_PATH includes: $CEF_DIST_DIR" else echo "[runner] Warning: no CEF Linux distribution found under $CEF_PATH — libcef.so may fail to load." fi ;; MINGW*|MSYS*|CYGWIN*|Windows_NT) CEF_DIST_DIR="$(pick_one_cef_dist 'cef_windows_*')" || exit 1 if [ -n "$CEF_DIST_DIR" ] && [ -d "$CEF_DIST_DIR" ]; then # Windows uses PATH for DLL resolution. Use the native Windows path form # so the CEF binary itself can find libcef.dll even though we're running # under git-bash. if command -v cygpath >/dev/null 2>&1; then WIN_CEF_DIST="$(cygpath -w "$CEF_DIST_DIR")" else WIN_CEF_DIST="$CEF_DIST_DIR" fi export PATH="$CEF_DIST_DIR:$PATH" echo "[runner] PATH includes: $CEF_DIST_DIR (win: $WIN_CEF_DIST)" else echo "[runner] Warning: no CEF Windows distribution found under $CEF_PATH — libcef.dll may fail to load." fi ;; esac # ------------------------------------------------------------------------------ # Launch CEF app — CDP on 19222 is already enabled in lib.rs (see CLAUDE.md). # ------------------------------------------------------------------------------ LOG_DIR="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" APP_LOG="$LOG_DIR/openhuman-e2e-app-${LOG_SUFFIX}.log" APP_ARGS=() # CEF/Chromium needs extra coaxing in headless / containerized Linux runs: # # --no-sandbox crbug.com/638180 — needed only when the runner is # uid 0 or the cached CEF chrome-sandbox helper is # missing/misconfigured. Non-root Linux runs with a # valid helper keep Chromium sandboxing. # --disable-dev-shm-usage docker /dev/shm is often 64 MB; Chromium # assumes ≥2 GB and crashes mid-startup # ("Failed global descriptor lookup: 7" in the # zygote helper). # --disable-gpu no GPU in the CI container. # --no-zygote skips the zygote launcher that wants dbus; Chromium # only permits this when sandboxing is also disabled. # # Apply only on Linux. macOS/Windows runners are unprivileged users with a # real display / GPU; leaving the sandbox on there is correct. case "$OS" in Linux) APP_ARGS+=( "--disable-dev-shm-usage" "--disable-gpu" ) NO_SANDBOX_REASON="" if [ "$(id -u)" -eq 0 ]; then NO_SANDBOX_REASON="runner is uid 0" elif [ -n "${CEF_DIST_DIR:-}" ]; then SANDBOX_HELPER="$CEF_DIST_DIR/chrome-sandbox" SANDBOX_HELPER_MODE="$(stat -c '%u:%a' "$SANDBOX_HELPER" 2>/dev/null || true)" if [ "$SANDBOX_HELPER_MODE" != "0:4755" ]; then NO_SANDBOX_REASON="chrome-sandbox helper is not root-owned mode 4755" fi fi if [ -n "$NO_SANDBOX_REASON" ]; then APP_ARGS+=("--no-sandbox" "--no-zygote") echo "[runner] Linux CEF sandbox disabled: $NO_SANDBOX_REASON" fi echo "[runner] Linux CEF args: ${APP_ARGS[*]}" ;; esac echo "[runner] Launching CEF app: $APP_BIN ${APP_ARGS[*]:-}" echo "[runner] App logs: $APP_LOG" # `${APP_ARGS[@]+"${APP_ARGS[@]}"}` is the idiom for expanding a possibly- # empty array under `set -u`. On macOS APP_ARGS is empty; on Linux it has # --no-sandbox etc. Without this guard bash errors with "APP_ARGS[@]: # unbound variable" and the app never launches. "$APP_BIN" ${APP_ARGS[@]+"${APP_ARGS[@]}"} > "$APP_LOG" 2>&1 & APP_PID=$! echo "[runner] Waiting for CDP at http://127.0.0.1:${CEF_CDP_PORT}/json/version ..." CDP_VERSION_JSON="" for i in $(seq 1 "$E2E_CDP_READY_TIMEOUT_SECONDS"); do CDP_VERSION_JSON="$(curl -sf "http://127.0.0.1:${CEF_CDP_PORT}/json/version" 2>/dev/null || true)" if [ -n "$CDP_VERSION_JSON" ]; then echo "[runner] CDP is ready." break fi if ! kill -0 "$APP_PID" 2>/dev/null; then echo "ERROR: CEF app exited before CDP came up. App log follows:" >&2 echo "----- $APP_LOG -----" >&2 cat "$APP_LOG" >&2 || true echo "----- end log -----" >&2 exit 1 fi if [ "$i" -eq "$E2E_CDP_READY_TIMEOUT_SECONDS" ]; then echo "ERROR: CDP did not come up within ${E2E_CDP_READY_TIMEOUT_SECONDS}s. App log follows:" >&2 echo "----- $APP_LOG -----" >&2 cat "$APP_LOG" >&2 || true echo "----- end log -----" >&2 exit 1 fi sleep 1 done # Wait for the main app target (tauri.localhost) to be visible, then close the # CEF prewarm child-webview slot (about:blank) so chromedriver attaches to the # real app. Without this, debuggerAddress picks the first page target — which # is the prewarm — and chromedriver's session is bound to a target CEF may # garbage-collect mid-test, killing the session with "session terminated". echo "[runner] Waiting for main app CDP target (tauri.localhost) ..." MAIN_TARGET_ID="" for i in $(seq 1 60); do TARGETS_JSON="$(curl -sf "http://127.0.0.1:${CEF_CDP_PORT}/json/list" 2>/dev/null || true)" MAIN_TARGET_ID="$(printf '%s' "$TARGETS_JSON" | python3 -c ' import json, sys try: data = json.load(sys.stdin) except Exception: sys.exit(0) for t in data: if t.get("type") == "page" and "tauri.localhost" in (t.get("url") or ""): print(t.get("id", "")) break ' 2>/dev/null || true)" if [ -n "$MAIN_TARGET_ID" ]; then echo "[runner] Main app target: $MAIN_TARGET_ID" break fi if [ "$i" -eq 60 ]; then echo "[runner] Warning: main app target never appeared — chromedriver may attach to the wrong target." break fi sleep 1 done if [ -n "$MAIN_TARGET_ID" ]; then printf '%s' "$TARGETS_JSON" | python3 -c ' import json, sys try: data = json.load(sys.stdin) except Exception: sys.exit(0) for t in data: if t.get("type") == "page" and (t.get("url") or "").startswith("about:"): print(t.get("id", "")) ' 2>/dev/null | while read -r STALE_ID; do if [ -n "$STALE_ID" ]; then echo "[runner] Closing prewarm/about:blank target: $STALE_ID" curl -sf "http://127.0.0.1:${CEF_CDP_PORT}/json/close/$STALE_ID" > /dev/null 2>&1 || true fi done fi # ------------------------------------------------------------------------------ # Resolve a chromedriver whose major matches CEF's bundled Chromium. # # chromedriver is strict about same-major-version matching (e.g. cd148 cannot # talk to Chrome 146). The Appium chromium driver ships its own bundled # chromedriver but that often drifts ahead of CEF's pinned Chromium. # Solution: parse Chromium's version from /json/version, then download the # matching chromedriver from Chrome for Testing into a workspace-local cache. # We pass the resulting binary to Appium via the `appium:chromedriverExecutable` # capability (wired in wdio.conf.ts via E2E_CHROMEDRIVER_PATH). # ------------------------------------------------------------------------------ CHROMIUM_FULL_VERSION="$(echo "$CDP_VERSION_JSON" | sed -n 's/.*"Browser": *"[^/]*\/\([^"]*\)".*/\1/p' | head -1)" if [ -z "$CHROMIUM_FULL_VERSION" ]; then CHROMIUM_FULL_VERSION="$(echo "$CDP_VERSION_JSON" | sed -n 's/.*"Browser": *"\([^"]*\)".*/\1/p' | sed 's/^[^/]*\///' | head -1)" fi echo "[runner] CEF Chromium version: ${CHROMIUM_FULL_VERSION:-}" case "$OS" in Darwin) ARCH="$(uname -m)" case "$ARCH" in arm64) CFT_PLATFORM="mac-arm64" ;; x86_64) CFT_PLATFORM="mac-x64" ;; *) CFT_PLATFORM="mac-arm64" ;; esac CD_EXE_NAME="chromedriver" ;; Linux) CFT_PLATFORM="linux64" CD_EXE_NAME="chromedriver" ;; MINGW*|MSYS*|CYGWIN*|Windows_NT) CFT_PLATFORM="win64" CD_EXE_NAME="chromedriver.exe" ;; esac CD_CACHE_DIR="$APP_DIR/test/e2e/.cache/chromedriver/${CHROMIUM_FULL_VERSION:-unknown}-${CFT_PLATFORM}" CD_BINARY="$CD_CACHE_DIR/chromedriver-${CFT_PLATFORM}/${CD_EXE_NAME}" if [ -n "$CHROMIUM_FULL_VERSION" ] && [ ! -x "$CD_BINARY" ]; then CD_URL="https://storage.googleapis.com/chrome-for-testing-public/${CHROMIUM_FULL_VERSION}/${CFT_PLATFORM}/chromedriver-${CFT_PLATFORM}.zip" echo "[runner] Downloading matching chromedriver: $CD_URL" mkdir -p "$CD_CACHE_DIR" CD_ZIP="$CD_CACHE_DIR/chromedriver.zip" if curl -fSL "$CD_URL" -o "$CD_ZIP"; then if command -v unzip >/dev/null 2>&1; then (cd "$CD_CACHE_DIR" && unzip -o -q chromedriver.zip) else # The openhuman_ci docker image doesn't ship `unzip`; use Python's # stdlib zipfile so we don't have to add a system package install. python3 -c "import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])" \ "$CD_ZIP" "$CD_CACHE_DIR" fi chmod +x "$CD_BINARY" 2>/dev/null || true else echo "[runner] Warning: chromedriver $CHROMIUM_FULL_VERSION not on Chrome for Testing; falling back to Appium-bundled chromedriver." fi fi if [ -x "$CD_BINARY" ]; then export E2E_CHROMEDRIVER_PATH="$CD_BINARY" echo "[runner] Using chromedriver: $E2E_CHROMEDRIVER_PATH" fi # ------------------------------------------------------------------------------ # Start Appium (chromium driver) # ------------------------------------------------------------------------------ # shellcheck source=/dev/null source "$SCRIPT_DIR/e2e-resolve-node-appium.sh" # Make sure the chromium driver is installed. `appium driver list --installed` # exits non-zero on parse errors in some Appium versions, so just attempt the # install and ignore "already installed" output. echo "[runner] Ensuring Appium chromium driver is installed..." APPIUM_HOME_DIR="${APPIUM_HOME:-$HOME/.appium}" if [ ! -d "$APPIUM_HOME_DIR/node_modules/appium" ]; then echo "[runner] Installing Appium into $APPIUM_HOME_DIR for chromium driver peer resolution..." npm install --prefix "$APPIUM_HOME_DIR" appium@3 >/dev/null fi "$APPIUM_BIN" driver install --source=npm appium-chromium-driver >/dev/null 2>&1 || true APPIUM_LOG="$LOG_DIR/appium-e2e-${LOG_SUFFIX}.log" # Fail fast if something else is already serving on the Appium port. Otherwise # `curl /status` succeeds against the stale server while our just-launched # Appium dies with EADDRINUSE — we'd silently drive the wrong instance. if curl -sf "http://127.0.0.1:$APPIUM_PORT/status" >/dev/null 2>&1; then echo "ERROR: Appium is already listening on port $APPIUM_PORT. Stop the stale server or set APPIUM_PORT." >&2 exit 1 fi echo "[runner] Starting Appium on port $APPIUM_PORT" echo "[runner] Appium logs: $APPIUM_LOG" "$APPIUM_BIN" --port "$APPIUM_PORT" --relaxed-security > "$APPIUM_LOG" 2>&1 & APPIUM_PID=$! for i in $(seq 1 30); do # If Appium crashed between forks (e.g. unhandled driver-load error), bail # out instead of polling /status for 30s. if ! kill -0 "$APPIUM_PID" 2>/dev/null; then echo "ERROR: Appium exited before becoming ready. Appium log follows:" >&2 echo "----- $APPIUM_LOG -----" >&2 cat "$APPIUM_LOG" >&2 || true echo "----- end log -----" >&2 exit 1 fi if curl -sf "http://127.0.0.1:$APPIUM_PORT/status" >/dev/null 2>&1; then echo "[runner] Appium is ready." break fi if [ "$i" -eq 30 ]; then echo "ERROR: Appium did not start within 30 seconds." >&2 exit 1 fi sleep 1 done # ------------------------------------------------------------------------------ # Run WDIO # ------------------------------------------------------------------------------ if [ "${#SPEC_ARGS[@]}" -gt 0 ]; then echo "[runner] Running ${#SPEC_ARGS[@]} spec(s) in a single shared session:" printf ' %s\n' "${SPEC_ARGS[@]}" WDIO_SPEC_ARGS=() for s in "${SPEC_ARGS[@]}"; do WDIO_SPEC_ARGS+=(--spec "$s") done pnpm exec wdio run test/wdio.conf.ts "${WDIO_SPEC_ARGS[@]}" else echo "[runner] Running full E2E suite (single shared session)..." pnpm exec wdio run test/wdio.conf.ts fi