From 556aaca2983b9d63de01b56b85cafcdfffa642d8 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 6 Aug 2026 09:40:50 -0400 Subject: [PATCH] fix: fail smoke:web:app on async crashes, not just sync page errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot's second pass (suppressed comments) caught a real gap: the smoke listened only for `pageerror`, which is the *synchronous* half of the uncaught-crash class. Its async twin — an unhandled rejection, or a failed dynamic import (this app lazy-loads chunks) — is not a `pageerror`; Chromium reports it on the console channel. So an async crash during the app-open path could leave the smoke green. smoke-web-browser.mjs already solved this; this now mirrors it exactly — same FATAL_CONSOLE pattern, same split between hard failures and diagnostics, so benign noise (a font-CDN miss, a React warning) still can't flake CI. Fatal console errors are now included both in the catch-block diagnostics and in the final assertion, which is where they were missing. Confirmed working on a passing run: a benign 409 subresource error is reported as a non-fatal diagnostic rather than failing the smoke. Full `npm run ci` green. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XAR2Nr9kXbrywFNUVoTe9F --- scripts/smoke-web-app.mjs | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/scripts/smoke-web-app.mjs b/scripts/smoke-web-app.mjs index e2c57b51..261663c0 100644 --- a/scripts/smoke-web-app.mjs +++ b/scripts/smoke-web-app.mjs @@ -89,6 +89,12 @@ const HOST = "127.0.0.1"; const PORT = process.env.SMOKE_WEB_APP_PORT ?? "6297"; const TOKEN = "smoke-web-app-token"; const APP_TOOL = "mcp_app_demo"; +// Console messages that are the async half of the uncaught-crash class (an +// unhandled rejection or a failed dynamic import). Hard failures; every other +// console error is a diagnostic, so benign font-CDN / React-warning noise can't +// flake CI. Kept identical to smoke-web-browser.mjs, which documents the +// reasoning at length. +const FATAL_CONSOLE = /^Uncaught\b|Failed to fetch dynamically imported module/; // The URL the test server announces on startup. NOT derived from the config's // port: createTestServerHttp resolves its port with findAvailablePort(), which // walks UPWARD from the configured value when it's taken — so the config port is @@ -219,10 +225,19 @@ try { browser = await loadChromium(); const page = await browser.newPage(); + // Uncaught *synchronous* page errors. Their *async* twin — an unhandled + // rejection or a failed dynamic import — is not a `pageerror`; Chromium + // reports it on the console channel instead, so both are captured and both + // are hard failures. Same split as smoke:web:browser; see FATAL_CONSOLE there. const pageErrors = []; + const consoleErrors = []; page.on("pageerror", (err) => pageErrors.push(err instanceof Error ? err.message : String(err)), ); + page.on("console", (msg) => { + if (msg.type() === "error") consoleErrors.push(msg.text()); + }); + const fatalConsole = () => consoleErrors.filter((m) => FATAL_CONSOLE.test(m)); // Deep link: connect, switch to the Apps tab, pre-select the app tool, and // fire "Open App". autoConnect/autoOpen must equal the session token (CSRF @@ -285,15 +300,33 @@ try { try { await Promise.race([server.whenChildExits(), drive()]); } catch (err) { + const diagnostics = [ + ...pageErrors, + ...fatalConsole().map((m) => `console: ${m}`), + ]; await fail( `${err instanceof Error ? err.message : String(err)}${ - pageErrors.length ? ` — page errors: ${pageErrors.join("; ")}` : "" + diagnostics.length + ? ` — page diagnostics: ${diagnostics.join("; ")}` + : "" }`, ); } - if (pageErrors.length > 0) { - await fail(`app logged uncaught error(s): ${pageErrors.join("; ")}`); + // Hard failures: any uncaught sync page error, plus the console errors that + // are the async half of the same class. + const fatal = [...pageErrors, ...fatalConsole()]; + if (fatal.length > 0) { + await fail(`app logged uncaught error(s): ${fatal.join("; ")}`); + } + + // Non-fatal console errors: surface them so a real problem isn't invisible, + // without failing on benign subresource/warning noise. + const benignConsole = consoleErrors.filter((m) => !FATAL_CONSOLE.test(m)); + if (benignConsole.length > 0) { + console.log( + `smoke:web:app note — ${benignConsole.length} non-fatal console error(s): ${benignConsole.join("; ")}`, + ); } console.log(