Files
Jyotisha/frontend/tests/rectification-message-cleanup-20261001.test.tsx
T
Jesse_ChenandClaude Opus 5.5 9d302e515c feat(rectification): the server writes the evidence recap — 「记下了 N 件事」 with the ledger list collapsed under it (BUG-1136)
- Evidence turns whose batch accepted items: the body is the server recap
  built from those items (two or fewer listed inline, more as a count);
  the model no longer restates them (both prompt sets updated).
- The case snapshot carries each assistant turn's recorded evidence from
  the ledger (rejected/superseded excluded); the message shows it in a
  collapsed <details> list when more than two.
- Chinese date labels read straight into the event phrase (2016年入学);
  ISO labels keep their space.
- 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
2026-10-01 10:40:29 +08:00

137 lines
6.0 KiB
TypeScript

/**
* 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 步/);
});
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, /<details class="rectification-recorded-evidence">/);
assert.match(html, /看记下的 12 件/);
assert.equal(html.split("<li").length - 1, 12);
for (const item of items) assert.ok(html.includes(item), item);
assert.doesNotMatch(html, /<details[^>]* 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/);
});