Files
Jyotisha/frontend/tests/chat-stream-settle-contract.test.ts
T
Jesse_Chen cfa824994e
Independent Staging Quality Gate / validate (push) Successful in 9m26s
Independent Staging Quality Gate / publish (push) Successful in 1m57s
fix(rectification): keep a live analyzing step and drop the range-stop exits
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>
2026-09-04 12:46:10 +08:00

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 })), "");
});