From ad8737af2c1de311819f9ff0715310df4a8a5eb6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 06:48:00 +0100 Subject: [PATCH] fix(browser): tighten WS3 status probes # Conflicts: # extensions/browser/src/browser/chrome-mcp.test.ts # extensions/browser/src/browser/chrome-mcp.ts # extensions/browser/src/browser/routes/basic.ts --- .../browser/src/browser/chrome-mcp.test.ts | 32 ++++++++++++++++++- extensions/browser/src/browser/chrome-mcp.ts | 31 ++++++++++++++++++ .../routes/basic.existing-session.test.ts | 23 ++++++++++++- .../server-context.existing-session.test.ts | 2 +- 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/extensions/browser/src/browser/chrome-mcp.test.ts b/extensions/browser/src/browser/chrome-mcp.test.ts index 0a9996a2661..38a0995189b 100644 --- a/extensions/browser/src/browser/chrome-mcp.test.ts +++ b/extensions/browser/src/browser/chrome-mcp.test.ts @@ -98,6 +98,7 @@ function createFakeSession(): ChromeMcpSession { describe("chrome MCP page parsing", () => { beforeEach(async () => { await resetChromeMcpSessionsForTest(); + vi.useRealTimers(); }); afterEach(() => { @@ -474,7 +475,6 @@ describe("chrome MCP page parsing", () => { expect(factoryCalls).toBe(2); expect(tabs).toHaveLength(2); }); - it("reconnects and retries list_pages once when Chrome MCP reports a stale selected page", async () => { let factoryCalls = 0; const factory: ChromeMcpSessionFactory = async () => { @@ -613,4 +613,34 @@ describe("chrome MCP page parsing", () => { expect(factoryCalls).toBe(2); expect(tabs).toHaveLength(2); }); + + it("honors timeoutMs for ephemeral availability probes", async () => { + vi.useFakeTimers(); + const closeMock = vi.fn().mockResolvedValue(undefined); + const factory: ChromeMcpSessionFactory = async () => + ({ + client: { + callTool: vi.fn(), + listTools: vi.fn(), + close: closeMock, + connect: vi.fn(), + }, + transport: { + pid: 123, + }, + ready: new Promise(() => {}), + }) as unknown as ChromeMcpSession; + setChromeMcpSessionFactoryForTest(factory); + + const promise = ensureChromeMcpAvailable("chrome-live", undefined, { + ephemeral: true, + timeoutMs: 50, + }); + const expectation = expect(promise).rejects.toThrow(/timed out after 50ms/i); + + await vi.advanceTimersByTimeAsync(50); + + await expectation; + expect(closeMock).toHaveBeenCalledTimes(1); + }); }); diff --git a/extensions/browser/src/browser/chrome-mcp.ts b/extensions/browser/src/browser/chrome-mcp.ts index a940bd0fbb8..7bf1b9bd612 100644 --- a/extensions/browser/src/browser/chrome-mcp.ts +++ b/extensions/browser/src/browser/chrome-mcp.ts @@ -463,6 +463,37 @@ async function getExistingSession( } } +async function waitForChromeMcpReady( + session: ChromeMcpSession, + profileName: string, + timeoutMs?: number, +): Promise { + if (!timeoutMs || timeoutMs <= 0) { + await session.ready; + return; + } + + let timer: ReturnType | undefined; + try { + await Promise.race([ + session.ready, + new Promise((_, reject) => { + timer = setTimeout(() => { + reject( + new BrowserProfileUnavailableError( + `Chrome MCP existing-session attach for profile "${profileName}" timed out after ${timeoutMs}ms.`, + ), + ); + }, timeoutMs); + }), + ]); + } finally { + if (timer) { + clearTimeout(timer); + } + } +} + async function createEphemeralSession( profileName: string, userDataDir?: string, diff --git a/extensions/browser/src/browser/routes/basic.existing-session.test.ts b/extensions/browser/src/browser/routes/basic.existing-session.test.ts index dd5285dfc7a..345b9b49988 100644 --- a/extensions/browser/src/browser/routes/basic.existing-session.test.ts +++ b/extensions/browser/src/browser/routes/basic.existing-session.test.ts @@ -63,7 +63,7 @@ describe("basic browser routes", () => { it("maps existing-session status failures to JSON browser errors", async () => { const response = await callBasicRouteWithState({ state: createExistingSessionProfileState({ - isHttpReachable: async () => { + isTransportAvailable: async () => { throw new BrowserProfileUnavailableError("attach failed"); }, }), @@ -109,4 +109,25 @@ describe("basic browser routes", () => { cdpReady: true, }); }); + + it("probes Chrome MCP transport only once for status", async () => { + const isHttpReachable = vi.fn(async () => true); + const isTransportAvailable = vi.fn(async () => true); + + const response = await callBasicRouteWithState({ + state: createExistingSessionProfileState({ + isHttpReachable, + isTransportAvailable, + }), + }); + + expect(response.statusCode).toBe(200); + expect(isTransportAvailable).toHaveBeenCalledTimes(1); + expect(isHttpReachable).not.toHaveBeenCalled(); + expect(response.body).toMatchObject({ + cdpHttp: true, + cdpReady: true, + running: true, + }); + }); }); diff --git a/extensions/browser/src/browser/server-context.existing-session.test.ts b/extensions/browser/src/browser/server-context.existing-session.test.ts index fad28383291..38c582b2db6 100644 --- a/extensions/browser/src/browser/server-context.existing-session.test.ts +++ b/extensions/browser/src/browser/server-context.existing-session.test.ts @@ -105,7 +105,7 @@ describe("browser server-context existing-session profile", () => { expect(chromeMcp.ensureChromeMcpAvailable).toHaveBeenCalledWith( "chrome-live", "/tmp/brave-profile", - { ephemeral: true }, + { ephemeral: true, timeoutMs: 300 }, ); expect(chromeMcp.listChromeMcpTabs).toHaveBeenCalledWith("chrome-live", "/tmp/brave-profile", { ephemeral: true,