import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { chatMessageViews } from "../src/lib/chat-message-view.ts"; const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); const messageRowSource = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8"); const activitySource = readFileSync(new URL("../src/components/agent-activity-status.tsx", import.meta.url), "utf8"); const previousMessages = [ { role: "user", text: "问题" }, ] as const; test("keeps the assistant render identity stable when streaming settles", () => { // Given: one assistant answer exists first as a transient stream. const streaming = chatMessageViews(previousMessages, true, "完整答案"); // When: the same answer becomes part of the persisted transcript. const settled = chatMessageViews([ ...previousMessages, { role: "assistant", text: "完整答案" }, ], false, ""); // Then: React receives the same key and can reuse the existing message shell. assert.equal(streaming.at(-1)?.renderKey, settled.at(-1)?.renderKey); assert.equal(streaming.at(-1)?.state, "streaming"); assert.equal(settled.at(-1)?.state, "settled"); }); test("does not duplicate a completed assistant answer while loading state settles", () => { // Given: the final answer has entered the transcript while request cleanup lags. const completedMessages = [ ...previousMessages, { role: "assistant", text: "完整答案" }, ] as const; // When: the render view is derived with the old loading flag still true. const views = chatMessageViews(completedMessages, true, "完整答案"); // Then: only the persisted answer is rendered. assert.equal(views.length, completedMessages.length); assert.equal(views.at(-1)?.state, "settled"); }); test("shows honest agent activity states before and during streamed text", () => { const activity = { phase: "chart-calculation", label: "正在计算本命盘…" } as const; const view = chatMessageViews(previousMessages, true, "", activity).at(-1); assert.deepEqual(view?.activity, activity); const thinkingView = chatMessageViews(previousMessages, true, "", activity, "先看事业宫。").at(-1); assert.equal(thinkingView?.thinkingText, "先看事业宫。"); assert.equal(thinkingView?.state, "thinking"); assert.match(messageRowSource, /"loading-method": "searching"/); assert.match(messageRowSource, /"chart-calculation": "solving"/); assert.match(messageRowSource, /"evidence-validation": "working"/); assert.match(messageRowSource, /"answer-composition": "composing"/); assert.match(messageRowSource, /message\.activity\?\.label/); assert.doesNotMatch(messageRowSource, /正在核对星盘信息/); assert.match(messageRowSource, /正在处理…/); assert.match(messageRowSource, /showActivity = message\.state !== "settled"/); assert.match(messageRowSource, /\{showActivity && \(/); assert.match(messageRowSource, /message\.text && \(/); assert.match(messageRowSource, /