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
82 lines
4.5 KiB
TypeScript
82 lines
4.5 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
import { rectificationChatSurface } from "./rectification-chat-surface.ts";
|
||
import { deriveRectificationChatView } from "../src/lib/rectification-chat-view.ts";
|
||
import { chatViewInput, offerableCandidateResult, settledAssistant } from "./rectification-chat-run-fixtures.ts";
|
||
|
||
const read = (relativePath: string) => readFileSync(new URL(relativePath, import.meta.url), "utf8");
|
||
|
||
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
|
||
const chat = rectificationChatSurface;
|
||
const delivery = read("../src/components/rectification-range-delivery.tsx");
|
||
const styles = read("../src/app/globals.css");
|
||
const messageLoop = chat.slice(
|
||
chat.indexOf("{messages.map((message) => {"),
|
||
chat.indexOf("{savedTime &&"),
|
||
);
|
||
|
||
test("clicking 更像这个 does not unmount the range card for busy", () => {
|
||
// 原值: 从 chat 源码切 `const keepSelectionCardsWhileBusy` 到 `const showReadonlyRange`,匹配
|
||
// `acceptingCandidateId || candidateResult?.selectedTime`、`!busy || keepSelectionCardsWhileBusy`,
|
||
// 且不含 `&& !busy && selectionCardMessageKey`。
|
||
// 新值: 调用 deriveRectificationChatView:可交付结果挂在最新已结算回复上——空闲出卡;busy 且没在采用、
|
||
// 没采用则收卡;busy 但正在采用(acceptingCandidateId)或已采用(带持久 offer)时卡仍在。
|
||
// 原因: TASK-rectification-code-split-20260926 派生逻辑原样搬到 rectification-chat-view.ts(纯函数);
|
||
// 行为断言覆盖原三条 token 的意图,且更强。
|
||
const result = offerableCandidateResult();
|
||
const messages = [settledAssistant("offer-1")];
|
||
const idle = deriveRectificationChatView(chatViewInput({ messages, candidateResult: result }));
|
||
assert.equal(idle.showSelectionCards, true);
|
||
assert.equal(idle.selectionCardMessageKey, "offer-1");
|
||
const busyTurn = deriveRectificationChatView(chatViewInput({ messages, candidateResult: result, busy: true }));
|
||
assert.equal(busyTurn.showSelectionCards, false);
|
||
const adopting = deriveRectificationChatView(chatViewInput({
|
||
messages,
|
||
candidateResult: result,
|
||
busy: true,
|
||
acceptingCandidateId: result.candidates[0].candidateId,
|
||
}));
|
||
assert.equal(adopting.showSelectionCards, true);
|
||
assert.equal(adopting.selectionCardMessageKey, "offer-1");
|
||
const adopted = deriveRectificationChatView(chatViewInput({
|
||
messages: [settledAssistant("offer-1", { candidateOffer: { resultId: result.resultId } })],
|
||
candidateResult: { ...result, selectedTime: "04:53" },
|
||
busy: true,
|
||
}));
|
||
assert.equal(adopted.showSelectionCards, true);
|
||
assert.equal(adopted.selectionCardMessageKey, "offer-1");
|
||
});
|
||
|
||
test("after adopt the card stays on the locked offering message", () => {
|
||
assert.match(chat, /resolveSelectionCardMessageKey/);
|
||
assert.match(chat, /nextSelectionCardLock/);
|
||
// 原值:selectionOfferLockRef.current = nextSelectionCardLock(...)
|
||
// 新值:useState + 渲染期 setSelectionOfferLock(与 seededTurns 同一模式)
|
||
// 原因:react-hooks/refs 禁止渲染期读写 ref;门禁 lint 0 error(BUG-622)
|
||
assert.match(chat, /const \[selectionOfferLock, setSelectionOfferLock\] = useState/);
|
||
assert.match(chat, /if \(nextOfferLock !== selectionOfferLock\) \{\s*setSelectionOfferLock\(nextOfferLock\);/);
|
||
assert.doesNotMatch(chat, /selectionOfferLockRef/);
|
||
assert.match(messageLoop, /showSelectionCards && candidateResult && message\.renderKey === selectionCardMessageKey/);
|
||
});
|
||
|
||
test("the closing line sits under the card on the assistant column", () => {
|
||
assert.match(messageLoop, /verifiedIdleCopy && \(/);
|
||
assert.match(messageLoop, /className="rectification-pending-note"/);
|
||
assert.match(
|
||
chat,
|
||
/questionGap === "verified_idle" && verifiedIdleCopy && !showSelectionCards/,
|
||
);
|
||
assert.match(
|
||
styles,
|
||
/\.rectification-pending-note \{[\s\S]*margin-inline-start: var\(--assistant-content-inset\)/,
|
||
);
|
||
assert.doesNotMatch(chat, /rectification-adopt-status/);
|
||
});
|
||
|
||
test("the adopted column button is pressed and disabled; others stay 更像这个", () => {
|
||
assert.match(delivery, /disabled=\{busy \|\| adopted\}/);
|
||
assert.match(delivery, /rangeDeliveryAdopted/);
|
||
assert.match(delivery, /rangeDeliveryMoreLikeThis/);
|
||
});
|