Staging gate 2558 failed because leftover tests still expected D9/D10 or leftover collect after the pool emptied. Discriminate sessions still deliver; dated precision cards still ask. Co-authored-by: Cursor <cursoragent@cursor.com>
596 lines
22 KiB
TypeScript
596 lines
22 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
|
||
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||
import {
|
||
inspectDiscriminatorProbes,
|
||
withCompletedContrastOptions,
|
||
type CandidateDiscriminatorProbe,
|
||
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
||
import type { ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts";
|
||
import {
|
||
contrastPacketFromDossier,
|
||
decideFromDossier,
|
||
followupAsksRenderableDiscriminator,
|
||
rectificationFollowupCatalog,
|
||
type DecisionDossier,
|
||
} from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||
import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||
|
||
const CAREER_2020_04: ConflictProbe = {
|
||
id: "probe:career.2020.04.dasha_boundary",
|
||
semantic_key: "career.2020.04.dasha_boundary",
|
||
candidate_split_hash: "career.2020.04",
|
||
domain: "career",
|
||
year: 2020,
|
||
question: "2020 年 4 月前后有没有入职或换工作?",
|
||
candidate_ids: ["05:00", "05:10"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||
],
|
||
information_gain: 0.9,
|
||
source: "dasha_boundary",
|
||
choice_kind: "existence",
|
||
};
|
||
|
||
const REL_2024_05: ConflictProbe = {
|
||
id: "probe:relationship.2024.05.dasha_boundary",
|
||
semantic_key: "relationship.2024.05.dasha_boundary",
|
||
candidate_split_hash: "relationship.2024.05",
|
||
domain: "relationship",
|
||
year: 2024,
|
||
question: "2024 年 5 月前后有没有开始一段认真关系?",
|
||
candidate_ids: ["05:00", "05:10"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||
],
|
||
information_gain: 0.85,
|
||
source: "dasha_boundary",
|
||
choice_kind: "existence",
|
||
};
|
||
|
||
const D24: ConflictProbe = {
|
||
id: "contrast:varga.d24.05:00/05:10",
|
||
semantic_key: "varga.d24.05:00/05:10",
|
||
candidate_split_hash: "varga.d24.05:00/05:10",
|
||
domain: "education",
|
||
year: 0,
|
||
question: "有没有学业或考试发挥明显失常、压力特别大的时候?",
|
||
candidate_ids: ["05:00", "05:10"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||
],
|
||
information_gain: 2.5,
|
||
source: "varga_contrast",
|
||
choice_kind: "event_quality",
|
||
};
|
||
|
||
const D12: ConflictProbe = {
|
||
id: "contrast:varga.d12.05:00/05:10",
|
||
semantic_key: "varga.d12.05:00/05:10",
|
||
candidate_split_hash: "varga.d12.05:00/05:10",
|
||
domain: "family",
|
||
year: 0,
|
||
question: "家里有没有结婚、添丁或住院这类事?",
|
||
candidate_ids: ["05:00", "05:10"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||
],
|
||
information_gain: 0.8,
|
||
source: "varga_contrast",
|
||
choice_kind: "existence",
|
||
};
|
||
|
||
const D9_STYLE_NO_OPTIONS: ConflictProbe = {
|
||
id: "contrast:varga.d9.巨蟹座/狮子座",
|
||
semantic_key: "varga.d9.巨蟹座/狮子座",
|
||
candidate_split_hash: "varga.d9.巨蟹座/狮子座",
|
||
domain: "relationship",
|
||
year: 0,
|
||
question: "亲密关系里更接近下面哪一种相处方式?",
|
||
candidate_ids: ["05:00", "05:10"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||
{ answer_class: "weak_yes", supports: ["05:10"], conflicts: ["05:00"] },
|
||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||
],
|
||
information_gain: 1.4,
|
||
source: "varga_contrast",
|
||
choice_kind: "varga_style",
|
||
};
|
||
|
||
const D10_STYLE_NO_OPTIONS: ConflictProbe = {
|
||
id: "contrast:varga.d10.天秤座/天蝎座",
|
||
semantic_key: "varga.d10.天秤座/天蝎座",
|
||
candidate_split_hash: "varga.d10.天秤座/天蝎座",
|
||
domain: "career",
|
||
year: 0,
|
||
question: "平时做事更接近下面哪一种职责风格?",
|
||
candidate_ids: ["05:00", "05:10"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||
{ answer_class: "weak_yes", supports: ["05:10"], conflicts: ["05:00"] },
|
||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||
],
|
||
information_gain: 1.2,
|
||
source: "varga_contrast",
|
||
choice_kind: "varga_style",
|
||
};
|
||
|
||
const FAMILY_2021_COLLECT = {
|
||
year: 2021,
|
||
year_label: "2021 年前后",
|
||
domain: "family" as const,
|
||
event_family: "家人结婚、添丁或住院",
|
||
source: "age_band" as const,
|
||
tracks: ["vimshottari", "narayana"] as const,
|
||
tracks_agree: false,
|
||
unique_minute_claim: false as const,
|
||
user_meaning: "时间范围锁定 2021 年前后;领域锁定 family。",
|
||
role: "collect" as const,
|
||
phase: "evidence_collection" as const,
|
||
information_gain: 0,
|
||
semantic_key: "family.2021",
|
||
candidate_split_hash: "family:2021",
|
||
candidate_ids: [] as const,
|
||
expected_outcomes: [] as const,
|
||
choice_kind: "existence" as const,
|
||
};
|
||
|
||
const EVIDENCE = [
|
||
{
|
||
id: "e-career-entry",
|
||
status: "confirmed",
|
||
domain: "career",
|
||
datePrecision: "month",
|
||
occurredFrom: "2020-04-01",
|
||
occurredTo: null,
|
||
eventKind: "career_entry",
|
||
},
|
||
{
|
||
id: "e-career-exit",
|
||
status: "confirmed",
|
||
domain: "career",
|
||
datePrecision: "month",
|
||
occurredFrom: "2020-10-01",
|
||
occurredTo: null,
|
||
eventKind: "career_exit",
|
||
},
|
||
{
|
||
id: "e-rel-start",
|
||
status: "confirmed",
|
||
domain: "relationship",
|
||
datePrecision: "month",
|
||
occurredFrom: "2024-05-01",
|
||
occurredTo: null,
|
||
eventKind: "relationship_start",
|
||
},
|
||
{
|
||
id: "e-rel-end",
|
||
status: "confirmed",
|
||
domain: "relationship",
|
||
datePrecision: "day",
|
||
occurredFrom: "2024-08-08",
|
||
occurredTo: null,
|
||
eventKind: "relationship_end",
|
||
},
|
||
] as const;
|
||
|
||
function contrastFromConflict(probe: ConflictProbe): CandidateDiscriminatorProbe {
|
||
return {
|
||
probeId: probe.id,
|
||
candidateSetVersion: "05:00-05:10",
|
||
question: probe.question,
|
||
expectedOutcomes: probe.expected_outcomes.map((row) => ({
|
||
outcomeId: row.answer_class,
|
||
supportsCandidateIds: row.supports,
|
||
conflictsCandidateIds: row.conflicts,
|
||
})),
|
||
candidateSplitHash: probe.candidate_split_hash,
|
||
informationGain: probe.information_gain,
|
||
sourceFeatures: [{ technique: probe.domain, calculationResultId: null }],
|
||
domain: probe.domain,
|
||
year: probe.year > 0 ? probe.year : null,
|
||
semanticKey: probe.semantic_key,
|
||
choiceKind: probe.choice_kind,
|
||
};
|
||
}
|
||
|
||
function deadlockDossier(): { dossier: DecisionDossier; state: ReturnType<typeof buildInferenceState> } {
|
||
const state = buildInferenceState({
|
||
range_start: "05:00",
|
||
range_end: "05:10",
|
||
candidates: [
|
||
{ id: "05:00", time: "05:00", relative_support: 18 },
|
||
{ id: "05:10", time: "05:10", relative_support: 16 },
|
||
],
|
||
events: [
|
||
{ id: "e-career-entry", domain: "career", year: 2020, precision: "month" },
|
||
{ id: "e-career-exit", domain: "career", year: 2020, precision: "month" },
|
||
{ id: "e-rel-start", domain: "relationship", year: 2024, precision: "month" },
|
||
{ id: "e-rel-end", domain: "relationship", year: 2024, precision: "day" },
|
||
],
|
||
probes: [CAREER_2020_04, REL_2024_05, D24, D12, D9_STYLE_NO_OPTIONS, D10_STYLE_NO_OPTIONS],
|
||
answered_probes: [
|
||
{
|
||
probe_id: CAREER_2020_04.id,
|
||
semantic_key: CAREER_2020_04.semantic_key,
|
||
candidate_split_hash: CAREER_2020_04.candidate_split_hash,
|
||
answer_class: "yes",
|
||
classified_from: "choice",
|
||
},
|
||
{
|
||
probe_id: REL_2024_05.id,
|
||
semantic_key: REL_2024_05.semantic_key,
|
||
candidate_split_hash: REL_2024_05.candidate_split_hash,
|
||
answer_class: "yes",
|
||
classified_from: "choice",
|
||
},
|
||
],
|
||
});
|
||
const dossier: DecisionDossier = {
|
||
evidence: EVIDENCE,
|
||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||
latestResult: {
|
||
resultId: "55555555-5555-4555-8555-555555555555",
|
||
candidates: state.candidates.map((item) => ({
|
||
candidateId: item.id,
|
||
time: item.time,
|
||
rank: item.rank,
|
||
relativeSupport: Math.round(item.posterior_score),
|
||
})),
|
||
representativeTime: state.representative_time,
|
||
evidenceLedgerFingerprint: evidenceLedgerFingerprint(EVIDENCE as never),
|
||
decisionReceipt: {
|
||
inference_state: state,
|
||
evidence_collection_probes: [FAMILY_2021_COLLECT],
|
||
},
|
||
},
|
||
case: { acceptedTime: null },
|
||
};
|
||
return { dossier, state };
|
||
}
|
||
|
||
test("deadlock case: dated pool empty delivers instead of a D9/D10 style card", () => {
|
||
const { dossier } = deadlockDossier();
|
||
const inspected = inspectDiscriminatorProbes(contrastPacketFromDossier(dossier));
|
||
assert.equal(inspected.selected?.semanticKey, D9_STYLE_NO_OPTIONS.semantic_key);
|
||
|
||
const d9 = withCompletedContrastOptions(contrastFromConflict(D9_STYLE_NO_OPTIONS));
|
||
const d10 = withCompletedContrastOptions(contrastFromConflict(D10_STYLE_NO_OPTIONS));
|
||
assert.equal(d9.ok, true);
|
||
assert.equal(d10.ok, true);
|
||
if (d9.ok) assert.equal(d9.probe.choiceKind, "varga_style");
|
||
if (d10.ok) assert.equal(d10.probe.choiceKind, "varga_style");
|
||
|
||
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence: dossier.evidence,
|
||
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
|
||
sessionOutcome: "discriminate_candidates",
|
||
...catalog,
|
||
candidatesSeparated: false,
|
||
});
|
||
assert.equal(followupAsksRenderableDiscriminator(plan.next_followup), false);
|
||
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
|
||
assert.equal(
|
||
plan.dropped_probes.some((item) => item.semantic_key.startsWith("varga.d9.") || item.semantic_key.startsWith("varga.d10.")),
|
||
true,
|
||
JSON.stringify(plan.dropped_probes),
|
||
);
|
||
|
||
const decision = decideFromDossier(dossier);
|
||
// 原值: ask_candidate_discriminator,出 D9/D10 性格卡
|
||
// 新值: 带年月池空即交付,性格题 yearless_deferred
|
||
// 原因: BUG-651 性格题不得挡在结果前面
|
||
assert.ok(
|
||
decision.nextAction === "offer_provisional_range"
|
||
|| decision.nextAction === "ready_to_adopt"
|
||
|| decision.nextAction === "complete_with_range"
|
||
|| decision.nextAction === "ask_fact_collection",
|
||
decision.nextAction,
|
||
);
|
||
assert.notEqual(decision.sessionOutcome, "discriminate_candidates");
|
||
assert.equal(decision.canConfirmExactMinute, false);
|
||
});
|
||
|
||
test("varga_style probes missing style_options complete the same way in inspect and followup ranking", () => {
|
||
const probe = contrastFromConflict(D9_STYLE_NO_OPTIONS);
|
||
const inspected = inspectDiscriminatorProbes({
|
||
candidateSetVersion: "05:00-05:10",
|
||
vargaDifferences: [],
|
||
probes: [probe],
|
||
});
|
||
assert.ok(inspected.selected);
|
||
assert.equal(inspected.selected?.choiceKind, "varga_style");
|
||
assert.equal(inspected.dropped.length, 0);
|
||
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence: EVIDENCE,
|
||
sessionOutcome: "discriminate_candidates",
|
||
candidatesSeparated: false,
|
||
contrastPacket: {
|
||
candidateSetVersion: "05:00-05:10",
|
||
vargaDifferences: [],
|
||
probes: [probe],
|
||
},
|
||
});
|
||
assert.notEqual(plan.next_followup?.semantic_key, probe.semanticKey);
|
||
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
|
||
// 原值: 补全 style_options 后仍把 D9 当下一道区分题
|
||
// 新值: D9 记 yearless_deferred,不再占用 discriminator
|
||
// 原因: BUG-651
|
||
assert.equal(
|
||
plan.dropped_probes.some((item) => (
|
||
item.semantic_key === probe.semanticKey && item.reason === "yearless_deferred"
|
||
)),
|
||
true,
|
||
JSON.stringify(plan.dropped_probes),
|
||
);
|
||
});
|
||
|
||
test("method coverage incomplete still asks a renderable D9 style card", () => {
|
||
const { dossier } = deadlockDossier();
|
||
const decision = decideFromDossier(dossier);
|
||
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence: dossier.evidence,
|
||
declinedTopics: [],
|
||
sessionOutcome: "discriminate_candidates",
|
||
...catalog,
|
||
candidatesSeparated: false,
|
||
});
|
||
assert.equal(plan.methods.find((item) => item.method_id === "relatives")?.status, "uncovered");
|
||
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
|
||
// 原值: 方法覆盖未齐仍问 D9 性格卡
|
||
// 新值: 训练门开、带年月池空即交付
|
||
// 原因: BUG-651 性格题不得挡在结果前面
|
||
assert.ok(
|
||
decision.nextAction === "offer_provisional_range"
|
||
|| decision.nextAction === "ready_to_adopt"
|
||
|| decision.nextAction === "complete_with_range"
|
||
|| decision.nextAction === "ask_fact_collection",
|
||
decision.nextAction,
|
||
);
|
||
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
|
||
assert.equal(decision.probe, null);
|
||
});
|
||
|
||
test("clock-key varga_style without signs fail-closes to dated family collect instead of a dead-end discriminator", () => {
|
||
const d9Clock: ConflictProbe = {
|
||
...D9_STYLE_NO_OPTIONS,
|
||
id: "contrast:varga.d9.05:00/05:10",
|
||
semantic_key: "varga.d9.05:00/05:10",
|
||
candidate_split_hash: "varga.d9.05:00/05:10",
|
||
};
|
||
const d10Clock: ConflictProbe = {
|
||
...D10_STYLE_NO_OPTIONS,
|
||
id: "contrast:varga.d10.05:00/05:10",
|
||
semantic_key: "varga.d10.05:00/05:10",
|
||
candidate_split_hash: "varga.d10.05:00/05:10",
|
||
};
|
||
const { dossier } = deadlockDossier();
|
||
const state = buildInferenceState({
|
||
range_start: "05:00",
|
||
range_end: "05:10",
|
||
candidates: [
|
||
{ id: "05:00", time: "05:00", relative_support: 18 },
|
||
{ id: "05:10", time: "05:10", relative_support: 16 },
|
||
],
|
||
events: [
|
||
{ id: "e-career-entry", domain: "career", year: 2020, precision: "month" },
|
||
{ id: "e-career-exit", domain: "career", year: 2020, precision: "month" },
|
||
{ id: "e-rel-start", domain: "relationship", year: 2024, precision: "month" },
|
||
{ id: "e-rel-end", domain: "relationship", year: 2024, precision: "day" },
|
||
],
|
||
probes: [CAREER_2020_04, REL_2024_05, D24, D12, d9Clock, d10Clock],
|
||
answered_probes: [
|
||
{
|
||
probe_id: CAREER_2020_04.id,
|
||
semantic_key: CAREER_2020_04.semantic_key,
|
||
candidate_split_hash: CAREER_2020_04.candidate_split_hash,
|
||
answer_class: "yes",
|
||
classified_from: "choice",
|
||
},
|
||
{
|
||
probe_id: REL_2024_05.id,
|
||
semantic_key: REL_2024_05.semantic_key,
|
||
candidate_split_hash: REL_2024_05.candidate_split_hash,
|
||
answer_class: "yes",
|
||
classified_from: "choice",
|
||
},
|
||
],
|
||
});
|
||
const clockDossier: DecisionDossier = {
|
||
...dossier,
|
||
latestResult: {
|
||
...dossier.latestResult!,
|
||
decisionReceipt: {
|
||
inference_state: state,
|
||
evidence_collection_probes: [FAMILY_2021_COLLECT],
|
||
},
|
||
},
|
||
};
|
||
const inspected = inspectDiscriminatorProbes(contrastPacketFromDossier(clockDossier));
|
||
assert.equal(inspected.selected, null);
|
||
assert.equal(inspected.dropped.some((item) => (
|
||
item.semantic_key === d9Clock.semantic_key && item.reason === "yearless_ungrounded_contrast"
|
||
)), true);
|
||
const d9 = withCompletedContrastOptions(contrastFromConflict(d9Clock));
|
||
assert.equal(d9.ok, true);
|
||
if (d9.ok) assert.equal(d9.probe.choiceKind, "existence");
|
||
|
||
const catalog = rectificationFollowupCatalog(clockDossier.latestResult, clockDossier.evidence);
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence: clockDossier.evidence,
|
||
declinedTopics: [],
|
||
sessionOutcome: "discriminate_candidates",
|
||
...catalog,
|
||
candidatesSeparated: false,
|
||
});
|
||
assert.equal(followupAsksRenderableDiscriminator(plan.next_followup), false);
|
||
// 原值: 落到家人采集 + probe_year 2021
|
||
// 新值: 训练门开后不落领域轮转;年龄段年份不进题干
|
||
// 原因: 撤回 BUG-642(BUG-648)
|
||
assert.notEqual(plan.next_followup?.domain, "family");
|
||
assert.notEqual(plan.next_followup?.probe_year, 2021);
|
||
assert.notEqual(plan.next_followup?.semantic_key, d9Clock.semantic_key);
|
||
assert.notEqual(plan.next_followup?.semantic_key, d10Clock.semantic_key);
|
||
|
||
const decision = decideFromDossier(clockDossier);
|
||
assert.notEqual(decision.nextAction, "ask_candidate_discriminator");
|
||
assert.equal(decision.probe, null);
|
||
assert.equal(decision.canConfirmExactMinute, false);
|
||
});
|
||
|
||
test("unrenderable contrast probes are recorded in dropped_probes instead of being dropped silently", () => {
|
||
const unrenderable = contrastFromConflict({
|
||
...D9_STYLE_NO_OPTIONS,
|
||
information_gain: 0,
|
||
semantic_key: "varga.d9.dropped",
|
||
candidate_split_hash: "varga.d9.dropped",
|
||
id: "contrast:varga.d9.dropped",
|
||
});
|
||
const inspected = inspectDiscriminatorProbes({
|
||
candidateSetVersion: "05:00-05:10",
|
||
vargaDifferences: [],
|
||
probes: [unrenderable],
|
||
});
|
||
assert.equal(inspected.selected, null);
|
||
assert.equal(inspected.dropped.some((item) => item.semantic_key === unrenderable.semanticKey), true);
|
||
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence: EVIDENCE,
|
||
sessionOutcome: "discriminate_candidates",
|
||
candidatesSeparated: false,
|
||
contrastPacket: {
|
||
candidateSetVersion: "05:00-05:10",
|
||
vargaDifferences: [],
|
||
probes: [unrenderable],
|
||
},
|
||
});
|
||
assert.equal(
|
||
plan.dropped_probes.some((item) => item.semantic_key === unrenderable.semanticKey),
|
||
true,
|
||
JSON.stringify(plan.dropped_probes),
|
||
);
|
||
});
|
||
|
||
test("agent message path no longer returns the discriminator dead-end copy", () => {
|
||
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
|
||
const fastPath = route.slice(
|
||
route.indexOf('if (action === "message")'),
|
||
route.indexOf("const requestTime"),
|
||
);
|
||
assert.doesNotMatch(fastPath, /目前没有可继续区分/);
|
||
assert.match(route, /nonConvergingRangeNarration/);
|
||
assert.match(fastPath, /persistNextInterviewIfIdle/);
|
||
assert.match(fastPath, /!plan\.next_followup/);
|
||
assert.ok(fastPath.includes("ask_candidate_discriminator"));
|
||
assert.match(fastPath, /isRenderableChoiceOpenQuestion\(open\)/);
|
||
assert.ok(route.indexOf("classifyRectificationTurnIntent") < route.indexOf("runV9AgentTurn({"));
|
||
});
|
||
|
||
test("decision.probe matches the followup the plan will actually ask", () => {
|
||
const { dossier } = deadlockDossier();
|
||
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence: dossier.evidence,
|
||
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
|
||
sessionOutcome: "discriminate_candidates",
|
||
...catalog,
|
||
candidatesSeparated: false,
|
||
});
|
||
const decision = decideFromDossier(dossier);
|
||
// 原值: 下一问是 D9/D10,decision.probe 对得上
|
||
// 新值: 性格题 deferred,decision.probe 与 next 都不是性格卡
|
||
// 原因: BUG-651
|
||
assert.notEqual(decision.nextAction, "ask_candidate_discriminator");
|
||
assert.equal(decision.probe, null);
|
||
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
|
||
assert.notEqual(plan.next_followup?.semantic_key, D24.semantic_key);
|
||
});
|
||
|
||
test("birthDate gating matches the real followup plan on an under-age event probe", () => {
|
||
const { dossier: base } = deadlockDossier();
|
||
const childhood = {
|
||
year: 2015,
|
||
year_label: "2015 年前后",
|
||
domain: "career" as const,
|
||
event_family: "入职或职责变化",
|
||
source: "dasha_activation" as const,
|
||
tracks: ["vimshottari", "narayana"] as const,
|
||
tracks_agree: true,
|
||
unique_minute_claim: false as const,
|
||
user_meaning: "2015 年前后有没有入职或职责变化?",
|
||
role: "distinguish" as const,
|
||
phase: "candidate_discriminator" as const,
|
||
information_gain: 3.2,
|
||
semantic_key: "career.2015.dasha_activation",
|
||
candidate_split_hash: "career.2015",
|
||
candidate_ids: ["05:00", "05:10"] as const,
|
||
expected_outcomes: [
|
||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||
],
|
||
style_options: [
|
||
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||
{ label: "明确没有发生", answer_class: "no" },
|
||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||
],
|
||
choice_kind: "existence" as const,
|
||
};
|
||
const dossier: DecisionDossier = {
|
||
...base,
|
||
latestResult: base.latestResult
|
||
? {
|
||
...base.latestResult,
|
||
decisionReceipt: {
|
||
...(base.latestResult.decisionReceipt ?? {}),
|
||
discriminating_event_probes: [childhood],
|
||
},
|
||
}
|
||
: null,
|
||
};
|
||
const birthDate = "2010-01-15";
|
||
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
|
||
const planOpen = buildMethodFollowupPlan({
|
||
evidence: dossier.evidence,
|
||
declinedTopics: [],
|
||
sessionOutcome: "discriminate_candidates",
|
||
...catalog,
|
||
candidatesSeparated: false,
|
||
});
|
||
const planGated = buildMethodFollowupPlan({
|
||
evidence: dossier.evidence,
|
||
declinedTopics: [],
|
||
sessionOutcome: "discriminate_candidates",
|
||
...catalog,
|
||
birthDate,
|
||
candidatesSeparated: false,
|
||
});
|
||
assert.equal(planOpen.next_followup?.semantic_key, childhood.semantic_key);
|
||
assert.notEqual(planGated.next_followup?.semantic_key, childhood.semantic_key);
|
||
const decision = decideFromDossier(dossier, { birthDate });
|
||
assert.equal(
|
||
decision.nextAction === "ask_candidate_discriminator",
|
||
followupAsksRenderableDiscriminator(planGated.next_followup),
|
||
);
|
||
if (followupAsksRenderableDiscriminator(planGated.next_followup)) {
|
||
assert.equal(decision.probe?.semanticKey, planGated.next_followup?.semantic_key);
|
||
} else {
|
||
assert.equal(decision.nextAction, "ask_fact_collection");
|
||
}
|
||
});
|