Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions playground/tests/e2e-headless/targets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// UI targets for the headless full-bundle driver — the single place selector
// knowledge lives; update here when the playground UI moves.

// Deep-links straight to DiagnosisView (playground/src/app/page.tsx), skipping
// the left-rail click-through.
export const DIAGNOSIS_PATH = "/?view=diagnosis";

// Diagnosis never auto-starts on mount (DiagnosisView) — this button's onClick
// is the only trigger.
export const RUN_ALL_SELECTOR = '[data-testid="diagnosis-run"]';

// Connection status chip rendered by StatusChip (playground/src/app/page.tsx).
export const STATUS_CHIP_SELECTOR = ".status__label";

// Summary text reads "{passCount} success · {failCount} failed" (DiagnosisView).
export const FAILED_COUNT_SELECTOR = '[data-testid="diagnosis-summary"]';

// Parses the "N success · M failed" summary text.
export const FAILED_COUNT_PATTERN = /(\d+)\s+failed\b/;

// Set once the run has finished rendering its report (DiagnosisView).
export const REPORT_READY_SELECTOR =
'[data-testid="diagnosis-report-markdown"][data-report-ready="true"]';

// A single Diagnosis result row in the failed state (DiagnosisView).
export const FAILED_ROW_SELECTOR = '[data-testid="diagnosis-row"][data-status="fail"]';
158 changes: 158 additions & 0 deletions rust/crates/truapi-host-cli/e2e/full-bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
# Full-bundle driver: the real playground bundle, served statically and run
# browserless under bun + happy-dom, drives a pairing host through the
# MessagePort->WS bridge; a signing host answers the pairing deeplink; every
# operation lands in METRICS_JSONL.
#
# make headless && e2e/full-bundle.sh
#
# Env:
# PRODUCT_ID product id the pairing host serves (default truapi-playground.dot)
# FRAME frame-server address (default 127.0.0.1:9955)
# METRICS_JSONL metrics sink (default /tmp/full-bundle-metrics.jsonl)
# SKIP_BUILD set to reuse an existing playground/out
# PLAYGROUND_OUT static export dir the shim serves (default playground/out)
# FULL_BUNDLE_DEADLINE_MS shim deadline ms (default 1200000 -- TS-side default)
# E2E_LIVE_CHAIN live-chain routing for Chain/* (default 1)
# HOST_CLI_SIGNER_MNEMONIC as in run.sh (e2e/.env supported)
# TRUAPI_HOST_BASE_PATH signing host only -- the pairing host always gets a fresh temp path
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
BIN="$ROOT/target/debug/truapi-host"
ENV_FILE="$(dirname "$0")/.env"
[ -f "$ENV_FILE" ] && { set -a; . "$ENV_FILE"; set +a; }

PRODUCT_ID="${PRODUCT_ID:-truapi-playground.dot}"
FRAME="${FRAME:-127.0.0.1:9955}"
export METRICS_JSONL="${METRICS_JSONL:-/tmp/full-bundle-metrics.jsonl}"
# Live-chain routing for the Chain/* methods, matching the dotli baseline.
# Upstream keeps this opt-in because live chainHead streaming over the shared
# product connection can still drop mid-run (see src/chain.rs) — expect the
# head-subscription methods to be the flaky tail.
export E2E_LIVE_CHAIN="${E2E_LIVE_CHAIN:-1}"

[ -x "$BIN" ] || { echo "missing $BIN — run: make headless" >&2; exit 2; }

if [ -z "${SKIP_BUILD:-}" ] || [ ! -d "$ROOT/playground/out" ]; then
(cd "$ROOT/js/packages/truapi" && npm run build)
(cd "$ROOT/playground" && yarn build)
fi

LOG="$(mktemp)"
PAIR_PID=""
SIGNER_PID=""
WATCHER_PID=""
KEEPALIVE_PID=""
# Fresh pairing-host state per run: the host persists its paired session under
# the base path, so a reused path silently restores the previous run's session
# and the run is not reproducible. The signing host keeps its own (default)
# base path on purpose, to reuse its attested account across runs.
PAIR_BASE="$(mktemp -d)"
cleanup() {
rm -f "$LOG.signerlog"
[ -n "$WATCHER_PID" ] && kill "$WATCHER_PID" 2>/dev/null || true
[ -n "$SIGNER_PID" ] && kill "$SIGNER_PID" 2>/dev/null || true
# Independent of $SIGNER_PID: if the script exits before the main flow
# reaps the signer (e.g. an early/interrupted exit), this is the only path
# left that still catches it.
if [ -f "$LOG.signer" ]; then
kill "$(cat "$LOG.signer")" 2>/dev/null || true
rm -f "$LOG.signer"
fi
if [ -n "$PAIR_PID" ]; then
pkill -TERM -P "$PAIR_PID" 2>/dev/null || true
kill -TERM "$PAIR_PID" 2>/dev/null || true
sleep 0.5
pkill -KILL -P "$PAIR_PID" 2>/dev/null || true
kill -KILL "$PAIR_PID" 2>/dev/null || true
fi
rm -rf "$PAIR_BASE"
# The keepalive tail runs under a process-substitution wrapper shell; kill
# the wrapper's children before the wrapper itself (killing only the wrapper
# reparents the tail to init and leaks it).
if [ -n "$KEEPALIVE_PID" ]; then
pkill -TERM -P "$KEEPALIVE_PID" 2>/dev/null || true
kill -TERM "$KEEPALIVE_PID" 2>/dev/null || true
fi
# Success path removes $LOG itself before this trap runs; if it's still
# here, this exit was a failure -- keep it for post-mortem instead of
# silently deleting the only evidence.
# `|| true`: as the trap's last command, a false [ -f ] would otherwise
# become the script's exit status and turn every SUCCESSFUL run into exit 1.
[ -f "$LOG" ] && echo "host log preserved: $LOG" >&2 || true
}
trap cleanup EXIT

