Files
Jyotisha/frontend/tests/chat-markdown-split.test.ts
T
Jesse_Chen ad9dba5c79 perf(chat): coalesce stream events per frame and pace text release
Every NDJSON event used to commit its own React update and re-parse the
whole partial answer through react-markdown, so long replies grew
quadratically slower. Stream events now land in a frame buffer that
flushes at most once per animation frame, releases answer and thinking
text at a steady pace with a twelve-frame catch-up, and settles
synchronously on completion, failure and abort. Streaming markdown is
split at the last completed block so only the tail is re-parsed each
frame. Applied to both the consultation hook and the rectification chat.

BUG-473

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-02 04:29:01 +00:00

75 lines
3.9 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { createElement } from "react";
import { renderToString } from "react-dom/server";
import { StreamingMarkdown } from "../src/components/chat-message-content.tsx";
import { splitStableMarkdown } from "../src/lib/chat-markdown-split.ts";
const contentSource = readFileSync(new URL("../src/components/chat-message-content.tsx", import.meta.url), "utf8");
const messageRowSource = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8");
test("the stable prefix ends at the last completed paragraph and the tail keeps streaming", () => {
assert.deepEqual(splitStableMarkdown("只有一段还没写完"), { stable: "", tail: "只有一段还没写完" });
assert.deepEqual(splitStableMarkdown("第一段。\n\n第二段还在"), { stable: "第一段。", tail: "第二段还在" });
assert.deepEqual(
splitStableMarkdown("## 标题\n\n第一段。\n\n第二段。\n\n第三"),
{ stable: "## 标题\n\n第一段。\n\n第二段。", tail: "第三" },
);
});
test("the cut never lands inside a fence, between list items, or inside a table", () => {
const fenced = "前言。\n\n```txt\n第一行\n\n第二行";
assert.deepEqual(splitStableMarkdown(fenced), { stable: "前言。", tail: "```txt\n第一行\n\n第二行" });
const looseList = "- 甲\n\n- 乙\n\n- 丙还在";
assert.deepEqual(splitStableMarkdown(looseList), { stable: "", tail: looseList });
const listThenParagraph = "- 甲\n- 乙\n\n总结一下";
assert.deepEqual(splitStableMarkdown(listThenParagraph), { stable: "- 甲\n- 乙", tail: "总结一下" });
const orderedListContinues = "1. 甲\n\n2. 乙\n\n 缩进的补充";
assert.deepEqual(splitStableMarkdown(orderedListContinues), { stable: "", tail: orderedListContinues });
const table = "说明。\n\n| 技法 | 状态 |\n| --- | --- |\n| 甲 | 已执行 |\n\n> 引用";
assert.deepEqual(
splitStableMarkdown(table),
{ stable: "说明。\n\n| 技法 | 状态 |\n| --- | --- |\n| 甲 | 已执行 |", tail: "> 引用" },
);
});
test("growing the tail keeps the prefix string identical so the memoised prefix is not re-parsed", () => {
const before = splitStableMarkdown("第一段。\n\n第二段。\n\n第三段正在");
const after = splitStableMarkdown("第一段。\n\n第二段。\n\n第三段正在写,还没有换段");
assert.equal(before.stable, after.stable);
assert.notEqual(before.tail, after.tail);
// Only once a new paragraph completes does the prefix move forward.
const later = splitStableMarkdown("第一段。\n\n第二段。\n\n第三段写完了。\n\n第四");
assert.equal(later.stable, "第一段。\n\n第二段。\n\n第三段写完了。");
});
test("the streaming renderer parses the prefix and the tail as two separate documents", () => {
const calls: string[] = [];
const renderMarkdown = (text: string) => {
calls.push(text);
return createElement("p", null, text);
};
renderToString(createElement(StreamingMarkdown, {
text: "第一段。\n\n第二段。\n\n第三段还在",
renderMarkdown,
}));
// Server rendering evaluates the tail in the parent and the memoised prefix as a child,
// so compare as a set: what matters is that neither call sees the whole answer.
assert.deepEqual([...calls].sort(), ["第一段。\n\n第二段。", "第三段还在"].sort());
// The prefix component is memoised on its text, so an unchanged prefix costs no parse.
assert.match(contentSource, /const StableMarkdownPrefix = memo\(function StableMarkdownPrefix/);
assert.match(contentSource, /splitStableMarkdown\(text\)/);
assert.match(contentSource, /streaming\s*\?\s*<StreamingMarkdown/);
// Settled answers still parse once as one document.
assert.match(contentSource, /: renderMarkdown\s*\?\s*renderMarkdown\(spoken\)/);
assert.match(messageRowSource, /streaming=\{message\.state !== "settled"\}/);
});