Files
Jyotisha/frontend/tests/rectification-collection-question-pool.test.ts
T
jesse-ux cfb41daf3d
Independent Staging Quality Gate / validate (push) Failing after 6m28s
Independent Staging Quality Gate / publish (push) Skipped
feat(rectification): 出卡加精度门槛,补经历改成系统点名
宽度超过 10 分钟或头名并列时不再出交付卡,改为按大运边界逐条问、
用类型芯片和年/月选择器录入。跳过的线换问法再问一次;答「这类事
都没有过」的不再问。用户说「没有了」仍立刻给目前范围。Skill 10.0.27。

BUG-740~743
2026-09-16 18:35:27 +08:00

238 lines
9.8 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_FLOW_BANNED_PHRASES,
SPLIT_ENDPOINT_PHRASE,
anchoredFollowups,
collectionQuestionPool,
inviteDeclined,
moreCollectHint,
pendingTargetedYearDomain,
preciseGapNarration,
rangeNarrowHint,
remainingCandidatesLine,
remainingSplitLayers,
targetedCollectPool,
TARGETED_EXISTENCE_PROMPT,
} from "../src/lib/rectification-agentic/v9/collection-question-pool.ts";
import { USER_COLLECT_QUESTION, USER_COLLECT_QUESTION_RETRY } from "../src/lib/rectification-agentic/user-copy.ts";
import { isOccupationCollectFocus } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import { followupFromPoolItem, targetedCollectFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { stableFollowupQuestionId } from "../src/lib/rectification-agentic/v9/server-focus.ts";
const educationStart = {
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-09-01",
occurredTo: "2016-09-30",
eventKind: "education_start",
summary: "上大学",
} as const;
const educationEnd = {
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2020-06-01",
occurredTo: "2020-06-30",
eventKind: "education_completion",
summary: "毕业",
} as const;
const inviteDeclinedTopic = {
target_domain: "other",
status: "declined",
intent: "collect_method_evidence",
questionId: "collect:invite:more",
target_kind: "invite_more",
};
function assertNoBanned(text: string) {
for (const phrase of COLLECT_FLOW_BANNED_PHRASES) {
assert.equal(text.includes(phrase), false, phrase);
}
}
test("invite stays first while it is producing, then drops after a decline", () => {
const first = collectionQuestionPool([educationStart]);
assert.equal(first[0]?.kind, "invite");
assert.equal(first[0]?.key, "collect:invite:more");
assert.match(first[0]?.prompt ?? "", /^还有吗?/);
const still = collectionQuestionPool([educationStart, educationEnd]);
assert.equal(still[0]?.kind, "invite");
assert.equal(inviteDeclined([inviteDeclinedTopic]), true);
const afterDecline = collectionQuestionPool(
[educationStart, educationEnd],
[inviteDeclinedTopic],
);
assert.notEqual(afterDecline[0]?.kind, "invite");
assert.equal(afterDecline.some((item) => item.kind === "invite"), false);
});
test("education completion anchors to the graduation job question from the user year", () => {
const asked = new Set<string>();
const anchors = anchoredFollowups([educationStart, educationEnd], new Set(), asked);
assert.match(anchors[0]?.prompt ?? "", /2020 年毕业后第一份工作/);
assert.doesNotMatch(anchors.map((item) => item.prompt).join("\n"), /年前后/);
const declinedAnchor = collectionQuestionPool(
[educationStart, educationEnd],
[
inviteDeclinedTopic,
{
target_domain: "career",
status: "declined",
intent: "collect_method_evidence",
questionId: "collect:anchor:education_completion:2020",
target_kind: "anchor:education_completion:2020",
},
],
);
assert.equal(
declinedAnchor.some((item) => item.key === "collect:anchor:education_completion:2020"),
false,
);
});
test("precise gap names the recorded events and at least two uncovered kinds", () => {
const gap = preciseGapNarration([educationStart, educationEnd], [inviteDeclinedTopic]);
assert.match(gap, /现在记下的是/);
assert.match(gap, /2016 年 9 月上大学/);
assert.match(gap, /2020 年 6 月毕业/);
assert.match(gap, /都是上学的事/);
assert.match(gap, /就能开始筛/);
const examples = (gap.match(/第一份工作|搬到别的城市|谈恋爱|家里添丁|生病受伤/g) ?? []).length;
assert.ok(examples >= 2, gap);
assertNoBanned(gap);
});
test("more-collect hint after delivery never uses banned collect-flow phrases", () => {
const hint = moreCollectHint([educationStart, educationEnd], [inviteDeclinedTopic]);
assert.match(hint, /范围还能再收一截/);
assertNoBanned(hint);
});
test("generic occupation collect does not ask for a year", () => {
assert.match(USER_COLLECT_QUESTION.occupation, /你平时主要做什么工作/);
assert.doesNotMatch(USER_COLLECT_QUESTION.occupation, /哪年|哪一年|年份/);
assert.doesNotMatch(USER_COLLECT_QUESTION_RETRY.occupation, /哪年|哪一年|年份/);
});
test("graduation job follow-up is a career anchor, not occupation collect", () => {
const asked = new Set<string>();
const anchors = anchoredFollowups([educationStart, educationEnd], new Set(), asked);
const job = anchors.find((item) => item.key === "collect:anchor:education_completion:2020");
assert.ok(job);
assert.equal(job?.domain, "career");
assert.match(job?.prompt ?? "", /2020 年毕业后第一份工作/);
const followup = followupFromPoolItem(job!);
assert.equal(followup.domain, "career");
const questionId = stableFollowupQuestionId(followup);
assert.equal(questionId, "collect:anchor:education_completion:2020");
assert.equal(questionId.startsWith("collect:occupation:"), false);
assert.equal(isOccupationCollectFocus({
intent: followup.intent,
targetDomain: followup.domain,
targetKind: followup.kind_hint,
questionId,
}), false);
});
test("targeted collect names remaining split layers without inferred years", () => {
const career = {
status: "confirmed",
domain: "career",
datePrecision: "month",
occurredFrom: "2020-04-01",
occurredTo: null,
eventKind: "career_entry",
summary: "入职实习",
} as const;
const layers = remainingSplitLayers({
transitions: [
{ layer: "d9", at: "04:52" },
{ layer: "d10", at: "05:00" },
{ layer: "d4", at: "05:00" },
{ layer: "d12", at: "05:00" },
{ layer: "d2", at: "05:00" },
{ layer: "d11", at: "05:07" },
],
activeTimes: ["04:48", "04:53", "05:06", "05:07"],
});
const evidence = [educationStart, educationEnd, career];
const splitTimes = ["04:48", "05:07"] as const;
const pool = targetedCollectPool(layers, evidence, [], splitTimes, 4);
// 原值: pool.length === 1prompt 含「能把 04:48 和 05:07 分开」
// 新值: 剩余未覆盖线各一张存在卡;题干是「有没有」,剩余候选句走 remainingLine
// 原因: BUG-661 / BUG-662 逐条点选 + 不再用范围端点当候选
// 原值: "relationship,relocation,family,finance"
// 新值: 加上 health_pressure
// 原因: D5 remainingTargetedDomains 不再按分盘层剔除,七条线都要轮到
assert.equal(pool.map((item) => item.domain).join(","), "relationship,relocation,family,finance,health_pressure");
assert.equal(pool[0]?.kind, "targeted");
assert.equal(pool[0]?.key, "collect:targeted:relationship");
// 原值: "结过婚或订过婚吗?"
// 新值: 领域全称问法,哪一年都算
// 原因: D5 存在性问题必须问整个领域
assert.equal(pool[0]?.prompt, TARGETED_EXISTENCE_PROMPT.relationship);
assert.doesNotMatch(pool.map((item) => item.prompt).join("\n"), SPLIT_ENDPOINT_PHRASE);
assert.doesNotMatch(pool[0]?.prompt ?? "", /1997|2012|推算/);
assert.match(pool[0]?.remainingLine ?? "", /现在还剩 04:48–05:07 里 4 个候选,能把它们分开的是这几条线/);
assert.ok((pool[0]?.examples?.length ?? 0) >= 2);
const followup = targetedCollectFollowup(layers, evidence, [], splitTimes, 4);
assert.equal(followup?.collection_key, "collect:targeted:relationship");
assert.equal(followup?.choice_frame?.scoring, false);
assert.equal(followup?.choice_kind, "existence");
assert.match(followup?.spoken_prompt ?? "", /现在还剩 04:4805:07 里 4 个候选/);
assert.doesNotMatch(followup?.spoken_prompt ?? "", SPLIT_ENDPOINT_PHRASE);
assert.equal(stableFollowupQuestionId(followupFromPoolItem(pool[0]!)), "collect:targeted:relationship");
assert.match(rangeNarrowHint(layers, evidence, [], splitTimes, 4), /现在还剩 04:4805:07 里 4 个候选/);
assert.match(rangeNarrowHint(layers, evidence, [], splitTimes, 4), /还能再收窄:如果记得/);
const declinedRelationship = targetedCollectPool(layers, evidence, [{
questionId: "collect:targeted:relationship",
status: "declined",
target_kind: "targeted:relationship",
}], splitTimes, 4);
assert.ok(declinedRelationship.length >= 1);
assert.notEqual(declinedRelationship[0]?.domain, "relationship");
assert.equal(pendingTargetedYearDomain([{
questionId: "collect:targeted:relationship",
status: "resolved",
target_kind: "targeted:relationship",
}], evidence), "relationship");
});
test("remaining candidates line uses credible range when it differs from active minute span", () => {
const splitTimes = ["04:50", "05:06"] as const;
const credibleRange = ["04:48", "05:07"] as const;
const examples = ["收入明显变过"];
assert.match(
remainingCandidatesLine(splitTimes, 5, examples, credibleRange) ?? "",
/现在还剩 04:4805:07 里 5 个候选/,
);
assert.match(
remainingCandidatesLine(splitTimes, 5, examples, null) ?? "",
/现在还剩 04:5005:06 里 5 个候选/,
);
const layers = remainingSplitLayers({
transitions: [
{ layer: "d9", at: "04:52" },
{ layer: "d2", at: "05:00" },
],
activeTimes: ["04:50", "04:53", "04:59", "05:03", "05:06"],
});
const evidence = [educationStart, educationEnd, {
status: "confirmed",
domain: "career",
datePrecision: "month",
occurredFrom: "2020-04-01",
occurredTo: null,
eventKind: "career_entry",
summary: "入职实习",
}];
const followup = targetedCollectFollowup(layers, evidence, [], splitTimes, 5, credibleRange);
assert.match(followup?.spoken_prompt ?? "", /现在还剩 04:4805:07 里 5 个候选/);
assert.match(rangeNarrowHint(layers, evidence, [], splitTimes, 5, credibleRange), /现在还剩 04:4805:07 里 5 个候选/);
});