d4627a5019
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>
208 lines
7.1 KiB
TypeScript
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} 年前后,/);
|
|
});
|