From 408b22749ec9446f39d1de442f327f51b77779b0 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 1 Oct 2026 09:29:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(rectification):=20settled=20replies=20carry?= =?UTF-8?q?=20no=20=E3=80=8C=E5=B7=B2=E5=AE=8C=E6=88=90=20N=20=E6=AD=A5?= =?UTF-8?q?=E3=80=8D=20receipt;=20steps=20stay=20the=20waiting=20state=20(?= =?UTF-8?q?BUG-1137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consultation replies keep their receipt (scope-lock test). Two source/DOM assertions updated with original/new/reason notes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8 --- .../rectification-message-entry.tsx | 11 ++- ...fication-message-cleanup-20261001.test.tsx | 79 +++++++++++++++++++ ...tification-opening-plain-20260926.test.tsx | 13 +-- .../rectification-timeline-adapter.test.ts | 5 +- 4 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 frontend/tests/rectification-message-cleanup-20261001.test.tsx diff --git a/frontend/src/components/rectification-message-entry.tsx b/frontend/src/components/rectification-message-entry.tsx index 2b02cb47..50e43048 100644 --- a/frontend/src/components/rectification-message-entry.tsx +++ b/frontend/src/components/rectification-message-entry.tsx @@ -115,16 +115,21 @@ function RectificationMessageEntryView({ && message.state === "settled" && !message.failed && Boolean(message.text); + // BUG-1137 (D3): the step rows are the waiting state while the reply is + // written; once it settles the rectification reply carries no + // 「已完成 N 步」 receipt — those steps are the system's own bookkeeping. + // Consultation replies keep theirs (ChatMessageRow is unchanged). + const settled = message.state === "settled"; const displayedMessage: RenderMessage = { ...message, - timeline: rectificationTimelineRows({ + timeline: settled ? [] : rectificationTimelineRows({ trace: message.activityTrace, receipt: message.completedReceipt, activity: message.activity, - settled: message.state === "settled", + settled, }), }; - const vargaSentence = !message.failed + const vargaSentence = !message.failed && !settled ? vargaSentenceFromMethods(message.completedReceipt?.methods) : null; const question = message.question; diff --git a/frontend/tests/rectification-message-cleanup-20261001.test.tsx b/frontend/tests/rectification-message-cleanup-20261001.test.tsx new file mode 100644 index 00000000..d31052a3 --- /dev/null +++ b/frontend/tests/rectification-message-cleanup-20261001.test.tsx @@ -0,0 +1,79 @@ +/** + * 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 步/); +}); diff --git a/frontend/tests/rectification-opening-plain-20260926.test.tsx b/frontend/tests/rectification-opening-plain-20260926.test.tsx index dc8b7bd8..a637d423 100644 --- a/frontend/tests/rectification-opening-plain-20260926.test.tsx +++ b/frontend/tests/rectification-opening-plain-20260926.test.tsx @@ -164,16 +164,19 @@ for (const [name, streamed, stored] of [ assert.equal(text.split(GENERIC_COLLECT_QUESTION).length - 1, 1, text); for (const example of EXAMPLES) assert.equal(text.split(example).length - 1, 1, example); assert.match(text, /例如「2015 年夏天换了工作」/); - // The opening turn (receipt + body + stem). The side board keeps its own + // The opening turn (body + stem). The side board keeps its own // 「当前盘面」 heading; that panel is not part of this turn. const turn = text.slice(0, text.indexOf(GENERIC_COLLECT_QUESTION) + GENERIC_COLLECT_QUESTION.length); - assert.ok(turn.startsWith("已完成 2 步"), turn); + // 原值: turn 以「已完成 2 步」开头;「看了你的资料」「准备好下一个问题」各出现 1 次(收起的步骤回执)。 + // 新值: 结算后无「已完成」回执,两条步骤名出现 0 次;turn 以开场正文开头。 + // 原因: BUG-1137(TASK-rectification-message-cleanup-20261001 D3)——步骤只在生成中作等待态,结算后不渲染;BUG-1050 的「回执去重」随回执一起不再出现。 + assert.ok(turn.startsWith(BODY_SENTENCES[0]!), turn); for (const jargon of ["大运", "盘面", "代表分钟", "精确到秒", "对话焦点", "校正记录"]) { assert.equal(turn.includes(jargon), false, jargon); } - assert.match(text, /已完成 2 步/); - assert.equal(text.split("看了你的资料").length - 1, 1); - assert.equal(text.split("准备好下一个问题").length - 1, 1); + assert.doesNotMatch(text, /已完成 \d+ 步/); + assert.equal(text.split("看了你的资料").length - 1, 0); + assert.equal(text.split("准备好下一个问题").length - 1, 0); } finally { await chat.close(); } diff --git a/frontend/tests/rectification-timeline-adapter.test.ts b/frontend/tests/rectification-timeline-adapter.test.ts index 4f0b9e17..f95ac1a5 100644 --- a/frontend/tests/rectification-timeline-adapter.test.ts +++ b/frontend/tests/rectification-timeline-adapter.test.ts @@ -165,7 +165,10 @@ test("both chat surfaces render one timeline, one live marker, and no second rec // 新值: 同一调用在行组件内;容器不得再每帧派生。 // 原因: BUG-725 已结算行记忆化。 assert.match(entrySource, /rectificationTimelineRows\(\{/); - assert.match(entrySource, /timeline: rectificationTimelineRows/); + // 原值: /timeline: rectificationTimelineRows/(结算后也派生步骤行)。 + // 新值: /timeline: settled \? \[\] : rectificationTimelineRows/。 + // 原因: BUG-1137(TASK-rectification-message-cleanup-20261001 D3)——校正回复结算后不再显示「已完成 N 步」,生成中照旧。 + assert.match(entrySource, /timeline: settled \? \[\] : rectificationTimelineRows/); assert.doesNotMatch(chatSource, /rectificationTimelineRows\s*\(/); assert.doesNotMatch(chatSource, /rectification-activity-failure|