fix(browser): report attach-only profile transport truthfully

# Conflicts:
#	extensions/browser/src/browser/routes/basic.ts
This commit is contained in:
Peter Steinberger
2026-04-25 06:44:24 +01:00
parent 0ff7aa5c3d
commit 8e18b3cc20
6 changed files with 79 additions and 18 deletions

View File

@@ -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<boolean> }) {
function createExistingSessionProfileState(params?: {
isHttpReachable?: () => Promise<boolean>;
isTransportAvailable?: () => Promise<boolean>;
isReachable?: () => Promise<boolean>;
}) {
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,
});
});
});

View File

@@ -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;

View File

@@ -46,6 +46,7 @@ type AvailabilityDeps = {
type AvailabilityOps = {
isHttpReachable: (timeoutMs?: number) => Promise<boolean>;
isTransportAvailable: (timeoutMs?: number) => Promise<boolean>;
isReachable: (timeoutMs?: number) => Promise<boolean>;
ensureBrowserAvailable: () => Promise<void>;
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,

View File

@@ -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();

View File

@@ -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),

View File

@@ -36,6 +36,7 @@ type BrowserProfileActions = {
ensureBrowserAvailable: () => Promise<void>;
ensureTabAvailable: (targetId?: string) => Promise<BrowserTab>;
isHttpReachable: (timeoutMs?: number) => Promise<boolean>;
isTransportAvailable: (timeoutMs?: number) => Promise<boolean>;
isReachable: (timeoutMs?: number) => Promise<boolean>;
listTabs: () => Promise<BrowserTab[]>;
openTab: (url: string, opts?: { label?: string }) => Promise<BrowserTab>;