import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { plainParagraphs } from "../src/components/chat-message-paragraphs.ts"; const readProjectFile = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8"); const contentSource = readProjectFile("src/components/chat-message-content.tsx"); const markdownViewSource = readProjectFile("src/components/chat-markdown-view.tsx"); const messageRowSource = readProjectFile("src/components/chat-message-row.tsx"); const activitySource = readProjectFile("src/components/agent-activity-status.tsx"); const prefetchSource = readProjectFile("src/components/chat-chunk-prefetch.ts"); test("the markdown pipeline is split out of the initial chat bundle", () => { // Given: the chat surface statically imports the message content component. assert.doesNotMatch(contentSource, /^import .*"react-markdown"/m); assert.doesNotMatch(contentSource, /^import .*"remark-gfm"/m); // Then: the renderer only arrives through a dynamic import. assert.match(contentSource, /import\("@\/components\/chat-markdown-view"\)/); assert.match(contentSource, /markdownRenderer = module\.renderChatMarkdown/); assert.match(markdownViewSource, /^import ReactMarkdown, \{ type Components \} from "react-markdown";$/m); assert.match(markdownViewSource, /^import remarkGfm from "remark-gfm";$/m); assert.match(markdownViewSource, /remarkPlugins=\{\[remarkGfm\]\}/); assert.match(markdownViewSource, /skipHtml/); }); test("the pre-markdown fallback renders prose but never raw markdown", () => { // Given: a reply that carries markdown syntax. assert.equal(plainParagraphs("**重点**在这里"), null); assert.equal(plainParagraphs("## 阶段判断"), null); assert.equal(plainParagraphs("- 第一点"), null); assert.equal(plainParagraphs("1. 第一点"), null); assert.equal(plainParagraphs("| 宫位 | 行星 |"), null); assert.equal(plainParagraphs("`Vimshottari`"), null); assert.equal(plainParagraphs("参见 https://example.com"), null); assert.equal(plainParagraphs("> 引用"), null); assert.equal(plainParagraphs("[链接](/x)"), null); assert.equal(plainParagraphs("行尾软换行 \n下一行"), null); // Then: only plain prose takes the fallback path, split exactly like CommonMark. assert.deepEqual(plainParagraphs("第一段。\n\n第二段。"), ["第一段。", "第二段。"]); assert.deepEqual(plainParagraphs("一行\n下一行"), ["一行\n下一行"]); assert.deepEqual(plainParagraphs("只有一段。"), ["只有一段。"]); // And: one stable wrapper serves both paths, so no styling or caret is lost on upgrade. assert.equal(contentSource.match(/className="message-markdown"/g)?.length, 1); assert.match(contentSource, /className="message-answer"/); assert.match(contentSource, /TechniqueAuditDisclosure/); assert.match(contentSource, /
\{paragraph\}<\/p>/); assert.match(contentSource, /plainParagraphs\(spoken\) \?\? \[\]/); }); test("gsap loads on demand while keeping the reduced-motion gate", () => { // Given: the animation engine is no longer part of the static import graph. assert.doesNotMatch(messageRowSource, /^import .*from "gsap"/m); assert.doesNotMatch(messageRowSource, /@gsap\/react/); assert.match(messageRowSource, /import\("gsap"\)/); // Then: reduced-motion users never request it, and the tween contract is unchanged. assert.match(messageRowSource, /prefers-reduced-motion: reduce/); assert.match(messageRowSource, /prefers-reduced-motion: no-preference/); assert.match(messageRowSource, /gsap\.matchMedia\(\)/); assert.match(messageRowSource, /duration: 0\.18/); assert.match(messageRowSource, /clearProps: "opacity,transform,visibility"/); // And: a row that mounts before the chunk lands renders unanimated instead of flashing. assert.match(messageRowSource, /if \(!gsapCore\) \{/); assert.match(messageRowSource, /useEntryEffect\(\(\) => \{/); }); test("the thinking orb loads on demand behind a same-size placeholder", () => { // Given: only the state type is imported at build time. assert.doesNotMatch(activitySource, /^import \{ ThinkingOrb/m); assert.match(activitySource, /^import type \{ OrbState \} from "thinking-orbs";$/m); assert.match(activitySource, /dynamic\(async \(\) => \(await importThinkingOrb\(\)\)\.ThinkingOrb/); assert.match(activitySource, /ssr: false/); // Then: the placeholder reserves the exact 20px orb box so the row cannot shift. assert.match(activitySource, /loading: \(\) => \(/); assert.match(activitySource, /height: 20, width: 20/); assert.match(activitySource, /flex: "0 0 auto"/); // And: the live region and the decorative marking survive. assert.match(activitySource, /role="status"/); assert.match(activitySource, /