Files
Jyotisha/frontend/tests/rectification-targeted-collect-cards-20260913.test.ts
T
66f087b588 feat(rectification): submit varga-resolution implementation for review
Review-only snapshot for BUG-1115 through BUG-1117; not merge-ready. New opening and append-turn PostgreSQL permission failures remain blocked. Persisted joint replay has zero completed questions; segment ordering remains off by default. The existing offline replay JSON is retained stale and unchanged after a denied overwrite, including its CRLF line endings. Browser/provider validation and final serial gates remain pending. No deployment, role permission changes, or staging/main push.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-01 08:04:04 +08:00

254 lines
9.5 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 { applyProbeOutcome } from "../src/lib/rectification-agentic/core/apply-probe-outcome.ts";
import type { ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts";
import { RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
import {
SPLIT_ENDPOINT_PHRASE,
TARGETED_EXISTENCE_PROMPT,
pendingTargetedYearDomain,
targetedCollectExhausted,
targetedCollectPool,
} from "../src/lib/rectification-agentic/v9/collection-question-pool.ts";
import { rangeDeliveryForSnapshot } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
import {
buildMethodFollowupPlan,
targetedCollectFollowup,
tieBreakPersonalityAvailable,
tieBreakPersonalityFollowup,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
const TIMES = ["04:48", "04:53", "05:06", "05:07"] as const;
const LAYERS = ["d9", "d10", "d4", "d12", "d2", "d11"];
const EVIDENCE = [
{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-09-01",
occurredTo: "2016-09-30",
eventKind: "education_start",
summary: "上大学",
},
{
status: "confirmed",
domain: "career",
datePrecision: "month",
occurredFrom: "2020-04-01",
occurredTo: null,
eventKind: "career_entry",
summary: "入职实习",
},
] as const;
function declined(domain: string) {
return {
target_domain: domain,
status: "declined" as const,
intent: "collect_method_evidence",
questionId: `collect:targeted:${domain}`,
target_kind: `targeted:${domain}`,
};
}
test("A resolves existence then asks the year; B/C close only that line", () => {
const first = targetedCollectFollowup(LAYERS, EVIDENCE, [], ["04:48", "05:07"], 4);
assert.equal(first?.collection_key, "collect:targeted:relationship");
assert.equal(first?.choice_frame?.scoring, false);
// 原值: "结过婚或订过婚吗?"
// 新值: TARGETED_EXISTENCE_PROMPT.relationship(问整个领域、任何时间)
// 原因: D5/T4b——窄题干答「没有」不得当成整条线没有(BUG-741 / BUG-751)
assert.equal(first?.choice_frame?.prompt, TARGETED_EXISTENCE_PROMPT.relationship);
assert.match(first?.spoken_prompt ?? "", /现在还剩 04:48–05:07 里 4 个候选/);
assert.doesNotMatch(first?.spoken_prompt ?? "", SPLIT_ENDPOINT_PHRASE);
const afterYes = targetedCollectFollowup(
LAYERS,
EVIDENCE,
[{ ...declined("relationship"), status: "resolved", questionId: "collect:targeted:relationship" }],
["04:48", "05:07"],
4,
);
assert.equal(pendingTargetedYearDomain([{
...declined("relationship"),
status: "resolved",
}], EVIDENCE), "relationship");
assert.equal(afterYes?.collection_key, "collect:targeted:relationship:year");
assert.equal(afterYes?.choice_frame, null);
assert.equal(afterYes?.spoken_prompt, "大概哪年几月?");
const afterNo = targetedCollectPool(
LAYERS,
EVIDENCE,
[declined("relationship")],
["04:48", "05:07"],
4,
);
assert.equal(afterNo[0]?.domain, "relocation");
// 原值: 拒答 4 条线即耗尽
// 新值: 七条线都要拒答才耗尽
// 原因: D5——remainingTargetedDomains 不再按分盘层剔除,七个 CollectKind
// 按 COLLECT_KIND_ORDER 全部轮到(BUG-751)
assert.equal(targetedCollectExhausted(LAYERS, EVIDENCE, [
declined("relationship"),
declined("relocation"),
declined("family"),
declined("finance"),
], ["04:48", "05:07"], 4), false);
assert.equal(targetedCollectExhausted(LAYERS, EVIDENCE, [
declined("education"),
declined("career"),
declined("relocation"),
declined("relationship"),
declined("family"),
declined("finance"),
declined("health_pressure"),
], ["04:48", "05:07"], 4), true);
});
test("personality D9/D10 stay out of the scoring pool until the range-card opt-in", () => {
const ledger = [
...EVIDENCE,
{
status: "confirmed",
domain: "relationship",
datePrecision: "month",
occurredFrom: "2024-05-01",
occurredTo: null,
eventKind: "relationship_start",
summary: "确定关系",
},
{
status: "confirmed",
domain: "family",
datePrecision: "month",
occurredFrom: "2016-05-01",
occurredTo: null,
eventKind: "family_event",
summary: "家里添丁",
},
];
const d9 = {
probeId: "contrast:varga.d9.巨蟹座/狮子座",
candidateSetVersion: "04:48-05:07",
question: "亲密关系里更接近下面哪一种相处方式?",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["04:53"], conflictsCandidateIds: ["05:06"] },
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:06"], conflictsCandidateIds: ["04:53"] },
],
candidateSplitHash: "varga.d9.巨蟹座/狮子座",
informationGain: 1.4,
sourceFeatures: [{ technique: "D9", calculationResultId: null }],
domain: "relationship",
year: null,
semanticKey: "varga.d9.巨蟹座/狮子座",
choiceKind: "varga_style" as const,
styleOptions: [
{ label: "相处里更在意照顾对方的感受", answerClass: "yes" as const, sign: "巨蟹座" },
{ label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes" as const, sign: "狮子座" },
],
};
const planInput = {
evidence: ledger,
declinedTopics: ["relocation", "finance"].map(declined),
sessionOutcome: "discriminate_candidates" as const,
candidatesSeparated: false,
remainingLayers: LAYERS,
remainingSplitTimes: ["04:48", "05:07"] as const,
remainingCandidateCount: 4,
topCandidateTimes: [...TIMES],
contrastPacket: {
candidateSetVersion: "04:48-05:07",
vargaDifferences: [],
probes: [d9],
},
};
const auto = buildMethodFollowupPlan(planInput);
assert.notEqual(auto.next_followup?.choice_kind, "varga_style");
const opted = tieBreakPersonalityFollowup(planInput);
assert.equal(opted?.choice_kind, "varga_style");
assert.match(opted?.semantic_key ?? "", /varga\.d9/);
assert.equal(opted?.tie_break_round, true);
const d10: ConflictProbe = {
id: "probe:varga.d10.狮子座/处女座",
semantic_key: "varga.d10.狮子座/处女座",
candidate_split_hash: "04:48-05:07:varga.d10",
domain: "career",
year: 0,
question: "平时做事,你更接近下面哪一种?",
candidate_ids: [...TIMES],
expected_outcomes: [
{ answer_class: "yes", supports: ["04:53"], conflicts: ["05:06"] },
{ answer_class: "weak_yes", supports: ["05:06"], conflicts: ["04:53"] },
{ answer_class: "no", supports: [], conflicts: [] },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: 0.9,
source: "varga_contrast",
choice_kind: "varga_style",
style_options: [
{ label: "相处里更在意照顾对方的感受", answer_class: "yes", sign: "巨蟹座" },
{ label: "习惯带头,也不排斥站到台前", answer_class: "weak_yes", sign: "狮子座" },
],
};
const scored = applyProbeOutcome(
{ "04:48": 10, "04:53": 12, "05:06": 11, "05:07": 10 },
d10,
"yes",
);
assert.equal(scored.kind, "tie_break");
assert.equal(scored.deltas["04:53"], 1);
assert.equal(scored.eliminated_ids.length, 0);
// Fictional legacy card control: declare the original window explicitly;
// credibleRange alone is not proof of the full score/scan window.
const delivery = rangeDeliveryForSnapshot({
candidateRange: { start_time: "04:48", end_time: "05:07" },
candidates: TIMES.map((time, index) => ({
candidateId: `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`,
time,
})),
representativeTime: "04:53",
credibleRange: ["04:48", "05:07"],
evidence: ledger,
declinedTopics: ["relocation", "finance"].map(declined),
window_scan: {
scanned: true,
d9_lagna_count: 2,
d10_lagna_count: 2,
d9_candidates_differ: true,
d10_candidates_differ: true,
d4_candidates_differ: true,
d12_candidates_differ: true,
d2_candidates_differ: true,
d11_candidates_differ: true,
},
tieBreakAvailable: tieBreakPersonalityAvailable(planInput),
});
// 原值: narrow_hint 含「不限领域」的自由文本邀请
// 新值: 引导句「现在还剩 HH:MM–HH:MM 里 N 个候选…」,且禁自由文本邀请
// 原因: D3——门槛未达时系统点名逐题问,不再邀请随便打字(BUG-740 / BUG-751)
assert.doesNotMatch(delivery.narrow_hint ?? "", /不限领域/);
assert.doesNotMatch(delivery.narrow_hint ?? "", /问完了/);
assert.match(delivery.narrow_hint ?? "", /现在还剩 .+ 里 \d+ 个候选/);
assert.doesNotMatch(delivery.narrow_hint ?? "", SPLIT_ENDPOINT_PHRASE);
assert.equal(delivery.tie_break_available, true);
const incompleteScan = rangeDeliveryForSnapshot({
candidates: TIMES.map((time, index) => ({
candidateId: `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`,
time,
})),
representativeTime: "04:53",
credibleRange: ["04:48", "05:07"],
window_scan: {
d9_candidates_differ: true,
d10_candidates_differ: false,
},
});
// 原值: true(window_scan.d9_candidates_differ 为真就显示入口)
// 新值: false(没有未问的 D9/D10 varga_style 时不显示)
// 原因: BUG-666 入口与 tie-break 路由共用「还有未答参考题」判定
assert.equal(incompleteScan.tie_break_available, false);
assert.equal(RECTIFICATION_USER_COPY.rangeDeliveryTieBreakEntry, "再答两道参考题微调排序");
});