fix(browser): identify scenario timeouts by Playwright error type

This commit is contained in:
morluto committed 2026-10-22 22:26:39 +08:00
1 parent 5d7d6ad9ab
commit 30fef57f75
2 files changed
+91 -3

No files matched your search

@@ -1322,3 +1322,85 @@ it("restores an environment mutation that completes after its deadline", async (
);
expect(browser.close).toHaveBeenCalledOnce();
});
it.each([
new Error("Timeout unrelated launch diagnostic"),
Object.assign(new Error("unrelated launch failure"), {
name: "TimeoutError",
}),
])(
"preserves a launch failure without inferring a Playwright deadline: %s",
async (failure) => {
const scenario = browserScenarioSchema.parse({
browser: {
mode: "launch",
executable_path: process.execPath,
headless: true,
},
start_url: { url: "https://example.test" },
command_timeout_ms: 4_321,
actions: scenarioActions,
});
let profilePath: string | undefined;
await expect(
openPlaywrightScenarioBrowser(
scenario,
{},
{
launcher: {
connectOverCDP: async () => {
throw new Error("Unexpected connection");
},
launchPersistentContext: async (profile) => {
profilePath = profile;
throw failure;
},
},
},
),
).rejects.toBe(failure);
expect(profilePath).toBeDefined();
await expect(access(profilePath!)).rejects.toMatchObject({
code: "ENOENT",
});
},
);
it("normalizes a genuine Playwright launch timeout and releases its profile", async () => {
const { errors } = await import("playwright-core");
const failure = new errors.TimeoutError("native launch deadline");
const scenario = browserScenarioSchema.parse({
browser: {
mode: "launch",
executable_path: process.execPath,
headless: true,
},
start_url: { url: "https://example.test" },
command_timeout_ms: 4_321,
actions: scenarioActions,
});
let profilePath: string | undefined;
await expect(
openPlaywrightScenarioBrowser(
scenario,
{},
{
launcher: {
connectOverCDP: async () => {
throw new Error("Unexpected connection");
},
launchPersistentContext: async (profile) => {
profilePath = profile;
throw failure;
},
},
},
),
).rejects.toMatchObject({
reason: "timeout",
userMessage: expect.stringContaining("4321 ms command_timeout_ms"),
cause: failure,
});
expect(profilePath).toBeDefined();
await expect(access(profilePath!)).rejects.toMatchObject({ code: "ENOENT" });
});
+9 -3
View File
@@ -1,3 +1,4 @@
import { createRequire } from "node:module";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import {
browserDeadlineRetry,
@@ -23,9 +24,14 @@ export const scenarioCommandTimeoutMs = (
scenario: Pick<BrowserScenario, "command_timeout_ms">,
): number => selectedCdpCommandTimeoutMs(scenario.command_timeout_ms);
const playwrightTimedOut = (cause: unknown): cause is Error =>
cause instanceof Error &&
(cause.name === "TimeoutError" || cause.message.startsWith("Timeout "));
const playwrightTimedOut = (cause: unknown): cause is Error => {
if (!(cause instanceof Error) || cause.name !== "TimeoutError") return false;
// Check the public producer type without loading Playwright at MCP startup.
const { errors } = createRequire(import.meta.url)(
"playwright-core",
) as typeof import("playwright-core");
return cause instanceof errors.TimeoutError;
};
/**
* `capture-browser-scenario` accepts only scenario JSON, so recovery names