Files
Jyotisha/frontend/tests/rectification-activity-receipt.test.ts
T
Jesse_ChenandClaude Opus 5.5 3a66c39f48 refactor(rectification): split the chat component into hooks, parameter functions and blocks
Move-only (TASK-rectification-code-split-20260926 T1/T4). No behavior, API,
copy, DB, Skill or scoring change.

- rectification-agentic-chat.tsx: 2043 -> 737 lines; component body
  1625 -> 630; useState 35 -> 12, useRef 16 -> 9, effects 9 -> 5,
  useCallback 12 -> 6.
- Snapshot sync -> hooks/use-rectification-case-snapshot.ts; board layout
  and live label clock -> two small hooks.
- send / submitStructuredChoice / acceptCandidate bodies ->
  lib/rectification-chat-{turn,choice,accept}-run.ts parameter functions
  (bodies verbatim, deps destructured to the same names); copy/regenerate,
  question repair, pure transcript and snapshot helpers and the per-render
  view derivation -> lib/rectification-chat-*.ts (no React hooks).
- Question-gap blocks and the read-only range line ->
  components/rectification-question-gap-notices.tsx.
- Dependency arrays kept exactly as before (lint warnings +3, listed in
  PROGRESS); no dep added to appease the linter.
- Tests: whole-source contracts read tests/rectification-chat-surface.ts
  (container + split files, like home-surface.ts); slices of moved code now
  call the extracted functions or render the extracted block, each with a
  原值/新值/原因 comment. New tests/rectification-growth-contract.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 19:23:52 +08:00

