Files
Jyotisha/frontend/tests/rectification-guided-collect-20260916.test.ts
T
Jesse_ChenandClaude Fable 5.1 dc732825d8 fix(rectification): 有题就接着问,题问完才出卡;引导窗口不再硬贴领域
出卡时机只看题源有没有空:撤回「门槛达标就短路采集线」的写法,同时
按 D2 保住「题源全空就按现行规则出卡」——门槛只在还有题可问时挡住
出卡,precision_gate_met 改成只上报(新挂在决策与公开投影上),不再
单独决定时机。引导窗口题在无领域轨道上改问开放题,一个时间窗只问一
次;录入卡提交的是「YYYY 年 M 月,<领域>方面有一件事」,不再是题干
的三选一列表。记忆化 golden 只补一个新键并冻结墙钟。离线回放改成注
入真值方向的边界事件,另跑一组反方向对照。Skill 10.0.28。

BUG-747~752

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-16 12:16:55 +00:00

282 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,
guidedCollectExhausted,
guidedRetryPool,
guidedWindowPool,
guidedWindowPrompt,
parseYearEntryQuestionId,
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";
import {
eventDateYearRange,
formatEventDateEntryMessage,
resolveEntryDomain,
} from "../src/components/event-date-entry-card.tsx";
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 year-entry card, 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",
},
];
const year = parseYearEntryQuestionId("collect:guided:window:2018:3:6:career:year");
assert.equal(year?.domain, "career");
assert.equal(year?.year, 2018);
const pending = targetedCollectFollowup([], evidence, afterYes, null, 5, ["04:51", "05:11"], WINDOWS);
assert.equal(pending?.collection_key?.endsWith(":year"), true);
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("date entry submit walks the dated evidence path as year-month text", () => {
// 原值: "2018 年 3 月,开始认真关系、分手或结婚"
// 新值: "2018 年 3 月,感情方面有一件事"
// 原因: F6/BUG-750——提交文本不得是选项列表,账本 summary 不得含「或」
assert.equal(
formatEventDateEntryMessage({ domain: "relationship", year: 2018, month: 3, day: null }),
"2018 年 3 月,感情方面有一件事",
);
assert.doesNotMatch(
formatEventDateEntryMessage({ domain: "relationship", year: 2018, month: 3, day: 12 }),
/或/,
);
assert.equal(
formatEventDateEntryMessage({ domain: "family", year: 2019, month: 1, day: 5 }),
"2019 年 1 月 5 日,家人方面有一件事",
);
});
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("the entry card opens on the first still-open domain for an open window", () => {
assert.equal(resolveEntryDomain(GUIDED_ANY_DOMAIN, ["finance", "family"]), "finance");
assert.equal(resolveEntryDomain("relationship", ["finance", "family"]), "relationship");
assert.equal(
firstOpenCollectKind(
[{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2014-09-01", occurredTo: "2014-09-01" }],
[{ target_domain: "career", status: "declined" }],
),
"relocation",
);
});
test("date picker year range is birth year through this year", () => {
assert.deepEqual(eventDateYearRange(1961, 2026), { minYear: 1961, maxYear: 2026 });
assert.deepEqual(eventDateYearRange(null, 2026), { minYear: 1946, maxYear: 2026 });
});
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);
});