diff --git a/extensions/browser/src/cli/browser-cli-inspect.test.ts b/extensions/browser/src/cli/browser-cli-inspect.test.ts index 9fcf65ef81c..3cf24e440e5 100644 --- a/extensions/browser/src/cli/browser-cli-inspect.test.ts +++ b/extensions/browser/src/cli/browser-cli-inspect.test.ts @@ -159,6 +159,26 @@ describe("browser cli snapshot defaults", () => { expect(params?.query?.urls).toBe(true); }); + it("rejects non-integer snapshot numeric options before dispatch", async () => { + await expect(runSnapshot(["--limit", "1e3"])).rejects.toThrow("__exit__:1"); + expect(runtime.error.mock.calls.at(-1)?.[0]).toContain( + "Invalid --limit: must be an integer >= 1", + ); + + resetRuntimeCapture(); + await expect(runSnapshot(["--depth", "-1"])).rejects.toThrow("__exit__:1"); + expect(runtime.error.mock.calls.at(-1)?.[0]).toContain( + "Invalid --depth: must be an integer >= 0", + ); + + expect(sharedMocks.callBrowserRequest).not.toHaveBeenCalled(); + }); + + it("passes zero snapshot depth because root depth is valid", async () => { + const params = await runSnapshot(["--depth", "0"]); + expect(params?.query?.depth).toBe(0); + }); + it("sends screenshot request with trimmed target id and jpeg type", async () => { const params = await runBrowserInspect(["screenshot", " tab-1 ", "--type", "jpeg"], true); expect(params?.path).toBe("/screenshot"); diff --git a/extensions/browser/src/cli/browser-cli-inspect.ts b/extensions/browser/src/cli/browser-cli-inspect.ts index ef7d634144e..8a00d01af35 100644 --- a/extensions/browser/src/cli/browser-cli-inspect.ts +++ b/extensions/browser/src/cli/browser-cli-inspect.ts @@ -10,6 +10,24 @@ import { type SnapshotResult, } from "./core-api.js"; +function parseOptionalIntegerOption( + value: string | undefined, + label: string, + opts: { min: number }, +): number | undefined { + if (value === undefined) { + return undefined; + } + const raw = value.trim(); + const parsed = /^\d+$/.test(raw) ? Number(raw) : Number.NaN; + if (!Number.isSafeInteger(parsed) || parsed < opts.min) { + defaultRuntime.error(danger(`Invalid ${label}: must be an integer >= ${opts.min}`)); + defaultRuntime.exit(1); + return undefined; + } + return parsed; +} + export function registerBrowserInspectCommands( browser: Command, parentOpts: (cmd: Command) => BrowserParentOpts, @@ -60,12 +78,12 @@ export function registerBrowserInspectCommands( .description("Capture a snapshot (default: ai; aria is the accessibility tree)") .option("--format ", "Snapshot format (default: ai)", "ai") .option("--target-id ", "CDP target id (or unique prefix)") - .option("--limit ", "Max nodes (default: 500/800)", (v: string) => Number(v)) + .option("--limit ", "Max nodes (default: 500/800)") .option("--mode ", "Snapshot preset (efficient)") .option("--efficient", "Use the efficient snapshot preset", false) .option("--interactive", "Role snapshot: interactive elements only", false) .option("--compact", "Role snapshot: compact output", false) - .option("--depth ", "Role snapshot: max depth", (v: string) => Number(v)) + .option("--depth ", "Role snapshot: max depth") .option("--selector ", "Role snapshot: scope to CSS selector") .option("--frame ", "Role snapshot: scope to an iframe selector") .option("--labels", "Include viewport label overlay screenshot", false) @@ -85,14 +103,22 @@ export function registerBrowserInspectCommands( ? "efficient" : undefined; const mode = opts.efficient === true || opts.mode === "efficient" ? "efficient" : configMode; + const limit = parseOptionalIntegerOption(opts.limit, "--limit", { min: 1 }); + const depth = parseOptionalIntegerOption(opts.depth, "--depth", { min: 0 }); + if ( + (opts.limit !== undefined && limit === undefined) || + (opts.depth !== undefined && depth === undefined) + ) { + return; + } try { const query: Record = { format, targetId: normalizeOptionalString(opts.targetId), - limit: Number.isFinite(opts.limit) ? opts.limit : undefined, + limit, interactive: opts.interactive ? true : undefined, compact: opts.compact ? true : undefined, - depth: Number.isFinite(opts.depth) ? opts.depth : undefined, + depth, selector: normalizeOptionalString(opts.selector), frame: normalizeOptionalString(opts.frame), labels: opts.labels ? true : undefined,