Files
Jyotisha/frontend/tests/rectification-guided-collect-20260916.test.ts
T
jesse-ux 6aabbe386f
Independent Staging Quality Gate / validate (push) Successful in 10m1s
Independent Staging Quality Gate / publish (push) Successful in 36m54s
feat(rectification): 删掉年月录入卡,年月阶段打字回答且不被追问覆盖
2026-09-16 23:22:32 +08:00

264 lines
11 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 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:5105: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);
});