import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; /** * 原值:这份文件断言回答的第二层是一个默认收起的 `
` * 「完整分析」控件(`chat-answer-detail.tsx`),并锁住它的 caption、流式期 * 不渲染子节点、以及它与 technique-audit 是两套样式。 * 新值:控件与组件文件一并删除,报告层直接渲染进正文 `.answer-report-body`。 * 原因:产品在真机走查后拍板——首个 H2 之后的全部内容(也就是带推理的那一半) * 默认不可见,用户「输出根本看不到」。回答本身就是产品,不该藏在一次点击后面。 * 技法审计表保留它自己的折叠:那是给人核对的证据,不是回复正文。 * * 下面保留的断言是原文件里与折叠无关、仍然成立的那些(记忆化、复制取原文、 * onboarding 走同一渲染链),并新增两条锁住「不得再把回答折起来」。 */ const contentSource = readFileSync(new URL("../src/components/chat-message-content.tsx", import.meta.url), "utf8"); const transcriptSource = readFileSync(new URL("../src/components/chat-transcript.tsx", import.meta.url), "utf8"); const onboardingSource = readFileSync(new URL("../src/components/onboarding-chat-message.tsx", import.meta.url), "utf8"); const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); test("the report layer renders inline; the answer is never folded away", () => { assert.match(contentSource, /
\{reportBody\}<\/div>/); assert.doesNotMatch(contentSource, /AnswerDetailDisclosure/); // 只查真正的折叠元素——解释这段历史的注释本身会提到「完整分析」四个字。 assert.doesNotMatch(contentSource, /
{ const voice = readFileSync(new URL("../src/mastra/product-voice.ts", import.meta.url), "utf8"); assert.match(voice, /renders the whole answer inline/); assert.doesNotMatch(voice, /folds everything from the first H2/); // 截图里 `## 适合推进 / 需要避开` 接在上一句后面,被当成字面量渲染。 assert.match(voice, /Every H2 must start its own line/); }); test("ChatMessageContent still splits spoken from report and keeps the audit fold", () => { assert.match(contentSource, /splitSpokenAndReport\(split\.spoken\)/); assert.match(contentSource, /: foldedAudit/); assert.match(contentSource, /showFoldedAudit = rows\.length > 0 && split\.rows\.length === 0/); }); test("the settled report layer stays memoised by text", () => { // 原值/新值不变,原因仍是 BUG-725:结算态 Markdown 按文本记忆化,不每次重 parse。 assert.match(contentSource, /
\s*/); assert.match(contentSource, /const reportBody = !streaming && report/); }); test("spoken streaming still uses StreamingMarkdown so the memoised prefix stays intact", () => { assert.match(contentSource, /streaming\s*\n\s*\? /); }); test("copy still sends the original message text, not the spoken layer", () => { assert.match(transcriptSource, /onCopy=\{\(\) => actionsRef\.current\.onCopy\(feedbackKey, message\.text\)\}/); }); test("onboarding typewriter messages still go through ChatMessageContent", () => { assert.match(onboardingSource, //); });