fix(browser): honor inherited timeouts across browser commands (#129176)

* fix(browser): honor inherited timeouts across browser commands

* test(browser): keep resize timeout regression type-safe
This commit is contained in:
Peter Steinberger
2026-08-25 02:14:50 -07:00
committed by GitHub
parent 303191d0e7
commit f15ec26994
16 changed files with 339 additions and 211 deletions
@@ -37,6 +37,7 @@ const { registerBrowserNavigationCommands } = await import("./register.navigatio
function createNavigationProgram(): Command {
const { program, browser, parentOpts } = createBrowserProgram();
browser.option("--timeout <ms>", "Timeout in ms", "30000");
registerBrowserNavigationCommands(browser, parentOpts);
return program;
}
@@ -48,44 +49,54 @@ describe("browser navigation commands", () => {
getBrowserCliRuntimeCapture().resetRuntimeCapture();
});
it("sends navigate requests with the URL and target id", async () => {
const program = createNavigationProgram();
it.each(["30000", "60000"])(
"sends navigate requests with the URL, target id, and inherited %s ms timeout",
async (timeout) => {
const program = createNavigationProgram();
const parentArgs = timeout === "30000" ? [] : ["--timeout", timeout];
await program.parseAsync(
["browser", "navigate", "https://example.test/page", "--target-id", "tab-1"],
{ from: "user" },
);
await program.parseAsync(
["browser", ...parentArgs, "navigate", "https://example.test/page", "--target-id", "tab-1"],
{ from: "user" },
);
const request = mocks.callBrowserRequest.mock.calls.at(-1)?.[1] as
| { method?: string; path?: string; body?: Record<string, unknown> }
| undefined;
const options = mocks.callBrowserRequest.mock.calls.at(-1)?.[2] as
| { timeoutMs?: number }
| undefined;
expect(request).toMatchObject({
method: "POST",
path: "/navigate",
body: { url: "https://example.test/page", targetId: "tab-1" },
});
expect(options?.timeoutMs).toBe(20000);
});
const request = mocks.callBrowserRequest.mock.calls.at(-1)?.[1] as
| { method?: string; path?: string; body?: Record<string, unknown> }
| undefined;
expect(request).toMatchObject({
method: "POST",
path: "/navigate",
body: { url: "https://example.test/page", targetId: "tab-1" },
});
expect(mocks.callBrowserRequest).toHaveBeenLastCalledWith(
expect.objectContaining({ timeout }),
expect.objectContaining({ path: "/navigate" }),
);
},
);
it("passes normalized resize dimensions and target id to the resize helper", async () => {
const program = createNavigationProgram();
await program.parseAsync(["browser", "resize", "1024", "768", "--target-id", "tab-2"], {
from: "user",
});
await program.parseAsync(
["browser", "--timeout", "60000", "resize", "1024", "768", "--target-id", "tab-2"],
{
from: "user",
},
);
expect(mocks.runBrowserResizeWithOutput).toHaveBeenCalledWith(
expect.objectContaining({
width: 1024,
height: 768,
targetId: "tab-2",
timeoutMs: 20000,
parent: expect.objectContaining({ timeout: "60000" }),
successMessage: "resized to 1024x768",
}),
);
expect(mocks.runBrowserResizeWithOutput).not.toHaveBeenCalledWith(
expect.objectContaining({ timeoutMs: expect.any(Number) }),
);
});
it("rejects non-decimal resize dimensions before dispatch", async () => {
@@ -28,19 +28,15 @@ export function registerBrowserNavigationCommands(
.action(async (url: string, opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await callBrowserRequest<{ url?: string }>(
parent,
{
method: "POST",
path: "/navigate",
query: profile ? { profile } : undefined,
body: {
url,
targetId: normalizeOptionalString(opts.targetId),
},
const result = await callBrowserRequest<{ url?: string }>(parent, {
method: "POST",
path: "/navigate",
query: profile ? { profile } : undefined,
body: {
url,
targetId: normalizeOptionalString(opts.targetId),
},
{ timeoutMs: 20000 },
);
});
if (parent?.json) {
defaultRuntime.writeJson(result);
return;
@@ -72,7 +68,6 @@ export function registerBrowserNavigationCommands(
width: normalizedWidth,
height: normalizedHeight,
targetId: opts.targetId,
timeoutMs: 20000,
successMessage: `resized to ${normalizedWidth}x${normalizedHeight}`,
});
} catch (err) {
@@ -32,6 +32,7 @@ const { registerBrowserActionObserveCommands } = await import("./browser-cli-act
function createActionObserveProgram(): Command {
const { program, browser, parentOpts } = createBrowserProgram();
browser.option("--timeout <ms>", "Timeout in ms", "30000");
registerBrowserActionObserveCommands(browser, parentOpts);
return program;
}
@@ -42,6 +43,23 @@ describe("browser action observe commands", () => {
getBrowserCliRuntimeCapture().resetRuntimeCapture();
});
it.each([
{ command: "console", path: "/console", timeout: "30000" },
{ command: "console", path: "/console", timeout: "60000" },
{ command: "pdf", path: "/pdf", timeout: "30000" },
{ command: "pdf", path: "/pdf", timeout: "60000" },
])("inherits parent $timeout ms timeout for $command", async ({ command, path, timeout }) => {
const program = createActionObserveProgram();
const parentArgs = timeout === "30000" ? ["--json"] : ["--json", "--timeout", timeout];
await program.parseAsync(["browser", ...parentArgs, command], { from: "user" });
expect(mocks.callBrowserRequest).toHaveBeenLastCalledWith(
expect.objectContaining({ timeout }),
expect.objectContaining({ path }),
);
});
it("rejects non-decimal responsebody numeric flags before dispatch", async () => {
const program = createActionObserveProgram();
@@ -43,19 +43,15 @@ export function registerBrowserActionObserveCommands(
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserObserve(async () => {
const result = await callBrowserRequest<{ messages: unknown[] }>(
parent,
{
method: "GET",
path: "/console",
query: {
level: normalizeOptionalString(opts.level),
targetId: normalizeOptionalString(opts.targetId),
profile,
},
const result = await callBrowserRequest<{ messages: unknown[] }>(parent, {
method: "GET",
path: "/console",
query: {
level: normalizeOptionalString(opts.level),
targetId: normalizeOptionalString(opts.targetId),
profile,
},
{ timeoutMs: 20000 },
);
});
if (printBrowserJsonResult(parent, result)) {
return;
}
@@ -71,16 +67,12 @@ export function registerBrowserActionObserveCommands(
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserObserve(async () => {
const result = await callBrowserRequest<{ path: string }>(
parent,
{
method: "POST",
path: "/pdf",
query: profile ? { profile } : undefined,
body: { targetId: normalizeOptionalString(opts.targetId) },
},
{ timeoutMs: 20000 },
);
const result = await callBrowserRequest<{ path: string }>(parent, {
method: "POST",
path: "/pdf",
query: profile ? { profile } : undefined,
body: { targetId: normalizeOptionalString(opts.targetId) },
});
if (printBrowserJsonResult(parent, result)) {
return;
}
@@ -0,0 +1,52 @@
// Browser tests cover browser cli debug plugin behavior.
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as browserCliSharedModule from "./browser-cli-shared.js";
import {
createBrowserProgram,
getBrowserCliRuntime,
getBrowserCliRuntimeCapture,
} from "./browser-cli.test-support.js";
import * as cliCoreApiModule from "./core-api.js";
const mocks = vi.hoisted(() => ({
callBrowserRequest: vi.fn(async (..._args: unknown[]) => ({ ok: true })),
}));
vi.spyOn(browserCliSharedModule, "callBrowserRequest").mockImplementation(mocks.callBrowserRequest);
const browserCliRuntime = getBrowserCliRuntime();
vi.spyOn(cliCoreApiModule.defaultRuntime, "writeJson").mockImplementation(
browserCliRuntime.writeJson,
);
vi.spyOn(cliCoreApiModule.defaultRuntime, "error").mockImplementation(browserCliRuntime.error);
vi.spyOn(cliCoreApiModule.defaultRuntime, "exit").mockImplementation(browserCliRuntime.exit);
const { registerBrowserDebugCommands } = await import("./browser-cli-debug.js");
describe("browser debug command timeouts", () => {
beforeEach(() => {
mocks.callBrowserRequest.mockClear();
getBrowserCliRuntimeCapture().resetRuntimeCapture();
});
it.each([
{ args: ["highlight", "e1"], path: "/highlight" },
{ args: ["errors"], path: "/errors" },
{ args: ["requests"], path: "/requests" },
{ args: ["trace", "start"], path: "/trace/start" },
{ args: ["trace", "stop"], path: "/trace/stop" },
])("inherits the parent timeout for $path", async ({ args, path }) => {
for (const timeout of ["30000", "60000"]) {
const { program, browser, parentOpts } = createBrowserProgram();
browser.option("--timeout <ms>", "Timeout in ms", "30000");
registerBrowserDebugCommands(browser, parentOpts);
const parentArgs = timeout === "30000" ? ["--json"] : ["--json", "--timeout", timeout];
await program.parseAsync(["browser", ...parentArgs, ...args], { from: "user" });
expect(mocks.callBrowserRequest).toHaveBeenLastCalledWith(
expect.objectContaining({ timeout }),
expect.objectContaining({ path }),
);
}
});
});
@@ -13,10 +13,6 @@ import {
} from "./browser-cli-shared.js";
import { defaultRuntime, shortenHomePath } from "./core-api.js";
const BROWSER_DEBUG_TIMEOUT_MS = 20000;
type BrowserRequestParams = Parameters<typeof callBrowserRequest>[1];
type DebugContext = {
parent: BrowserParentOpts;
profile?: string;
@@ -36,13 +32,6 @@ async function withDebugContext(
);
}
async function callDebugRequest<T>(
parent: BrowserParentOpts,
params: BrowserRequestParams,
): Promise<T> {
return callBrowserRequest<T>(parent, params, { timeoutMs: BROWSER_DEBUG_TIMEOUT_MS });
}
function resolveDebugQuery(params: {
targetId?: unknown;
clear?: unknown;
@@ -69,7 +58,7 @@ export function registerBrowserDebugCommands(
.option("--target-id <id>", BROWSER_TAB_REFERENCE_HELP)
.action(async (ref: string, opts, cmd) => {
await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => {
const result = await callDebugRequest(parent, {
const result = await callBrowserRequest(parent, {
method: "POST",
path: "/highlight",
query: resolveProfileQuery(profile),
@@ -92,7 +81,7 @@ export function registerBrowserDebugCommands(
.option("--target-id <id>", BROWSER_TAB_REFERENCE_HELP)
.action(async (opts, cmd) => {
await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => {
const result = await callDebugRequest<{
const result = await callBrowserRequest<{
errors: Array<{ timestamp: string; name?: string; message: string }>;
}>(parent, {
method: "GET",
@@ -126,7 +115,7 @@ export function registerBrowserDebugCommands(
.option("--target-id <id>", BROWSER_TAB_REFERENCE_HELP)
.action(async (opts, cmd) => {
await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => {
const result = await callDebugRequest<{
const result = await callBrowserRequest<{
requests: Array<{
timestamp: string;
method: string;
@@ -176,7 +165,7 @@ export function registerBrowserDebugCommands(
.option("--sources", "Include sources (bigger traces)", false)
.action(async (opts, cmd) => {
await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => {
const result = await callDebugRequest(parent, {
const result = await callBrowserRequest(parent, {
method: "POST",
path: "/trace/start",
query: resolveProfileQuery(profile),
@@ -204,7 +193,7 @@ export function registerBrowserDebugCommands(
.option("--target-id <id>", BROWSER_TAB_REFERENCE_HELP)
.action(async (opts, cmd) => {
await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => {
const result = await callDebugRequest<{ path: string }>(parent, {
const result = await callBrowserRequest<{ path: string }>(parent, {
method: "POST",
path: "/trace/stop",
query: resolveProfileQuery(profile),
@@ -95,7 +95,10 @@ function installInspectSpies() {
describe("browser cli snapshot defaults", () => {
const runBrowserInspect = async (args: string[], withJson = false) => {
const program = new Command();
const browser = program.command("browser").option("--json", "JSON output", false);
const browser = program
.command("browser")
.option("--json", "JSON output", false)
.option("--timeout <ms>", "Timeout in ms", "30000");
registerBrowserInspectCommands(browser, (cmd) => cmd.parent?.opts() ?? {});
await program.parseAsync(withJson ? ["browser", "--json", ...args] : ["browser", ...args], {
from: "user",
@@ -123,6 +126,24 @@ describe("browser cli snapshot defaults", () => {
configMocks.loadConfig.mockReturnValue({ browser: {} });
});
it.each([
{ command: "screenshot", requestPath: "/screenshot", timeout: "30000" },
{ command: "screenshot", requestPath: "/screenshot", timeout: "60000" },
{ command: "snapshot", requestPath: "/snapshot", timeout: "30000" },
{ command: "snapshot", requestPath: "/snapshot", timeout: "60000" },
])(
"inherits parent $timeout ms timeout for $command",
async ({ command, requestPath, timeout }) => {
const args = timeout === "30000" ? [command] : ["--timeout", timeout, command];
await runBrowserInspect(args, true);
expect(sharedMocks.callBrowserRequest).toHaveBeenLastCalledWith(
expect.objectContaining({ timeout }),
expect.objectContaining({ path: requestPath }),
);
},
);
it.each<SnapshotDefaultsCase>([
{
label: "uses config snapshot defaults when mode is not provided",
@@ -80,23 +80,19 @@ export function registerBrowserInspectCommands(
return;
}
try {
const result = await callBrowserRequest<{ path: string }>(
parent,
{
method: "POST",
path: "/screenshot",
query: profile ? { profile } : undefined,
body: {
targetId: normalizeOptionalString(targetId),
fullPage: Boolean(opts.fullPage),
ref: normalizeOptionalString(opts.ref),
element: normalizeOptionalString(opts.element),
labels: Boolean(opts.labels),
type,
},
const result = await callBrowserRequest<{ path: string }>(parent, {
method: "POST",
path: "/screenshot",
query: profile ? { profile } : undefined,
body: {
targetId: normalizeOptionalString(targetId),
fullPage: Boolean(opts.fullPage),
ref: normalizeOptionalString(opts.ref),
element: normalizeOptionalString(opts.element),
labels: Boolean(opts.labels),
type,
},
{ timeoutMs: 20000 },
);
});
if (parent?.json) {
defaultRuntime.writeJson(result);
return;
@@ -170,15 +166,11 @@ export function registerBrowserInspectCommands(
mode,
profile,
};
const result = await callBrowserRequest<SnapshotResult>(
parent,
{
method: "GET",
path: "/snapshot",
query,
},
{ timeoutMs: 20000 },
);
const result = await callBrowserRequest<SnapshotResult>(parent, {
method: "GET",
path: "/snapshot",
query,
});
if (opts.out) {
const payload =
@@ -25,6 +25,21 @@ describe("browser manage start timeout option", () => {
expect(startCall[2]).toBeUndefined();
});
it.each([
{ args: ["reset-profile"], path: "/reset-profile" },
{ args: ["create-profile", "--name", "work"], path: "/profiles/create" },
{ args: ["delete-profile", "--name", "work"], path: "/profiles/work" },
])("inherits parent --timeout for $path", async ({ args, path }) => {
const program = createBrowserManageProgram({ withParentTimeout: true });
await program.parseAsync(["browser", "--timeout", "60000", "--json", ...args], {
from: "user",
});
const request = findBrowserManageCall(path);
expect(request?.[0]).toEqual(expect.objectContaining({ timeout: "60000" }));
expect(request?.[2]).toBeUndefined();
});
it("passes headless=true for browser start --headless", async () => {
const program = createBrowserManageProgram({ withParentTimeout: true });
await program.parseAsync(["browser", "start", "--headless"], { from: "user" });
@@ -429,15 +429,11 @@ export function registerBrowserManageCommands(
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const result = await callBrowserRequest<BrowserResetProfileResult>(
parent,
{
method: "POST",
path: "/reset-profile",
query: resolveProfileQuery(profile),
},
{ timeoutMs: 20000 },
);
const result = await callBrowserRequest<BrowserResetProfileResult>(parent, {
method: "POST",
path: "/reset-profile",
query: resolveProfileQuery(profile),
});
if (printJsonResult(parent, result)) {
return;
}
@@ -817,21 +813,17 @@ export function registerBrowserManageCommands(
) {
throw new Error("--driver must be openclaw or existing-session");
}
const result = await callBrowserRequest<BrowserCreateProfileResult>(
parent,
{
method: "POST",
path: "/profiles/create",
body: {
name: opts.name,
color: opts.color,
cdpUrl: opts.cdpUrl,
userDataDir: opts.userDataDir,
driver: opts.driver === "existing-session" ? "existing-session" : undefined,
},
const result = await callBrowserRequest<BrowserCreateProfileResult>(parent, {
method: "POST",
path: "/profiles/create",
body: {
name: opts.name,
color: opts.color,
cdpUrl: opts.cdpUrl,
userDataDir: opts.userDataDir,
driver: opts.driver === "existing-session" ? "existing-session" : undefined,
},
{ timeoutMs: 10_000 },
);
});
if (printJsonResult(parent, result)) {
return;
}
@@ -854,14 +846,10 @@ export function registerBrowserManageCommands(
.action(async (opts: { name: string }, cmd) => {
const parent = parentOpts(cmd);
await runBrowserCommand(async () => {
const result = await callBrowserRequest<BrowserDeleteProfileResult>(
parent,
{
method: "DELETE",
path: `/profiles/${encodeURIComponent(opts.name)}`,
},
{ timeoutMs: 20_000 },
);
const result = await callBrowserRequest<BrowserDeleteProfileResult>(parent, {
method: "DELETE",
path: `/profiles/${encodeURIComponent(opts.name)}`,
});
if (printJsonResult(parent, result)) {
return;
}
@@ -1,9 +1,10 @@
/**
* Shared Browser CLI resize runner used by resize and set viewport commands.
*/
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { ACT_MAX_VIEWPORT_DIMENSION } from "../browser/act-policy.js";
import {
callBrowserResize,
callBrowserRequest,
parseBrowserPositiveIntegerValue,
type BrowserParentOpts,
} from "./browser-cli-shared.js";
@@ -31,7 +32,6 @@ export async function runBrowserResizeWithOutput(params: {
width: number;
height: number;
targetId?: string;
timeoutMs?: number;
successMessage: string;
}): Promise<void> {
const { width, height } = params;
@@ -46,16 +46,17 @@ export async function runBrowserResizeWithOutput(params: {
return;
}
const result = await callBrowserResize(
params.parent,
{
profile: params.profile,
const result = await callBrowserRequest(params.parent, {
method: "POST",
path: "/act",
query: params.profile ? { profile: params.profile } : undefined,
body: {
kind: "resize",
width,
height,
targetId: params.targetId,
targetId: normalizeOptionalString(params.targetId),
},
{ timeoutMs: params.timeoutMs ?? 20000 },
);
});
if (params.parent?.json) {
defaultRuntime.writeJson(result);
@@ -5,7 +5,6 @@ import {
parseStrictNonNegativeInteger,
parseStrictPositiveInteger,
} from "openclaw/plugin-sdk/number-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
BROWSER_REQUEST_GATEWAY_METHOD,
BROWSER_REQUEST_GATEWAY_SCOPES,
@@ -129,26 +128,3 @@ export async function callBrowserRequest<T>(
}
return payload as T;
}
/** Sends a Browser resize action through the shared request helper. */
export async function callBrowserResize(
opts: BrowserParentOpts,
params: { profile?: string; width: number; height: number; targetId?: string },
extra?: { timeoutMs?: number },
): Promise<unknown> {
return callBrowserRequest(
opts,
{
method: "POST",
path: "/act",
query: params.profile ? { profile: params.profile } : undefined,
body: {
kind: "resize",
width: params.width,
height: params.height,
targetId: normalizeOptionalString(params.targetId),
},
},
extra,
);
}
@@ -27,7 +27,7 @@ async function runMutationRequest(params: {
successMessage: string;
}) {
try {
const result = await callBrowserRequest(params.parent, params.request, { timeoutMs: 20000 });
const result = await callBrowserRequest(params.parent, params.request);
if (params.parent?.json) {
defaultRuntime.writeJson(result);
return;
@@ -51,18 +51,14 @@ export function registerBrowserCookiesAndStorageCommands(
const profile = parent?.browserProfile;
const targetId = resolveTargetId(opts.targetId, cmd);
try {
const result = await callBrowserRequest<{ cookies?: unknown[] }>(
parent,
{
method: "GET",
path: "/cookies",
query: {
targetId,
profile,
},
const result = await callBrowserRequest<{ cookies?: unknown[] }>(parent, {
method: "GET",
path: "/cookies",
query: {
targetId,
profile,
},
{ timeoutMs: 20000 },
);
});
if (parent?.json) {
defaultRuntime.writeJson(result);
return;
@@ -143,19 +139,15 @@ export function registerBrowserCookiesAndStorageCommands(
const profile = parent?.browserProfile;
const targetId = resolveTargetId(opts.targetId, cmd2);
try {
const result = await callBrowserRequest<{ values?: Record<string, string> }>(
parent,
{
method: "GET",
path: `/storage/${kind}`,
query: {
key: normalizeOptionalString(key),
targetId,
profile,
},
const result = await callBrowserRequest<{ values?: Record<string, string> }>(parent, {
method: "GET",
path: `/storage/${kind}`,
query: {
key: normalizeOptionalString(key),
targetId,
profile,
},
{ timeoutMs: 20000 },
);
});
if (parent?.json) {
defaultRuntime.writeJson(result);
return;
@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
runBrowserResizeWithOutput: vi.fn(async (_params: unknown) => {}),
}));
const runActualBrowserResizeWithOutput = browserCliResizeModule.runBrowserResizeWithOutput;
vi.spyOn(browserCliSharedModule, "callBrowserRequest").mockImplementation(mocks.callBrowserRequest);
vi.spyOn(browserCliResizeModule, "runBrowserResizeWithOutput").mockImplementation(
mocks.runBrowserResizeWithOutput,
@@ -44,6 +45,7 @@ describe("browser state option collisions", () => {
const createStateProgram = ({ withGatewayUrl = false } = {}) => {
const { program, browser, parentOpts } = createBrowserProgramShared({ withGatewayUrl });
browser.option("--timeout <ms>", "Timeout in ms", "30000");
registerBrowserStateCommands(browser, parentOpts);
return program;
};
@@ -80,6 +82,69 @@ describe("browser state option collisions", () => {
getBrowserCliRuntime().exit.mockImplementation(() => {});
});
it.each([
{ args: ["cookies"], path: "/cookies" },
{
args: ["cookies", "set", "session", "value", "--url", "https://example.com"],
path: "/cookies/set",
},
{ args: ["cookies", "clear"], path: "/cookies/clear" },
{ args: ["storage", "local", "get"], path: "/storage/local" },
{ args: ["storage", "local", "set", "key", "value"], path: "/storage/local/set" },
{ args: ["storage", "local", "clear"], path: "/storage/local/clear" },
{ args: ["storage", "session", "get"], path: "/storage/session" },
{ args: ["storage", "session", "set", "key", "value"], path: "/storage/session/set" },
{ args: ["storage", "session", "clear"], path: "/storage/session/clear" },
{ args: ["set", "offline", "on"], path: "/set/offline" },
{ args: ["set", "headers", "{}"], path: "/set/headers" },
{ args: ["set", "credentials", "name", "value"], path: "/set/credentials" },
{ args: ["set", "geo", "48", "16"], path: "/set/geolocation" },
{ args: ["set", "media", "dark"], path: "/set/media" },
{ args: ["set", "timezone", "UTC"], path: "/set/timezone" },
{ args: ["set", "locale", "en-US"], path: "/set/locale" },
{ args: ["set", "device", "iPhone 14"], path: "/set/device" },
])("inherits parent timeout for $path", async ({ args, path }) => {
await runBrowserCommand(["--timeout", "60000", "--json", ...args]);
expect(mocks.callBrowserRequest).toHaveBeenLastCalledWith(
expect.objectContaining({ timeout: "60000" }),
expect.objectContaining({ path }),
);
});
it("inherits the parent timeout for the viewport resize alias", async () => {
await runBrowserCommand(["--timeout", "60000", "set", "viewport", "1024", "768"]);
expect(mocks.runBrowserResizeWithOutput).toHaveBeenCalledWith(
expect.objectContaining({
parent: expect.objectContaining({ timeout: "60000" }),
width: 1024,
height: 768,
}),
);
expect(mocks.runBrowserResizeWithOutput.mock.calls.at(-1)?.[0]).not.toHaveProperty("timeoutMs");
});
it("keeps the parent timeout and normalized target at the shared resize request boundary", async () => {
await runActualBrowserResizeWithOutput({
parent: { timeout: "60000", json: true },
profile: "work",
width: 1024,
height: 768,
targetId: " tab-1 ",
successMessage: "unused",
});
expect(mocks.callBrowserRequest).toHaveBeenLastCalledWith(
expect.objectContaining({ timeout: "60000" }),
expect.objectContaining({
path: "/act",
query: { profile: "work" },
body: { kind: "resize", width: 1024, height: 768, targetId: "tab-1" },
}),
);
});
it("forwards parent-captured --target-id on `browser cookies set`", async () => {
const request = await runBrowserCommandAndGetRequest([
"cookies",
+14 -23
View File
@@ -45,16 +45,12 @@ async function runBrowserSetRequest(params: {
}) {
await runBrowserCommand(async () => {
const profile = params.parent?.browserProfile;
const result = await callBrowserRequest(
params.parent,
{
method: "POST",
path: params.path,
query: profile ? { profile } : undefined,
body: params.body,
},
{ timeoutMs: 20000 },
);
const result = await callBrowserRequest(params.parent, {
method: "POST",
path: params.path,
query: profile ? { profile } : undefined,
body: params.body,
});
if (printBrowserJsonResult(params.parent, result)) {
return;
}
@@ -92,7 +88,6 @@ export function registerBrowserStateCommands(
width,
height,
targetId: opts.targetId,
timeoutMs: 20000,
successMessage: `viewport set: ${width}x${height}`,
});
});
@@ -147,19 +142,15 @@ export function registerBrowserStateCommands(
}
}
const profile = parent?.browserProfile;
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/set/headers",
query: profile ? { profile } : undefined,
body: {
headers,
targetId: normalizeOptionalString(opts.targetId),
},
const result = await callBrowserRequest(parent, {
method: "POST",
path: "/set/headers",
query: profile ? { profile } : undefined,
body: {
headers,
targetId: normalizeOptionalString(opts.targetId),
},
{ timeoutMs: 20000 },
);
});
if (printBrowserJsonResult(parent, result)) {
return;
}
@@ -373,6 +373,36 @@ describe("registerBrowserCli lazy browser subcommands", () => {
expect(tabsCommand.parent?.opts().browserProfile).toBe("remote");
});
it.each([
["before", ["browser", "--timeout", "60000", "status"]],
["after", ["browser", "status", "--timeout", "60000"]],
])(
"preserves parent timeout %s a lazily loaded leaf in positional mode",
async (_place, args) => {
const program = new Command().name("openclaw").enablePositionalOptions();
registerBrowserCli(program, ["node", "openclaw", ...args]);
await program.parseAsync(args, { from: "user" });
const command = requireTrailingCommand(
requireFirstCall(manageMocks.statusAction, "status action call"),
"status action",
);
expect(command.parent?.opts().timeout).toBe("60000");
},
);
it("preserves parent timeout before a nested lazily loaded storage-family leaf", async () => {
const program = new Command().name("openclaw").enablePositionalOptions();
const args = ["browser", "--timeout", "60000", "cookies", "set", "session", "abc"];
registerBrowserCli(program, ["node", "openclaw", ...args]);
await program.parseAsync(args, { from: "user" });
const cookieCall = requireFirstCall(stateMocks.cookieSetAction, "cookie set action call");
expect(cookieCall[3]).toMatchObject({ timeout: "60000" });
});
it("skips browser option values when selecting the lazy command group", async () => {
const program = new Command();
program.name("openclaw");