diff --git a/packages/opencode/src/mcp/browser.ts b/packages/opencode/src/mcp/browser.ts index 73aecc846b..68f4f59b21 100644 --- a/packages/opencode/src/mcp/browser.ts +++ b/packages/opencode/src/mcp/browser.ts @@ -22,11 +22,14 @@ const layer = Layer.succeed( clearTimeout(timer) resume(Effect.fail(error)) }) - subprocess.on("exit", (code) => { + const onExit = (code: number | null) => { if (code === null || code === 0) return clearTimeout(timer) resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`))) - }) + } + subprocess.on("exit", onExit) + // On Windows and WSL, open() can return only after the launcher has exited. + onExit(subprocess.exitCode) }) }), }), diff --git a/packages/opencode/test/mcp/browser.test.ts b/packages/opencode/test/mcp/browser.test.ts new file mode 100644 index 0000000000..81dea236ac --- /dev/null +++ b/packages/opencode/test/mcp/browser.test.ts @@ -0,0 +1,36 @@ +import { expect, mock, test } from "bun:test" +import { spawn } from "node:child_process" +import { once } from "node:events" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { Effect } from "effect" + +await mock.module("@opencode-ai/core/open", () => ({ + openUrl: async (url: string) => { + const code = new URL(url).pathname === "/successful" ? 0 : 23 + const subprocess = spawn(process.execPath, ["-e", `process.exit(${code})`], { stdio: "ignore" }) + await once(subprocess, "close") + return subprocess + }, +})) + +const { McpBrowser } = await import("../../src/mcp/browser") + +test("reports a browser launcher that exited before openUrl returned", async () => { + const error = await Effect.runPromise( + Effect.gen(function* () { + const browser = yield* McpBrowser.Service + return yield* Effect.flip(browser.open("https://example.com/authorize")) + }).pipe(Effect.provide(LayerNode.compile(McpBrowser.node))), + ) + + expect(error.message).toBe("Browser open failed with exit code 23") +}) + +test("accepts a browser launcher that exited successfully before openUrl returned", async () => { + await Effect.runPromise( + Effect.gen(function* () { + const browser = yield* McpBrowser.Service + yield* browser.open("https://example.com/successful") + }).pipe(Effect.provide(LayerNode.compile(McpBrowser.node))), + ) +})