Files
Jyotisha/frontend/tests/rectification-collection-question-pool.test.ts
T
jesse-ux 2dbcf2661b
Independent Staging Quality Gate / validate (push) Failing after 11m54s
Independent Staging Quality Gate / publish (push) Skipped
fix(rectification): hang spoken year follow-ups and drop engine representative from model tables (BUG-678/676)
Spoken collect without askedTurnId now hangs on the last assistant message, remaining-candidate copy uses credible_range, and engine representativeTime no longer keeps eliminated minutes in model house tables.
2026-09-14 12:12:27 +08:00

231 lines
9.4 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,
} 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");
});
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 个候选/);
});