fix(browser): retain launch failure diagnostics (#7826)

This commit is contained in:
morluto
2026-10-29 01:56:39 +08:00
committed by GitHub
parent 0a272d5cf5
commit 44331d04d2
4 changed files with 115 additions and 9 deletions
+27 -7
View File
@@ -86,7 +86,9 @@ it("passes the selected environment to the actual browser launch boundary", asyn
},
},
}),
).rejects.toThrow("launch boundary reached");
).rejects.toMatchObject({
diagnostics: { startup_error: "launch boundary reached" },
});
expect(launchedEnvironment).toEqual({
LANG: "selected-language",
PATH: "/selected/bin",
@@ -126,7 +128,9 @@ it.skipIf(process.platform === "win32")(
},
},
),
).rejects.toThrow("fake launch boundary reached");
).rejects.toMatchObject({
diagnostics: { startup_error: "fake launch boundary reached" },
});
expect(launchedExecutable).toBe(selected);
expect(selected).toBe(`${root}/link/../browser`);
@@ -163,7 +167,9 @@ it("keeps Termux execution support when launching Chromium on Android", async ()
},
},
}),
).rejects.toThrow("launch boundary reached");
).rejects.toMatchObject({
diagnostics: { startup_error: "launch boundary reached" },
});
expect(launchOptions?.args).toEqual(["--no-sandbox", "--single-process"]);
expect(launchOptions?.env).toEqual({
@@ -705,7 +711,9 @@ it("cancels same-target queued connection before acquiring a browser", async ()
});
await expect(
openPlaywrightScenarioBrowser(launch, environment, { launcher }),
).rejects.toThrow("launch boundary reached");
).rejects.toMatchObject({
diagnostics: { startup_error: "launch boundary reached" },
});
expect(launchPersistentContext).toHaveBeenCalledTimes(1);
if (rejectFirst === undefined)
@@ -1054,7 +1062,9 @@ it("applies command_timeout_ms to launch startup", async () => {
},
},
),
).rejects.toThrow("launch boundary reached");
).rejects.toMatchObject({
diagnostics: { startup_error: "launch boundary reached" },
});
expect(timeout).toBe(4_321);
});
@@ -1081,7 +1091,9 @@ it("uses the default command deadline for an omitted launch timeout", async () =
},
},
),
).rejects.toThrow("launch boundary reached");
).rejects.toMatchObject({
diagnostics: { startup_error: "launch boundary reached" },
});
expect(timeout).toBe(DEFAULT_CDP_COMMAND_TIMEOUT_MS);
});
@@ -1399,7 +1411,14 @@ it.each([
},
},
),
).rejects.toBe(failure);
).rejects.toMatchObject({
_tag: "ProviderAdapterError",
cause: failure,
diagnostics: {
executable_path: process.execPath,
startup_error: failure.message,
},
});
expect(profilePath).toBeDefined();
await expect(access(profilePath!)).rejects.toMatchObject({
code: "ENOENT",
@@ -1444,6 +1463,7 @@ it("normalizes a genuine Playwright launch timeout and releases its profile", as
diagnostics: {
phase: "launch",
command_timeout_ms: 4_321,
executable_path: process.execPath,
producer_message: "native launch deadline",
},
});
+27 -2
View File
@@ -869,8 +869,10 @@ export const openPlaywrightScenarioBrowser = async (
let context: BrowserContext;
const executablePath = scenario.browser.executable_path;
const headless = scenario.browser.headless;
let launchStarted = false;
try {
await chmod(profilePath, 0o700);
launchStarted = true;
context = await withScenarioCommandDeadline(
() =>
launcher.launchPersistentContext(profilePath, {
@@ -902,16 +904,39 @@ export const openPlaywrightScenarioBrowser = async (
}),
commandTimeoutMs,
"launch",
undefined,
executablePath,
);
} catch (cause: unknown) {
const cleanup = new AnalysisCleanupLease(async () => {
await releaseLateScenarioCommand(cause);
await rm(profilePath, { recursive: true, force: true, maxRetries: 3 });
}, ["browser_profile"]);
// Preserve Playwright's launch diagnostics before the generic provider
// boundary discards an untyped error. Keep the original cause for late
// command cleanup and leave typed timeout/cancellation failures intact.
const primary =
launchStarted && !(cause instanceof AnalysisError)
? new ProviderAdapterError(
PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY.id,
OPERATION,
{
cause,
userMessage: "The selected browser could not start.",
remediationAction:
"Review the selected executable and startup diagnostics, then correct the launch configuration before retrying.",
diagnostics: {
executable_path: executablePath,
startup_error:
cause instanceof Error ? cause.message : String(cause),
},
},
)
: cause;
try {
return await failBrowserScenarioOperation(cleanup, cause);
return await failBrowserScenarioOperation(cleanup, primary);
} catch (failure: unknown) {
if (failure !== cause) options.retainCleanup?.(cleanup);
if (failure !== primary) options.retainCleanup?.(cleanup);
throw failure;
}
}
+4
View File
@@ -113,6 +113,7 @@ export const withScenarioCommandDeadline = async <T>(
timeoutMs: number,
phase: ScenarioCommandDeadlinePhase,
method?: string,
launchExecutablePath?: string,
): Promise<T> => {
let operation: Promise<T>;
try {
@@ -131,6 +132,9 @@ export const withScenarioCommandDeadline = async <T>(
diagnostics: {
phase,
command_timeout_ms: timeoutMs,
...(launchExecutablePath === undefined
? {}
: { executable_path: launchExecutablePath }),
...(method === undefined ? {} : { method }),
// Discovered CDP URLs can carry transport userinfo in Playwright's
// native message. Preserve that cause without projecting it here.
@@ -0,0 +1,57 @@
import { chmod, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest.skipIf(process.platform === "win32")(
"retains the selected browser and its startup failure through the CLI",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-browser-launch-");
const executable = join(root, "selected-browser");
const secret = "explicit-browser-startup-secret";
await writeFile(
executable,
`#!/bin/sh\nprintf 'fixture startup failure ${secret}\\n' >&2\nexit 42\n`,
);
await chmod(executable, 0o700);
for (const [path, marker] of [
[executable, "fixture startup failure"],
[join(root, "missing-browser"), "doesn't exist"],
] as const) {
const scenario = {
browser: { mode: "launch", executable_path: path },
start_url: { url: "http://127.0.0.1:1/" },
secrets: [
{
secret_id: "credential",
environment_variable: "BROWSER_FIXTURE_SECRET",
},
],
actions: [
{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 },
],
};
const response = await cli.run({
arguments: [
"capture-browser-scenario",
JSON.stringify(scenario),
"--json",
],
environment: { BROWSER_FIXTURE_SECRET: secret },
timeoutMs: 20_000,
});
expect(response.exitCode, response.stdout + response.stderr).toBe(1);
expect(response.stdout).not.toContain(secret);
expect(response.json).toMatchObject({
code: expect.any(String),
details: {
diagnostics: {
executable_path: path,
startup_error: expect.stringContaining(marker),
},
},
});
}
},
);