import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { createElement, createRef } from "react"; import { renderToString } from "react-dom/server"; import test from "node:test"; import type { ChatMessageFeedback } from "../src/components/chat-message-actions.tsx"; import { RectificationMessageEntry, UnsplitRectificationMessageList, type RectificationMessageActions, type RectificationMessageEntryProps, type RenderMessage, } from "../src/components/rectification-message-entry.tsx"; import { disableHomeStreamingRenderProbe, enableHomeStreamingRenderProbe, homeStreamingRenderProbeSnapshot, resetHomeStreamingRenderProbe, } from "../src/lib/home-streaming-render-probe.ts"; const chatSource = readFileSync( new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8", ); const entrySource = readFileSync( new URL("../src/components/rectification-message-entry.tsx", import.meta.url), "utf8", ); const rowSource = readFileSync( new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8", ); function settled(role: "user" | "assistant", text: string, key: string): RenderMessage { return { role, text, renderKey: key, state: "settled", }; } function liveAssistant(text: string): RenderMessage { return { role: "assistant", text, renderKey: "live-assistant", state: "streaming", }; } function emptyActions(): RectificationMessageEntryProps["actionsRef"] { const actionsRef = createRef() as RectificationMessageEntryProps["actionsRef"]; actionsRef.current = { submitChoice() {}, submitStop() {}, copyMessage() {}, regenerateMessage() {}, onFeedback() {}, }; return actionsRef; } function sharedProps(): Omit< RectificationMessageEntryProps, "message" | "regenerating" | "canRegenerate" | "copied" | "feedback" > { return { busy: true, readonly: false, currentQuestionFocusId: null, interactive: false, liveChoiceCard: null, choiceNonce: 0, savedTime: null, actionsRef: emptyActions(), }; } function entryProps( message: RenderMessage, extras: Partial> = {}, ): RectificationMessageEntryProps { return { ...sharedProps(), message, regenerating: extras.regenerating ?? false, canRegenerate: extras.canRegenerate ?? false, copied: extras.copied ?? false, feedback: extras.feedback, }; } test("the chat container no longer derives per-row timeline, varga, or choice cards", () => { assert.doesNotMatch(chatSource, /rectificationTimelineRows\s*\(/); assert.doesNotMatch(chatSource, /vargaSentenceFromMethods\s*\(/); assert.doesNotMatch(chatSource, /choiceCardFromQuestion\s*\(/); assert.match(entrySource, /rectificationTimelineRows\s*\(/); assert.match(entrySource, /vargaSentenceFromMethods\s*\(/); assert.match(entrySource, /choiceCardFromQuestion\s*\(/); }); test("the extracted row is memoised and takes no function props except actionsRef", () => { assert.match(entrySource, /export const RectificationMessageEntry = memo\(function RectificationMessageEntry/); const propsBlock = entrySource.slice( entrySource.indexOf("export type RectificationMessageEntryProps"), entrySource.indexOf("export function choiceCardFromQuestion"), ); assert.match(propsBlock, /actionsRef: MutableRefObject/); assert.doesNotMatch(propsBlock, /=>/); assert.doesNotMatch(propsBlock, /on[A-Z]\w+\?:/); assert.match(rowSource, /export const ChatMessageRow = memo\(function ChatMessageRow/); }); test("the split architecture renders settled rectification rows once while streaming tokens", () => { const history: RenderMessage[] = [ settled("user", "第一件经历", "u1"), settled("assistant", "记下了第一件。", "a1"), settled("user", "第二件经历", "u2"), settled("assistant", "记下了第二件。", "a2"), ]; const tokens = ["甲", "甲乙", "甲乙丙", "甲乙丙丁", "甲乙丙丁戊"]; const shared = sharedProps(); const feedbackByKey: Readonly> = {}; resetHomeStreamingRenderProbe(); enableHomeStreamingRenderProbe(); for (const message of history) { renderToString(createElement(RectificationMessageEntry, entryProps(message))); } for (const streamingText of tokens) { renderToString(createElement(RectificationMessageEntry, entryProps(liveAssistant(streamingText)))); } const split = homeStreamingRenderProbeSnapshot(); disableHomeStreamingRenderProbe(); resetHomeStreamingRenderProbe(); enableHomeStreamingRenderProbe(); for (const streamingText of tokens) { renderToString(createElement(UnsplitRectificationMessageList, { ...shared, messages: [...history, liveAssistant(streamingText)], latestRegeneratableKey: "a2", copiedMessageKey: null, feedbackByKey, regeneratingMessageKey: null, })); } const unsplit = homeStreamingRenderProbeSnapshot(); disableHomeStreamingRenderProbe(); assert.equal(split.settledRowRenders, history.length); assert.ok(split.streamingRowRenders <= tokens.length); assert.ok(split.streamingRowRenders >= 1); assert.equal(unsplit.unsplitListRenders, tokens.length); // Unsplit counts every row on every frame, including the live one. assert.equal(unsplit.settledRowRenders, (history.length + 1) * tokens.length); assert.ok(unsplit.settledRowRenders > split.settledRowRenders); });