diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts index f9d618f0520e3..0009befe0d457 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts @@ -5,6 +5,7 @@ import { PreviewAutomationClientDisconnectedError, PreviewAutomationMalformedResponseError, PreviewAutomationNoAvailableHostError, + PreviewAutomationTimeoutError, PreviewTabId, ProviderInstanceId, ThreadId, @@ -1031,3 +1032,51 @@ it.effect("keeps each thread's current tab when threads share a provider session }), ), ); + +it.effect("reports a browser operation timeout without claiming the broker deadline elapsed", () => + Effect.scoped( + Effect.gen(function* () { + const broker = yield* makeBroker; + const connected = yield* Deferred.make(); + const requests = requestsFrom(yield* broker.connect(makeHost()), () => { + Deferred.doneUnsafe(connected, Effect.void); + }); + const connections: string[] = []; + yield* Stream.runForEach(requests, (request) => { + connections.push(request.connectionId); + return broker.respond({ + clientId: "client-1", + connectionId: request.connectionId, + requestId: request.requestId, + ...(request.operation === "snapshot" + ? { + ok: false, + error: { + _tag: "PreviewAutomationTimeoutError", + message: "The page was too busy to capture a snapshot in time.", + }, + } + : { ok: true, result: "responsive" }), + }); + }).pipe(Effect.forkScoped); + yield* Deferred.await(connected); + + const tabId = PreviewTabId.make("tab-1"); + const error = yield* broker + .invoke({ scope, operation: "snapshot", input: {}, tabId }) + .pipe(Effect.flip); + + expect(error).toBeInstanceOf(PreviewAutomationTimeoutError); + expect(error.message).toContain("browser's operation deadline"); + expect(error.message).not.toContain("15000ms"); + expect(error.message).toContain("preview_status"); + expect(yield* broker.invoke({ scope, operation: "status", input: {}, tabId })).toBe( + "responsive", + ); + expect(yield* broker.invoke({ scope, operation: "evaluate", input: {}, tabId })).toBe( + "responsive", + ); + expect(new Set(connections).size).toBe(1); + }), + ), +); diff --git a/packages/contracts/src/previewAutomation.ts b/packages/contracts/src/previewAutomation.ts index abaa03512d1ce..073306bb31d0c 100644 --- a/packages/contracts/src/previewAutomation.ts +++ b/packages/contracts/src/previewAutomation.ts @@ -723,6 +723,9 @@ export class PreviewAutomationTimeoutError extends Schema.TaggedError