diff --git a/src/cli/environmentCommands.ts b/src/cli/environmentCommands.ts index fa3eab11bb..238ea53c46 100644 --- a/src/cli/environmentCommands.ts +++ b/src/cli/environmentCommands.ts @@ -38,6 +38,7 @@ import { inspectWebNetworkCaptureInputSchema } from "../domain/webNetworkCapture import { withCommandCancellation } from "./commandCancellation.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; import type { CliInstance } from "./types.js"; +import { cliDecimalField } from "./options.js"; /** Same envelope label as the other JSON-input commands and `parseCliJsonInput`. */ const cliError = (error: AnalysisError): JsonValue => ({ @@ -512,8 +513,10 @@ const registerBuildCommand = ( .boolean() .default(false) .describe("Run every stage and report, but write nothing"), - captureTimeoutMs: + // Decimal tokens only: numeric coercion would read "0x1" as 1 ms. + captureTimeoutMs: cliDecimalField( inspectWebNetworkCaptureInputSchema.shape.capture_timeout_ms, + ), }), run: ({ args, options }) => withCommandCancellation((signal) => diff --git a/src/cli/webNetworkCaptureCommands.ts b/src/cli/webNetworkCaptureCommands.ts index b31296d907..9cffd44753 100644 --- a/src/cli/webNetworkCaptureCommands.ts +++ b/src/cli/webNetworkCaptureCommands.ts @@ -8,7 +8,7 @@ import { CLI_COMMANDS } from "../cliCommandNames.js"; import { logCliCommand } from "../cliLogging.js"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; -import { cliIntegerArgument } from "./options.js"; +import { cliDecimalField, cliIntegerArgument } from "./options.js"; import { withCommandCancellation } from "./commandCancellation.js"; import { analysisEvidenceDeliveryOptions, @@ -55,8 +55,10 @@ export const registerWebNetworkCaptureCommands = ( .optional() .describe("Zero-based producer ordinals; repeat to select several"), ...explicitSensitiveValueOptions, - captureTimeoutMs: + // Decimal tokens only: numeric coercion would read "0x1" as 1 ms. + captureTimeoutMs: cliDecimalField( inspectWebNetworkCaptureInputSchema.shape.capture_timeout_ms, + ), }), run: ({ args, options }) => withCommandCancellation((signal) => diff --git a/tests/boundary/cli/environmentBuildCli.test.ts b/tests/boundary/cli/environmentBuildCli.test.ts index 712b49edc6..8f2377923c 100644 --- a/tests/boundary/cli/environmentBuildCli.test.ts +++ b/tests/boundary/cli/environmentBuildCli.test.ts @@ -152,7 +152,13 @@ cliTest( const har = join(root, "grafana.har"); await writeFile(har, JSON.stringify(environmentManifestHar())); const planned = await cli.run({ - arguments: ["environment-build", har, "--dry-run", "--json"], + arguments: [ + "environment-build", + har, + "--dry-run", + "--capture-timeout-ms=030000", + "--json", + ], timeoutMs: 60_000, }); expect(planned.exitCode, planned.stderr).toBe(0); @@ -188,6 +194,22 @@ cliTest( expect(conflicting.exitCode).toBe(1); expect(conflicting.json).toMatchObject({ code: "invalid_request" }); + // Both capture readers share the field. Numeric coercion would read + // "0x1" as a 1 ms capture deadline. + for (const command of [ + ["environment-build", har, "--dry-run"], + ["inspect-web-network-capture", har, "har"], + ]) { + const hexDeadline = await cli.run({ + arguments: [...command, "--capture-timeout-ms=0x1", "--json"], + }); + expect(hexDeadline.exitCode).toBe(1); + expect(hexDeadline.json).toMatchObject({ + code: "VALIDATION_ERROR", + fieldErrors: [expect.objectContaining({ path: "captureTimeoutMs" })], + }); + } + const noDirectory = await cli.run({ arguments: ["environment-build", har, "--json"], timeoutMs: 60_000,