diff --git a/src/cli/utilityCommands.ts b/src/cli/utilityCommands.ts index e84cc46a1e..382c48bba3 100644 --- a/src/cli/utilityCommands.ts +++ b/src/cli/utilityCommands.ts @@ -1,5 +1,6 @@ import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js"; import { z } from "incur"; +import { cliIntegerArgument } from "./options.js"; import type { DirectAnalysis } from "../composition/directAnalysis.js"; import type { ReferenceSourcePolicy } from "../domain/referenceSourcePolicy.js"; @@ -288,13 +289,7 @@ const registerReferenceSourceCommand = ( .describe( "Complete committed graph or compact historical-source summary", ), - outlineDepth: z - .number() - .int() - .safe() - // After .safe(): its safe-range check overwrites an earlier minimum, - // which advertised negative depths the parser rejects. - .nonnegative() + outlineDepth: cliIntegerArgument(0) .optional() .describe( "Summary outline path depth (default: 2; 0 omits entries); requires --detail summary", diff --git a/tests/boundary/cli/cliInputSchemaRequired.test.ts b/tests/boundary/cli/cliInputSchemaRequired.test.ts index 0764147e61..2c57d1a1ac 100644 --- a/tests/boundary/cli/cliInputSchemaRequired.test.ts +++ b/tests/boundary/cli/cliInputSchemaRequired.test.ts @@ -394,25 +394,44 @@ describe("Apktool locale selection input schema", () => { }); describe("reference-source outline-depth input schema", () => { - it("aligns advertised bounds with the parser's accepted values", async () => { + it("parses decimal CLI tokens into the advertised safe integer schema", async () => { const cli = createCli({}); const advertised = await advertisedSchema(cli, "import-reference-source"); const outlineDepth = advertised.options?.properties?.outlineDepth; expect(outlineDepth).toMatchObject({ type: "integer", minimum: 0 }); const parser = parserSchema(cli, "import-reference-source", "options"); - expect(parser?.safeParse({ outlineDepth: -1 }).success).toBe(false); - expect(parser?.safeParse({ outlineDepth: 0 }).success).toBe(true); - expect(parser?.safeParse({ outlineDepth: 2 }).success).toBe(true); + expect(parser?.safeParse({}).success).toBe(true); + const parsedValues = [ + ["0", 0], + ["2", 2], + ["02", 2], + [String(Number.MAX_SAFE_INTEGER), Number.MAX_SAFE_INTEGER], + ] as const; + for (const [token, expected] of parsedValues) { + const parsed = parser?.safeParse({ outlineDepth: token }); + expect(parsed?.success).toBe(true); + if (parsed === undefined || !parsed.success) + throw new Error(`Expected outline-depth token ${token} to parse`); + expect(parsed.data).toMatchObject({ outlineDepth: expected }); + } + for (const token of ["", "-1", "9007199254740992"]) { + expect(parser?.safeParse({ outlineDepth: token }).success).toBe(false); + } const ajv = new Ajv2020({ strict: false }); const validate = ajv.compile({ ...advertised.options, $schema: "https://json-schema.org/draft/2020-12/schema", }); + for (const [token] of parsedValues) { + const parsed = parser?.safeParse({ outlineDepth: token }); + if (parsed === undefined || !parsed.success) + throw new Error(`Expected outline-depth token ${token} to parse`); + expect(validate(parsed.data)).toBe(true); + } expect(validate({ outlineDepth: -1 })).toBe(false); - expect(validate({ outlineDepth: 0 })).toBe(true); - expect(validate({ outlineDepth: 2 })).toBe(true); + expect(validate({ outlineDepth: 9007199254740992 })).toBe(false); }); }); diff --git a/tests/boundary/cli/referenceSourceSummary.test.ts b/tests/boundary/cli/referenceSourceSummary.test.ts index ba5398a866..b0cc69d5cf 100644 --- a/tests/boundary/cli/referenceSourceSummary.test.ts +++ b/tests/boundary/cli/referenceSourceSummary.test.ts @@ -58,11 +58,22 @@ describe.skipIf(process.platform === "win32")( manifest_id: `hsm_${createHash("sha256").update(semanticJson).digest("hex")}`, }; expect((await run("--detail", "full")).stdout).toBe(complete.stdout); - for (const depth of [undefined, 0, 1, 10]) { + for (const { depth, token } of [ + { depth: undefined, token: undefined }, + { depth: 0, token: "0" }, + { depth: 1, token: "1" }, + { depth: 2, token: "2" }, + { depth: 2, token: "02" }, + { depth: 10, token: "10" }, + { + depth: Number.MAX_SAFE_INTEGER, + token: String(Number.MAX_SAFE_INTEGER), + }, + ]) { const result = await run( "--detail", "summary", - ...(depth === undefined ? [] : ["--outline-depth", String(depth)]), + ...(token === undefined ? [] : ["--outline-depth", token]), ); expect(result.exitCode).toBe(0); expect(result.json).toHaveProperty("manifest", expectedManifest); @@ -155,12 +166,55 @@ describe("compiled historical-source summary input boundaries", () => { }, ); + for (const [label, options] of [ + ["--outline-depth=", ["--detail", "summary", "--outline-depth="]], + [ + "an empty outline-depth argument", + ["--detail", "summary", "--outline-depth", ""], + ], + ...[ + " ", + "\t", + "1\n", + "0x1", + "0b1", + "1e0", + "+1", + "1.0", + "-0", + "-1", + "1.5", + "NaN", + "9007199254740992", + ].map((value) => [ + JSON.stringify(value), + ["--detail", "summary", "--outline-depth", value], + ]), + ] as const) { + cliTest(`rejects ${label} as an outline depth`, async ({ cli }) => { + const root = await createTestTempDirectory( + "rea-reference-summary-input-", + ); + const result = await cli.run({ + arguments: ["import-reference-source", root, ...options, "--json"], + cwd: root, + }); + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + code: "VALIDATION_ERROR", + fieldErrors: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("outlineDepth"), + }), + ]), + }); + expect(result.stdout).not.toContain("HistoricalSourceGraph"); + expect(result.stdout).not.toContain("HistoricalSourceSummary"); + }); + } + for (const options of [ ["--detail", "summry"], - ["--detail", "summary", "--outline-depth", "-1"], - ["--detail", "summary", "--outline-depth", "1.5"], - ["--detail", "summary", "--outline-depth", "NaN"], - ["--detail", "summary", "--outline-depth", "9007199254740992"], ["--outline-depth", "1"], ["--detail", "full", "--outline-depth", "1"], ]) {