Files
Jyotisha/frontend/tests/rectification-tiebreak-card-loss-20260915.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

138 lines
5.8 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { vargaStyleFollowupRenderable } from "../src/lib/rectification-agentic/v9/probe-question-contract.ts";
import { interviewChoiceCardUnavailable } from "../src/lib/rectification-surface-state.ts";
import { MACHINE_VOICE_LEXICON } from "../src/lib/rectification-agentic/v9/agent-voice-lexicon.ts";
import { validateAdoptNarration, type AdoptDeliveryFacts } from "../src/lib/rectification-agentic/v9/adopt-narration.ts";
import { RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
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";
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
const chat = rectificationChatSurface;
const card = readFileSync(
new URL("../src/components/rectification-range-delivery.tsx", import.meta.url),
"utf8",
);
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const followup = readFileSync(
new URL("../src/lib/rectification-agentic/v9/method-followup.ts", import.meta.url),
"utf8",
);
const facts: AdoptDeliveryFacts = {
precision_stage: "ready_to_adopt",
already_accepted: false,
credible_range: ["04:48", "05:07"],
representative_minute: "04:53",
runner_up_minute: "05:00",
opening_window: ["04:45", "05:15"],
active_candidates: [
{ time: "04:53", relative_support: 16 },
{ time: "05:00", relative_support: 16 },
],
answered_rounds: 6,
stop_facts: [{ kind: "indistinguishable", label: "再问下去也分不开 04:53 和 05:00", count: 1 }],
post_adopt_verification: [],
minutes: ["04:48", "05:07", "04:53", "05:00", "04:45", "05:15"],
years: [],
};
test("the delivery card no longer renders a post-card tie-break entry", () => {
assert.doesNotMatch(card, /onTieBreak/);
assert.doesNotMatch(card, /rangeDeliveryTieBreakEntry/);
assert.doesNotMatch(chat, /onTieBreak/);
assert.doesNotMatch(chat, /requestTieBreak/);
assert.doesNotMatch(css, /rectification-range-delivery__tie-break/);
assert.match(chat, /adoptLocked=\{liveInterviewBlockingAdopt\}/);
assert.match(chat, /liveInterviewBlockingAdopt/);
// 原值: 从 chat 源码切 `const showSelectionCards` 到 `const showReadonlyRange`,断言不含 interviewQuestionBlocksAdoptOffer。
// 新值: 调用 deriveRectificationChatView:最新回复上挂着一道没答的采集题时,交付卡照常出(showSelectionCards
// 不变),只把采用锁上(liveInterviewBlockingAdopt)。
// 原因: TASK-rectification-code-split-20260926 派生逻辑原样搬进纯函数 rectification-chat-view.ts。
const result = offerableCandidateResult();
const openCollect = {
focus_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1",
question_id: "collect:career",
kind: "collect_spoken" as const,
prompt: "先说你最容易想起的一两件,年月大概就行",
options: null,
status: "active",
answer_option: null,
probe_id: null,
};
const quiet = deriveRectificationChatView(chatViewInput({
messages: [settledAssistant("offer-1")],
candidateResult: result,
}));
const withOpenQuestion = deriveRectificationChatView(chatViewInput({
messages: [settledAssistant("offer-1", { question: openCollect })],
candidateResult: result,
}));
assert.equal(quiet.showSelectionCards, true);
assert.equal(quiet.liveInterviewBlockingAdopt, false);
assert.equal(withOpenQuestion.showSelectionCards, true);
assert.equal(withOpenQuestion.liveInterviewBlockingAdopt, true);
});
test("tie-break availability reuses the choice-card renderability gate", () => {
assert.match(followup, /vargaStyleFollowupRenderable/);
assert.equal(
vargaStyleFollowupRenderable({
choiceKind: "varga_style",
styleOptions: [
{ label: "相处里更在意照顾对方的感受", answer_class: "yes", sign: "巨蟹座" },
{ label: "习惯带头,也不排斥站到台前", answer_class: "weak_yes", sign: "狮子座" },
],
}),
true,
);
assert.equal(
vargaStyleFollowupRenderable({
choiceKind: "varga_style",
styleOptions: [
{ label: "相处里更在意照顾对方的感受", answer_class: "yes" },
{ label: "习惯带头,也不排斥站到台前", answer_class: "weak_yes" },
],
}),
false,
);
});
test("a varga_style focus without a choice card is a dead question, not a spoken stem", () => {
assert.equal(
interviewChoiceCardUnavailable({
questionKind: "choice",
hasChoiceCard: false,
questionId: "varga.d9.cancer/leo",
}),
true,
);
assert.equal(
interviewChoiceCardUnavailable({
questionKind: "collect_spoken",
hasChoiceCard: false,
questionId: "varga.d9.cancer/leo",
}),
true,
);
assert.equal(
interviewChoiceCardUnavailable({
questionKind: "choice",
hasChoiceCard: true,
questionId: "varga.d9.cancer/leo",
}),
false,
);
});
test("adopt narration may not speak 相对支持度 even when the number is in facts", () => {
assert.ok(MACHINE_VOICE_LEXICON.includes("相对支持度"));
assert.equal(validateAdoptNarration("相对支持度都是 16。", facts).ok, false);
assert.equal(validateAdoptNarration("这两个时间按现有信息分不开。", facts).ok, true);
assert.equal(RECTIFICATION_USER_COPY.rangeDeliveryAdoptLocked, "先答完上面这道,再选时间。");
});