178 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
createRectificationActivityReceiptState,
reduceRectificationActivityReceipt,
receiptFromRectificationActivityState,
} from "../src/lib/rectification-activity-receipt";
import { rectificationChatSurface } from "./rectification-chat-surface.ts";
import type { RenderMessage } from "../src/components/rectification-message-entry.tsx";
import {
chatRunRecorder,
ndjsonResponse,
runRectificationChatTurn,
withFakeFetch,
} from "./rectification-chat-run-fixtures.ts";
test("a successful retry clears the same tool's earlier failure", () => {
let state = createRectificationActivityReceiptState();
for (const event of [
{ tool: "rectification-read-case", status: "completed" },
{ tool: "rectification-propose-evidence", status: "failed" },
{ tool: "rectification-propose-evidence", status: "started" },
{ tool: "rectification-propose-evidence", status: "completed" },
{ tool: "rectification-confirm-evidence", status: "completed" },
] as const) {
state = reduceRectificationActivityReceipt(state, event);
}
assert.deepEqual(receiptFromRectificationActivityState(state), {
steps: [
"rectification-read-case",
"rectification-propose-evidence",
"rectification-confirm-evidence",
],
methods: [],
});
});
test("an unrecovered latest tool failure remains visible", () => {
let state = createRectificationActivityReceiptState();
state = reduceRectificationActivityReceipt(state, {
tool: "rectification-read-case",
status: "completed",
});
state = reduceRectificationActivityReceipt(state, {
tool: "rectification-propose-evidence",
status: "failed",
});
assert.deepEqual(receiptFromRectificationActivityState(state), {
steps: ["rectification-read-case"],
methods: [],
failedTool: "rectification-propose-evidence",
});
});
test("a later failure overrides an earlier completion for the same tool", () => {
let state = createRectificationActivityReceiptState();
state = reduceRectificationActivityReceipt(state, {
tool: "rectification-read-case",
status: "completed",
});
state = reduceRectificationActivityReceipt(state, {
tool: "rectification-read-case",
status: "failed",
});
assert.deepEqual(receiptFromRectificationActivityState(state), {
steps: [],
methods: [],
failedTool: "rectification-read-case",
});
});
test("a later failure removes methods contributed by the failed tool", () => {
let state = createRectificationActivityReceiptState();
state = reduceRectificationActivityReceipt(state, {
tool: "rectification-compare-candidates",
status: "completed",
methods: ["d1-rashi", "vimshottari-dasha"],
});
state = reduceRectificationActivityReceipt(state, {
tool: "rectification-read-diagnostics",
status: "completed",
methods: ["d1-rashi", "shadbala"],
});
state = reduceRectificationActivityReceipt(state, {
tool: "rectification-compare-candidates",
status: "failed",
});
assert.deepEqual(receiptFromRectificationActivityState(state), {
steps: ["rectification-read-diagnostics"],
methods: ["d1-rashi", "shadbala"],
failedTool: "rectification-compare-candidates",
});
});
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
const chatSource = rectificationChatSurface;
test("answer deltas preserve a still-running server activity", async () => {
// Former locks: `state: raw.trim() ? "streaming" : "thinking"` and `text: raw,` inside the
// delta branch. Those pinned one setMessages per network chunk (BUG-473); the same state
// derivation now lives in the frame buffer's flush, which the delta branch feeds.
// 原值: 从 chat 源码切 answer.delta 分支(到 run.failed)与 frames 定义(到 new AbortController),匹配
// flush 里 `state: text.trim() ? "streaming" : "thinking"`、`text,`,delta 分支里 `frames.setAnswer(raw)`、
// `event.replace === true ? event.text : raw + event.text`、「正在组织回答」,且不含 settled.spoken /
// `activeActivity: undefined`。
// 新值: 调用 runRectificationChatTurn(假 NDJSON 流):
// 1) 只有工具开始、没有正文时,flush 出的活行是 thinking、正文为空;
// 2) 两段 answer.delta(第二段 replace)后,flush 出的活行是 streaming、正文等于 replace 后的全文、
// 活动标签是「正在组织回答…」且活动没被清掉;每段 delta 不各自 setMessages(一次提交)。
// 原因: TASK-rectification-code-split-20260926 `send` 函数体原样搬进 rectification-chat-turn-run.ts;
// 对流读取结果断言比源码 token 更强。
const liveRows = (history: RenderMessage[][]) => history
.map((messages) => messages.find((message) => message.renderKey === "v9-assistant-1"))
.filter((row): row is RenderMessage => Boolean(row));
const toolOnly = chatRunRecorder();
await withFakeFetch(() => ndjsonResponse([
{ type: "tool.activity", tool: "rectification-read-case", status: "started" },
]), () => runRectificationChatTurn(toolOnly.turnDeps(), "message", "我 2019 年换了工作"));
const thinkingRow = liveRows(toolOnly.messageHistory).find((row) => row.state === "thinking" && row.activityTrace?.length);
assert.ok(thinkingRow, "the flush of a tool-only stream keeps a thinking row");
assert.equal(thinkingRow.text, "");
const streamed = chatRunRecorder();
await withFakeFetch(() => ndjsonResponse([
{ type: "tool.activity", tool: "rectification-read-case", status: "started" },
{ type: "answer.delta", text: "第一段" },
{ type: "answer.delta", text: "改写后的全文", replace: true },
]), () => runRectificationChatTurn(streamed.turnDeps(), "message", "我 2019 年换了工作"));
const streamingRows = liveRows(streamed.messageHistory).filter((row) => row.state === "streaming");
assert.equal(streamingRows.length, 1, "deltas commit through the frame buffer, not one setMessages each");
assert.equal(streamingRows[0].text, "改写后的全文");
assert.match(streamingRows[0].activity?.label ?? "", /正在组织回答/);
assert.equal(streamingRows[0].activity?.phase, "answer-composition");
assert.ok(streamed.liveLabels.includes("正在组织回答…"));
});
test("failed server turns retain their receipt state without parsing activity from answer text", () => {
assert.match(chatSource, /failed:\s*true/);
assert.match(chatSource, /completedReceipt/);
// Former lock: `message.state === "settled" && message.failed`, the inline red banner above a
// failed reply. That banner was the second, rectification-only failure surface (BUG-476);
// failure now reaches the shared error notice while the receipt state still gates actions.
assert.match(chatSource, /&& !message\.failed/);
assert.doesNotMatch(chatSource, /rectification-activity-failure/);
assert.doesNotMatch(chatSource, /parsed\.(?:activity|receipt|tools|methods)/);
});
test("candidate acceptance copy never presents adoption as confirmation", () => {
// 旧:分钟卡「当前可能的出生时间」+ 「采用不等于确认出生时间」
// 新:区间交付卡;动作是「按这个范围用」;已采用仍不是确认
// 原因:TASK-rectification-range-delivery-card-20260907 交付对象改成区间
const delivery = readFileSync(
new URL("../src/components/rectification-range-delivery.tsx", import.meta.url),
"utf8",
);
assert.match(chatSource, /<RectificationRangeDelivery/);
assert.match(delivery, /rangeDeliveryAdopting/);
// 原值: 组件源码直写「已采用」
// 新值: 行内状态走 rangeDeliveryAdopted
// 原因: BUG-595 决策 1
assert.match(delivery, /rangeDeliveryAdopted/);
assert.doesNotMatch(delivery, /确认校正时间/);
assert.match(chatSource, /已确认校正时间/);
// 原值: 采用后状态条 rectification-adopt-status
// 新值: 采用状态只在卡片行内
// 原因: BUG-595 决策 3
assert.doesNotMatch(chatSource, /rectification-adopt-status/);
assert.doesNotMatch(chatSource, /rectification-consult-handoff/);
assert.doesNotMatch(chatSource, /本轮校正已收口|当前排盘时间(代表性时间|本会话以代表性时间收口/);
assert.doesNotMatch(chatSource, /candidateResult\.confirmationAllowed \? "确认校正时间"/);
});