refactor: dedupe agent and browser cli helpers

This commit is contained in:
Peter Steinberger
2026-03-03 00:14:48 +00:00
parent fe14be2352
commit fd3ca8a34c
46 changed files with 1051 additions and 1117 deletions

View File

@@ -5,6 +5,15 @@ import { shortenHomePath } from "../utils.js";
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
import { runCommandWithRuntime } from "./cli-utils.js";
const BROWSER_DEBUG_TIMEOUT_MS = 20000;
type BrowserRequestParams = Parameters<typeof callBrowserRequest>[1];
type DebugContext = {
parent: BrowserParentOpts;
profile?: string;
};
function runBrowserDebug(action: () => Promise<void>) {
return runCommandWithRuntime(defaultRuntime, action, (err) => {
defaultRuntime.error(danger(String(err)));
@@ -12,6 +21,39 @@ function runBrowserDebug(action: () => Promise<void>) {
});
}
async function withDebugContext(
cmd: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
action: (context: DebugContext) => Promise<void>,
) {
const parent = parentOpts(cmd);
await runBrowserDebug(() =>
action({
parent,
profile: parent.browserProfile,
}),
);
}
function printJsonResult(parent: BrowserParentOpts, result: unknown): boolean {
if (!parent.json) {
return false;
}
defaultRuntime.log(JSON.stringify(result, null, 2));
return true;
}
async function callDebugRequest<T>(
parent: BrowserParentOpts,
params: BrowserRequestParams,
): Promise<T> {
return callBrowserRequest<T>(parent, params, { timeoutMs: BROWSER_DEBUG_TIMEOUT_MS });
}
function resolveProfileQuery(profile?: string) {
return profile ? { profile } : undefined;
}
function resolveDebugQuery(params: {
targetId?: unknown;
clear?: unknown;
@@ -36,24 +78,17 @@ export function registerBrowserDebugCommands(
.argument("<ref>", "Ref id from snapshot")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (ref: string, opts, cmd) => {
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserDebug(async () => {
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/highlight",
query: profile ? { profile } : undefined,
body: {
ref: ref.trim(),
targetId: opts.targetId?.trim() || undefined,
},
await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => {
const result = await callDebugRequest(parent, {
method: "POST",
path: "/highlight",
query: resolveProfileQuery(profile),
body: {
ref: ref.trim(),
targetId: opts.targetId?.trim() || undefined,
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
});
if (printJsonResult(parent, result)) {
return;
}
defaultRuntime.log(`highlighted ${ref.trim()}`);
@@ -66,26 +101,19 @@ export function registerBrowserDebugCommands(
.option("--clear", "Clear stored errors after reading", false)
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserDebug(async () => {
const result = await callBrowserRequest<{
await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => {
const result = await callDebugRequest<{
errors: Array<{ timestamp: string; name?: string; message: string }>;
}>(
parent,
{
method: "GET",
path: "/errors",
query: resolveDebugQuery({
targetId: opts.targetId,
clear: opts.clear,
profile,
}),
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
}>(parent, {
method: "GET",
path: "/errors",
query: resolveDebugQuery({
targetId: opts.targetId,
clear: opts.clear,
profile,
}),
});
if (printJsonResult(parent, result)) {
return;
}
if (!result.errors.length) {
@@ -107,10 +135,8 @@ export function registerBrowserDebugCommands(
.option("--clear", "Clear stored requests after reading", false)
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserDebug(async () => {
const result = await callBrowserRequest<{
await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => {
const result = await callDebugRequest<{
requests: Array<{
timestamp: string;
method: string;
@@ -119,22 +145,17 @@ export function registerBrowserDebugCommands(
url: string;
failureText?: string;
}>;
}>(
parent,
{
method: "GET",
path: "/requests",
query: resolveDebugQuery({
targetId: opts.targetId,
filter: opts.filter,
clear: opts.clear,
profile,
}),
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
}>(parent, {
method: "GET",
path: "/requests",
query: resolveDebugQuery({
targetId: opts.targetId,
filter: opts.filter,
clear: opts.clear,
profile,
}),
});
if (printJsonResult(parent, result)) {
return;
}
if (!result.requests.length) {
@@ -164,26 +185,19 @@ export function registerBrowserDebugCommands(
.option("--no-snapshots", "Disable snapshots")
.option("--sources", "Include sources (bigger traces)", false)
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserDebug(async () => {
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/trace/start",
query: profile ? { profile } : undefined,
body: {
targetId: opts.targetId?.trim() || undefined,
screenshots: Boolean(opts.screenshots),
snapshots: Boolean(opts.snapshots),
sources: Boolean(opts.sources),
},
await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => {
const result = await callDebugRequest(parent, {
method: "POST",
path: "/trace/start",
query: resolveProfileQuery(profile),
body: {
targetId: opts.targetId?.trim() || undefined,
screenshots: Boolean(opts.screenshots),
snapshots: Boolean(opts.snapshots),
sources: Boolean(opts.sources),
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
});
if (printJsonResult(parent, result)) {
return;
}
defaultRuntime.log("trace started");
@@ -199,24 +213,17 @@ export function registerBrowserDebugCommands(
)
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserDebug(async () => {
const result = await callBrowserRequest<{ path: string }>(
parent,
{
method: "POST",
path: "/trace/stop",
query: profile ? { profile } : undefined,
body: {
targetId: opts.targetId?.trim() || undefined,
path: opts.out?.trim() || undefined,
},
await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => {
const result = await callDebugRequest<{ path: string }>(parent, {
method: "POST",
path: "/trace/stop",
query: resolveProfileQuery(profile),
body: {
targetId: opts.targetId?.trim() || undefined,
path: opts.out?.trim() || undefined,
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
});
if (printJsonResult(parent, result)) {
return;
}
defaultRuntime.log(`TRACE:${shortenHomePath(result.path)}`);

View File

@@ -2,28 +2,36 @@ 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(() => ({
callBrowserRequest: vi.fn(async (_opts: unknown, req: { path?: string }) =>
req.path === "/"
? {
enabled: true,
running: true,
pid: 1,
cdpPort: 18800,
chosenBrowser: "chrome",
userDataDir: "/tmp/openclaw",
color: "blue",
headless: true,
attachOnly: false,
}
: {},
),
runtime: {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
},
}));
const mocks = vi.hoisted(() => {
const runtimeLog = vi.fn();
const runtimeError = vi.fn();
const runtimeExit = vi.fn();
return {
callBrowserRequest: vi.fn(async (_opts: unknown, req: { path?: string }) =>
req.path === "/"
? {
enabled: true,
running: true,
pid: 1,
cdpPort: 18800,
chosenBrowser: "chrome",
userDataDir: "/tmp/openclaw",
color: "blue",
headless: true,
attachOnly: false,
}
: {},
),
runtimeLog,
runtimeError,
runtimeExit,
runtime: {
log: runtimeLog,
error: runtimeError,
exit: runtimeExit,
},
};
});
vi.mock("./browser-cli-shared.js", () => ({
callBrowserRequest: mocks.callBrowserRequest,
@@ -51,9 +59,9 @@ describe("browser manage start timeout option", () => {
beforeEach(() => {
mocks.callBrowserRequest.mockClear();
mocks.runtime.log.mockClear();
mocks.runtime.error.mockClear();
mocks.runtime.exit.mockClear();
mocks.runtimeLog.mockClear();
mocks.runtimeError.mockClear();
mocks.runtimeExit.mockClear();
});
it("uses parent --timeout for browser start instead of hardcoded 15s", async () => {

View File

@@ -13,6 +13,35 @@ import { shortenHomePath } from "../utils.js";
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
import { runCommandWithRuntime } from "./cli-utils.js";
function resolveProfileQuery(profile?: string) {
return profile ? { profile } : undefined;
}
function printJsonResult(parent: BrowserParentOpts, payload: unknown): boolean {
if (!parent?.json) {
return false;
}
defaultRuntime.log(JSON.stringify(payload, null, 2));
return true;
}
async function callTabAction(
parent: BrowserParentOpts,
profile: string | undefined,
body: { action: "new" | "select" | "close"; index?: number },
) {
return callBrowserRequest(
parent,
{
method: "POST",
path: "/tabs/action",
query: resolveProfileQuery(profile),
body,
},
{ timeoutMs: 10_000 },
);
}
async function fetchBrowserStatus(
parent: BrowserParentOpts,
profile?: string,
@@ -22,7 +51,7 @@ async function fetchBrowserStatus(
{
method: "GET",
path: "/",
query: profile ? { profile } : undefined,
query: resolveProfileQuery(profile),
},
{
timeoutMs: 1500,
@@ -37,11 +66,10 @@ async function runBrowserToggle(
await callBrowserRequest(parent, {
method: "POST",
path: params.path,
query: params.profile ? { profile: params.profile } : undefined,
query: resolveProfileQuery(params.profile),
});
const status = await fetchBrowserStatus(parent, params.profile);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(status, null, 2));
if (printJsonResult(parent, status)) {
return;
}
const name = status.profile ?? "openclaw";
@@ -82,8 +110,7 @@ export function registerBrowserManageCommands(
const parent = parentOpts(cmd);
await runBrowserCommand(async () => {
const status = await fetchBrowserStatus(parent, parent?.browserProfile);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(status, null, 2));
if (printJsonResult(parent, status)) {
return;
}
const detectedPath = status.detectedExecutablePath ?? status.executablePath;
@@ -139,12 +166,11 @@ export function registerBrowserManageCommands(
{
method: "POST",
path: "/reset-profile",
query: profile ? { profile } : undefined,
query: resolveProfileQuery(profile),
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
if (printJsonResult(parent, result)) {
return;
}
if (!result.moved) {
@@ -168,7 +194,7 @@ export function registerBrowserManageCommands(
{
method: "GET",
path: "/tabs",
query: profile ? { profile } : undefined,
query: resolveProfileQuery(profile),
},
{ timeoutMs: 3000 },
);
@@ -189,7 +215,7 @@ export function registerBrowserManageCommands(
{
method: "POST",
path: "/tabs/action",
query: profile ? { profile } : undefined,
query: resolveProfileQuery(profile),
body: {
action: "list",
},
@@ -208,18 +234,8 @@ export function registerBrowserManageCommands(
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/tabs/action",
query: profile ? { profile } : undefined,
body: { action: "new" },
},
{ timeoutMs: 10_000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
const result = await callTabAction(parent, profile, { action: "new" });
if (printJsonResult(parent, result)) {
return;
}
defaultRuntime.log("opened new tab");
@@ -239,18 +255,11 @@ export function registerBrowserManageCommands(
return;
}
await runBrowserCommand(async () => {
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/tabs/action",
query: profile ? { profile } : undefined,
body: { action: "select", index: Math.floor(index) - 1 },
},
{ timeoutMs: 10_000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
const result = await callTabAction(parent, profile, {
action: "select",
index: Math.floor(index) - 1,
});
if (printJsonResult(parent, result)) {
return;
}
defaultRuntime.log(`selected tab ${Math.floor(index)}`);
@@ -272,18 +281,8 @@ export function registerBrowserManageCommands(
return;
}
await runBrowserCommand(async () => {
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/tabs/action",
query: profile ? { profile } : undefined,
body: { action: "close", index: idx },
},
{ timeoutMs: 10_000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
const result = await callTabAction(parent, profile, { action: "close", index: idx });
if (printJsonResult(parent, result)) {
return;
}
defaultRuntime.log("closed tab");
@@ -303,13 +302,12 @@ export function registerBrowserManageCommands(
{
method: "POST",
path: "/tabs/open",
query: profile ? { profile } : undefined,
query: resolveProfileQuery(profile),
body: { url },
},
{ timeoutMs: 15000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(tab, null, 2));
if (printJsonResult(parent, tab)) {
return;
}
defaultRuntime.log(`opened: ${tab.url}\nid: ${tab.targetId}`);
@@ -329,13 +327,12 @@ export function registerBrowserManageCommands(
{
method: "POST",
path: "/tabs/focus",
query: profile ? { profile } : undefined,
query: resolveProfileQuery(profile),
body: { targetId },
},
{ timeoutMs: 5000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify({ ok: true }, null, 2));
if (printJsonResult(parent, { ok: true })) {
return;
}
defaultRuntime.log(`focused tab ${targetId}`);
@@ -356,7 +353,7 @@ export function registerBrowserManageCommands(
{
method: "DELETE",
path: `/tabs/${encodeURIComponent(targetId.trim())}`,
query: profile ? { profile } : undefined,
query: resolveProfileQuery(profile),
},
{ timeoutMs: 5000 },
);
@@ -366,14 +363,13 @@ export function registerBrowserManageCommands(
{
method: "POST",
path: "/act",
query: profile ? { profile } : undefined,
query: resolveProfileQuery(profile),
body: { kind: "close" },
},
{ timeoutMs: 20000 },
);
}
if (parent?.json) {
defaultRuntime.log(JSON.stringify({ ok: true }, null, 2));
if (printJsonResult(parent, { ok: true })) {
return;
}
defaultRuntime.log("closed tab");
@@ -396,8 +392,7 @@ export function registerBrowserManageCommands(
{ timeoutMs: 3000 },
);
const profiles = result.profiles ?? [];
if (parent?.json) {
defaultRuntime.log(JSON.stringify({ profiles }, null, 2));
if (printJsonResult(parent, { profiles })) {
return;
}
if (profiles.length === 0) {
@@ -444,8 +439,7 @@ export function registerBrowserManageCommands(
},
{ timeoutMs: 10_000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
if (printJsonResult(parent, result)) {
return;
}
const loc = result.isRemote ? ` cdpUrl: ${result.cdpUrl}` : ` port: ${result.cdpPort}`;
@@ -475,8 +469,7 @@ export function registerBrowserManageCommands(
},
{ timeoutMs: 20_000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
if (printJsonResult(parent, result)) {
return;
}
const msg = result.deleted

View File

@@ -1,5 +1,6 @@
import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { buildSystemRunPreparePayload } from "../test-utils/system-run-prepare-payload.js";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
type NodeInvokeCall = {
@@ -40,25 +41,7 @@ const callGateway = vi.fn(async (opts: NodeInvokeCall) => {
cwd?: unknown;
agentId?: unknown;
};
const argv = Array.isArray(params.command)
? params.command.map((entry) => String(entry))
: [];
const rawCommand =
typeof params.rawCommand === "string" && params.rawCommand.trim().length > 0
? params.rawCommand
: null;
return {
payload: {
cmdText: rawCommand ?? argv.join(" "),
plan: {
argv,
cwd: typeof params.cwd === "string" ? params.cwd : null,
rawCommand,
agentId: typeof params.agentId === "string" ? params.agentId : null,
sessionKey: null,
},
},
};
return buildSystemRunPreparePayload(params);
}
return {
payload: {