真机走查第二批: 1. 普通对话的回答,首个 H2 之后的全部内容(带推理的那一半)被 <details class="answer-detail">「完整分析」默认收起——用户「输出根本 看不到」。折叠控件与组件文件一并删除,报告层直接渲染进正文。 技法审计表保留自己的折叠:那是给人核对的证据,不是回复正文。 product-voice.ts 同步改掉「UI 会折叠」那句,否则模型继续按折叠写。 2. 次级页侧栏点不了头像、头像样式与首页不一致。根因是 R4 的取舍: SecondaryShell 共用,但里面挂的是新写的 AppNavRail 而不是 AppSidebar。 现在 use-nav-rail 也取 avatar,渲染同一个 UserAvatar;页脚改成真链接, 指向 / —— 账户菜单连着设置弹窗栈,留在那儿,不在这里复制第二份。 3. 「新建对话」整页刷新。原来走 location.assign,现在是 next/link。 /login 保留硬跳转:它跨鉴权边界,要先存返回目标。 注意:第一版我改成了 useRouter(),它在 app-router 上下文外会抛 「invariant expected app router to be mounted」,打红 12 条渲染测试。 改用 <Link>,静态渲染也安全;折叠态 tooltip 换成原生 title。 测试 3369,fail 仍 31 且与基线逐条一致;四个路由标记不变; CSS gzip 40,002。 未修:截图里 `## 适合推进 / 需要避开` 被当字面量渲染,是模型输出里 那两个 H2 前面没有换行,属生成层,另记。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
63 lines
3.7 KiB
TypeScript
63 lines
3.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
/**
|
|
* 原值:这份文件断言回答的第二层是一个默认收起的 `<details class="answer-detail">`
|
|
* 「完整分析」控件(`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, /<div className="answer-report-body">\{reportBody\}<\/div>/);
|
|
assert.doesNotMatch(contentSource, /AnswerDetailDisclosure/);
|
|
// 只查真正的折叠元素——解释这段历史的注释本身会提到「完整分析」四个字。
|
|
assert.doesNotMatch(contentSource, /<details/);
|
|
assert.match(globalStyles, /\.answer-report-body \{/);
|
|
assert.doesNotMatch(globalStyles, /\.answer-detail\b/);
|
|
});
|
|
|
|
test("the model is told the answer renders inline, so it stops writing for a fold", () => {
|
|
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, /<div className="message-markdown">\s*<StableMarkdownPrefix text=\{report\} renderMarkdown=\{renderMarkdown\} \/>/);
|
|
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*\? <StreamingMarkdown text=\{spoken\} renderMarkdown=\{renderMarkdown\} \/>/);
|
|
});
|
|
|
|
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, /<ChatMessageContent text=\{protectedVisibleText\} \/>/);
|
|
});
|