Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions apps/server/src/mcp/PreviewAutomationBroker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as Deferred from "effect/Deferred";
import * as Fiber from "effect/Fiber";
import * as Result from "effect/Result";
import * as Stream from "effect/Stream";
import * as TestClock from "effect/testing/TestClock";

import { PreviewBrowserRootError } from "../preview/PreviewBrowserHost.ts";
import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts";
Expand Down Expand Up @@ -358,6 +359,67 @@ it.effect("names the click target that matched nothing when the server browser t
}),
);

it.effect("keeps the server browser host for other threads when one request times out", () =>
Effect.scoped(
Effect.gen(function* () {
const broker = yield* makeBroker;
const clientId = SERVER_BROWSER_AUTOMATION_CLIENT_ID;
const otherScope = {
...scope,
requestNamespace: "provider-session-2",
thread: {
threadId: ThreadId.make("thread-2"),
providerSessionId: "provider-session-2",
providerInstanceId: ProviderInstanceId.make("codex"),
},
};
const openDelivered = yield* Deferred.make<RoutedRequest>();
const requests = requestsFrom(
yield* broker.connect(makeHost({ clientId }), { preferred: true }),
);
yield* Stream.runForEach(requests, (request) => {
if (request.operation === "open") return Deferred.succeed(openDelivered, request);
if (request.operation === "snapshot") return Effect.void;
return broker.respond({
clientId,
connectionId: request.connectionId,
requestId: request.requestId,
ok: true,
result: { available: true },
});
}).pipe(Effect.forkScoped);
yield* Effect.yieldNow;

const stalledSnapshot = yield* broker
.invoke<void>({ scope, operation: "snapshot", input: {} })
.pipe(Effect.flip, Effect.forkScoped);
const unrelatedOpen = yield* broker
.invoke<{ available: boolean }>({ scope: otherScope, operation: "open", input: {} })
.pipe(Effect.result, Effect.forkScoped);
const openRequest = yield* Deferred.await(openDelivered);

yield* TestClock.adjust("17 seconds");
expect((yield* Fiber.join(stalledSnapshot))._tag).toBe("PreviewAutomationTimeoutError");

yield* broker.respond({
clientId,
connectionId: openRequest.connectionId,
requestId: openRequest.requestId,
ok: true,
result: { available: true },
});
expect(yield* Fiber.join(unrelatedOpen)).toEqual(Result.succeed({ available: true }));
expect(
yield* broker.invoke<{ available: boolean }>({
scope: otherScope,
operation: "status",
input: {},
}),
).toEqual({ available: true });
}),
),
);

it.effect("distinguishes malformed remote failures", () =>
Effect.gen(function* () {
const broker = yield* makeBroker;
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/mcp/PreviewAutomationBroker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,11 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {
// The enclosing operation owns the span for this inline handler.
onNone: Effect.fnUntraced(function* () {
// Never replay possibly applied actions; slow metadata reads must not evict the host.
if (input.updateCurrentTab !== false) {
// The in-process server browser is shared by every thread, so one stall must not evict it.
if (
input.updateCurrentTab !== false &&
connection.clientId !== SERVER_BROWSER_AUTOMATION_CLIENT_ID
) {
yield* disconnect(connection.clientId, connection.queue, true);
}
return yield* new PreviewAutomationTimeoutError(requestContext);
Expand Down
Loading