/** * Regression tests for progressive (streaming) markdown rendering. * * Run with: npm test (node --test, no extra dev deps) * * The invariant under test: at EVERY prefix of the stream, the visible output * must contain no raw markdown syntax — no `|---|` separators, no `**`, no * `#` heading markers, no ``` fences — and the final frame must contain the * same structure as the completed-message render. * * Frames are rendered through the REAL production parser (react-markdown + * remark-gfm, via react-dom/server), so these tests exercise exactly what the * browser shows, not a simulation. */ import test from "node:test"; import assert from "node:assert/strict"; import React from "react"; import { renderToStaticMarkup } from "react-dom/server"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; import { extractOpenCodeFence, repairMarkdownTables, splitStreaming, withoutOpenCodeFence, withoutOpenMathBlock, withoutNascentTable, isTableSeparatorLine, cleanStreamTail, } from "./streamRender.js"; // ---------- helpers ---------- /** Render markdown exactly like Message.jsx's does. */ function renderMd(md) { return renderToStaticMarkup( React.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm, [remarkMath, { singleDollarTextMath: false }]], rehypePlugins: [[rehypeKatex, { strict: false, throwOnError: false }]], }, repairMarkdownTables(md)), ); } /** One streaming frame as HTML: committed markdown + sanitized tail. */ function renderFrame(buf) { const { thinking, committed, tail, liveCode } = splitStreaming(buf); if (thinking) return ""; return ( (committed ? renderMd(committed) : "") + (liveCode ? `
${escapeHtml(liveCode.code)}
` : "") + (tail ? `
${tail}
` : "") ); } function escapeHtml(value) { return String(value || "") .replace(/&/g, "&") .replace(//g, ">"); } /** Visible text of a rendered frame: strip all HTML tags. */ function visibleText(html) { return html.replace(/<[^>]+>/g, ""); } /** Assert no raw markdown syntax is visible at ANY prefix of `answer`. * Rendered code blocks are exempt: characters like ** or | inside *
 are literal code content, not leaked markdown syntax.
 *  (The old regex renderer even bolded text inside code blocks — a bug
 *  react-markdown fixes, which is why the exemption is needed now.) */
function assertNoLeaksAtEveryPrefix(answer) {
  for (let i = 1; i <= answer.length; i++) {
    const frame = renderFrame(answer.slice(0, i))
      .replace(/]*)?>[\s\S]*?<\/pre>/g, "");
    const v = visibleText(frame);
    assert.ok(!/\|\s*:?-{3,}/.test(v), `frame ${i}: leaked table separator:\n${v}`);
    assert.ok(!v.includes("**"), `frame ${i}: leaked ** stars:\n${v}`);
    assert.ok(!/(^|\n)\s*#{1,6}\s/.test(v), `frame ${i}: leaked # heading marker:\n${v}`);
    assert.ok(!v.includes("```"), `frame ${i}: leaked \`\`\` fence:\n${v}`);
    assert.ok(!v.includes("$$"), `frame ${i}: leaked $$ math fence:\n${v}`);
  }
}

/** The completed-message render must contain the expected structural tags. */
function assertFinalHas(answer, tags) {
  const done = renderMd(answer);
  for (const t of tags) {
    assert.ok(done.includes(t), `completed render missing ${t}:\n${done}`);
  }
}

// ---------- tables ----------

const TABLE = [
  "Here is the comparison.",
  "",
  "| Aspect | Roth IRA | Traditional IRA |",
  "|--------|----------|-----------------|",
  "| **Tax now** | After-tax | Pre-tax |",
  "| Withdrawals | Tax-free | Taxed |",
].join("\n");

test("table: no raw pipes/stars at any prefix", () => {
  assertNoLeaksAtEveryPrefix(TABLE);
  for (let i = 1; i <= TABLE.length; i++) {
    const v = visibleText(renderFrame(TABLE.slice(0, i)));
    assert.ok(!v.includes("Aspect | Roth"), `frame ${i}: raw header row visible`);
  }
});

test("table: renders as  once separator lands, then grows row by row", () => {
  const sepEnd = TABLE.indexOf("\n", TABLE.indexOf("|----"));
  const after = renderFrame(TABLE.slice(0, sepEnd + 2));
  assert.ok(after.includes("
"), "table absent right after separator"); const before = renderFrame(TABLE.slice(0, TABLE.indexOf("|----") - 1)); assert.ok(!before.includes("
")); assert.ok(!visibleText(before).includes("|")); }); test("table: completed render intact", () => { assertFinalHas(TABLE, ["
", "", ""]); }); // ---------- code fences ---------- const CODE = [ "Use this snippet:", "", "```python", "x = compute(1) # comment with **stars** and |pipes|", "print(x)", "```", "", "Done.", ].join("\n"); test("code fence: body streams inside a live code block without raw fences", () => { assertNoLeaksAtEveryPrefix(CODE); const openIdx = CODE.indexOf("print(x)") + 4; // mid-body const html = renderFrame(CODE.slice(0, openIdx)); const v = visibleText(html); assert.ok(html.includes('class="stream-code"'), "live code block absent while fence open"); assert.ok(v.includes("compute"), "code body did not stream inside live code block"); assert.ok(!v.includes("```"), "raw code fence visible while streaming"); const closeIdx = CODE.indexOf("```", CODE.indexOf("python")) + 4; const closed = renderFrame(CODE.slice(0, closeIdx)); assert.ok(closed.includes("
"), "closed fence did not render");
});

test("code fence: completed render intact", () => {
  assertFinalHas(CODE, ["
", " {
  assertNoLeaksAtEveryPrefix(LISTS);
  const afterFirstItem = LISTS.indexOf("money") + 6;
  const html = renderFrame(LISTS.slice(0, afterFirstItem));
  assert.ok(html.includes("
")); }); test("isTableSeparatorLine", () => { assert.ok(isTableSeparatorLine("|---|---|")); assert.ok(isTableSeparatorLine("| :--- | ---: |")); assert.ok(!isTableSeparatorLine("| a | b |")); assert.ok(!isTableSeparatorLine("---")); }); test("cleanStreamTail: hides table rows, strips inline markers", () => { assert.equal(cleanStreamTail("| a | b"), ""); assert.equal(cleanStreamTail("## Head"), "Head"); assert.equal(cleanStreamTail("some **bo"), "some bo"); assert.equal(cleanStreamTail("a *wor"), "a wor"); assert.equal(cleanStreamTail("> quo"), "quo"); assert.equal(cleanStreamTail("run `np"), "run np"); }); // ---------- spinner ---------- test("empty buffer reports thinking state", () => { assert.equal(splitStreaming("").thinking, true); assert.equal(splitStreaming(" ").thinking, true); assert.equal(splitStreaming("hi").thinking, false); });