fix(install-e2e): boot the app-update legs against a REAL configured provider

The app-update legs died on the onboarding overlay - a fullscreen div
that intercepts every click (the Settings click timed out under it).
The old plan seeded a fake provider key, which lies: the overlay
vanishes but the app is broken. Instead the driver now runs the
desktop E2E suite's own mock inference server
(tests-js/scripts/mock-server.ts, zero deps, bare-node type
stripping >=22.18) and configures it into HERMES_HOME byte-for-byte
like the dev:mock flow: config.yaml provider + MOCK_API_KEY env. The
app boots genuinely configured - no overlay, real chat surface.

e2e-assets/mock-provider.{sh,mjs} own start/stop (pid + url files;
the wrapper lives until SIGTERM - gating on stdin-close made the
server die instantly, a background process's stdin is already EOF)
and the config write. Wired into the posix script driver's
hermes-desktop-app-update arm and the macos driver's update phase
(both app-update methods). The Playwright flow keeps its defense-in-
depth: the real escape hatch ('I'll choose a provider later') and the
verified 'Open settings' selector.

Probed locally: models + streamed/non-streamed completions answer,
server stops cleanly on kill.
This commit is contained in:
ethernet
2026-08-12 10:36:09 -04:00
parent fd02bd8621
commit 7721b26a9a
5 changed files with 134 additions and 7 deletions
+16 -7
View File
@@ -120,15 +120,24 @@ async function main() {
}
const deadline = Date.now() + Number(values['timeout-ms']);
// Dismiss the onboarding overlay when present (fresh HERMES_HOME).
const skip = window.getByRole('button', { name: /skip|get started|continue/i }).first();
if (await skip.isVisible({ timeout: 5_000 }).catch(() => false)) {
await skip.click().catch(() => {});
// Dismiss the onboarding overlay when present. Two layers of defense:
// the drivers seed a provider key so the app's runtime check reports
// configured=true and the overlay never mounts; if it shows anyway
// (fresh HERMES_HOME, slow readiness check), click the real escape
// hatch - "I'll choose a provider later" (i18n en: chooseLater). The
// overlay is a fullscreen div that intercepts ALL clicks, so this must
// resolve before any Settings navigation.
const later = window
.getByRole('button', { name: /choose a provider later|skip/i })
.first();
if (await later.isVisible({ timeout: 10_000 }).catch(() => false)) {
await later.click().catch(() => {});
await later.waitFor({ state: 'hidden', timeout: 15_000 }).catch(() => {});
}
// Settings -> About -> Update now. Selectors favor accessible names over
// DOM structure so renderer refactors don't break the leg.
await window.getByRole('button', { name: /settings/i }).first().click();
// Settings -> About -> Update now. The settings trigger is an icon
// button whose accessible name is "Open settings".
await window.getByRole('button', { name: /open settings|settings/i }).first().click();
await window.getByRole('tab', { name: /about/i }).or(
window.getByRole('button', { name: /about/i })).first().click();
const updateNow = window.getByRole('button', { name: /update now/i }).first();
@@ -0,0 +1,31 @@
// Driver-side wrapper around tests-js/scripts/mock-server.ts (the desktop
// E2E suite's OpenAI-compatible mock): starts the server as a LIBRARY
// (importing, not executing, so the dev-launcher block never runs) and
// publishes its URL to a file for the shell driver to consume.
//
// Usage: node mock-provider.mjs <url-file>
// Writes "<url-file>" with the base URL (http://127.0.0.1:<port>) once
// the server is listening, then stays alive until stdin closes.
// @ts-check
import fs from 'node:fs';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const mockUrl = fileURLToPath(new URL('../../../tests-js/scripts/mock-server.ts', import.meta.url));
const { startMockServer } = await import(mockUrl);
const mock = await startMockServer();
const urlFile = process.argv[2];
if (!urlFile) {
console.error('usage: node mock-provider.mjs <url-file>');
process.exit(1);
}
fs.writeFileSync(urlFile, mock.url);
console.log(`[mock-provider] listening at ${mock.url}`);
// Live until killed. NOT gated on stdin closing: a backgrounded process's
// stdin is already at EOF, so an end-event exit would fire immediately
// and the server would die right after writing its URL.
await new Promise(() => {});
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Start/stop the mock inference server and point the app at it as a REAL
# configured provider.
#
# Why: the desktop app boots to the onboarding overlay when no provider is
# configured - a fullscreen div that intercepts every click, which killed
# the app-update legs. Seeding a fake key makes the overlay vanish but
# leaves a lying app; this makes the app GENUINELY configured (config.yaml
# provider + key env, exactly what tests-js/scripts/mock-server.ts's
# dev:mock flow writes) with a real, chat-capable backend.
#
# Usage (sourced from a driver):
# mock_start <workroot> start, write config into $HERMES_HOME
# mock_stop kill the background server
#
# Requires: ASSETS (e2e-assets dir), LOG_DIR, HERMES_HOME, ok/fail helpers.
MOCK_PIDFILE=""
MOCK_URLFILE=""
mock_start() {
local workroot="${1:?mock_start needs a workroot}"
MOCK_PIDFILE="$workroot/mock.pid"
MOCK_URLFILE="$workroot/mock.url"
rm -f "$MOCK_PIDFILE" "$MOCK_URLFILE"
# Bare `node file.ts` type-stripping works on node >=22.18 (the images
# ship 22.22+; the installed managed node is >=26) - same contract as
# the repo's own `dev:mock` script.
node "$ASSETS/mock-provider.mjs" "$MOCK_URLFILE" > "$LOG_DIR/mock.log" 2>&1 &
echo $! > "$MOCK_PIDFILE"
local _i
for _i in 1 2 3 4 5 6 7 8 9 10; do
[ -s "$MOCK_URLFILE" ] && break
sleep 0.2
done
if [ ! -s "$MOCK_URLFILE" ]; then
log_group "mock server transcript" "$LOG_DIR/mock.log"
fail "mock inference server did not come up; transcript above"
fi
local url
url="$(cat "$MOCK_URLFILE")"
ok "mock inference server: $url"
# The provider config, byte-compatible with writeMockConfig() in
# tests-js/scripts/mock-server.ts.
cat > "$HERMES_HOME/config.yaml" <<EOF
model:
default: mock-model
provider: mock
providers:
mock:
api: $url/v1
name: Mock
api_mode: chat_completions
key_env: MOCK_API_KEY
models:
mock-model: {}
context_length: 4096
EOF
printf 'MOCK_API_KEY=e2e-mock-key\n' >> "$HERMES_HOME/.env"
ok "provider 'mock' configured in $HERMES_HOME (api $url/v1)"
}
mock_stop() {
if [ -n "$MOCK_PIDFILE" ] && [ -f "$MOCK_PIDFILE" ]; then
kill "$(cat "$MOCK_PIDFILE")" 2>/dev/null || true
rm -f "$MOCK_PIDFILE"
fi
}
+9
View File
@@ -357,6 +357,15 @@ case "$UPDATE_METHOD" in
ASSETS="$REPO_ROOT/tests/install/e2e-assets"
SPEC="$WORK_ROOT/launch-spec.json"
# A REAL configured provider: the mock inference server (the desktop E2E
# suite's own) is configured into HERMES_HOME exactly like the dev:mock
# flow does. The app then boots genuinely configured - no onboarding
# overlay (a fullscreen div that intercepts every click) - and the chat
# surface is real too.
source "$ASSETS/mock-provider.sh"
mock_start "$WORK_ROOT"
trap mock_stop EXIT
step "capturing the hermes desktop launch spec (build runs for real)"
rc=0
(cd "$INSTALL_DIR" && \
+7
View File
@@ -270,6 +270,13 @@ phase_update() {
ok "serve.git main = $HEAD_SHA"
step "updating via $UPDATE_METHOD"
# The app must boot configured or the onboarding overlay (a fullscreen
# div) eats every click: configure the mock inference server exactly like
# the dev:mock flow does, so the app is genuinely configured.
# shellcheck source=../install/e2e-assets/mock-provider.sh
source "$ASSETS/mock-provider.sh"
mock_start "$WORK_ROOT"
trap mock_stop EXIT
case "$UPDATE_METHOD" in
open-app-update)
# The installed app IS the user surface here (double-click the .app);