cfa824994e
A completed tool left only checkmarks under 正在分析, so the run looked stuck. The composer and scoring-card 先这样 buttons were unused early exits. Co-authored-by: Cursor <cursoragent@cursor.com>
92 lines
5.3 KiB
TypeScript
92 lines
5.3 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\}/);
|
|
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"/);
|
|
assert.match(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 })), "");
|
|
});
|