diff --git a/src/cli/cliOutputFormatter.ts b/src/cli/cliOutputFormatter.ts index 37ef0b3338..86056e100c 100644 --- a/src/cli/cliOutputFormatter.ts +++ b/src/cli/cliOutputFormatter.ts @@ -40,7 +40,8 @@ const isObjectArray = (value: unknown): value is Record[] => value.length > 0 && value.every( (item) => typeof item === "object" && item !== null && !Array.isArray(item), - ); + ) && + value.some((item) => Object.keys(item).length > 0); const escapeTableCell = (value: string): string => value @@ -55,6 +56,7 @@ const renderTable = ( headers: readonly string[], rows: readonly string[][], ): string => { + if (headers.length === 0) return ""; const escapedHeaders = headers.map(escapeTableCell); const widths = escapedHeaders.map((header) => header.length); for (const row of rows) { @@ -105,6 +107,7 @@ const formatObjectTable = ( seen.add(key); headers.push(key); } + if (headers.length === 0) return markdownCell(items); return renderTable( headers, items.map((item) => diff --git a/tests/boundary/cli/cliOutput.test.ts b/tests/boundary/cli/cliOutput.test.ts index 4decca57fa..568b6c3eb7 100644 --- a/tests/boundary/cli/cliOutput.test.ts +++ b/tests/boundary/cli/cliOutput.test.ts @@ -34,6 +34,14 @@ describe("CLI output argument boundary", () => { expect(JSON.parse(renderCliDocument(value, "json", false))).toEqual(value); }); + it("formats arrays of empty objects without column keys as JSON in Markdown", () => { + expect(renderCliDocument([{}], "md", false)).toBe("[{}]\n"); + expect(renderCliDocument([{}, {}], "md", false)).toBe("[{},{}]\n"); + expect(renderCliDocument({ empty: [{}] }, "md", false)).toBe( + "## empty\n\n[{}]\n", + ); + }); + it("rejects token windows that would corrupt structured output", () => { for (const format of ["json", "jsonl", "yaml"] as const) { const validation = parseCliOutputArguments([ diff --git a/tests/boundary/cli/filteredArrayOutput.test.ts b/tests/boundary/cli/filteredArrayOutput.test.ts index beec75c0e1..c0dbd382ee 100644 --- a/tests/boundary/cli/filteredArrayOutput.test.ts +++ b/tests/boundary/cli/filteredArrayOutput.test.ts @@ -91,6 +91,15 @@ it("renders only each record's own fields in heterogeneous Markdown tables", asy ]); }); +it("formats arrays of empty objects without columns as JSON in Markdown", async () => { + const value: unknown = [{}, {}]; + freezeRecursively(value); + const json = await runFilteredCli(value, "json"); + expect(JSON.parse(json)).toEqual(value); + const markdown = await runFilteredCli(value, "md"); + expect(markdown).toBe("[{},{}]\n"); +}); + it.each( (["json", "jsonl"] as const).flatMap((format) => [false, true].map((full) => ({ format, full })),