From f8bb792f1535aaba7e720d996301d24c66f7c060 Mon Sep 17 00:00:00 2001 From: Heo Hao <130041734+Shelden-Hao@users.noreply.github.com> Date: Sat, 7 Nov 2026 22:38:03 +0800 Subject: [PATCH] fix(cli): stop restating schema defaults in help descriptions (#9278) The help renderer appends "(default: X)" whenever the option field declares a schema default, so descriptions that also spell the default out print it twice: --block-size Power-of-two block size from 256 to 65536 bytes (default 4096) (default: 4096) Remove the hand-written default from the eleven options that own a schema default across inspect-byte-entropy, inspect-devicetree, inspect-packet-capture, inspect-packet-fields, inspect-signal-decoders and decode-signal-recording, matching the rule in cliDecimalField's contract that the domain field owns bounds, defaults and help. Options without a schema default keep their prose: --match_limit, --line_limit, --limit and --packet_limit are nullable or optional, so the renderer emits no annotation and the description is the only place the effective value appears. Add a boundary test asserting each such option states its default exactly once, so a restated default cannot come back unnoticed. --- src/cli/binaryDiagnosticsCommands.ts | 14 ++- src/domain/captures/packetCapture.ts | 2 +- src/domain/captures/packetFields.ts | 6 +- src/domain/signals/signalDecoders.ts | 4 +- src/domain/signals/signalRecording.ts | 2 +- .../cli/cliHelpDefaultAnnotations.test.ts | 88 +++++++++++++++++++ 6 files changed, 99 insertions(+), 17 deletions(-) create mode 100644 tests/boundary/cli/cliHelpDefaultAnnotations.test.ts diff --git a/src/cli/binaryDiagnosticsCommands.ts b/src/cli/binaryDiagnosticsCommands.ts index 3e8962bdcb..f98564f987 100644 --- a/src/cli/binaryDiagnosticsCommands.ts +++ b/src/cli/binaryDiagnosticsCommands.ts @@ -266,12 +266,10 @@ export const registerBinaryDiagnosticsCommands = ( // "0x10" or "1e2". blockSize: cliDecimalField( inspectByteEntropyInputSchema.shape.block_size, - ).describe( - "Power-of-two block size from 256 to 65536 bytes (default 4096)", - ), + ).describe("Power-of-two block size from 256 to 65536 bytes"), offset: cliDecimalField( inspectByteEntropyInputSchema.shape.offset, - ).describe("Window start as a decimal file byte offset (default 0)"), + ).describe("Window start as a decimal file byte offset"), length: cliDecimalField( inspectByteEntropyInputSchema.shape.length, ).describe("Window length in bytes; omit to extend through EOF"), @@ -429,16 +427,16 @@ export const registerBinaryDiagnosticsCommands = ( options: z.object({ maxFileBytes: cliDecimalField( inspectDevicetreeInputSchema.shape.max_file_bytes, - ).describe("Maximum FDT file size in bytes (default 4194304)"), + ).describe("Maximum FDT file size in bytes"), maxNodes: cliDecimalField( inspectDevicetreeInputSchema.shape.max_nodes, - ).describe("Maximum number of device-tree nodes (default 8192)"), + ).describe("Maximum number of device-tree nodes"), maxProperties: cliDecimalField( inspectDevicetreeInputSchema.shape.max_properties, - ).describe("Maximum number of device-tree properties (default 32768)"), + ).describe("Maximum number of device-tree properties"), maxDepth: cliDecimalField( inspectDevicetreeInputSchema.shape.max_depth, - ).describe("Maximum device-tree nesting depth (default 64)"), + ).describe("Maximum device-tree nesting depth"), ...analysisEvidenceDeliveryOptions, }), run: ({ args, options }) => diff --git a/src/domain/captures/packetCapture.ts b/src/domain/captures/packetCapture.ts index 6f1bbabd54..bafec50bbf 100644 --- a/src/domain/captures/packetCapture.ts +++ b/src/domain/captures/packetCapture.ts @@ -51,7 +51,7 @@ const captureInputShape = z.strictObject({ .int() .nonnegative() .default(0) - .describe("Skip this many matching packets after filtering; default 0"), + .describe("Skip this many matching packets after filtering"), match_limit: z .number() .int() diff --git a/src/domain/captures/packetFields.ts b/src/domain/captures/packetFields.ts index 23b25cfcd3..1cd75c6e36 100644 --- a/src/domain/captures/packetFields.ts +++ b/src/domain/captures/packetFields.ts @@ -5,9 +5,7 @@ export const inspectPacketFieldsInputSchema = z.strictObject({ kind: z .enum(["protocols", "fields"]) .default("protocols") - .describe( - "Return protocol registrations or field definitions; defaults to protocols", - ), + .describe("Return protocol registrations or field definitions"), protocol: z .string() .min(1) @@ -30,7 +28,7 @@ export const inspectPacketFieldsInputSchema = z.strictObject({ .nonnegative() .max(Number.MAX_SAFE_INTEGER) .default(0) - .describe("Number of matching records to skip; defaults to 0"), + .describe("Number of matching records to skip"), limit: z .number() .int() diff --git a/src/domain/signals/signalDecoders.ts b/src/domain/signals/signalDecoders.ts index ae6754cca5..4d86b36ea6 100644 --- a/src/domain/signals/signalDecoders.ts +++ b/src/domain/signals/signalDecoders.ts @@ -24,9 +24,7 @@ export const inspectSignalDecodersInputSchema = z.strictObject({ .nonnegative() .max(Number.MAX_SAFE_INTEGER) .default(0) - .describe( - "Number of matching decoders or help lines to skip; defaults to 0", - ), + .describe("Number of matching decoders or help lines to skip"), limit: z .number() .int() diff --git a/src/domain/signals/signalRecording.ts b/src/domain/signals/signalRecording.ts index 483421c34c..ebce5bd09a 100644 --- a/src/domain/signals/signalRecording.ts +++ b/src/domain/signals/signalRecording.ts @@ -31,7 +31,7 @@ const signalInputShape = z.strictObject({ .int() .nonnegative() .default(0) - .describe("Skip this many literal LF-separated output lines; default 0"), + .describe("Skip this many literal LF-separated output lines"), line_limit: z .number() .int() diff --git a/tests/boundary/cli/cliHelpDefaultAnnotations.test.ts b/tests/boundary/cli/cliHelpDefaultAnnotations.test.ts new file mode 100644 index 0000000000..0f4e75fe80 --- /dev/null +++ b/tests/boundary/cli/cliHelpDefaultAnnotations.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; + +import { createCli } from "../../../src/cli.js"; + +type CliInstance = ReturnType; + +const serve = async ( + cli: CliInstance, + argv: readonly string[], +): Promise<{ readonly stdout: string; readonly exitCode: number }> => { + let stdout = ""; + let exitCode = 0; + await cli.serve([...argv], { + env: {}, + exit: (code) => { + exitCode = code; + }, + stdout: (text) => { + stdout += text; + }, + }); + return { stdout, exitCode }; +}; + +/** + * Options whose owning field already declares a schema default. The help + * renderer appends that default, so the description must not restate it. + */ +const singleDefaultOptions: readonly { + readonly command: string; + readonly options: readonly string[]; +}[] = [ + { command: "inspect-byte-entropy", options: ["--block-size", "--offset"] }, + { + command: "inspect-devicetree", + options: [ + "--max-file-bytes", + "--max-nodes", + "--max-properties", + "--max-depth", + ], + }, + { + command: "decode-signal-recording", + options: ["--line_offset", "--line_limit"], + }, + { + command: "inspect-packet-capture", + options: ["--match_offset", "--match_limit"], + }, + { + command: "inspect-packet-fields", + options: ["--kind", "--offset"], + }, + { command: "inspect-signal-decoders", options: ["--offset"] }, +]; + +const optionLine = ( + help: string, + option: string, +): string | undefined => { + const line = help + .split(/\r?\n/u) + .find((candidate) => candidate.trimStart().startsWith(`${option} `)); + return line?.trim(); +}; + +describe("CLI help default annotations", () => { + it("states each schema-owned option default exactly once", async () => { + const cli = createCli({}); + const repeated: string[] = []; + for (const { command, options } of singleDefaultOptions) { + const { stdout, exitCode } = await serve(cli, [command, "--help"]); + expect(exitCode, command).toBe(0); + for (const option of options) { + const line = optionLine(stdout, option); + if (line === undefined) { + repeated.push(`${command} ${option}: option line missing`); + continue; + } + const statements = line.match(/default/giu)?.length ?? 0; + if (statements !== 1) + repeated.push(`${command} ${option}: ${statements} default statements`); + } + } + expect(repeated).toEqual([]); + }); +});