From e124bdfc54707da138a58ae61b901f0bf93a9bac Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:07:17 +0800 Subject: [PATCH] fix(cli): preserve literal Markdown rendering (#7938) * fix(cli): preserve literal Markdown rendering * fix(cli): retain literal Markdown attributes * fix(cli): preserve literal punctuation in markdown * test(cli): update escaped empty object expectations * style(cli): format the literal empty-object Markdown expectation * test(mcp): assert bundle path issues by field compare_bundles reports each relative path as an invalid_format issue instead of one joined diagnostic sentence. * test(flutter): assert ABI selector refusals as structured issues inspect_dart_aot reports the ABI label as an issue on path abi. The message is the selector contract, without an abi: prefix. --------- Co-authored-by: Cursor Agent --- src/cli/cliOutputFormatter.ts | 35 ++++-- tests/boundary/cli/cliOutput.test.ts | 18 +-- .../boundary/cli/filteredArrayOutput.test.ts | 2 +- .../boundary/cli/markdownLiteralCells.test.ts | 104 +++++++++++++++++- .../boundary/cli/markdownNestedFacts.test.ts | 37 +++++-- 5 files changed, 166 insertions(+), 30 deletions(-) diff --git a/src/cli/cliOutputFormatter.ts b/src/cli/cliOutputFormatter.ts index 049600d4ed..ed4cd5910b 100644 --- a/src/cli/cliOutputFormatter.ts +++ b/src/cli/cliOutputFormatter.ts @@ -323,21 +323,36 @@ const escapeTableCell = (value: string): string => (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`, ) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/[\\|`*[\]~]|_+/g, escapeInlineSyntax) + .replace(/[\\|`*[\]~{}#]|_+/g, escapeInlineSyntax) + // Encode renderer-extension introducers at the same boundary as HTML + // characters. Generated entities must not themselves be re-escaped. + .replace(/[&<>:]|;(?=-?[)(])/g, (character) => { + switch (character) { + case "&": + return "&"; + case "<": + return "<"; + case ">": + return ">"; + case ":": + return ":"; + default: + return ";"; + } + }) + .replace(/([xXB8]-)\)/g, "$1)") .replace(/\r\n|\r|\n/g, "
"); +const encodeMarkdownWhitespace = (whitespace: string): string => + whitespace.replace(/ /g, " ").replace(/\t/g, " "); + /** Heading and paragraph facts are literal data, including block syntax. */ const escapeMarkdownText = (value: string): string => escapeTableCell(value) - .replace(/#/g, "\\#") .replace(/^(\s*)([-+=])/, "$1\\$2") .replace(/^(\s*\d{1,9})([.)])(?=\s)/, "$1\\$2") - .replace(/^[ \t]+/, (indent) => - indent.replace(/ /g, " ").replace(/\t/g, " "), - ); + .replace(/^[ \t]+/, encodeMarkdownWhitespace) + .replace(/[ \t]+$/u, encodeMarkdownWhitespace); const markdownHeading = (path: readonly string[]): string => `## ${escapeMarkdownText(path.join("."))}\n\n`; @@ -348,8 +363,8 @@ const renderTable = ( ): string => { if (headers.length === 0) return ""; const encodeTableCell = (value: string): string => - escapeTableCell(value).replace(/^ +| +$/g, (spaces) => - " ".repeat(spaces.length), + escapeTableCell(value).replace(/^[ \t]+|[ \t]+$/g, (spaces) => + encodeMarkdownWhitespace(spaces), ); const escapedHeaders = headers.map(encodeTableCell); const widths = escapedHeaders.map((header) => header.length); diff --git a/tests/boundary/cli/cliOutput.test.ts b/tests/boundary/cli/cliOutput.test.ts index 984f0c4cea..40b1299153 100644 --- a/tests/boundary/cli/cliOutput.test.ts +++ b/tests/boundary/cli/cliOutput.test.ts @@ -113,14 +113,6 @@ 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([ @@ -174,6 +166,16 @@ describe("CLI output argument boundary", () => { }); }); +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("preserves flag-shaped values consumed by valued output options", () => { expect( adaptCliOutputArgumentsForIncur([ diff --git a/tests/boundary/cli/filteredArrayOutput.test.ts b/tests/boundary/cli/filteredArrayOutput.test.ts index 33fd871f0a..9d2b13aa90 100644 --- a/tests/boundary/cli/filteredArrayOutput.test.ts +++ b/tests/boundary/cli/filteredArrayOutput.test.ts @@ -242,7 +242,7 @@ it("formats arrays of empty objects without columns as JSON in Markdown", async const json = await runFilteredCli(value, "json"); expect(JSON.parse(json)).toEqual(value); const markdown = await runFilteredCli(value, "md"); - expect(markdown).toBe("\\[{},{}\\]\n"); + expect(markdown).toBe("\\[\\{\\},\\{\\}\\]\n"); }); it.each( diff --git a/tests/boundary/cli/markdownLiteralCells.test.ts b/tests/boundary/cli/markdownLiteralCells.test.ts index 559b7e0e32..4942240f23 100644 --- a/tests/boundary/cli/markdownLiteralCells.test.ts +++ b/tests/boundary/cli/markdownLiteralCells.test.ts @@ -95,11 +95,15 @@ cliTest( " leading ": " leading ", leading: "leading", "trailing ": "trailing ", + "\tleading tab": "trailing tab\t", + " mixed\t ": " mixed\t ", " ": " ", " ": " ", "": "", " δΈ­ζ–‡πŸ™‚ ": " δΈ­ζ–‡πŸ™‚ ", " © a|b ": " © a|b ", + "{.hidden}": "literal {.hidden}", + "{name=value}": "{name=value}", }; const row = { [literal]: literal, @@ -168,6 +172,96 @@ cliTest( }, ); +cliTest( + "preserves capability provider identities in rendered Markdown", + async ({ cli }) => { + const json = await cli.run({ arguments: ["capabilities", "--json"] }); + const markdown = await cli.run({ + arguments: ["capabilities", "--format", "md"], + }); + expect(json.exitCode, json.stderr).toBe(0); + expect(markdown.exitCode, markdown.stderr).toBe(0); + const data = JSON.parse(json.stdout) as { + capabilities: { provider: unknown }[]; + }; + const renderer = await createMarkdownRenderer(process.cwd(), { + linkify: false, + }); + expect( + tableColumnByHeader(renderer.render(markdown.stdout), "provider"), + ).toEqual( + data.capabilities.map(({ provider }) => JSON.stringify(provider)), + ); + }, +); + +cliTest( + "keeps scalar markers, edge spaces, and emoji-like names literal in rendered reports", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-markdown-literal-markers-"); + const path = join(root, "bundle.json"); + const result = { + container: "::: details", + trailing: "archive ", + trailingTab: "archive\t", + trailingMixed: "archive \t ", + emoji: ":)", + shortcuts: ":smile: :D :-( ;) ;-() x-) X-) 8-) B-) <3 &", + rows: [{ value: ":)", nested: { path: ";) :smile: 8-)" } }], + after: "still visible", + }; + const record = createEvidence( + undefined, + { id: "fixture", name: "Fixture", version: "1" }, + { operation: "ordinary-json", parameters: {}, result }, + ); + await writeFile(path, JSON.stringify(createEvidenceBundle([record]))); + const base = [ + "inspect-evidence-bundle", + JSON.stringify({ path, detail: "complete" }), + "--filter-output", + "records.0.normalized_result", + ]; + const markdown = await cli.run({ arguments: [...base, "--format", "md"] }); + const json = await cli.run({ arguments: [...base, "--json"] }); + expect(markdown.exitCode, markdown.stderr).toBe(0); + expect(json.exitCode, json.stderr).toBe(0); + expect(JSON.parse(json.stdout)).toEqual({ + records: { "0": { normalized_result: result } }, + }); + const renderer = await createMarkdownRenderer(root, { linkify: false }); + const html = renderer.render(markdown.stdout); + const text = (node: DefaultTreeAdapterTypes.Node): string => + node.nodeName === "#text" && "value" in node + ? node.value + : "childNodes" in node + ? node.childNodes.map(text).join("") + : ""; + const children = parseFragment(html).childNodes; + for (const [key, value] of Object.entries(result).filter( + ([, value]) => typeof value === "string", + )) { + const index = children.findIndex( + (node) => + "tagName" in node && + node.tagName === "h2" && + text(node).replaceAll("\u200b", "").trim().endsWith(`.${key}`), + ); + expect(index, key).toBeGreaterThanOrEqual(0); + const body = children.slice(index + 1).find((node) => "tagName" in node); + expect(body && "tagName" in body ? body.tagName : undefined, key).toBe( + "p", + ); + expect(body ? text(body) : undefined, key).toBe(value); + } + expect(html).not.toMatch(/<(?:details|ol|li)(?:\s|>)/u); + expect(renderedCells(html)).toContain( + JSON.stringify(result.rows[0]?.nested), + ); + expect(renderedCells(html)).toContain(":)"); + }, +); + cliTest( "preserves safetensors identifier edge spaces in rendered CLI tables", async ({ cli }) => { @@ -258,9 +352,13 @@ cliTest( renderCliDocument(result, "md", false), ]) { const html = renderer.render(document); - expect(html).toContain( - "**literal** [source](wrong) <T> &copy;", - ); + const text = (node: DefaultTreeAdapterTypes.Node): string => + node.nodeName === "#text" && "value" in node + ? node.value + : "childNodes" in node + ? node.childNodes.map(text).join("") + : ""; + expect(text(parseFragment(html))).toContain(literal); expect(html).toContain("**key**
# injected"); expect(html).toContain("literal
- next
# heading"); expect(html).not.toMatch(/<(?:strong|em|ul|ol|li|T|pre|hr)(?:\s|>)/u); diff --git a/tests/boundary/cli/markdownNestedFacts.test.ts b/tests/boundary/cli/markdownNestedFacts.test.ts index 07408de1a2..d5d9daaffc 100644 --- a/tests/boundary/cli/markdownNestedFacts.test.ts +++ b/tests/boundary/cli/markdownNestedFacts.test.ts @@ -2,6 +2,8 @@ import { join } from "node:path"; import { Writable } from "node:stream"; import { Cli } from "incur"; import { expect, it } from "vitest"; +import { createMarkdownRenderer } from "vitepress"; +import { parseFragment, type DefaultTreeAdapterTypes } from "parse5"; import { parseCliOutputArguments, @@ -70,7 +72,7 @@ it("keeps source keys literal in Markdown headings through CLI result output", a expect(JSON.stringify(value)).toBe(original); }); -it("retains nested facts in standalone Markdown table cells and mixed arrays", () => { +it("retains nested facts in standalone Markdown table cells and mixed arrays", async () => { const value = [ { details: { path: "alpha.txt" }, @@ -79,13 +81,32 @@ it("retains nested facts in standalone Markdown table cells and mixed arrays", ( }, ]; const markdown = renderCliDocument(value, "md", false); - expect(markdown).toContain('{"path":"alpha.txt"}'); - expect(markdown).toContain('\\[{"path":"sub/beta.txt"}\\]'); - expect(markdown).toContain("caller\\|value"); - expect(markdown).not.toContain("[object Object]"); - expect( - renderCliDocument({ mixed: [1, { path: "retained" }] }, "md", false), - ).toContain('{"path":"retained"}'); + const renderer = await createMarkdownRenderer(process.cwd(), { + linkify: false, + }); + const text = (node: DefaultTreeAdapterTypes.Node): string => + node.nodeName === "#text" && "value" in node + ? node.value + : "childNodes" in node + ? node.childNodes.map(text).join("") + : ""; + const rendered = text(parseFragment(renderer.render(markdown))); + expect(rendered).toContain('{"path":"alpha.txt"}'); + expect(rendered).toContain('[{"path":"sub/beta.txt"}]'); + const literal = "colon: :) ;-) x) #tag :"; + const document = renderCliDocument({ [literal]: literal }, "md", false); + const renderedLiteral = text(parseFragment(renderer.render(document))); + expect(renderedLiteral.split(literal)).toHaveLength(3); + expect(rendered).toContain("caller|value"); + expect(rendered).not.toContain("[object Object]"); + const mixed = renderCliDocument( + { mixed: [1, { path: "retained" }] }, + "md", + false, + ); + expect(text(parseFragment(renderer.render(mixed)))).toContain( + '{"path":"retained"}', + ); expect(JSON.parse(renderCliDocument(value, "json", false))).toEqual(value); });