fix(cli): validate outline-depth before numeric coercion (#7505)

Fixes morluto/rea#7425

Signed-off-by: pqminh27 <minh8a1hb@gmail.com>
This commit is contained in:
PM authored and GitHub committed 2026-10-28 05:16:49 +08:00
1 parent 423d190026
commit baec375c34
3 files changed
+87 -19

No files matched your search

+2 -7
View File
@@ -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",
@@ -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);
});
});
@@ -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"],
]) {