/** * TASK-rectification-message-cleanup-20261001. * BUG-1137 (D3): a settled rectification reply carries no 「已完成 N 步」 * receipt; while it is written the steps are its waiting state. Consultation * replies keep their settled receipt. */ import assert from "node:assert/strict"; import test from "node:test"; import { createElement, type MutableRefObject } from "react"; import { renderToString } from "react-dom/server"; import { ChatMessageRow } from "../src/components/chat-message-row.tsx"; import { RectificationMessageEntry, type RectificationMessageActions, type RenderMessage, } from "../src/components/rectification-message-entry.tsx"; const actionsRef = { current: { submitChoice() {}, submitStop() {}, copyMessage() {}, onFeedback() {}, } } as MutableRefObject; const TRACE = [ { id: "s1", kind: "tool" as const, tool: "rectification-read-case", status: "done" as const, label: "看了你的资料" }, { id: "s2", kind: "tool" as const, tool: "rectification-record-evidence-batch", status: "done" as const, label: "记录你说的经历" }, { id: "s3", kind: "tool" as const, tool: "rectification-set-focus", status: "done" as const, label: "准备好下一个问题" }, ]; function entry(state: RenderMessage["state"]): string { const message = { role: "assistant", text: state === "settled" ? "记下了 3 件事。" : "", renderKey: "m1", state, activityTrace: TRACE, } as unknown as RenderMessage; return renderToString(createElement(RectificationMessageEntry, { message, busy: state !== "settled", readonly: false, currentQuestionFocusId: null, interactive: state === "settled", liveChoiceCard: null, choiceNonce: 0, savedTime: null, copied: false, feedback: undefined, actionsRef, })); } test("a settled rectification reply renders no step receipt (BUG-1137)", () => { const html = entry("settled"); assert.match(html, /记下了 3 件事/); assert.doesNotMatch(html, /已完成 \d+ 步/); assert.doesNotMatch(html, /consultation-run-timeline/); for (const step of ["看了你的资料", "记录你说的经历", "准备好下一个问题"]) assert.doesNotMatch(html, new RegExp(step)); }); test("while a rectification reply is written its steps stay the waiting state (BUG-1137)", () => { const html = entry("streaming"); assert.match(html, /consultation-run-timeline/); assert.match(html, /正在分析/); assert.match(html, /准备好下一个问题/); }); test("a settled consultation reply keeps its 「已完成 N 步」 receipt (BUG-1137 scope lock)", () => { const html = renderToString(createElement(ChatMessageRow, { message: { role: "assistant", content: "回答正文。", text: "回答正文。", renderKey: "c1", state: "settled", timeline: [{ id: "method", kind: "method", status: "done", label: "已加载方法" }], } as never, })); assert.match(html, /已完成 1 步/); }); import { evidenceTurnRecap } from "../src/lib/rectification-agentic/user-copy.ts"; import { recordedEvidenceByTurn } from "../src/lib/rectification-agentic/v9/turn-question.ts"; import { messagesFromTurns } from "../src/lib/rectification-chat-messages.ts"; test("the server recap lists two or fewer items and counts more (BUG-1136)", () => { assert.equal(evidenceTurnRecap([]), null); assert.equal(evidenceTurnRecap(["2016年 入学"]), "记下了:2016年 入学。"); assert.equal(evidenceTurnRecap(["2016年 入学", "2020年 毕业"]), "记下了:2016年 入学、2020年 毕业。"); assert.equal(evidenceTurnRecap(Array.from({ length: 12 }, (_, i) => `${2000 + i}年 事${i}`)), "记下了 12 件事。"); }); test("recorded items come from the ledger by turn, without rejected or superseded rows (BUG-1136)", () => { const row = (sourceTurnId: string, summary: string, status = "confirmed", datePrecision = "year", occurredFrom = "2016-01-01") => ({ sourceTurnId, status, datePrecision, occurredFrom, occurredTo: null, summary, }); const byTurn = recordedEvidenceByTurn([ row("t2", "上大学"), row("t2", "毕业", "pending_confirmation", "month", "2020-06-01"), row("t2", "被拒的", "rejected"), row("t2", "被替换的", "superseded"), row("t4", "换工作", "confirmed", "day", "2021-03-05"), ]); assert.deepEqual(byTurn.get("t2"), ["2016年上大学", "2020-06 毕业"]); assert.deepEqual(byTurn.get("t4"), ["2021-03-05 换工作"]); }); function settledEntry(message: RenderMessage): string { return renderToString(createElement(RectificationMessageEntry, { message, busy: false, readonly: false, currentQuestionFocusId: null, interactive: true, liveChoiceCard: null, choiceNonce: 0, savedTime: null, copied: false, feedback: undefined, actionsRef, })); } test("twelve recorded items: one body line and the twelve items collapsed under it (BUG-1136)", () => { const items = Array.from({ length: 12 }, (_, i) => `${2000 + i}年 虚构事件${i + 1}`); const [message] = messagesFromTurns([ { id: "t2", role: "assistant", text: "记下了 12 件事。", status: "completed", recorded_evidence: items }, ]); assert.deepEqual(message!.recordedEvidence, items); const html = settledEntry(message!); assert.match(html, /记下了 12 件事。/); assert.match(html, /
/); assert.match(html, /看记下的 12 件/); assert.equal(html.split("]* open/); }); test("two recorded items stay inline in the body with no list (BUG-1136)", () => { const [message] = messagesFromTurns([ { id: "t2", role: "assistant", text: "记下了:2016年 入学、2020年 毕业。", status: "completed", recorded_evidence: ["2016年 入学", "2020年 毕业"] }, ]); const html = settledEntry(message!); assert.match(html, /记下了:2016年 入学、2020年 毕业。/); assert.doesNotMatch(html, /rectification-recorded-evidence/); });