fix(cli): reject unterminated output filter slices (#2315)

This commit is contained in:
Liu Jie authored and GitHub committed 2026-10-13 20:04:33 +08:00
1 parent fb28ec1537
commit c95315d2ce
2 files changed
+118 -3

No files matched your search

+29 -3
View File
@@ -9,7 +9,7 @@ export type CliOutputArgumentValidation =
| {
readonly ok: false;
readonly format: StructuredOutputFormat;
readonly code: "UNSUPPORTED_OUTPUT_COMBINATION";
readonly code: "UNSUPPORTED_OUTPUT_COMBINATION" | "INVALID_OUTPUT_FILTER";
readonly message: string;
};
@@ -89,13 +89,39 @@ export const parseCliOutputArguments = (
return parsed(false);
};
/** Reject text-window controls that would corrupt a structured document. */
/** Reject slices that cannot advance within Incur's comma-delimited paths. */
const hasUnclosedFilterSlice = (expression: string): boolean => {
let depth = 0;
let unclosed = false;
for (const character of expression) {
if (character === "[") {
depth += 1;
unclosed = true;
} else if (character === "]") {
depth -= 1;
// Incur consumes everything through the next closing bracket, including
// nested opening brackets. Do not impose a different filter grammar.
unclosed = false;
} else if (character === "," && depth === 0 && unclosed) return true;
}
return unclosed;
};
/** Reject output controls that cannot safely produce a complete result. */
export const validateCliOutputArguments = (
arguments_: readonly string[],
): CliOutputArgumentValidation => {
const { format, parseError, tokenWindow } =
const { filterOutput, format, parseError, tokenWindow } =
parseCliOutputArguments(arguments_);
if (parseError) return { ok: true };
if (filterOutput !== undefined && hasUnclosedFilterSlice(filterOutput))
return {
ok: false,
format: format === "yaml" || format === "jsonl" ? format : "json",
code: "INVALID_OUTPUT_FILTER",
message:
"The --filter-output expression has a slice without a closing ]. Close the slice or remove --filter-output.",
};
if (
tokenWindow &&
(format === "json" || format === "jsonl" || format === "yaml")
+89
View File
@@ -58,6 +58,95 @@ describe("CLI output argument boundary", () => {
});
});
describe("unclosed CLI output filter slices", () => {
it.each([
"providers[",
"providers[0,1].name[",
"name,providers[",
"][,]",
"providers],name[0,1]",
])("rejects %s before the filter parser runs", (filter) => {
expect(
validateCliOutputArguments([
"providers",
"--json",
"--filter-output",
filter,
]),
).toMatchObject({ ok: false, code: "INVALID_OUTPUT_FILTER" });
});
it("preserves effective-filter and earlier flag-error precedence", () => {
for (const arguments_ of [
[
"--filter-output",
"providers[",
"--filter-output",
"capabilities[0,1].provider.id,open",
],
["--filter-output", "capabilities[-1,].provider.id"],
["--filter-output", "capabilities[[0,1]"],
["--format", "invalid", "--filter-output", "providers["],
["--token-limit", "invalid", "--filter-output", "providers["],
])
expect(validateCliOutputArguments(arguments_)).toEqual({ ok: true });
});
workspaceCliTest(
"fails promptly across output formats without invoking the command",
async ({ cli }) => {
for (const format of ["json", "jsonl", "yaml", "toon", "md"]) {
const result = await cli.run({
arguments: [
"analyze",
"/absent/target",
"--format",
format,
"--filter-output",
"result[0,1].name[",
],
timeoutMs: 3000,
});
expect(result.exitCode).toBe(1);
expect(result.stdout).toContain("INVALID_OUTPUT_FILTER");
expect(result.stdout).toContain("closing ]");
expect(result.stderr).toBe("");
}
const splitPath = await cli.run({
arguments: ["providers", "--json", "--filter-output", "][,]"],
timeoutMs: 3000,
});
expect(splitPath.exitCode).toBe(1);
expect(splitPath.json).toMatchObject({
ok: false,
error: { code: "INVALID_OUTPUT_FILTER" },
});
},
CLI_INTEGRATION_TIMEOUT_MS,
);
workspaceCliTest(
"retains closed slices and multiple filter paths",
async ({ cli }) => {
const result = await cli.run({
arguments: [
"providers",
"--json",
"--filter-output",
"capabilities[0,1].provider.id,open",
],
timeoutMs: 3000,
});
expect(result.exitCode).toBe(0);
expect(result.json).toEqual({
capabilities: [{ provider: { id: expect.any(String) } }],
open: false,
});
},
CLI_INTEGRATION_TIMEOUT_MS,
);
});
describe("CLI output valued-flag parsing", () => {
workspaceCliTest(
"preserves complete JSON errors when native builtin flags follow unusual arguments",