openpencil/tools/check-web-browser-smoke.sh
Kayshen-X c4a2d5e0d7 fix(web): browser smoke reads the mount marker from the console stream
The editor now holds a live SSE stream, and an in-flight network
request pauses headless Chrome's virtual clock — so --dump-dom never
flushes, the DOM grep never matches, and the smoke either timed out
(CI, 45s) or hung on a TERM-immune Chrome behind a bare wait (observed
12 hours locally). The mount marker is now mirrored to the console,
which reaches the stderr log incrementally, and the poll loop treats
that line as readiness, synthesizing the DOM marker the assertions
expect; the timeout path uses SIGKILL. Verified end to end locally:
both the pure CanvasKit page and the daemon host page mount.
2026-08-09 11:27:59 +08:00

199 lines
5.8 KiB
Bash
Executable file

#!/usr/bin/env bash
# Browser smoke for the Rust CanvasKit web bundle.
#
# Covers both browser paths:
# 1. `/smoke/step-1b.html` — pure CanvasKit mount harness.
# 2. `/` — daemon-served host page, including `/api/mcp/sync-reset`.
#
# The script intentionally uses Chrome/Chromium headless directly instead of a
# new test dependency. The pages write `data-op-smoke="ok"` only after the wasm
# bundle has loaded and `mount_ck()` has resolved.
set -euo pipefail
cd "$(dirname "$0")/.."
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
need() {
command -v "$1" >/dev/null 2>&1 || {
printf 'missing prerequisite: %s\n' "$1" >&2
exit 2
}
}
find_chrome() {
if [ -n "${OPENPENCIL_CHROME_BIN:-}" ]; then
printf '%s\n' "$OPENPENCIL_CHROME_BIN"
return
fi
for bin in google-chrome-stable google-chrome chromium chromium-browser; do
if command -v "$bin" >/dev/null 2>&1; then
command -v "$bin"
return
fi
done
for app in \
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
"/Applications/Chromium.app/Contents/MacOS/Chromium"; do
if [ -x "$app" ]; then
printf '%s\n' "$app"
return
fi
done
return 1
}
pick_port() {
python3 - <<'PY'
import socket
s = socket.socket()
s.bind(("127.0.0.1", 0))
print(s.getsockname()[1])
s.close()
PY
}
wait_for_health() {
local url="$1"
local tries="${OPENPENCIL_BROWSER_SMOKE_WAIT_TRIES:-100}"
for _ in $(seq 1 "$tries"); do
if curl -fsS "$url" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
done
return 1
}
assert_contains() {
local file="$1"
local needle="$2"
local label="$3"
if ! grep -Fq "$needle" "$file"; then
printf 'FAIL: %s\nexpected to find: %s\n--- %s ---\n' "$label" "$needle" "$file" >&2
sed -n '1,220p' "$file" >&2
return 1
fi
}
chrome_dump() {
local url="$1"
local out="$2"
local profile="$TMP_DIR/chrome-$(basename "$out" .html)"
local timeout="${OPENPENCIL_BROWSER_SMOKE_TIMEOUT_SECS:-45}"
mkdir -p "$profile"
"$CHROME_BIN" \
--headless=new \
--no-sandbox \
--disable-dev-shm-usage \
--disable-background-networking \
--disable-component-update \
--disable-default-apps \
--disable-sync \
--ignore-gpu-blocklist \
--metrics-recording-only \
--no-default-browser-check \
--no-first-run \
--use-gl=swiftshader \
--enable-unsafe-swiftshader \
--window-size=1280,800 \
--user-data-dir="$profile" \
--enable-logging=stderr \
--virtual-time-budget="${OPENPENCIL_BROWSER_SMOKE_VIRTUAL_TIME_MS:-15000}" \
--dump-dom "$url" >"$out" 2>"$out.stderr" &
local chrome_pid=$!
for _ in $(seq 1 "$timeout"); do
# The editor holds a live SSE stream, which pauses headless Chrome's
# virtual clock and keeps --dump-dom from ever flushing. The console
# mirror on stderr arrives incrementally, so it is the readiness
# signal; a successful mount synthesizes the DOM line the assertions
# expect.
if grep -Fq 'op-smoke:ok' "$out.stderr" 2>/dev/null; then
printf '<html data-op-smoke="ok"><!-- synthesized from console marker --></html>\n' >"$out"
kill -9 "$chrome_pid" >/dev/null 2>&1 || true
wait "$chrome_pid" >/dev/null 2>&1 || true
return 0
fi
if grep -Fq 'data-op-smoke="ok"' "$out" 2>/dev/null; then
kill "$chrome_pid" >/dev/null 2>&1 || true
wait "$chrome_pid" >/dev/null 2>&1 || true
return 0
fi
if ! kill -0 "$chrome_pid" >/dev/null 2>&1; then
wait "$chrome_pid"
return $?
fi
sleep 1
done
# SIGKILL, not SIGTERM: a headless Chrome wedged mid-GPU-stall has been
# observed ignoring TERM, which turns the wait below into a permanent hang.
kill -9 "$chrome_pid" >/dev/null 2>&1 || true
wait "$chrome_pid" >/dev/null 2>&1 || true
printf 'headless Chrome timed out for %s after %ss\n' "$url" "$timeout" >&2
return 124
}
need curl
need python3
CHROME_BIN="$(find_chrome)" || {
printf 'missing prerequisite: Chrome/Chromium (set OPENPENCIL_CHROME_BIN)\n' >&2
exit 2
}
if [ "${OPENPENCIL_SKIP_WASM_BUILD:-0}" != "1" ]; then
bash tools/check-wasm-bundle.sh
fi
SERVER_BIN="${OPENPENCIL_WEB_SERVER_BIN:-target/release/op-host-web-server}"
if [ "${OPENPENCIL_SKIP_SERVER_BUILD:-0}" != "1" ]; then
cargo build -p op-host-web-server --release >/dev/null
fi
[ -x "$SERVER_BIN" ] || fail "server binary is not executable: $SERVER_BIN"
TMP_DIR="$(mktemp -d)"
SERVER_PID=""
cleanup() {
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" >/dev/null 2>&1; then
kill "$SERVER_PID" >/dev/null 2>&1 || true
wait "$SERVER_PID" >/dev/null 2>&1 || true
fi
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
PORT="${OPENPENCIL_WEB_SMOKE_PORT:-$(pick_port)}"
BASE_URL="http://127.0.0.1:${PORT}"
SERVER_LOG="$TMP_DIR/server.log"
OPENPENCIL_WEB_BUNDLE_DIR="${OPENPENCIL_WEB_BUNDLE_DIR:-$PWD/crates/op-host-web/pkg}" \
OPENPENCIL_CANVASKIT_DIR="${OPENPENCIL_CANVASKIT_DIR:-$PWD/crates/op-host-web/assets/canvaskit}" \
"$SERVER_BIN" --serve-web "$PORT" --host 127.0.0.1 >"$SERVER_LOG" 2>&1 &
SERVER_PID="$!"
wait_for_health "$BASE_URL/api/mcp/server" || {
printf 'server did not become healthy\n--- server log ---\n' >&2
cat "$SERVER_LOG" >&2
exit 1
}
health="$TMP_DIR/health.json"
curl -fsS "$BASE_URL/api/mcp/server" >"$health"
assert_contains "$health" '"mode":"web-canvas"' "daemon health should report web-canvas mode"
curl -fsS "$BASE_URL/pkg/op_host_web.js" >/dev/null
curl -fsS "$BASE_URL/canvaskit/canvaskit.js" >/dev/null
curl -fsS "$BASE_URL/assets/iconify-catalog-brands.json" >/dev/null
pure="$TMP_DIR/pure-smoke.html"
chrome_dump "$BASE_URL/smoke/step-1b.html" "$pure"
assert_contains "$pure" 'data-op-smoke="ok"' "pure CanvasKit smoke should mount"
daemon="$TMP_DIR/daemon-smoke.html"
chrome_dump "$BASE_URL/" "$daemon"
assert_contains "$daemon" 'data-op-smoke="ok"' "daemon host page should mount"
printf 'PASS: browser smoke mounted CanvasKit via /smoke/step-1b.html and daemon /\n'