fix(browser): report launch timeout stage and startup diagnostics

This commit is contained in:
morluto committed 2026-10-24 08:22:38 +08:00
1 parent f2dfda4b36
commit c765b3b413
4 files changed
+44 -24

No files matched your search

@@ -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
+13 -1
View File
@@ -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" });
+1 -1
View File
@@ -872,7 +872,7 @@ export const openPlaywrightScenarioBrowser = async (
: [],
}),
commandTimeoutMs,
"handshake",
"launch",
);
} catch (cause: unknown) {
const cleanup = new AnalysisCleanupLease(async () => {
+27 -22
View File
@@ -13,6 +13,9 @@ import {
import type { BrowserScenario } from "../domain/browserScenario.js";
const OPERATION = "capture_browser_scenario" as const;
type ScenarioCommandDeadlinePhase =
| Extract<CdpCommandDeadlinePhase, "handshake" | "command">
| "launch";
const pendingScenarioCommands = new WeakMap<
BrowserObservationError,
@@ -39,16 +42,19 @@ const playwrightTimedOut = (cause: unknown): cause is Error => {
*/
const scenarioCommandDeadlineRecovery = (
timeoutMs: number,
phase: Extract<CdpCommandDeadlinePhase, "handshake" | "command">,
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 <T>(
work: () => Promise<T>,
timeoutMs: number,
phase: Extract<CdpCommandDeadlinePhase, "handshake" | "command">,
phase: ScenarioCommandDeadlinePhase,
method?: string,
): Promise<T> => {
let operation: Promise<T>;
@@ -116,10 +122,20 @@ export const withScenarioCommandDeadline = async <T>(
}
let timer: ReturnType<typeof setTimeout> | 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<T>((resolve, reject) => {
const fail = (cause: unknown): void => {
@@ -128,11 +144,7 @@ export const withScenarioCommandDeadline = async <T>(
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 <T>(
},
(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);