fix(cli): preserve blank JSON paths for input validation (#1068)

* fix(cli): preserve blank JSON paths for input validation

* test(cli): avoid unchecked path field assertions

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
This commit is contained in:
ASVLCII 2026-10-08 19:57:54 +07:00 committed by GitHub
parent 6a7650e93c
commit 3dcb732da3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 100 additions and 4 deletions

View File

@ -37,9 +37,9 @@ export const parseCliJsonInput = async (
/**
* Resolve named string fields of CLI-supplied JSON against the operator
* working directory. Each entry is a key path from the document root; only
* fields holding strings are resolved, everything else passes through
* untouched. Shared MCP contracts reject relative paths, so CLI workflows
* resolve operator-relative values before validation.
* fields holding nonblank strings are resolved, everything else passes through
* untouched so shared schemas can reject blank paths. Shared MCP contracts reject
* relative paths, so CLI workflows resolve operator-relative values before validation.
*/
export const resolveCliJsonPaths = (
value: unknown,
@ -61,7 +61,12 @@ const resolveJsonPath = (
([key, entry]: readonly [string, unknown]): readonly [string, unknown] => {
if (key !== head) return [key, entry];
if (tail.length === 0)
return [key, typeof entry === "string" ? resolve(entry) : entry];
return [
key,
typeof entry === "string" && entry.trim().length > 0
? resolve(entry)
: entry,
];
return [key, resolveJsonPath(entry, tail)];
},
);

59
src/cliJsonPaths.test.ts Normal file
View File

@ -0,0 +1,59 @@
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { resolveCliJsonPaths } from "./cliJsonInput.js";
import { electronActiveObservationInputSchema } from "./domain/javascript/electronActiveObservation.js";
const paths = [
["executable_path"],
["application_path"],
["application_root"],
] as const;
describe("CLI JSON path normalization", () => {
it.each([
["executable_path", ""],
["executable_path", " \t "],
["application_path", ""],
["application_path", " \t "],
["application_root", ""],
["application_root", " \t "],
])(
"keeps blank %s (%j) invalid at the shared input boundary",
(field, value) => {
const input = {
executable_path: "/synthetic/electron",
application_path: "/synthetic/app",
application_root: "/synthetic",
[field]: value,
};
const parsed = electronActiveObservationInputSchema.safeParse(
resolveCliJsonPaths(input, paths),
);
expect(parsed.success).toBe(false);
if (parsed.success) throw new Error("Expected blank path rejection");
expect(parsed.error.issues).toEqual(
expect.arrayContaining([expect.objectContaining({ path: [field] })]),
);
expect(input).toHaveProperty(field, value);
},
);
it("resolves selected relative paths without trimming names or changing unrelated fields", () => {
const input = {
browser: { executable_path: " browser ", label: "relative-label" },
application_path: "app",
};
expect(
resolveCliJsonPaths(input, [["browser", "executable_path"]]),
).toEqual({
browser: {
executable_path: resolve(" browser "),
label: "relative-label",
},
application_path: "app",
});
expect(input.browser.executable_path).toBe(" browser ");
});
});

View File

@ -0,0 +1,32 @@
import { expect } from "vitest";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest.for(["", " \t "])(
"rejects blank Electron application_root (%j) before filesystem or provider work",
async (applicationRoot, { cli }) => {
const response = await cli.run({
arguments: [
"capture-electron-scenario",
JSON.stringify({
executable_path: "/synthetic/missing-electron",
application_path: "/synthetic/missing-app",
application_root: applicationRoot,
}),
"--json",
],
environment: { REA_LOG_LEVEL: "silent" },
timeoutMs: 15000,
});
expect(response.exitCode).toBe(1);
expect(response.json).toMatchObject({
code: "invalid_request",
details: {
operation: "capture_electron_scenario",
issues: expect.arrayContaining([
{ path: ["application_root"], reason: "out_of_range", minimum: 1 },
]),
},
});
},
);