From a135e59d9a9c59e4ee53c619b2c9ca5ed50eb4d9 Mon Sep 17 00:00:00 2001 From: ethereal49 Date: Fri, 23 Oct 2026 20:21:48 -1000 Subject: [PATCH 1/2] fix(preview): report navigation timeout accurately --- .../server/src/mcp/PreviewAutomationBroker.ts | 15 ++++++++- apps/server/src/preview/ServerBrowser.test.ts | 32 +++++++++++++++++++ apps/server/src/preview/ServerBrowser.ts | 6 ++-- apps/server/src/preview/ServerBrowserPage.ts | 11 +++++-- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/apps/server/src/mcp/PreviewAutomationBroker.ts b/apps/server/src/mcp/PreviewAutomationBroker.ts index c1ae21dfd3b4c..4e16f702b25c1 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.ts @@ -205,6 +205,14 @@ function remoteDetailKind(detail: unknown): RemoteDetailKind { } } +const readRemoteTimeoutMs = (detail: unknown): number | undefined => { + if (typeof detail !== "object" || detail === null || !("timeoutMs" in detail)) return undefined; + const timeoutMs = detail.timeoutMs; + return typeof timeoutMs === "number" && Number.isInteger(timeoutMs) && timeoutMs > 0 + ? timeoutMs + : undefined; +}; + const MAX_REASON_CHARS = 500; const classifyResponseError = ( @@ -253,11 +261,16 @@ const classifyResponseError = ( ...context, ...remoteDiagnostics, }); - case "PreviewAutomationTimeoutError": + case "PreviewAutomationTimeoutError": { + const remoteTimeoutMs = readRemoteTimeoutMs(error.detail); return new PreviewAutomationTimeoutError({ ...context, ...remoteDiagnostics, + ...(remoteTimeoutMs === undefined + ? {} + : { timeoutMs: Math.min(context.timeoutMs, remoteTimeoutMs) }), }); + } case "PreviewAutomationControlInterruptedError": { const reason = decodeControlReason(error.detail); return new PreviewAutomationControlInterruptedError({ diff --git a/apps/server/src/preview/ServerBrowser.test.ts b/apps/server/src/preview/ServerBrowser.test.ts index 2b33f3b370804..ad59e494daaa1 100644 --- a/apps/server/src/preview/ServerBrowser.test.ts +++ b/apps/server/src/preview/ServerBrowser.test.ts @@ -379,6 +379,38 @@ beforeEach(() => { desktopConnections.length = 0; }); +it.live("preview_open reports its navigation timeout instead of the request budget", () => + Effect.scoped( + Effect.gen(function* () { + const broker = yield* Broker.PreviewAutomationBroker; + yield* Effect.yieldNow; + initialNavigation = async () => { + throw Object.assign(new Error("page.goto: Timeout 15000ms exceeded."), { + name: "TimeoutError", + }); + }; + + const error = yield* broker + .invoke({ + scope, + operation: "open", + input: { + url: "http://localhost:1337/", + reuseExistingTab: false, + show: false, + }, + }) + .pipe(Effect.flip); + + expect(error).toMatchObject({ + _tag: "PreviewAutomationTimeoutError", + timeoutMs: 15_000, + }); + expect(error.message).toBe("Preview automation open timed out after 15000ms."); + }), + ).pipe(Effect.provide(layer)), +); + it.live.each(["desktop" as const, undefined])( "explicit desktop targets ($0 runtime) fail without creating a server tab", (runtime) => diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index 3544cd172a048..641c0eb41a40a 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -902,7 +902,9 @@ const make = Effect.gen(function* () { (desktop || adopted) && page.url() === snapshot.navStatus.url ? page.reload(options) : page.goto(snapshot.navStatus.url, options) - ).then(constVoid); + ).then(constVoid, (cause: unknown) => { + throw ServerBrowserPage.toOperationError(cause, NAVIGATION_TIMEOUT_MS); + }); if (tab.control.agentId !== null) tab.control.track(tab.initialNavigation); else void tab.initialNavigation.catch(constVoid); @@ -1458,7 +1460,7 @@ const make = Effect.gen(function* () { `Navigation to ${url} failed: ${ServerBrowserPage.parseNetError(message).description}`, ); } - throw cause; + throw ServerBrowserPage.toOperationError(cause, timeout); }); }; diff --git a/apps/server/src/preview/ServerBrowserPage.ts b/apps/server/src/preview/ServerBrowserPage.ts index 2355029258396..c6ca0e4a0f8f2 100644 --- a/apps/server/src/preview/ServerBrowserPage.ts +++ b/apps/server/src/preview/ServerBrowserPage.ts @@ -40,7 +40,10 @@ export class ServerBrowserOperationError extends Error { } } -export const toOperationError = (cause: unknown): ServerBrowserOperationError => { +export const toOperationError = ( + cause: unknown, + timeoutMs?: number, +): ServerBrowserOperationError => { if (cause instanceof BrowserControlInterrupted) return new ServerBrowserOperationError( "PreviewAutomationControlInterruptedError", @@ -51,7 +54,11 @@ export const toOperationError = (cause: unknown): ServerBrowserOperationError => const message = cause instanceof Error ? cause.message : String(cause); const firstLine = message.split("\n")[0] ?? message; if (cause instanceof Error && cause.name === "TimeoutError") { - return new ServerBrowserOperationError("PreviewAutomationTimeoutError", firstLine); + return new ServerBrowserOperationError( + "PreviewAutomationTimeoutError", + firstLine, + timeoutMs === undefined ? undefined : { timeoutMs }, + ); } if ( /while parsing selector|Unknown engine|Unexpected token|strict mode violation/i.test(message) From 76d14e2e69209c4aadb6050928e277e66e61642f Mon Sep 17 00:00:00 2001 From: ethereal49 Date: Fri, 23 Oct 2026 22:45:21 -1000 Subject: [PATCH 2/2] test(preview): await browser host readiness --- apps/server/src/preview/ServerBrowser.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/preview/ServerBrowser.test.ts b/apps/server/src/preview/ServerBrowser.test.ts index ad59e494daaa1..8ac47a279478e 100644 --- a/apps/server/src/preview/ServerBrowser.test.ts +++ b/apps/server/src/preview/ServerBrowser.test.ts @@ -382,8 +382,7 @@ beforeEach(() => { it.live("preview_open reports its navigation timeout instead of the request budget", () => Effect.scoped( Effect.gen(function* () { - const broker = yield* Broker.PreviewAutomationBroker; - yield* Effect.yieldNow; + const { broker } = yield* ready; initialNavigation = async () => { throw Object.assign(new Error("page.goto: Timeout 15000ms exceeded."), { name: "TimeoutError",