fix(cli)!: keep a selected JSON null in --filter-output output (#2333)

createCliResultOutput rendered an explicit null selected by
--filter-output as the empty document {}, the same output it uses when
the selection has no value at all. On the streamed JSON/JSONL path,
a consumer could not distinguish a selected null from a missing field.

Write a selected null as null in streamed JSON and JSONL. This also
preserves an unfiltered top-level null. The {} fallback stays for
undefined, functions and symbols. The --full-output envelope already
carried data: null for a selected null. The --token-count path still
hands results that fit the formatted-string limit back to Incur and
prints 0 for a selected null. Other formats and Incur-owned result
paths are unchanged.

Consumers must accept JSON null for a present null and distinguish it
from a missing selection. Use --full-output if an object envelope is
required. Keeping the old shared {} representation would continue
hiding this distinction.


Claude-Session: https://claude.ai/code/session_019TTGAv6fe859ebbCLLfLHP

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jack Liang
2026-10-13 05:43:20 -07:00
committed by GitHub
parent 84cf7791a8
commit 1ca5fd38b4
2 changed files with 35 additions and 2 deletions
+3 -2
View File
@@ -216,8 +216,9 @@ export const createCliResultOutput = (
: filtered === undefined
? { ok: true, meta }
: { ok: true, data: filtered, meta }
: filtered === undefined ||
filtered === null ||
: // A selected null is a value; only a selection with no JSON value
// falls back to the empty document.
filtered === undefined ||
typeof filtered === "function" ||
typeof filtered === "symbol"
? {}
+32
View File
@@ -561,3 +561,35 @@ describe("CLI result output surface", () => {
});
}, 60_000);
});
describe("streamed CLI filtered selections", () => {
it.each([
["raw_result", "json", "null\n"],
["raw_result", "jsonl", "null\n"],
["nested.value", "json", '{"nested":{"value":null}}\n'],
["missing", "json", "{}\n"],
["count", "json", "0\n"],
["enabled", "json", "false\n"],
] as const)(
"keeps the selected value of --filter-output %s in %s",
async (filter, format, expected) => {
const chunks: Buffer[] = [];
const destination = new Writable({
write(chunk: Buffer, _encoding, callback) {
chunks.push(chunk);
callback();
},
});
const output = createCliResultOutput(
["--format", format, "--filter-output", filter],
destination,
);
if (output === undefined) throw new Error("Missing JSON output surface");
await output.write(
{ raw_result: null, nested: { value: null }, count: 0, enabled: false },
{ command: "fixture", duration: "1ms", format },
);
expect(Buffer.concat(chunks).toString("utf8")).toBe(expected);
},
);
});