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 f5cabfc42479..a496f2015010 100644 --- a/extensions/browser/src/browser/routes/basic.existing-session.test.ts +++ b/extensions/browser/src/browser/routes/basic.existing-session.test.ts @@ -78,6 +78,7 @@ function createManagedProfileState( isHttpReachable?: (timeoutMs?: number, signal?: AbortSignal) => Promise; isTransportAvailable?: (timeoutMs?: number, signal?: AbortSignal) => Promise; }, + executablePath?: string, ) { return { resolved: { @@ -85,7 +86,7 @@ function createManagedProfileState( headless: false, headlessSource: "default", noSandbox: false, - executablePath: undefined, + executablePath, }, profiles: new Map(), forProfile: () => @@ -112,8 +113,9 @@ function createManagedProfileState( } async function callBasicRouteWithState(params: { + route?: "/" | "/doctor"; query?: Record; - state: ReturnType; + state: ReturnType; signal?: AbortSignal; }) { const { app, getHandlers } = createBrowserRouteApp(); @@ -122,7 +124,7 @@ async function callBasicRouteWithState(params: { forProfile: params.state.forProfile, } as never); - const handler = getHandlers.get("/"); + const handler = getHandlers.get(params.route ?? "/"); expect(handler).toBeTypeOf("function"); const response = createBrowserRouteResponse(); @@ -226,6 +228,66 @@ describe("basic browser routes", () => { expect(ensureTabAvailable).toHaveBeenCalledTimes(2); }); + it.each(["/", "/doctor"] as const)( + "detects the local managed profile executable for %s", + async (route) => { + const response = await callBasicRouteWithState({ + route, + query: { profile: "openclaw" }, + state: createManagedProfileState( + { executablePath: process.execPath, headless: true }, + undefined, + "/definitely-missing-global-chromium", + ), + }); + + expect(response.statusCode).toBe(200); + const body = responseBodyRecord(response); + const browserStatus = route === "/doctor" ? responseBodyRecord({ body: body.status }) : body; + expect(browserStatus).toMatchObject({ + executablePath: process.execPath, + detectedBrowser: "custom", + detectedExecutablePath: process.execPath, + detectError: null, + }); + if (route === "/doctor") { + expect(body.ok).toBe(true); + } + }, + ); + + it.each([ + { name: "loopback attach-only", profile: { attachOnly: true } }, + { + name: "remote CDP", + profile: { + cdpHost: "remote.example", + cdpIsLoopback: false, + cdpUrl: "http://remote.example:9222", + }, + }, + { name: "extension relay", profile: { driver: "extension", attachOnly: true } }, + { name: "existing session", profile: { driver: "existing-session", attachOnly: true } }, + ])("ignores a non-owning $name profile executable override", async ({ profile }) => { + const ignoredExecutable = "/definitely-missing-ignored-profile-chromium"; + const response = await callBasicRouteWithState({ + query: { profile: "openclaw" }, + state: createManagedProfileState( + { ...profile, executablePath: ignoredExecutable, headless: true }, + undefined, + process.execPath, + ), + }); + + expect(response.statusCode).toBe(200); + expect(responseBodyRecord(response)).toMatchObject({ + executablePath: ignoredExecutable, + detectedBrowser: "custom", + detectedExecutablePath: process.execPath, + detectError: null, + }); + }); + it("reports Linux no-display headless fallback for local managed profiles", async () => { const originalPlatform = process.platform; const originalDisplay = process.env.DISPLAY; diff --git a/extensions/browser/src/browser/routes/basic.ts b/extensions/browser/src/browser/routes/basic.ts index 61a954851eed..e7bcb21ce908 100644 --- a/extensions/browser/src/browser/routes/basic.ts +++ b/extensions/browser/src/browser/routes/basic.ts @@ -202,16 +202,16 @@ async function buildBrowserStatus( }), ) : null; - let detectedBrowser: string | null = null; - let detectedExecutablePath: string | null = null; + let detected: ReturnType = null; let detectError: string | null = null; try { - const detected = resolveBrowserExecutableForPlatform(current.resolved, process.platform); - if (detected) { - detectedBrowser = detected.kind; - detectedExecutablePath = detected.path; - } + detected = resolveBrowserExecutableForPlatform( + capabilities.mode === "local-managed" && capabilities.browserFilesystemLocal + ? { ...current.resolved, executablePath: profileCtx.profile.executablePath } + : current.resolved, + process.platform, + ); } catch (err) { detectError = String(err); } @@ -248,8 +248,8 @@ async function buildBrowserStatus( cdpPort: capabilities.usesChromeMcp ? null : profileCtx.profile.cdpPort, cdpUrl: profileCtx.profile.cdpUrl ? (redactCdpUrl(profileCtx.profile.cdpUrl) ?? null) : null, chosenBrowser: profileState?.running?.exe.kind ?? null, - detectedBrowser, - detectedExecutablePath, + detectedBrowser: detected?.kind ?? null, + detectedExecutablePath: detected?.path ?? null, detectError, userDataDir: profileState?.running?.userDataDir ?? profileCtx.profile.userDataDir ?? null, color: profileCtx.profile.color,