264 lines
11 KiB
TypeScript
264 lines
11 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
COLLECT_KIND_ORDER,
|
||
GUIDED_ANY_DOMAIN,
|
||
GUIDED_NARROW_HINT,
|
||
firstOpenCollectKind,
|
||
guidedAnyExamples,
|
||
TARGETED_EXISTENCE_PROMPT,
|
||
TARGETED_EXISTENCE_PROMPT_RETRY,
|
||
TARGETED_YEAR_PROMPT,
|
||
guidedCollectExhausted,
|
||
guidedRetryPool,
|
||
guidedWindowPool,
|
||
guidedWindowPrompt,
|
||
parseGuidedWindowQuestionId,
|
||
targetedCollectPool,
|
||
rangeNarrowHint,
|
||
remainingGuidedLine,
|
||
type CollectionEvidence,
|
||
type GuidedCollectWindow,
|
||
} from "../src/lib/rectification-agentic/v9/collection-question-pool.ts";
|
||
import { targetedCollectFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||
import {
|
||
TARGETED_COLLECT_OPTION_B,
|
||
GUIDED_WINDOW_OPTION_B,
|
||
} from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||
import { RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
|
||
|
||
const WINDOWS: readonly GuidedCollectWindow[] = [
|
||
{ year: 2018, month_lo: 3, month_hi: 6, domain: "career", split: { left: 2, right: 3 } },
|
||
{ year: 2021, month_lo: 8, month_hi: 8, domain: "relationship", split: { left: 3, right: 2 } },
|
||
];
|
||
|
||
test("guided pool order is windows, skip retry, then uncovered domains", () => {
|
||
const evidence: CollectionEvidence[] = [
|
||
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2014-09-01", occurredTo: "2014-09-01" },
|
||
];
|
||
const skipped = [
|
||
{
|
||
questionId: "collect:targeted:family",
|
||
target_domain: "family",
|
||
status: "skipped",
|
||
intent: "collect_method_evidence",
|
||
target_kind: "targeted:family",
|
||
},
|
||
];
|
||
const windows = guidedWindowPool(WINDOWS, evidence, skipped);
|
||
const retry = guidedRetryPool(evidence, skipped);
|
||
const open = targetedCollectPool(["d9", "d10"], evidence, skipped);
|
||
assert.equal(windows[0]?.guidedSource, "window");
|
||
assert.equal(windows[0]?.domain, "career");
|
||
assert.equal(retry[0]?.guidedSource, "retry");
|
||
assert.equal(retry[0]?.domain, "family");
|
||
assert.ok(open.some((item) => item.domain === "relationship"));
|
||
assert.equal(open.some((item) => item.domain === "family"), false);
|
||
|
||
const followup = targetedCollectFollowup(["d9"], evidence, skipped, ["04:51", "05:11"], 5, ["04:51", "05:11"], WINDOWS);
|
||
assert.equal(followup?.collection_key, windows[0]?.key);
|
||
});
|
||
|
||
test("A on a window yields a typed year question, B/C close only that window", () => {
|
||
const evidence: CollectionEvidence[] = [];
|
||
const afterYes = [
|
||
{
|
||
questionId: "collect:guided:window:2018:3:6:career",
|
||
target_domain: "career",
|
||
status: "resolved",
|
||
intent: "collect_method_evidence",
|
||
target_kind: "guided:window:career",
|
||
},
|
||
];
|
||
// 原值: parseYearEntryQuestionId + EventDateEntryCard
|
||
// 新值: 年月阶段是口述题「大概哪年几月?」
|
||
// 原因: T0 产品删除录入卡
|
||
const year = parseGuidedWindowQuestionId("collect:guided:window:2018:3:6:career:year");
|
||
assert.equal(year?.domain, "career");
|
||
assert.equal(year?.year, 2018);
|
||
assert.equal(year?.stage, "year");
|
||
const pending = targetedCollectFollowup([], evidence, afterYes, null, 5, ["04:51", "05:11"], WINDOWS);
|
||
assert.equal(pending?.collection_key?.endsWith(":year"), true);
|
||
assert.equal(pending?.spoken_prompt, TARGETED_YEAR_PROMPT);
|
||
assert.equal(pending?.choice_frame, null);
|
||
|
||
const afterNo = [
|
||
{
|
||
questionId: "collect:guided:window:2018:3:6:career",
|
||
target_domain: "career",
|
||
status: "declined",
|
||
intent: "collect_method_evidence",
|
||
target_kind: "guided:window:career",
|
||
},
|
||
];
|
||
const next = targetedCollectFollowup(["d10"], evidence, afterNo, null, 5, null, WINDOWS);
|
||
assert.notEqual(next?.collection_key, "collect:guided:window:2018:3:6:career");
|
||
assert.ok(targetedCollectPool(["d10"], evidence, afterNo).some((item) => item.domain === "career"));
|
||
});
|
||
|
||
test("skipped once retries, declined does not, second skip closes", () => {
|
||
const evidence: CollectionEvidence[] = [];
|
||
const skipped = [{
|
||
questionId: "collect:targeted:relationship",
|
||
target_domain: "relationship",
|
||
status: "skipped",
|
||
intent: "collect_method_evidence",
|
||
target_kind: "targeted:relationship",
|
||
}];
|
||
const retry = guidedRetryPool(evidence, skipped);
|
||
assert.equal(retry[0]?.domain, "relationship");
|
||
assert.match(retry[0]?.prompt ?? "", /认真关系/);
|
||
|
||
const declined = [{
|
||
questionId: "collect:targeted:relationship",
|
||
target_domain: "relationship",
|
||
status: "declined",
|
||
intent: "collect_method_evidence",
|
||
target_kind: "targeted:relationship",
|
||
}];
|
||
assert.equal(guidedRetryPool(evidence, declined).length, 0);
|
||
assert.equal(targetedCollectPool(["d9"], evidence, declined).some((item) => item.domain === "relationship"), false);
|
||
|
||
const skippedTwice = [
|
||
...skipped,
|
||
{
|
||
questionId: "collect:targeted:relationship:retry",
|
||
target_domain: "relationship",
|
||
status: "skipped",
|
||
intent: "collect_method_evidence",
|
||
target_kind: "targeted:relationship:retry",
|
||
retry: true,
|
||
},
|
||
];
|
||
assert.equal(guidedRetryPool(evidence, skippedTwice).length, 0);
|
||
assert.equal(targetedCollectPool(["d9"], evidence, skippedTwice).some((item) => item.domain === "relationship"), false);
|
||
});
|
||
|
||
test("existence prompts cover the whole domain for every collect kind", () => {
|
||
for (const domain of COLLECT_KIND_ORDER) {
|
||
assert.match(TARGETED_EXISTENCE_PROMPT[domain], /哪一年都算/);
|
||
assert.ok(TARGETED_EXISTENCE_PROMPT_RETRY[domain].length > 8);
|
||
}
|
||
assert.equal(TARGETED_COLLECT_OPTION_B, "这类事都没有过");
|
||
assert.equal(GUIDED_WINDOW_OPTION_B, "这段没有");
|
||
assert.match(RECTIFICATION_USER_COPY.collectDeclinedAck, /没有发生过/);
|
||
assert.match(RECTIFICATION_USER_COPY.collectSkippedAck, /换个问法再问一次/);
|
||
});
|
||
|
||
test("a layer that cannot split candidates still leaves that domain in the open pool", () => {
|
||
const evidence: CollectionEvidence[] = [];
|
||
const open = targetedCollectPool([], evidence, []);
|
||
assert.deepEqual(open.map((item) => item.domain), [...COLLECT_KIND_ORDER]);
|
||
});
|
||
|
||
test("time-point no does not close a domain", () => {
|
||
const evidence: CollectionEvidence[] = [];
|
||
const probeNo = [{
|
||
questionId: "distinguish:relationship.2018.05.dasha_boundary",
|
||
target_domain: "relationship",
|
||
status: "declined",
|
||
source: "event_probe",
|
||
}];
|
||
assert.ok(targetedCollectPool(["d9"], evidence, probeNo).some((item) => item.domain === "relationship"));
|
||
});
|
||
|
||
test("unmet-gate hint has no free-text invite", () => {
|
||
const declined = COLLECT_KIND_ORDER.map((domain) => ({
|
||
questionId: `collect:targeted:${domain}`,
|
||
target_domain: domain,
|
||
status: "declined",
|
||
intent: "collect_method_evidence",
|
||
target_kind: `targeted:${domain}`,
|
||
}));
|
||
const copy = rangeNarrowHint(
|
||
["d9", "d10", "d4", "d5", "d7", "d2", "d30"],
|
||
[],
|
||
declined,
|
||
["04:51", "05:11"],
|
||
5,
|
||
["04:51", "05:11"],
|
||
);
|
||
assert.match(copy, /再对照几件经历会更准/);
|
||
assert.doesNotMatch(copy, /你要是还记得确切哪一天/);
|
||
assert.doesNotMatch(copy, /不限领域/);
|
||
assert.ok(copy.includes(GUIDED_NARROW_HINT));
|
||
const line = remainingGuidedLine(["04:51", "05:11"], 5, ["04:51", "05:11"]);
|
||
assert.equal(line, "现在还剩 04:51–05:11 里 5 个候选,再对照几件经历会更准");
|
||
});
|
||
|
||
test("window prompt uses oral phrases, not server labels", () => {
|
||
const prompt = guidedWindowPrompt({
|
||
year: 2018,
|
||
month_lo: 3,
|
||
month_hi: 6,
|
||
domain: "career",
|
||
});
|
||
assert.equal(prompt, "2018 年 3 到 6 月之间,有没有入职、换工作或职责变重?");
|
||
assert.doesNotMatch(prompt, /event_family|career_entry/);
|
||
});
|
||
|
||
|
||
|
||
test("open windows ask without a domain and list open-domain orals", () => {
|
||
const open = guidedWindowPrompt({
|
||
year: 2018,
|
||
month_lo: 3,
|
||
month_hi: 6,
|
||
domain: GUIDED_ANY_DOMAIN,
|
||
}, ["relocation", "relationship", "family", "finance"]);
|
||
assert.equal(
|
||
open,
|
||
"2018 年 3 到 6 月之间,有没有什么事,比如搬家或开始长期住外地、开始认真关系、分手或结婚、添丁、长辈住院或做过手术?",
|
||
);
|
||
assert.equal(guidedAnyExamples(["relocation", "relationship", "family", "finance"]).length, 3);
|
||
assert.equal(
|
||
guidedWindowPrompt({ year: 2018, month_lo: 3, month_hi: 3, domain: GUIDED_ANY_DOMAIN }, []),
|
||
"2018 年 3 月前后,有没有什么事?",
|
||
);
|
||
});
|
||
|
||
test("an open window keeps its question id and is asked once for any domain", () => {
|
||
const pool = guidedWindowPool(
|
||
[{ year: 2018, month_lo: 3, month_hi: 6, domain: GUIDED_ANY_DOMAIN, split: { left: 2, right: 3 } }],
|
||
[],
|
||
[],
|
||
);
|
||
assert.equal(pool.length, 1);
|
||
assert.equal(pool[0]?.domain, GUIDED_ANY_DOMAIN);
|
||
assert.equal(pool[0]?.key, "collect:guided:window:2018:3:6:any");
|
||
// The same window answered under a domain label is the same window.
|
||
const asked = guidedWindowPool(
|
||
[{ year: 2018, month_lo: 3, month_hi: 6, domain: GUIDED_ANY_DOMAIN, split: { left: 2, right: 3 } }],
|
||
[],
|
||
[{ questionId: "collect:guided:window:2018:3:6:career", status: "resolved" }],
|
||
);
|
||
assert.equal(asked.length, 0);
|
||
});
|
||
|
||
test("open windows still rank remaining collect kinds without an entry card", () => {
|
||
// 原值: resolveEntryDomain + EventDateEntryCard 默认芯片
|
||
// 新值: 只保留 firstOpenCollectKind 表驱动排序
|
||
// 原因: T0 删除录入卡
|
||
assert.equal(
|
||
firstOpenCollectKind(
|
||
[{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2014-09-01", occurredTo: "2014-09-01" }],
|
||
[{ target_domain: "career", status: "declined" }],
|
||
),
|
||
"relocation",
|
||
);
|
||
});
|
||
|
||
test("guided collect is not exhausted while a window remains", () => {
|
||
assert.equal(guidedCollectExhausted([], [], [], WINDOWS, ["04:51", "05:11"], 5), false);
|
||
assert.equal(guidedCollectExhausted(["d9", "d10", "d4", "d5", "d7", "d2", "d30"], [
|
||
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2014-09-01", occurredTo: "2014-09-01" },
|
||
{ status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2018-07-01", occurredTo: "2018-07-01" },
|
||
{ status: "confirmed", domain: "relocation", datePrecision: "month", occurredFrom: "2016-08-01", occurredTo: "2016-08-01" },
|
||
{ status: "confirmed", domain: "relationship", datePrecision: "month", occurredFrom: "2021-08-01", occurredTo: "2021-08-01" },
|
||
{ status: "confirmed", domain: "family", datePrecision: "month", occurredFrom: "2019-01-01", occurredTo: "2019-01-01" },
|
||
{ status: "confirmed", domain: "finance", datePrecision: "month", occurredFrom: "2020-03-01", occurredTo: "2020-03-01" },
|
||
{ status: "confirmed", domain: "health_pressure", datePrecision: "month", occurredFrom: "2022-04-01", occurredTo: "2022-04-01" },
|
||
], [], [], ["04:51", "05:11"], 5), true);
|
||
});
|