: > "$METRICS_JSONL"
# stdin keepalive: with </dev/null the interactive loop sees EOF, returns, and
# with_frame_server aborts the frame server. tail -f
# keeps stdin open without ever writing to it. The substitution is exec'd onto
# fd 3 instead of inlined on the host command so its pid lands in $! and the
# EXIT trap can reap it — inlined, the tail outlives cleanup as an orphan.
exec 3< <(tail -f /dev/null)
KEEPALIVE_PID=$!
"$BIN" pairing-host --product-id "$PRODUCT_ID" --frame-listen "$FRAME" \
--base-path "$PAIR_BASE" \
--auto-accept <&3 > >(tee "$LOG") 2>&1 &
PAIR_PID=$!

# Don't race the shim run against a socket that isn't bound yet: wait
# for the host's own "listening" line, checking every 0.5s for up to 60s that
# it hasn't died in the meantime.
for _ in $(seq 1 120); do
grep -q '^FRAMES_LISTENING' "$LOG" && break
kill -0 "$PAIR_PID" 2>/dev/null || { echo "pairing host exited before FRAMES_LISTENING" >&2; exit 1; }
sleep 0.5
done
grep -q '^FRAMES_LISTENING' "$LOG" || { echo "pairing host: no FRAMES_LISTENING within 60s" >&2; exit 1; }

# The deeplink appears on the first requestLogin, so watch for it concurrently
# and answer with the signing host (its output goes to $LOG.signerlog so the
# allowance wait below can grep it).
(
for _ in $(seq 1 600); do
deeplink="$(grep -m1 -oE 'PAIRING_DEEPLINK .+' "$LOG" | cut -d' ' -f2- || true)"
if [ -n "$deeplink" ]; then
"$BIN" signing-host --deeplink "$deeplink" --auto-accept > >(tee "$LOG.signerlog") 2>&1 &
echo "$!" > "$LOG.signer"
exit 0
fi
sleep 0.5
done
echo "watcher: no deeplink within 300s" >&2
) &
WATCHER_PID=$!

# Login-first, like the dotli baseline and the fleet: a minimal requestLogin
# script triggers pairing, then we wait for the device-key statement-store
# allowance — submits race the allowance registration otherwise.
echo "pre-login: requesting session"
(cd "$ROOT/rust/crates/truapi-host-cli/js" && TRUAPI_FRAME_URL="ws://$FRAME" \
TRUAPI_PRODUCT_ID="$PRODUCT_ID" \
TRUAPI_SCRIPT="$ROOT/rust/crates/truapi-host-cli/js/scripts/pre-login.ts" \
bun runner.ts) || { echo "pre-login failed" >&2; exit 1; }
for _ in $(seq 1 240); do
grep -q "SIGNING_HOST_ALLOWANCE device seq=" "$LOG.signerlog" 2>/dev/null && break
sleep 0.5
done
grep -q "SIGNING_HOST_ALLOWANCE device seq=" "$LOG.signerlog" 2>/dev/null \
|| echo "pre-login: device allowance not confirmed within 120s; continuing" >&2

STATUS=0
(cd "$ROOT/rust/crates/truapi-host-cli/js" && { [ -d node_modules ] || bun install; } \
&& TRUAPI_FRAME_URL="ws://$FRAME" bun scripts/load-bundle.ts) || STATUS=$?
[ -f "$LOG.signer" ] && SIGNER_PID="$(cat "$LOG.signer")" && rm -f "$LOG.signer"

[ "$STATUS" -eq 0 ] || exit "$STATUS"
[ -s "$METRICS_JSONL" ] || { echo "no metrics recorded in $METRICS_JSONL" >&2; exit 1; }
# A single "category":"signing" grep passes even if every signing op failed.
# Gate on each battery-critical op individually succeeding (op field always
# precedes outcome in HostMetricRecord's field order, so one pattern per line
# suffices -- see rust/crates/truapi-host-cli/src/metrics.rs).
for op in signing_sign_raw signing_sign_payload signing_create_transaction; do
grep -q "\"op\":\"$op\".*\"outcome\":\"success\"" "$METRICS_JSONL" || {
echo "no successful $op recorded in $METRICS_JSONL" >&2; exit 1; }
done
echo "full-bundle spike OK -> $METRICS_JSONL ($(wc -l < "$METRICS_JSONL") records)"
rm -f "$LOG"
34 changes: 34 additions & 0 deletions rust/crates/truapi-host-cli/js/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions rust/crates/truapi-host-cli/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "truapi-host-cli-js",
"private": true,
"dependencies": {
"@happy-dom/global-registrator": "20.11.0",
"happy-dom": "20.11.0"
}
}
Loading