From 8e18b3cc20d6c8a5c761315b94303885dd7d2773 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 06:44:24 +0100 Subject: [PATCH] fix(browser): report attach-only profile transport truthfully # Conflicts: # extensions/browser/src/browser/routes/basic.ts --- .../routes/basic.existing-session.test.ts | 27 +++++++++++++++++-- .../browser/src/browser/routes/basic.ts | 12 +++++---- .../browser/server-context.availability.ts | 12 ++++++++- .../server-context.existing-session.test.ts | 18 +++++++++++++ .../browser/src/browser/server-context.ts | 27 ++++++++++++------- .../src/browser/server-context.types.ts | 1 + 6 files changed, 79 insertions(+), 18 deletions(-) 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 631463e3273..dd5285dfc7a 100644 --- a/extensions/browser/src/browser/routes/basic.existing-session.test.ts +++ b/extensions/browser/src/browser/routes/basic.existing-session.test.ts @@ -8,7 +8,11 @@ vi.mock("../chrome-mcp.js", () => ({ const { BrowserProfileUnavailableError } = await import("../errors.js"); const { registerBrowserBasicRoutes } = await import("./basic.js"); -function createExistingSessionProfileState(params?: { isHttpReachable?: () => Promise }) { +function createExistingSessionProfileState(params?: { + isHttpReachable?: () => Promise; + isTransportAvailable?: () => Promise; + isReachable?: () => Promise; +}) { return { resolved: { enabled: true, @@ -31,7 +35,8 @@ function createExistingSessionProfileState(params?: { isHttpReachable?: () => Pr attachOnly: true, }, isHttpReachable: params?.isHttpReachable ?? (async () => true), - isReachable: async () => true, + isTransportAvailable: params?.isTransportAvailable ?? (async () => true), + isReachable: params?.isReachable ?? (async () => true), }) as never, }; } @@ -86,4 +91,22 @@ describe("basic browser routes", () => { pid: 4321, }); }); + + it("treats attach-only profiles as running when transport is available even if page reachability is false", async () => { + const response = await callBasicRouteWithState({ + state: createExistingSessionProfileState({ + isTransportAvailable: async () => true, + isReachable: async () => false, + }), + }); + + expect(response.statusCode).toBe(200); + expect(response.body).toMatchObject({ + profile: "chrome-live", + driver: "existing-session", + transport: "chrome-mcp", + running: true, + cdpReady: true, + }); + }); }); diff --git a/extensions/browser/src/browser/routes/basic.ts b/extensions/browser/src/browser/routes/basic.ts index 8a60899f47c..aa42235e7e1 100644 --- a/extensions/browser/src/browser/routes/basic.ts +++ b/extensions/browser/src/browser/routes/basic.ts @@ -61,13 +61,15 @@ async function buildBrowserStatus(req: BrowserRequest, ctx: BrowserRouteContext) throw new BrowserError(profileCtx.error, profileCtx.status); } - const [cdpHttp, cdpReady] = await Promise.all([ - profileCtx.isHttpReachable(300), - profileCtx.isReachable(600), - ]); + const capabilities = getBrowserProfileCapabilities(profileCtx.profile); + const [cdpHttp, cdpReady] = capabilities.usesChromeMcp + ? await (async () => { + const ready = await profileCtx.isTransportAvailable(600); + return [ready, ready] as const; + })() + : await Promise.all([profileCtx.isHttpReachable(300), profileCtx.isTransportAvailable(600)]); const profileState = current.profiles.get(profileCtx.profile.name); - const capabilities = getBrowserProfileCapabilities(profileCtx.profile); let detectedBrowser: string | null = null; let detectedExecutablePath: string | null = null; let detectError: string | null = null; diff --git a/extensions/browser/src/browser/server-context.availability.ts b/extensions/browser/src/browser/server-context.availability.ts index 27bc09f17fd..3bbc9743eb0 100644 --- a/extensions/browser/src/browser/server-context.availability.ts +++ b/extensions/browser/src/browser/server-context.availability.ts @@ -46,6 +46,7 @@ type AvailabilityDeps = { type AvailabilityOps = { isHttpReachable: (timeoutMs?: number) => Promise; + isTransportAvailable: (timeoutMs?: number) => Promise; isReachable: (timeoutMs?: number) => Promise; ensureBrowserAvailable: () => Promise; stopRunningBrowser: () => Promise<{ stopped: boolean }>; @@ -87,9 +88,17 @@ export function createProfileAvailability({ ); }; + const isTransportAvailable = async (timeoutMs?: number) => { + if (capabilities.usesChromeMcp) { + await ensureChromeMcpAvailable(profile.name, profile.userDataDir); + return true; + } + return await isReachable(timeoutMs); + }; + const isHttpReachable = async (timeoutMs?: number) => { if (capabilities.usesChromeMcp) { - return await isReachable(timeoutMs); + return await isTransportAvailable(timeoutMs); } const { httpTimeoutMs } = resolveTimeouts(timeoutMs); return await isChromeReachable(profile.cdpUrl, httpTimeoutMs, getCdpReachabilityPolicy()); @@ -341,6 +350,7 @@ export function createProfileAvailability({ return { isHttpReachable, + isTransportAvailable, isReachable, ensureBrowserAvailable, stopRunningBrowser, 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 7c412261fdc..48b2c0a5b17 100644 --- a/extensions/browser/src/browser/server-context.existing-session.test.ts +++ b/extensions/browser/src/browser/server-context.existing-session.test.ts @@ -85,6 +85,24 @@ afterEach(() => { }); describe("browser server-context existing-session profile", () => { + it("reports attach-only profiles as running when the MCP session is available but no page is selected", async () => { + fs.mkdirSync("/tmp/brave-profile", { recursive: true }); + const state = makeState(); + const ctx = createBrowserRouteContext({ getState: () => state }); + + vi.mocked(chromeMcp.ensureChromeMcpAvailable).mockResolvedValueOnce(); + vi.mocked(chromeMcp.listChromeMcpTabs).mockRejectedValueOnce(new Error("No page selected")); + + await expect(ctx.listProfiles()).resolves.toEqual([ + expect.objectContaining({ + name: "chrome-live", + transport: "chrome-mcp", + running: true, + tabCount: 0, + }), + ]); + }); + it("routes tab operations through the Chrome MCP backend", async () => { fs.mkdirSync("/tmp/brave-profile", { recursive: true }); const state = makeState(); diff --git a/extensions/browser/src/browser/server-context.ts b/extensions/browser/src/browser/server-context.ts index 166b0442e96..a8193c67b21 100644 --- a/extensions/browser/src/browser/server-context.ts +++ b/extensions/browser/src/browser/server-context.ts @@ -79,14 +79,19 @@ function createProfileContext( getProfileState, }); - const { ensureBrowserAvailable, isHttpReachable, isReachable, stopRunningBrowser } = - createProfileAvailability({ - opts, - profile, - state, - getProfileState, - setProfileRunning, - }); + const { + ensureBrowserAvailable, + isHttpReachable, + isTransportAvailable, + isReachable, + stopRunningBrowser, + } = createProfileAvailability({ + opts, + profile, + state, + getProfileState, + setProfileRunning, + }); const { ensureTabAvailable, focusTab, closeTab } = createProfileSelectionOps({ profile, @@ -110,6 +115,7 @@ function createProfileContext( ensureBrowserAvailable, ensureTabAvailable, isHttpReachable, + isTransportAvailable, isReachable, listTabs, openTab, @@ -173,9 +179,9 @@ export function createBrowserRouteContext(opts: ContextOptions): BrowserRouteCon if (capabilities.usesChromeMcp) { try { - running = await profileCtx.isReachable(300); + running = await profileCtx.isTransportAvailable(300); if (running) { - const tabs = await profileCtx.listTabs(); + const tabs = await profileCtx.listTabs().catch(() => [] as BrowserTab[]); tabCount = tabs.filter((t) => t.type === "page").length; } } catch { @@ -251,6 +257,7 @@ export function createBrowserRouteContext(opts: ContextOptions): BrowserRouteCon ensureBrowserAvailable: () => getDefaultContext().ensureBrowserAvailable(), ensureTabAvailable: (targetId) => getDefaultContext().ensureTabAvailable(targetId), isHttpReachable: (timeoutMs) => getDefaultContext().isHttpReachable(timeoutMs), + isTransportAvailable: (timeoutMs) => getDefaultContext().isTransportAvailable(timeoutMs), isReachable: (timeoutMs) => getDefaultContext().isReachable(timeoutMs), listTabs: () => getDefaultContext().listTabs(), openTab: (url, opts) => getDefaultContext().openTab(url, opts), diff --git a/extensions/browser/src/browser/server-context.types.ts b/extensions/browser/src/browser/server-context.types.ts index 88a4ee4bcc4..8be63c52426 100644 --- a/extensions/browser/src/browser/server-context.types.ts +++ b/extensions/browser/src/browser/server-context.types.ts @@ -36,6 +36,7 @@ type BrowserProfileActions = { ensureBrowserAvailable: () => Promise; ensureTabAvailable: (targetId?: string) => Promise; isHttpReachable: (timeoutMs?: number) => Promise; + isTransportAvailable: (timeoutMs?: number) => Promise; isReachable: (timeoutMs?: number) => Promise; listTabs: () => Promise; openTab: (url: string, opts?: { label?: string }) => Promise;