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>
197 lines
8.1 KiB
TypeScript
197 lines
8.1 KiB
TypeScript
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 === 1,prompt 含「能把 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:48–05: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:48–05: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");
|
||
});
|