globals.css 从 7249 行减到 7011 行,删了 56 个已经没人用的类,16 个关键帧都还在用。手写控件名单从 9 个文件收到 2 个:天空绘制和校正顶栏探头。星盘和星图的图标豁免仍在规则里。设计说明改成以 shadcn base-nova 和 Motion 预设为准。页面用法没有再改。
自测:tsc --noEmit 退出码 0;frontend 下 npm run lint 0 error、122 warning;白名单、样式隔离、时间线和错误边界测试 18 条通过、失败 0、取消 0;npm run build 后 / 仍是 Static。首屏 JS gzip-9 相对 728ead88 从 644683 到 648993(+0.67%)。没有跑全量 npm test、Python 快速门和 test:db。
Co-Authored-By: Grok (agent loop)
99 lines
5.8 KiB
TypeScript
99 lines
5.8 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 { ConsultationRunTimeline, timelineSummaryLabel, visibleTimelineRows } from "../src/components/consultation-run-timeline.tsx";
|
|
import { latestAssistantView, type ChatMessage } from "../src/lib/chat-message-view.ts";
|
|
|
|
const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
|
|
const transcriptSource = read("src/components/chat-transcript.tsx");
|
|
const timelineSource = read("src/components/consultation-run-timeline.tsx");
|
|
const messageRowSource = read("src/components/chat-message-row.tsx");
|
|
const globalStyles = read("src/app/globals.css");
|
|
|
|
test("the trailing assistant reply keeps one view identity from first token to settlement", () => {
|
|
const history: ChatMessage[] = [{ role: "user", text: "问题" }];
|
|
const streaming = latestAssistantView(history, true, "完整答案");
|
|
const settled = latestAssistantView([...history, { role: "assistant", text: "完整答案" }], false, "");
|
|
assert.ok(streaming && settled);
|
|
assert.equal(streaming.view.renderKey, settled.view.renderKey);
|
|
assert.equal(streaming.view.state, "streaming");
|
|
assert.equal(settled.view.state, "settled");
|
|
assert.equal(streaming.views.length, 2);
|
|
assert.equal(settled.views.length, 2);
|
|
|
|
// A trailing user message means nothing is pending and nothing is latest.
|
|
assert.equal(latestAssistantView(history, false, ""), undefined);
|
|
// A settled reply still in the loading gap is rendered once, not duplicated.
|
|
const lagging = latestAssistantView([...history, { role: "assistant", text: "完整答案" }], true, "完整答案");
|
|
assert.equal(lagging?.view.state, "settled");
|
|
});
|
|
|
|
test("one component renders the latest reply in both states; nothing remounts on settle", () => {
|
|
assert.match(transcriptSource, /export const LatestAssistantEntry = memo\(/);
|
|
assert.match(transcriptSource, /<LatestAssistantEntry\s+key=\{latest\.view\.renderKey\}/);
|
|
assert.match(transcriptSource, /excludeLatestAssistant/);
|
|
assert.match(transcriptSource, /noteLatestEntryMount\(\)/);
|
|
assert.doesNotMatch(transcriptSource, /StreamingMessageEntry|SettledMessageEntry/);
|
|
// Exactly one place in the split transcript renders the trailing reply.
|
|
assert.equal(transcriptSource.match(/<LatestAssistantEntry\b/g)?.length, 1);
|
|
});
|
|
|
|
test("a message enters once, through GSAP at the documented 160ms, never twice", () => {
|
|
assert.match(messageRowSource, /duration: 0\.16/);
|
|
assert.doesNotMatch(globalStyles, /message-enter/);
|
|
const messageRule = globalStyles.match(/\n\.message \{[^}]*\}/)?.[0] ?? "";
|
|
assert.ok(messageRule, "the .message rule exists");
|
|
assert.doesNotMatch(messageRule, /animation/);
|
|
});
|
|
|
|
test("the timeline collapses in place with a 180ms height transition and honours the reader's toggle", () => {
|
|
assert.doesNotMatch(timelineSource, /key=\{live/);
|
|
assert.match(timelineSource, /const open = userOpen \?\? live/);
|
|
assert.match(timelineSource, /aria-expanded=\{open\}/);
|
|
assert.match(timelineSource, /inert=\{open \? undefined : true\}/);
|
|
// 原值:className="consultation-run-timeline__summary"
|
|
// 新值:同一类名仍在,后面可以再接压掉按钮默认高度和换行的类
|
|
// 原因:K7 把这一行换成组件库 Button。类名、展开和 180ms 高度过渡不变
|
|
assert.match(timelineSource, /className="consultation-run-timeline__summary(?: [^"]*)?"/);
|
|
assert.match(timelineSource, /agent-activity-status__text/);
|
|
assert.match(globalStyles, /\.consultation-run-timeline__body-wrap \{[^}]*grid-template-rows: 0fr/);
|
|
assert.match(globalStyles, /\.consultation-run-timeline__body-wrap \{[^}]*transition: grid-template-rows 180ms var\(--ease-out\)/);
|
|
assert.match(globalStyles, /\.consultation-run-timeline\.is-open > \.consultation-run-timeline__body-wrap \{[^}]*grid-template-rows: 1fr/);
|
|
assert.match(globalStyles, /\.consultation-run-timeline__summary-label \{[^}]*animation: agent-activity-status-in 120ms/);
|
|
assert.match(globalStyles, /@media \(prefers-reduced-motion: reduce\) \{\s*\.consultation-run-timeline__summary-label,\s*\.consultation-run-timeline__body-wrap,/);
|
|
|
|
assert.equal(timelineSummaryLabel([], true), "正在分析");
|
|
assert.equal(timelineSummaryLabel([{ id: "method", kind: "method", status: "done", label: "已加载方法" }], false), "已完成 1 步");
|
|
|
|
const live = renderToString(createElement(ConsultationRunTimeline, { rows: [], live: true }));
|
|
assert.match(live, /aria-expanded="true"/);
|
|
// 原值: /正在处理…/
|
|
// 新值: /收到,正在看你的问题…/
|
|
// 原因: TASK-consult-first-frame-and-pacing-20260928 D1 / BUG-1074:排队行从发送那一帧起可见,文案与服务端首帧一致
|
|
assert.match(live, /收到,正在看你的问题…/);
|
|
assert.doesNotMatch(live, /正在处理…/);
|
|
assert.match(live, /inline-spinner/);
|
|
const liveAfterTool = renderToString(createElement(ConsultationRunTimeline, {
|
|
rows: [{ id: "read", kind: "calculate", status: "done", label: "读取校正记录" }],
|
|
live: true,
|
|
}));
|
|
assert.match(liveAfterTool, /读取校正记录/);
|
|
assert.match(liveAfterTool, /正在分析…/);
|
|
assert.match(liveAfterTool, /inline-spinner/);
|
|
assert.deepEqual(
|
|
visibleTimelineRows([{ id: "read", kind: "calculate", status: "done", label: "读取校正记录" }], true).map((row) => row.status),
|
|
["done", "live"],
|
|
);
|
|
const settled = renderToString(createElement(ConsultationRunTimeline, {
|
|
rows: [{ id: "method", kind: "method", status: "done", label: "已加载方法" }],
|
|
live: false,
|
|
}));
|
|
assert.match(settled, /aria-expanded="false"/);
|
|
assert.match(settled, /inert=""/);
|
|
assert.match(settled, /已完成 1 步/);
|
|
assert.equal(renderToString(createElement(ConsultationRunTimeline, { rows: [], live: false })), "");
|
|
});
|