fix(rectification): settled replies carry no 「已完成 N 步」 receipt; steps stay the waiting state (BUG-1137)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-01 10:40:29 +08:00
co-authored by Claude Opus 5.5
parent bdf4c31bc4
commit 408b22749e
4 changed files with 99 additions and 9 deletions
@@ -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;
@@ -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<RectificationMessageActions>;
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 步/);
});
@@ -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();
}
@@ -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|<CompletedActivityReceipt/);
assert.doesNotMatch(chatSource, /label: "正在组织回答…",\s*\}\),\s*\}\s*:\s*message;/);