Files
Jyotisha/frontend/tests/rectification-delivery-ui-simplify-20260908.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

119 lines
6.0 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import {
deliveryTurnNarration,
postAdoptVerifyDoneCopy,
RECTIFICATION_USER_COPY,
} from "../src/lib/rectification-agentic/user-copy.ts";
import {
alreadyDelivered,
markDeliveryTurn,
resetDeliveryTurnGuardForTests,
} from "../src/lib/rectification-agentic/v9/delivery-turn-guard.ts";
import { RectificationVerificationReport } from "../src/components/rectification-verification-report.tsx";
import { rectificationChatSurface } from "./rectification-chat-surface.ts";
test("delivery body is three sentences and omits the eight-method report", () => {
const text = deliveryTurnNarration({
credibleRange: ["04:51", "04:59"],
representativeTime: "04:53",
eventCount: 7,
fitPercent: 80,
});
// 原值: 这次给出的范围 04:51–04:59。对照了 7 件经历…
// 新值: 目前范围 04:51–04:59。对照了 7 件经历…
// 原因: BUG-654 禁用「这次给出」
assert.equal(text, "目前范围 04:51–04:59。对照了 7 件经历,事件吻合率 80%。这只是代表性候选,不是已确认的唯一出生分钟。");
assert.doesNotMatch(text, /方法1/);
assert.doesNotMatch(text, /Technique Audit/);
assert.doesNotMatch(text, /排盘用/);
});
test("skipping the last reverse-verify keeps the original post-adopt sentence", () => {
assert.equal(
postAdoptVerifyDoneCopy("05:07", true),
RECTIFICATION_USER_COPY.postAdoptVerifyDone,
);
assert.equal(
postAdoptVerifyDoneCopy("05:07", false),
"已采用 05:07,新建对话即按这个时间排盘。",
);
});
test("verification report fold is closed and renders a real table", () => {
const html = renderToStaticMarkup(createElement(RectificationVerificationReport, {
markdown: "| 方法 | 结果 |\n| --- | --- |\n| 1 | 吻合 |",
}));
assert.match(html, /<details/);
assert.doesNotMatch(html, /<details[^>]*\sopen/);
assert.match(html, /查看验证报告/);
assert.match(html, /<table/);
});
test("already_delivered blocks a second no-message run for the same result within 60s", () => {
resetDeliveryTurnGuardForTests();
const caseId = "11111111-1111-4111-8111-111111111111";
const resultId = "22222222-2222-4222-8222-222222222222";
assert.equal(alreadyDelivered({ caseId, resultId, hasUserMessage: false, now: 1_000 }), false);
markDeliveryTurn({ caseId, resultId, now: 1_000 });
assert.equal(alreadyDelivered({ caseId, resultId, hasUserMessage: false, now: 1_500 }), true);
assert.equal(alreadyDelivered({ caseId, resultId, hasUserMessage: true, now: 1_500 }), false);
assert.equal(alreadyDelivered({
caseId,
resultId: "33333333-3333-4333-8333-333333333333",
hasUserMessage: false,
now: 1_500,
}), false);
assert.equal(alreadyDelivered({ caseId, resultId, hasUserMessage: false, now: 62_000 }), false);
resetDeliveryTurnGuardForTests();
});
test("agent-run returns already_delivered before billing and the route skips a second turn", () => {
const agentRun = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url), "utf8");
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
const exit = readFileSync(new URL("../src/lib/rectification-agentic/v9/turn-exit.ts", import.meta.url), "utf8");
const regenerate = readFileSync(
new URL("../src/app/api/rectification/cases/[caseId]/turns/[turnId]/regenerate/route.ts", import.meta.url),
"utf8",
);
const guardStart = agentRun.indexOf("alreadyDelivered({");
const reserve = agentRun.indexOf("const reserve = await billing.reserve()");
assert.ok(guardStart >= 0 && guardStart < reserve);
assert.match(agentRun, /errorCode: "already_delivered"/);
assert.match(route, /result\.errorCode === "already_delivered"/);
assert.match(route, /trigger: "opening"/);
assert.match(exit, /trigger: "finalizeSuccessfulTurnExit"/);
assert.match(regenerate, /trigger: "regenerate"/);
assert.match(route, /logRectificationDeliveryTurn/);
});
test("agent-run trims evidence after persistNextInterviewIfIdle so delivery can keep three sentences", () => {
const agentRun = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run.ts", import.meta.url), "utf8");
const persistIdx = agentRun.lastIndexOf("persistNextInterviewIfIdle({");
const trimIdx = agentRun.lastIndexOf("trimSpokenTurnForInterview(");
const replaceIdx = agentRun.lastIndexOf('type: "answer.delta", text: spokenAnswer, replace: true');
assert.ok(persistIdx >= 0 && trimIdx > persistIdx);
assert.ok(replaceIdx > trimIdx);
assert.match(agentRun, /trimSpokenTurnForInterview\(answerText, interviewIdle\?\.terminalNote === true\)/);
});
test("composer wrap no longer hosts 先这样 or the adopt status bar", () => {
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
const chat = rectificationChatSurface;
const wrap = chat.slice(chat.indexOf("className=\"composer-wrap\""), chat.indexOf("className=\"composer-footer\""));
assert.doesNotMatch(wrap, /CHOICE_STOP_LABEL/);
assert.doesNotMatch(wrap, /rectification-collect-stop/);
assert.doesNotMatch(wrap, /rectification-composer-meta/);
assert.doesNotMatch(wrap, /rectification-adopt-status/);
// 原值: 容器源码含 CHOICE_STOP_LABEL(嵌入卡 stop 文案)。
// 新值: 常量在行组件的 choiceCardFromQuestion 里;composer-wrap 仍不得托管。
// 原因: BUG-725。
const messageEntry = readFileSync(new URL("../src/components/rectification-message-entry.tsx", import.meta.url), "utf8");
assert.match(messageEntry, /CHOICE_STOP_LABEL/);
assert.match(chat, /postAdoptVerifyDoneCopy/);
});