Files
Jyotisha/frontend/tests/rectification-post-adopt-verify-20260904.test.ts
T
Jesse_Chen d4627a5019
Independent Staging Quality Gate / validate (push) Successful in 14m31s
Independent Staging Quality Gate / publish (push) Successful in 1m59s
fix(rectification): dedupe post-adopt verify and skip this probe only
Adopted reverse-verify repeated already-asked collect stems, treated 「这题跳过」 as stopping the case, and promised holdout/OOS checks that never ran. Collect spoken stems no longer prefix a year.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 21:51:59 +08:00

208 lines
7.1 KiB
TypeScript

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 { USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.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, /RECTIFICATION_USER_COPY\.postAdoptVerifyDone/);
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/);
assert.match(chat, /已采用 \{savedTime\}/);
assert.match(chat, /之后新建对话即按此时间排盘/);
});
test("family collect spoken stem equals USER_COLLECT_QUESTION.family 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",
}],
});
assert.equal(plan.next_followup?.domain, "family");
assert.equal(plan.next_followup?.probe_year, 2021);
assert.equal(plan.next_followup?.choice_frame, null);
const spoken = spokenFollowupForUser(plan.next_followup);
assert.equal(spoken, USER_COLLECT_QUESTION.family);
assert.doesNotMatch(spoken ?? "", /^\d{4} 年前后,/);
});