Files
Jyotisha/frontend/tests/rectification-settled-render-split.test.ts
T
Jesse_ChenandClaude Opus 5.5 84eb23152a feat(rectification): remove the rectification regenerate button and endpoint (BUG-1056)
T2 of TASK-rectification-grounding-20260927 (product decision P1).
The rectification 「重试(重新生成)」 was a blind agent.generate rewrite that
persisted unchecked text (invented range / fit rate reproduced). Removed:
the regenerate route, regenerate-turn.ts, the regeneration agent and its
read-only tool set, the client regenerate action/state and the
regenerating/canRegenerate props. ChatMessageActions renders the regenerate
button only when onRegenerate is passed; ordinary consultation is unchanged.
The DB function regenerate_agentic_rectification_turn is kept (AGENTS §7.6,
retire in a later round). DESIGN.md / VOICE.md updated; source-contract
tests follow with 原值/新值/原因 notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-27 03:28:27 +08:00

156 lines
5.7 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { createElement, createRef } from "react";
import { renderToString } from "react-dom/server";
import test from "node:test";
import type { ChatMessageFeedback } from "../src/components/chat-message-actions.tsx";
import {
RectificationMessageEntry,
UnsplitRectificationMessageList,
type RectificationMessageActions,
type RectificationMessageEntryProps,
type RenderMessage,
} from "../src/components/rectification-message-entry.tsx";
import {
disableHomeStreamingRenderProbe,
enableHomeStreamingRenderProbe,
homeStreamingRenderProbeSnapshot,
resetHomeStreamingRenderProbe,
} from "../src/lib/home-streaming-render-probe.ts";
import { rectificationChatSurface } from "./rectification-chat-surface.ts";
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
const chatSource = rectificationChatSurface;
const entrySource = readFileSync(
new URL("../src/components/rectification-message-entry.tsx", import.meta.url),
"utf8",
);
const rowSource = readFileSync(
new URL("../src/components/chat-message-row.tsx", import.meta.url),
"utf8",
);
function settled(role: "user" | "assistant", text: string, key: string): RenderMessage {
return {
role,
text,
renderKey: key,
state: "settled",
};
}
function liveAssistant(text: string): RenderMessage {
return {
role: "assistant",
text,
renderKey: "live-assistant",
state: "streaming",
};
}
function emptyActions(): RectificationMessageEntryProps["actionsRef"] {
const actionsRef = createRef<RectificationMessageActions>() as RectificationMessageEntryProps["actionsRef"];
actionsRef.current = {
submitChoice() {},
submitStop() {},
copyMessage() {},
onFeedback() {},
};
return actionsRef;
}
function sharedProps(): Omit<
RectificationMessageEntryProps,
"message" | "copied" | "feedback"
> {
return {
busy: true,
readonly: false,
currentQuestionFocusId: null,
interactive: false,
liveChoiceCard: null,
choiceNonce: 0,
savedTime: null,
actionsRef: emptyActions(),
};
}
function entryProps(
message: RenderMessage,
extras: Partial<Pick<RectificationMessageEntryProps, "copied" | "feedback">> = {},
): RectificationMessageEntryProps {
// 原值: 另传 regenerating / canRegenerate。新值: 不再有这两项。原因: BUG-1056 删除生时校正重新生成。
return {
...sharedProps(),
message,
copied: extras.copied ?? false,
feedback: extras.feedback,
};
}
test("the chat container no longer derives per-row timeline, varga, or choice cards", () => {
assert.doesNotMatch(chatSource, /rectificationTimelineRows\s*\(/);
assert.doesNotMatch(chatSource, /vargaSentenceFromMethods\s*\(/);
assert.doesNotMatch(chatSource, /choiceCardFromQuestion\s*\(/);
assert.match(entrySource, /rectificationTimelineRows\s*\(/);
assert.match(entrySource, /vargaSentenceFromMethods\s*\(/);
assert.match(entrySource, /choiceCardFromQuestion\s*\(/);
});
test("the extracted row is memoised and takes no function props except actionsRef", () => {
assert.match(entrySource, /export const RectificationMessageEntry = memo\(function RectificationMessageEntry/);
const propsBlock = entrySource.slice(
entrySource.indexOf("export type RectificationMessageEntryProps"),
entrySource.indexOf("export function choiceCardFromQuestion"),
);
assert.match(propsBlock, /actionsRef: MutableRefObject<RectificationMessageActions>/);
assert.doesNotMatch(propsBlock, /=>/);
assert.doesNotMatch(propsBlock, /on[A-Z]\w+\?:/);
assert.match(rowSource, /export const ChatMessageRow = memo\(function ChatMessageRow/);
});
test("the split architecture renders settled rectification rows once while streaming tokens", () => {
const history: RenderMessage[] = [
settled("user", "第一件经历", "u1"),
settled("assistant", "记下了第一件。", "a1"),
settled("user", "第二件经历", "u2"),
settled("assistant", "记下了第二件。", "a2"),
];
const tokens = ["甲", "甲乙", "甲乙丙", "甲乙丙丁", "甲乙丙丁戊"];
const shared = sharedProps();
const feedbackByKey: Readonly<Record<string, ChatMessageFeedback | undefined>> = {};
resetHomeStreamingRenderProbe();
enableHomeStreamingRenderProbe();
for (const message of history) {
renderToString(createElement(RectificationMessageEntry, entryProps(message)));
}
for (const streamingText of tokens) {
renderToString(createElement(RectificationMessageEntry, entryProps(liveAssistant(streamingText))));
}
const split = homeStreamingRenderProbeSnapshot();
disableHomeStreamingRenderProbe();
resetHomeStreamingRenderProbe();
enableHomeStreamingRenderProbe();
for (const streamingText of tokens) {
renderToString(createElement(UnsplitRectificationMessageList, {
...shared,
messages: [...history, liveAssistant(streamingText)],
copiedMessageKey: null,
feedbackByKey,
}));
}
const unsplit = homeStreamingRenderProbeSnapshot();
disableHomeStreamingRenderProbe();
assert.equal(split.settledRowRenders, history.length);
assert.ok(split.streamingRowRenders <= tokens.length);
assert.ok(split.streamingRowRenders >= 1);
assert.equal(unsplit.unsplitListRenders, tokens.length);
// Unsplit counts every row on every frame, including the live one.
assert.equal(unsplit.settledRowRenders, (history.length + 1) * tokens.length);
assert.ok(unsplit.settledRowRenders > split.settledRowRenders);
});