From 96fcb963ec6ef4254898aa2afa91d85b61ce677a Mon Sep 17 00:00:00 2001 From: HCL Date: Tue, 24 Feb 2026 12:04:06 +0800 Subject: [PATCH] fix(cli): resolve --url option collision in browser cookies set When addGatewayClientOptions registers --url on the parent browser command, Commander.js captures it before the cookies set subcommand can receive it. Switch from requiredOption to option and resolve via inheritOptionFromParent, matching the existing pattern used for --target-id. Fixes #24811 Co-Authored-By: Claude Opus 4.6 --- src/cli/browser-cli-state.cookies-storage.ts | 21 ++++++++++++-- ...rowser-cli-state.option-collisions.test.ts | 29 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/cli/browser-cli-state.cookies-storage.ts b/src/cli/browser-cli-state.cookies-storage.ts index d71cb9a0434..c3b03404f3a 100644 --- a/src/cli/browser-cli-state.cookies-storage.ts +++ b/src/cli/browser-cli-state.cookies-storage.ts @@ -4,6 +4,17 @@ import { defaultRuntime } from "../runtime.js"; import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js"; import { inheritOptionFromParent } from "./command-options.js"; +function resolveUrl(opts: { url?: string }, command: Command): string | undefined { + if (typeof opts.url === "string" && opts.url.trim()) { + return opts.url.trim(); + } + const inherited = inheritOptionFromParent(command, "url"); + if (typeof inherited === "string" && inherited.trim()) { + return inherited.trim(); + } + return undefined; +} + function resolveTargetId(rawTargetId: unknown, command: Command): string | undefined { const local = typeof rawTargetId === "string" ? rawTargetId.trim() : ""; if (local) { @@ -58,12 +69,18 @@ export function registerBrowserCookiesAndStorageCommands( .description("Set a cookie (requires --url or domain+path)") .argument("", "Cookie name") .argument("", "Cookie value") - .requiredOption("--url ", "Cookie URL scope (recommended)") + .option("--url ", "Cookie URL scope (recommended)") .option("--target-id ", "CDP target id (or unique prefix)") .action(async (name: string, value: string, opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; const targetId = resolveTargetId(opts.targetId, cmd); + const url = resolveUrl(opts, cmd); + if (!url) { + defaultRuntime.error(danger("Missing required --url option for cookies set")); + defaultRuntime.exit(1); + return; + } try { const result = await callBrowserRequest( parent, @@ -73,7 +90,7 @@ export function registerBrowserCookiesAndStorageCommands( query: profile ? { profile } : undefined, body: { targetId, - cookie: { name, value, url: opts.url }, + cookie: { name, value, url }, }, }, { timeoutMs: 20000 }, diff --git a/src/cli/browser-cli-state.option-collisions.test.ts b/src/cli/browser-cli-state.option-collisions.test.ts index 7284a2de048..45ec5c6a5c1 100644 --- a/src/cli/browser-cli-state.option-collisions.test.ts +++ b/src/cli/browser-cli-state.option-collisions.test.ts @@ -26,12 +26,15 @@ vi.mock("../runtime.js", () => ({ })); describe("browser state option collisions", () => { - const createBrowserProgram = () => { + const createBrowserProgram = ({ withGatewayUrl = false } = {}) => { const program = new Command(); const browser = program .command("browser") .option("--browser-profile ", "Browser profile") .option("--json", "Output JSON", false); + if (withGatewayUrl) { + browser.option("--url ", "Gateway WebSocket URL"); + } const parentOpts = (cmd: Command) => cmd.parent?.opts?.() as BrowserParentOpts; registerBrowserStateCommands(browser, parentOpts); return program; @@ -79,6 +82,30 @@ describe("browser state option collisions", () => { expect((request as { body?: { targetId?: string } }).body?.targetId).toBe("tab-1"); }); + it("resolves --url via parent when addGatewayClientOptions captures it", async () => { + const program = createBrowserProgram({ withGatewayUrl: true }); + await program.parseAsync( + ["browser", "--url", "ws://gw", "cookies", "set", "session", "abc", "--url", "https://example.com"], + { from: "user" }, + ); + const call = mocks.callBrowserRequest.mock.calls.at(-1); + expect(call).toBeDefined(); + const request = call![1] as { body?: { cookie?: { url?: string } } }; + expect(request.body?.cookie?.url).toBe("https://example.com"); + }); + + it("inherits --url from parent when subcommand does not provide it", async () => { + const program = createBrowserProgram({ withGatewayUrl: true }); + await program.parseAsync( + ["browser", "--url", "https://inherited.example.com", "cookies", "set", "session", "abc"], + { from: "user" }, + ); + const call = mocks.callBrowserRequest.mock.calls.at(-1); + expect(call).toBeDefined(); + const request = call![1] as { body?: { cookie?: { url?: string } } }; + expect(request.body?.cookie?.url).toBe("https://inherited.example.com"); + }); + it("accepts legacy parent `--json` by parsing payload via positional headers fallback", async () => { const request = (await runBrowserCommandAndGetRequest([ "set",