Files
Jyotisha/frontend/tests/rectification-collection-question-pool.test.ts
T
Jesse_ChenandCursor 530f260f76
Independent Staging Quality Gate / validate (push) Successful in 12m9s
Independent Staging Quality Gate / publish (push) Successful in 2m17s
fix(rectification): ask remaining split lines one card at a time (BUG-661~663)
Dated-pool empty now yields existence cards per remaining layer, leftover-candidate copy, and an optional D9/D10 tie-break on the range card. Skill 10.0.25.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 16:50:25 +08:00

197 lines
8.1 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,
remainingSplitLayers,
targetedCollectPool,
} 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 逐条点选 + 不再用范围端点当候选
assert.equal(pool.map((item) => item.domain).join(","), "relationship,relocation,family,finance");
assert.equal(pool[0]?.kind, "targeted");
assert.equal(pool[0]?.key, "collect:targeted:relationship");
assert.equal(pool[0]?.prompt, "结过婚或订过婚吗?");
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");
});