Files
Jyotisha/frontend/tests/rectification-post-adopt-verify-20260904.test.ts
T
Jesse_ChenandCursor dd8f35f7ba fix(rectification): invite-first collect, holdout at 4 events, Skill 10.0.23 (BUG-646–648)
Stop domain-wheel collecting and age-band years in prompts. Ask until the training gate, then discriminate until convergence, then deliver a range plus a concrete follow-up. Reserve holdout only with four dated events.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 01:43:02 +08:00

212 lines
7.3 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 {
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";
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,
regenerating: 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: [],
};
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", () => {
const chat = readFileSync(
new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url),
"utf8",
);
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/);
const idle = chat.slice(chat.indexOf('questionGap === "verified_idle"'));
assert.doesNotMatch(idle.slice(0, 400), /rectification-question-gap__copy/);
assert.doesNotMatch(idle.slice(0, 400), /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-642BUG-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 年前后/);
});