About 30 fixed rectification lines lose the receipt tone: no 「已记录。」 openings (收到 when the range holds, 好 when it moves), 麻烦 instead of 请, 时间 instead of 候选 in what the user reads, and no 「新建对话」 after adoption. Numbers, clocks, ranges and the honesty boundaries are unchanged. CHOICE_ACK_RE also recognises the new acknowledgements, so a model sentence that repeats one is still dropped; the delivery guard matches the delivery sentence by containment and does not depend on the opening. Full suite 4973 / fail 24, identical to 61d6f248; build keeps / Static. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
235 lines
9.1 KiB
TypeScript
235 lines
9.1 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
templatePostAdoptExplain,
|
||
validateAdoptNarration,
|
||
} from "../src/lib/rectification-agentic/v9/adopt-narration.ts";
|
||
import {
|
||
buildMethodFollowupPlan,
|
||
buildNextUserAction,
|
||
spokenFollowupForUser,
|
||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||
import {
|
||
RECTIFICATION_QUESTION_RETRY_LIMIT,
|
||
rectificationQuestionGapState,
|
||
rectificationQuestionRetryActive,
|
||
type RectificationQuestionGapInput,
|
||
} from "../src/lib/rectification-surface-state.ts";
|
||
import { rectificationChatSurface } from "./rectification-chat-surface.ts";
|
||
import { createElement } from "react";
|
||
import { renderToStaticMarkup } from "react-dom/server";
|
||
import { RectificationQuestionGapNotices } from "../src/components/rectification-question-gap-notices.tsx";
|
||
import { RECTIFICATION_QUESTION_RELOAD_LABEL } from "../src/lib/rectification-surface-state.ts";
|
||
|
||
const STYLE_OPTIONS = [
|
||
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
|
||
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
|
||
{ label: "明确没有发生", answer_class: "no" as const },
|
||
{ label: "这段记不清楚", answer_class: "unsure" as const },
|
||
];
|
||
|
||
function verifyProbe(input: {
|
||
domain: "finance" | "relocation";
|
||
year: number;
|
||
semantic: string;
|
||
split: string;
|
||
}) {
|
||
return {
|
||
year: input.year,
|
||
year_label: `${input.year} 年前后`,
|
||
domain: input.domain,
|
||
event_family: input.domain === "finance"
|
||
? "收入明显变化、大笔支出或欠债"
|
||
: "搬家或长期住到外地",
|
||
source: "dasha_activation" as const,
|
||
tracks: ["vimshottari", "narayana"] as const,
|
||
tracks_agree: true,
|
||
unique_minute_claim: false as const,
|
||
user_meaning: `${input.year} 年前后是否有相关经历。`,
|
||
role: "distinguish" as const,
|
||
style_options: STYLE_OPTIONS,
|
||
semantic_key: input.semantic,
|
||
candidate_split_hash: input.split,
|
||
};
|
||
}
|
||
|
||
const FINANCE_2013 = verifyProbe({
|
||
domain: "finance",
|
||
year: 2013,
|
||
semantic: "finance.2013.verify",
|
||
split: "finance-2013-split",
|
||
});
|
||
const RELOC_2017 = verifyProbe({
|
||
domain: "relocation",
|
||
year: 2017,
|
||
semantic: "relocation.2017.verify",
|
||
split: "reloc-2017-split",
|
||
});
|
||
|
||
const EDUCATION_EVIDENCE = [{
|
||
status: "confirmed" as const,
|
||
domain: "education",
|
||
datePrecision: "year" as const,
|
||
occurredFrom: "2008-01-01",
|
||
occurredTo: null,
|
||
}];
|
||
|
||
const gapBase: RectificationQuestionGapInput = {
|
||
liveQuestionVisible: false,
|
||
questionMissing: true,
|
||
questionLoadFailed: false,
|
||
busy: false,
|
||
readonly: false,
|
||
snapshotLoaded: true,
|
||
resumableCase: true,
|
||
retryAttempts: 0,
|
||
};
|
||
|
||
test("post-adopt reverse-verify skips a probe already present in askedKeys", () => {
|
||
const remaining = buildMethodFollowupPlan({
|
||
evidence: EDUCATION_EVIDENCE,
|
||
accepted: true,
|
||
eventProbes: [FINANCE_2013, RELOC_2017],
|
||
askedProbeKeys: [FINANCE_2013.semantic_key],
|
||
});
|
||
assert.equal(remaining.next_followup?.domain, "relocation");
|
||
assert.equal(remaining.next_followup?.semantic_key, RELOC_2017.semantic_key);
|
||
|
||
const none = buildMethodFollowupPlan({
|
||
evidence: EDUCATION_EVIDENCE,
|
||
accepted: true,
|
||
eventProbes: [FINANCE_2013, RELOC_2017],
|
||
askedProbeKeys: [FINANCE_2013.semantic_key, RELOC_2017.semantic_key],
|
||
});
|
||
assert.equal(none.next_followup, null);
|
||
const action = buildNextUserAction({
|
||
scorableCount: 1,
|
||
evidenceCount: 1,
|
||
hasLatestResult: true,
|
||
selectionAllowed: true,
|
||
sessionOutcome: "adopt_representative",
|
||
nextFollowup: none.next_followup,
|
||
workingTime: "05:07",
|
||
accepted: true,
|
||
});
|
||
assert.equal(action.id, "start_consultation");
|
||
});
|
||
|
||
test("post-adopt reverse-verify also dedupes by candidate_split_hash", () => {
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence: EDUCATION_EVIDENCE,
|
||
accepted: true,
|
||
eventProbes: [FINANCE_2013, RELOC_2017],
|
||
askedProbeKeys: [FINANCE_2013.candidate_split_hash],
|
||
});
|
||
assert.equal(plan.next_followup?.domain, "relocation");
|
||
assert.notEqual(plan.next_followup?.semantic_key, FINANCE_2013.semantic_key);
|
||
});
|
||
|
||
test("empty reverse-verify list forbids a 会拿…核对 promise", () => {
|
||
const facts = {
|
||
precision_stage: "ready_to_adopt" as const,
|
||
already_accepted: false,
|
||
credible_range: ["05:00", "05:06"] as const,
|
||
representative_minute: "05:00",
|
||
runner_up_minute: "05:06",
|
||
opening_window: ["04:45", "05:15"] as const,
|
||
active_candidates: [],
|
||
answered_rounds: 5,
|
||
stop_facts: [],
|
||
post_adopt_verification: [],
|
||
minutes: ["05:00", "05:06"],
|
||
years: [],
|
||
};
|
||
// 原值: /没有还能核对的前事/ / 新值: /没有还能核对的过去的事/ / 原因: BUG-1258:校正固定文案改为知心姐姐口吻(产品 2026-10-07 认可样稿)
|
||
assert.match(templatePostAdoptExplain(facts), /没有还能核对的过去的事/);
|
||
const denied = validateAdoptNarration("采用后会拿 2016 年教育题来核对。", facts);
|
||
assert.equal(denied.ok, false);
|
||
if (!denied.ok) assert.equal(denied.reason, "promise");
|
||
});
|
||
|
||
test("question gap: start_consultation is verified_idle; missing next_user_action keeps the old path", () => {
|
||
assert.equal(rectificationQuestionGapState({
|
||
...gapBase,
|
||
nextUserActionId: "start_consultation",
|
||
}), "verified_idle");
|
||
assert.equal(rectificationQuestionRetryActive("verified_idle"), false);
|
||
assert.equal(rectificationQuestionGapState(gapBase), "preparing");
|
||
assert.equal(rectificationQuestionGapState({
|
||
...gapBase,
|
||
retryAttempts: RECTIFICATION_QUESTION_RETRY_LIMIT,
|
||
}), "unavailable");
|
||
});
|
||
|
||
test("verified_idle UI is a closing line without reload, gap copy, or consult handoff", () => {
|
||
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
|
||
const chat = rectificationChatSurface;
|
||
assert.match(chat, /questionGap === "verified_idle"/);
|
||
assert.match(chat, /postAdoptVerifyDoneCopy/);
|
||
assert.match(chat, /skip_this_probe/);
|
||
assert.match(chat, /SKIP_PROBE_ACTION/);
|
||
assert.doesNotMatch(chat, /rectification-consult-handoff/);
|
||
// 原值: 从 chat 源码第一处 `questionGap === "verified_idle"` 起取 400 字,断言不含
|
||
// rectification-question-gap__copy 与 RECTIFICATION_QUESTION_RELOAD_LABEL。
|
||
// 新值: 真渲染缺题块(verified_idle + 收尾句、无对照卡):只有一行 role="status" 的收尾句,
|
||
// 没有缺题文案、没有「接着问」重载按钮。
|
||
// 原因: TASK-rectification-code-split-20260926 该 JSX 原样搬进 rectification-question-gap-notices.tsx;
|
||
// 渲染断言比按字数截源码更准。
|
||
const idleHtml = renderToStaticMarkup(createElement(RectificationQuestionGapNotices, {
|
||
view: { showStandaloneQuestion: false, questionGap: "verified_idle", showSelectionCards: false, verifiedIdleCopy: "已按 05:07 核对完毕。" },
|
||
currentQuestion: null,
|
||
choiceCard: null,
|
||
choiceNonce: 0,
|
||
busy: false,
|
||
readonly: false,
|
||
snapshotFailure: null,
|
||
questionRepairAttempts: 0,
|
||
questionRepairing: false,
|
||
submitChoice() {},
|
||
submitStop() {},
|
||
async refetchQuestion() {},
|
||
async repairQuestion() {},
|
||
}));
|
||
assert.equal(idleHtml, '<p class="rectification-pending-note" role="status">已按 05:07 核对完毕。</p>');
|
||
assert.doesNotMatch(idleHtml, /rectification-question-gap__copy/);
|
||
assert.ok(!idleHtml.includes(RECTIFICATION_QUESTION_RELOAD_LABEL));
|
||
// 原值: 状态条「已采用 {savedTime}」+「之后新建对话即按此时间排盘」
|
||
// 新值: verified_idle 走 postAdoptVerifyDoneCopy;无核对题用新句
|
||
// 原因: BUG-595 决策 3
|
||
assert.doesNotMatch(chat, /之后新建对话即按此时间排盘/);
|
||
});
|
||
|
||
test("family collect spoken stem uses a year cue while probe_year stays dated", () => {
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence: [
|
||
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2011-01-01", occurredTo: null },
|
||
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2014-01-01", occurredTo: null },
|
||
],
|
||
evidenceCollectionProbes: [{
|
||
year: 2021,
|
||
year_label: "2021 年前后",
|
||
domain: "family",
|
||
event_family: "家人结婚、添丁或住院",
|
||
source: "age_band",
|
||
tracks: ["vimshottari", "narayana"],
|
||
tracks_agree: false,
|
||
unique_minute_claim: false,
|
||
user_meaning: "时间范围锁定 2021 年前后;领域锁定 family。",
|
||
role: "collect",
|
||
phase: "evidence_collection",
|
||
information_gain: 0,
|
||
semantic_key: "family.2021",
|
||
}],
|
||
});
|
||
// 原值: family + 2021 年前后
|
||
// 新值: 训练门关时邀请优先,题干不含年龄段年份
|
||
// 原因: 撤回 BUG-642(BUG-648)
|
||
assert.equal(plan.next_followup?.domain, "other");
|
||
assert.equal(plan.next_followup?.collection_key, "collect:invite:more");
|
||
assert.equal(plan.next_followup?.choice_frame, null);
|
||
const spoken = spokenFollowupForUser(plan.next_followup);
|
||
assert.match(spoken ?? "", /还有吗/);
|
||
assert.doesNotMatch(spoken ?? "", /2021 年前后/);
|
||
});
|