diff --git a/src/cli/electronCommands.ts b/src/cli/electronCommands.ts index 73a4d4cd38..087ecfa023 100644 --- a/src/cli/electronCommands.ts +++ b/src/cli/electronCommands.ts @@ -37,6 +37,7 @@ import { evidenceBundleOption, runEvidenceInputCommand, } from "./evidenceInputCommand.js"; +import { localPathStringSchema } from "../domain/localPath.js"; /** Register CLI equivalents of the Electron MCP tools. */ export const registerElectronCommands = ( @@ -344,7 +345,9 @@ const registerJavaScriptApplicationCommand = ( description: "Statically reconstruct a local JavaScript/Electron application", args: z.object({ - path: z.string().describe("ASAR or extracted application path"), + path: localPathStringSchema.describe( + "ASAR or extracted application path", + ), }), options: javascriptApplicationOptions, run: ({ args, options }) => @@ -377,9 +380,7 @@ const registerJavaScriptApplicationCommand = ( return precision.error; return runCliJavaScriptApplicationAnalysis( { - // An empty path names no file; leave it for input validation. - input_path: - args.path === "" ? args.path : absoluteCallerPath(args.path), + input_path: absoluteCallerPath(args.path), ...(precision === undefined ? {} : { semantic_precision: precision.value }), diff --git a/tests/boundary/cli/javascriptApplicationPathAdmission.test.ts b/tests/boundary/cli/javascriptApplicationPathAdmission.test.ts new file mode 100644 index 0000000000..ea7d45ec40 --- /dev/null +++ b/tests/boundary/cli/javascriptApplicationPathAdmission.test.ts @@ -0,0 +1,35 @@ +import { Cli } from "incur"; +import { expect, it } from "vitest"; +import { registerElectronCommands } from "../../../src/cli/electronCommands.js"; +import { silentLogger } from "../../../src/logger.js"; + +const serve = async (argv: readonly string[]): Promise => { + const cli = Cli.create("javascript-application-path-admission", { + sync: false, + }); + registerElectronCommands(cli, silentLogger, {}); + let stdout = ""; + await cli.serve([...argv, "--json"], { + env: {}, + stdout: (text) => { + stdout += text; + }, + exit: () => undefined, + }); + return JSON.parse(stdout); +}; + +it("rejects an empty or NUL analyze-javascript-application path before analysis", async () => { + expect(await serve(["analyze-javascript-application", ""])).toMatchObject({ + code: "VALIDATION_ERROR", + fieldErrors: [expect.objectContaining({ path: "path", code: "too_small" })], + }); + expect( + await serve(["analyze-javascript-application", "target\x00name"]), + ).toMatchObject({ + code: "VALIDATION_ERROR", + fieldErrors: [ + expect.objectContaining({ path: "path", code: "invalid_format" }), + ], + }); +}); diff --git a/tests/boundary/javascript/javascriptSelectedPathEvidence.test.ts b/tests/boundary/javascript/javascriptSelectedPathEvidence.test.ts index bc5adad3fd..a7b74b8486 100644 --- a/tests/boundary/javascript/javascriptSelectedPathEvidence.test.ts +++ b/tests/boundary/javascript/javascriptSelectedPathEvidence.test.ts @@ -178,18 +178,9 @@ cliTest( ); // Anchoring must not turn an empty selection into the working directory. -cliTest.for([ - { - command: "analyze-javascript-application", - rejection: { - code: "invalid_request", - details: { issues: [expect.objectContaining({ path: ["input_path"] })] }, - }, - }, - { command: "analyze", rejection: { code: "VALIDATION_ERROR" } }, -])( - "$command rejects an empty path instead of analyzing the working directory", - async ({ command, rejection }, { cli }) => { +cliTest.for(["analyze-javascript-application", "analyze"])( + "%s rejects an empty path instead of analyzing the working directory", + async (command, { cli }) => { const root = await createTestTempDirectory("rea-js-empty-path-"); await writeFile(join(root, "main.js"), "export const value = 7;\n"); const response = await cli.run({ @@ -198,6 +189,11 @@ cliTest.for([ environment: { REA_LOG_LEVEL: "silent" }, }); expect(response.exitCode, response.stderr).toBe(1); - expect(response.json).toMatchObject(rejection); + expect(response.json).toMatchObject({ + code: "VALIDATION_ERROR", + fieldErrors: [ + expect.objectContaining({ path: "path", code: "too_small" }), + ], + }); }, );