diff --git a/.github/workflows/real-web-source-map.yml b/.github/workflows/real-web-source-map.yml index 8de1bc66d0..1b7c3c7d15 100644 --- a/.github/workflows/real-web-source-map.yml +++ b/.github/workflows/real-web-source-map.yml @@ -48,6 +48,9 @@ jobs: run: npm install --prefix "$RUNNER_TEMP/rea-web-source-map-compiler" --no-audit --no-fund --package-lock=false esbuild@0.25.10 - run: npm run verify:browser:source-maps env: + # Retain browser launch argv/PID/stderr in the verifier's captured + # CLI failure diagnostics without changing its deadline or retrying. + DEBUG: pw:browser REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome REA_BROWSER_NO_SANDBOX: "true" REA_WEB_SOURCE_MAP_COMPILER: ${{ runner.temp }}/rea-web-source-map-compiler/node_modules/esbuild/lib/main.js diff --git a/src/browser/PlaywrightScenarioBrowser.test.ts b/src/browser/PlaywrightScenarioBrowser.test.ts index f2f46d8ba2..a14f555a51 100644 --- a/src/browser/PlaywrightScenarioBrowser.test.ts +++ b/src/browser/PlaywrightScenarioBrowser.test.ts @@ -1109,6 +1109,7 @@ it("applies command_timeout_ms to the primary connect handshake when service wor reason: "timeout", userMessage: expect.stringContaining("50 ms command_timeout_ms"), remediationAction: expect.stringContaining("capture-browser-scenario JSON"), + diagnostics: { phase: "handshake", command_timeout_ms: 50 }, }); expect( (failure as { remediationAction?: string }).remediationAction, @@ -1201,7 +1202,13 @@ it("closes a context that launches after its deadline before removing the profil () => undefined, (cause: unknown) => cause, ); - expect(failure).toMatchObject({ reason: "timeout" }); + expect(failure).toMatchObject({ + reason: "timeout", + userMessage: expect.stringContaining( + "Browser launch and persistent-context startup", + ), + diagnostics: { phase: "launch", command_timeout_ms: 40 }, + }); expect( (failure as { remediationAction?: string }).remediationAction, ).not.toContain("--command-timeout-ms"); @@ -1400,6 +1407,11 @@ it("normalizes a genuine Playwright launch timeout and releases its profile", as reason: "timeout", userMessage: expect.stringContaining("4321 ms command_timeout_ms"), cause: failure, + diagnostics: { + phase: "launch", + command_timeout_ms: 4_321, + producer_message: "native launch deadline", + }, }); expect(profilePath).toBeDefined(); await expect(access(profilePath!)).rejects.toMatchObject({ code: "ENOENT" }); diff --git a/src/browser/PlaywrightScenarioBrowser.ts b/src/browser/PlaywrightScenarioBrowser.ts index 6f62c8541c..11824f709c 100644 --- a/src/browser/PlaywrightScenarioBrowser.ts +++ b/src/browser/PlaywrightScenarioBrowser.ts @@ -872,7 +872,7 @@ export const openPlaywrightScenarioBrowser = async ( : [], }), commandTimeoutMs, - "handshake", + "launch", ); } catch (cause: unknown) { const cleanup = new AnalysisCleanupLease(async () => { diff --git a/src/browser/ScenarioCommandDeadline.ts b/src/browser/ScenarioCommandDeadline.ts index 37fe3cfde8..432eb9a925 100644 --- a/src/browser/ScenarioCommandDeadline.ts +++ b/src/browser/ScenarioCommandDeadline.ts @@ -13,6 +13,9 @@ import { import type { BrowserScenario } from "../domain/browserScenario.js"; const OPERATION = "capture_browser_scenario" as const; +type ScenarioCommandDeadlinePhase = + | Extract + | "launch"; const pendingScenarioCommands = new WeakMap< BrowserObservationError, @@ -39,16 +42,19 @@ const playwrightTimedOut = (cause: unknown): cause is Error => { */ const scenarioCommandDeadlineRecovery = ( timeoutMs: number, - phase: Extract, + phase: ScenarioCommandDeadlinePhase, method?: string, ): { readonly userMessage: string; readonly remediationAction: string } => { - const shared = cdpCommandDeadlineRecovery(timeoutMs, phase, method); + const userMessage = + phase === "launch" + ? `Browser launch and persistent-context startup exceeded its ${String(timeoutMs)} ms command_timeout_ms deadline (deadlineExpired). This is not the caller observation window, the ${String(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)} ms CDP discovery deadline, or the ${String(CDP_CLEANUP_GRACE_MS)} ms cleanup grace.` + : cdpCommandDeadlineRecovery(timeoutMs, phase, method).userMessage; const increase = timeoutMs < MAX_BROWSER_DEADLINE_MS ? `Set command_timeout_ms in the capture-browser-scenario JSON above ${String(timeoutMs)} (integer milliseconds from 1 to ${String(MAX_BROWSER_DEADLINE_MS)}; omitted means ${String(DEFAULT_CDP_COMMAND_TIMEOUT_MS)}).` : `command_timeout_ms is already at the maximum ${String(MAX_BROWSER_DEADLINE_MS)} ms; do not increase it further.`; return { - userMessage: shared.userMessage, + userMessage, remediationAction: `${increase} ${browserDeadlineRetry} command_timeout_ms does not raise the fixed protocol, retained-event, or source byte budgets, the ${String(CDP_DISCOVERY_RESPONSE_TIMEOUT_MS)} ms CDP discovery response deadline, or the ${String(CDP_CLEANUP_GRACE_MS)} ms cleanup grace. Those have no runtime override.`, }; }; @@ -97,7 +103,7 @@ export const releaseLateScenarioCommand = async ( }; /** - * Bound one Playwright CDP handshake or command by the scenario command deadline. + * Bound browser startup, a CDP handshake or a command by the scenario deadline. * The underlying operation stays reachable after a timeout so its owner can * close the returned handle or restore a mutation that lands late. * Playwright's own timeout, when it rejects first, is reported as that deadline. @@ -105,7 +111,7 @@ export const releaseLateScenarioCommand = async ( export const withScenarioCommandDeadline = async ( work: () => Promise, timeoutMs: number, - phase: Extract, + phase: ScenarioCommandDeadlinePhase, method?: string, ): Promise => { let operation: Promise; @@ -116,10 +122,20 @@ export const withScenarioCommandDeadline = async ( } let timer: ReturnType | undefined; let settled = false; - const recovery = (): { - readonly userMessage: string; - readonly remediationAction: string; - } => scenarioCommandDeadlineRecovery(timeoutMs, phase, method); + const deadlineFailure = (cause?: Error): BrowserObservationError => { + const deadline = scenarioCommandDeadlineRecovery(timeoutMs, phase, method); + return new BrowserObservationError(OPERATION, "timeout", { + detail: deadline.userMessage, + remediationAction: deadline.remediationAction, + cause, + diagnostics: { + phase, + command_timeout_ms: timeoutMs, + ...(method === undefined ? {} : { method }), + ...(cause === undefined ? {} : { producer_message: cause.message }), + }, + }); + }; try { return await new Promise((resolve, reject) => { const fail = (cause: unknown): void => { @@ -128,11 +144,7 @@ export const withScenarioCommandDeadline = async ( reject(cause); }; timer = setTimeout(() => { - const deadline = recovery(); - const timeout = new BrowserObservationError(OPERATION, "timeout", { - detail: deadline.userMessage, - remediationAction: deadline.remediationAction, - }); + const timeout = deadlineFailure(); pendingScenarioCommands.set(timeout, operation); fail(timeout); }, timeoutMs); @@ -144,14 +156,7 @@ export const withScenarioCommandDeadline = async ( }, (cause: unknown) => { if (playwrightTimedOut(cause)) { - const deadline = recovery(); - fail( - new BrowserObservationError(OPERATION, "timeout", { - detail: deadline.userMessage, - remediationAction: deadline.remediationAction, - cause, - }), - ); + fail(deadlineFailure(cause)); return; } fail(cause);