fix(browser): add browser session selection

This commit is contained in:
Peter Steinberger
2026-03-14 03:46:34 +00:00
parent b857a8d8bc
commit 5c40c1c78a
19 changed files with 575 additions and 36 deletions

View File

@@ -0,0 +1,151 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
import { createBrowserProgram } from "./browser-cli-test-helpers.js";
const mocks = vi.hoisted(() => {
const runtimeLog = vi.fn();
const runtimeError = vi.fn();
const runtimeExit = vi.fn();
return {
callBrowserRequest: vi.fn<
(
opts: unknown,
req: { path?: string },
runtimeOpts?: { timeoutMs?: number },
) => Promise<Record<string, unknown>>
>(async () => ({})),
runtimeLog,
runtimeError,
runtimeExit,
runtime: {
log: runtimeLog,
error: runtimeError,
exit: runtimeExit,
},
};
});
vi.mock("./browser-cli-shared.js", () => ({
callBrowserRequest: mocks.callBrowserRequest,
}));
vi.mock("./cli-utils.js", () => ({
runCommandWithRuntime: async (
_runtime: unknown,
action: () => Promise<void>,
onError: (err: unknown) => void,
) => await action().catch(onError),
}));
vi.mock("../runtime.js", () => ({
defaultRuntime: mocks.runtime,
}));
function createProgram() {
const { program, browser, parentOpts } = createBrowserProgram();
registerBrowserManageCommands(browser, parentOpts);
return program;
}
describe("browser manage output", () => {
beforeEach(() => {
mocks.callBrowserRequest.mockClear();
mocks.runtimeLog.mockClear();
mocks.runtimeError.mockClear();
mocks.runtimeExit.mockClear();
});
it("shows chrome-mcp transport for existing-session status without fake CDP fields", async () => {
mocks.callBrowserRequest.mockImplementation(async (_opts: unknown, req: { path?: string }) =>
req.path === "/"
? {
enabled: true,
profile: "chrome-live",
driver: "existing-session",
transport: "chrome-mcp",
running: true,
cdpReady: true,
cdpHttp: true,
pid: 4321,
cdpPort: null,
cdpUrl: null,
chosenBrowser: null,
userDataDir: null,
color: "#00AA00",
headless: false,
noSandbox: false,
executablePath: null,
attachOnly: true,
}
: {},
);
const program = createProgram();
await program.parseAsync(["browser", "--browser-profile", "chrome-live", "status"], {
from: "user",
});
const output = mocks.runtimeLog.mock.calls.at(-1)?.[0] as string;
expect(output).toContain("transport: chrome-mcp");
expect(output).not.toContain("cdpPort:");
expect(output).not.toContain("cdpUrl:");
});
it("shows chrome-mcp transport in browser profiles output", async () => {
mocks.callBrowserRequest.mockImplementation(async (_opts: unknown, req: { path?: string }) =>
req.path === "/profiles"
? {
profiles: [
{
name: "chrome-live",
driver: "existing-session",
transport: "chrome-mcp",
running: true,
tabCount: 2,
isDefault: false,
isRemote: false,
cdpPort: null,
cdpUrl: null,
color: "#00AA00",
},
],
}
: {},
);
const program = createProgram();
await program.parseAsync(["browser", "profiles"], { from: "user" });
const output = mocks.runtimeLog.mock.calls.at(-1)?.[0] as string;
expect(output).toContain("chrome-live: running (2 tabs) [existing-session]");
expect(output).toContain("transport: chrome-mcp");
expect(output).not.toContain("port: 0");
});
it("shows chrome-mcp transport after creating an existing-session profile", async () => {
mocks.callBrowserRequest.mockImplementation(async (_opts: unknown, req: { path?: string }) =>
req.path === "/profiles/create"
? {
ok: true,
profile: "chrome-live",
transport: "chrome-mcp",
cdpPort: null,
cdpUrl: null,
color: "#00AA00",
isRemote: false,
}
: {},
);
const program = createProgram();
await program.parseAsync(
["browser", "create-profile", "--name", "chrome-live", "--driver", "existing-session"],
{ from: "user" },
);
const output = mocks.runtimeLog.mock.calls.at(-1)?.[0] as string;
expect(output).toContain('Created profile "chrome-live"');
expect(output).toContain("transport: chrome-mcp");
expect(output).not.toContain("port: 0");
});
});

View File

@@ -1,5 +1,6 @@
import type { Command } from "commander";
import type {
BrowserTransport,
BrowserCreateProfileResult,
BrowserDeleteProfileResult,
BrowserResetProfileResult,
@@ -101,6 +102,29 @@ function logBrowserTabs(tabs: BrowserTab[], json?: boolean) {
);
}
function usesChromeMcpTransport(params: {
transport?: BrowserTransport;
driver?: "openclaw" | "extension" | "existing-session";
}): boolean {
return params.transport === "chrome-mcp" || params.driver === "existing-session";
}
function formatBrowserConnectionSummary(params: {
transport?: BrowserTransport;
driver?: "openclaw" | "extension" | "existing-session";
isRemote?: boolean;
cdpPort?: number | null;
cdpUrl?: string | null;
}): string {
if (usesChromeMcpTransport(params)) {
return "transport: chrome-mcp";
}
if (params.isRemote) {
return `cdpUrl: ${params.cdpUrl ?? "(unset)"}`;
}
return `port: ${params.cdpPort ?? "(unset)"}`;
}
export function registerBrowserManageCommands(
browser: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
@@ -122,8 +146,15 @@ export function registerBrowserManageCommands(
`profile: ${status.profile ?? "openclaw"}`,
`enabled: ${status.enabled}`,
`running: ${status.running}`,
`cdpPort: ${status.cdpPort}`,
`cdpUrl: ${status.cdpUrl ?? `http://127.0.0.1:${status.cdpPort}`}`,
`transport: ${
usesChromeMcpTransport(status) ? "chrome-mcp" : (status.transport ?? "cdp")
}`,
...(!usesChromeMcpTransport(status)
? [
`cdpPort: ${status.cdpPort ?? "(unset)"}`,
`cdpUrl: ${status.cdpUrl ?? `http://127.0.0.1:${status.cdpPort}`}`,
]
: []),
`browser: ${status.chosenBrowser ?? "unknown"}`,
`detectedBrowser: ${status.detectedBrowser ?? "unknown"}`,
`detectedPath: ${detectedDisplay}`,
@@ -407,7 +438,7 @@ export function registerBrowserManageCommands(
const status = p.running ? "running" : "stopped";
const tabs = p.running ? ` (${p.tabCount} tabs)` : "";
const def = p.isDefault ? " [default]" : "";
const loc = p.isRemote ? `cdpUrl: ${p.cdpUrl}` : `port: ${p.cdpPort}`;
const loc = formatBrowserConnectionSummary(p);
const remote = p.isRemote ? " [remote]" : "";
const driver = p.driver !== "openclaw" ? ` [${p.driver}]` : "";
return `${p.name}: ${status}${tabs}${def}${remote}${driver}\n ${loc}, color: ${p.color}`;
@@ -453,7 +484,7 @@ export function registerBrowserManageCommands(
if (printJsonResult(parent, result)) {
return;
}
const loc = result.isRemote ? ` cdpUrl: ${result.cdpUrl}` : ` port: ${result.cdpPort}`;
const loc = ` ${formatBrowserConnectionSummary(result)}`;
defaultRuntime.log(
info(
`🦞 Created profile "${result.profile}"\n${loc}\n color: ${result.color}${