From 641ab511c9388665ec632257f41af13a01f4caa7 Mon Sep 17 00:00:00 2001 From: nothingnesses <18732253+nothingnesses@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:50:34 +0100 Subject: [PATCH 1/2] fix(frontend): stop Trunk watching Playwright output during e2e just e2e serves the frontend with trunk serve, whose file watcher covers frontend/. Playwright writes traces, screenshots, and videos into frontend/test-results/ throughout a run, so the watcher fired a storm of rebuilds mid-test (500+ events in one CI run). On a CPU-contended runner this starved the browser's debug-WASM compile/instantiate, leaving the first authenticated page blank past the 10s assertion timeout and intermittently failing the suite. Add the Playwright output dirs to the Trunk [watch] ignore list, and pre-create them in the e2e recipe because Trunk canonicalizes ignore paths at startup and errors on missing ones. Verified locally: the watcher no longer reacts to test artifacts (0 events, down from 533) and the suite passes 4/4. --- frontend/Trunk.toml | 16 ++++++++++++++-- justfile | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/frontend/Trunk.toml b/frontend/Trunk.toml index 62f1692..00943eb 100644 --- a/frontend/Trunk.toml +++ b/frontend/Trunk.toml @@ -29,8 +29,20 @@ inject_scripts = true [watch] # Paths to watch. The `build.target`'s parent folder is watched by default. watch = [] -# Paths to ignore. -ignore = ["node_modules", "style/uno.css"] +# Paths to ignore. Besides build inputs, this excludes the Playwright output +# directories: during `just e2e` the dev server watches `frontend/`, and +# Playwright continuously writes traces, screenshots, and videos into +# `test-results/` (and the HTML report into `playwright-report/`). Without these +# ignores the watcher triggers a storm of rebuilds mid-test, starving the +# browser's WASM compile/instantiate and intermittently leaving pages blank past +# the test timeout. Trunk canonicalizes these paths at startup, so the `e2e` +# recipe creates them first. +ignore = [ + "node_modules", + "style/uno.css", + "test-results", + "playwright-report", +] [serve] # The address to serve on. diff --git a/justfile b/justfile index ed5560c..a8aa7c3 100644 --- a/justfile +++ b/justfile @@ -219,6 +219,10 @@ e2e: frontend-config frontend-e2e-typecheck source scripts/e2e-env.sh source scripts/service-graph.sh mkdir -p "$E2E_LOG_DIR" + # Pre-create Playwright's output dirs so the trunk dev server can canonicalize + # them as watch-ignore paths at startup (see frontend/Trunk.toml). Without this + # the server fails to start on a fresh checkout. + mkdir -p frontend/test-results frontend/playwright-report run_e2e() { local backend_pid="" From 0072363760e54ddc8ec6c852b704a669f41e59ed Mon Sep 17 00:00:00 2001 From: nothingnesses <18732253+nothingnesses@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:56:42 +0100 Subject: [PATCH 2/2] fix(e2e): stream WASM and free server ports on cleanup Two e2e-harness robustness follow-ups flagged while diagnosing the flake: - Disable SRI for the dev server (trunk serve --no-sri). Trunk's default subresource-integrity attribute on the wasm makes the browser fall back from WebAssembly.instantiateStreaming to the slower non-streaming instantiate, which on the large debug bundle slows cold boots. Scoped to e2e; production builds are unchanged. - Stop orphaning the app servers. The backend now execs the prebuilt binary instead of cargo run (avoiding an extra wrapper process), and cleanup frees the backend and frontend ports directly via a new memory_map_free_port helper. The servers run behind a direnv exec/bash wrapper whose pid is what was tracked, so the real server could survive on its port and block the next local run. CI is unaffected (no direnv wrapper, fresh runners). Verified locally: e2e passes 4/4, the served build has no wasm SRI, and both ports are free after the run. --- justfile | 9 +++++++-- scripts/service-graph.sh | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index a8aa7c3..0e46071 100644 --- a/justfile +++ b/justfile @@ -234,11 +234,16 @@ e2e: frontend-config frontend-e2e-typecheck trap - EXIT INT TERM memory_map_stop_pid "${frontend_pid:-}" memory_map_stop_pid "${backend_pid:-}" + # The servers run behind a `direnv exec`/bash wrapper, so the tracked + # pid is the wrapper and the real server can survive on its port. Free + # the ports directly so local re-runs are not blocked by an orphan. + memory_map_free_port "${E2E_FRONTEND_PORT}" + memory_map_free_port "${E2E_BACKEND_PORT}" exit "$status" } trap cleanup_app_servers EXIT INT TERM - {{ direnv_prefix }} bash -c 'cd backend && exec cargo run --bin backend' > "$E2E_LOG_DIR/backend.log" 2>&1 & + {{ direnv_prefix }} bash -c 'cd backend && exec ../target/debug/backend' > "$E2E_LOG_DIR/backend.log" 2>&1 & backend_pid=$! if ! memory_map_wait_for_http "$E2E_BACKEND_URL/" 120 "GraphiQL"; then echo "ERROR: backend did not become ready; see $E2E_LOG_DIR/backend.log." >&2 @@ -246,7 +251,7 @@ e2e: frontend-config frontend-e2e-typecheck return 1 fi - {{ direnv_prefix }} bash -c 'cd frontend && exec env -u NO_COLOR trunk serve --address "$E2E_FRONTEND_HOST" --port "$E2E_FRONTEND_PORT" --no-autoreload --skip-version-check --offline' > "$E2E_LOG_DIR/frontend.log" 2>&1 & + {{ direnv_prefix }} bash -c 'cd frontend && exec env -u NO_COLOR trunk serve --address "$E2E_FRONTEND_HOST" --port "$E2E_FRONTEND_PORT" --no-autoreload --skip-version-check --offline --no-sri' > "$E2E_LOG_DIR/frontend.log" 2>&1 & frontend_pid=$! if ! memory_map_wait_for_http "$E2E_FRONTEND_URL/" 120; then echo "ERROR: frontend did not become ready; see $E2E_LOG_DIR/frontend.log." >&2 diff --git a/scripts/service-graph.sh b/scripts/service-graph.sh index 9d9386e..03350dc 100644 --- a/scripts/service-graph.sh +++ b/scripts/service-graph.sh @@ -65,6 +65,27 @@ memory_map_stop_pid() { fi } +# Stop whatever is still listening on a local TCP port. The e2e servers run +# behind a `direnv exec`/bash wrapper, so the tracked pid is the wrapper and the +# real server (backend, trunk) can survive on its port after the wrapper is +# killed, blocking the next local run. This targets only the given port, so it +# never touches unrelated processes. No-op if `ss` is unavailable (e.g. CI, +# which has no wrapper and so does not orphan in the first place). +memory_map_free_port() { + local port="${1:-}" + local pid + + [[ -n "${port}" ]] || return 0 + command -v ss >/dev/null 2>&1 || return 0 + + # Best-effort cleanup; the pipeline's intermediate exit codes are irrelevant. + # shellcheck disable=SC2312 + for pid in $(ss -ltnp 2>/dev/null | grep ":${port} " | + grep -oE 'pid=[0-9]+' | grep -oE '[0-9]+' | sort -u); do + kill "${pid}" 2>/dev/null || true + done +} + memory_map_with_process_compose() { local port="$1" local log_path="$2"