Files
Jyotisha/frontend/tests/rectification-provisional-adopt.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

695 lines
25 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 { readFileSync } from "node:fs";
import test from "node:test";
import { candidateSetId } from "../src/lib/rectification-agentic/core/build-state.ts";
import {
decideRectification,
engineCapabilityCeilingFromReceipt,
publicDecisionFields,
publicNextAction,
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import type { ConflictProbe, InferenceState } from "../src/lib/rectification-agentic/core/types.ts";
import {
decideFromDossier,
overlayPublicDecision,
type DecisionDossier,
} from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import { persistNextInterviewAfterChoice, persistNextInterviewIfIdle } from "../src/lib/rectification-agentic/v9/answer-choice.ts";
import { USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts";
import {
buildMethodFollowupPlan,
buildNextUserAction,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
canRenderRectificationSelectionCards,
parseRectificationCandidateResult,
} from "../src/lib/rectification-candidate-result.ts";
import {
CASE_ID,
FOCUS_ID,
TURN_ID,
USER_ID,
candidateSnapshotFixture,
computeFixture,
dossierFixture,
fakeAccounting,
receiptHandlers,
} from "./rectification-v9-test-support.ts";
const EXISTENCE_OPTIONS = [
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
{ label: "明确没有发生", answer_class: "no" as const },
{ label: "这段记不清楚", answer_class: "unsure" as const },
];
const INCIDENT_TIMES = ["05:00", "05:07", "04:53"] as const;
const INCIDENT_SCORES = { "05:00": 24, "05:07": 19, "04:53": 8 } as const;
const INCIDENT_CANDIDATE_IDS = {
"05:00": "88888888-8888-4888-8888-888888888881",
"05:07": "88888888-8888-4888-8888-888888888882",
"04:53": "88888888-8888-4888-8888-888888888883",
} as const;
const ENGINE_OPEN = {
acceptanceAllowed: true,
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: true,
} as const;
const INCIDENT_EVIDENCE = [
{
id: "e-education",
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
eventKind: "education_start",
},
{
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",
},
{
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",
},
] as const;
function answeredProbe(index: number, domain: string, year: number): ConflictProbe {
const semantic = `${domain}.${year}.dasha_boundary`;
return {
id: `probe:${semantic}`,
semantic_key: semantic,
candidate_split_hash: `${semantic}:split`,
domain,
year,
question: `${year} 年前后这件事有没有发生?`,
candidate_ids: [...INCIDENT_TIMES],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00", "05:07", "04:53"], conflicts: [] },
{ answer_class: "weak_yes", supports: [], conflicts: [] },
{ answer_class: "no", supports: [], conflicts: ["05:00", "05:07", "04:53"] },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: 0.4,
source: "dasha_boundary",
choice_kind: "existence",
style_options: EXISTENCE_OPTIONS,
};
}
const ANSWERED_PROBES: ConflictProbe[] = [
answeredProbe(1, "education", 2016),
answeredProbe(2, "relationship", 2024),
answeredProbe(3, "relationship", 2023),
answeredProbe(4, "career", 2020),
answeredProbe(5, "career", 2023),
];
const REMAINING_UNSPLIT: ConflictProbe = {
id: "probe:career.2023.dasha_activation",
semantic_key: "career.2023.dasha_activation",
candidate_split_hash: "career.2023.activation",
domain: "career",
year: 2023,
question: "2023 年前后大运有没有启动?",
candidate_ids: [...INCIDENT_TIMES],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00", "05:07", "04:53"], conflicts: [] },
{ answer_class: "weak_yes", supports: [], conflicts: [] },
{ answer_class: "no", supports: [], conflicts: ["05:00", "05:07", "04:53"] },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: 0.56,
source: "dasha_activation",
choice_kind: "existence",
style_options: EXISTENCE_OPTIONS,
};
function incidentEvents(evidence: typeof INCIDENT_EVIDENCE | readonly DecisionDossier["evidence"][number][]) {
return evidence.map((item) => {
const yearRaw = item.occurredFrom ? Number(String(item.occurredFrom).slice(0, 4)) : NaN;
const year = Number.isInteger(yearRaw) ? yearRaw : null;
return {
id: item.id ?? `${item.domain}:${item.occurredFrom ?? "undated"}`,
domain: item.domain,
year,
precision: (year === null
? "unknown"
: item.datePrecision === "day" || item.datePrecision === "month"
? item.datePrecision
: "year") as "day" | "month" | "year" | "unknown",
usage: item.domain === "education"
? "holdout" as const
: year === null
? "unused" as const
: "training" as const,
};
});
}
function incidentInference(evidence: typeof INCIDENT_EVIDENCE | readonly DecisionDossier["evidence"][number][]): InferenceState {
const ranked = [...INCIDENT_TIMES].sort((left, right) => INCIDENT_SCORES[right] - INCIDENT_SCORES[left]);
return {
algorithm_version: "rectification-inference-v1",
candidate_set_id: candidateSetId("04:53", "05:07", INCIDENT_TIMES),
revision: 6,
phase: "discrimination",
result_status: "discriminating",
range_start: "04:53",
range_end: "05:07",
candidates: ranked.map((time, index) => ({
id: INCIDENT_CANDIDATE_IDS[time],
time,
cluster_range: [time, time] as const,
prior_score: INCIDENT_SCORES[time],
posterior_score: INCIDENT_SCORES[time],
probability: INCIDENT_SCORES[time] / 51,
status: "active" as const,
rank: index + 1,
strong_conflict_count: 0,
})),
events: incidentEvents(evidence),
probes: [...ANSWERED_PROBES, REMAINING_UNSPLIT],
answered_probes: ANSWERED_PROBES.map((probe) => ({
probe_id: probe.id,
semantic_key: probe.semantic_key,
candidate_split_hash: probe.candidate_split_hash,
answer_class: "no" as const,
classified_from: "choice" as const,
})),
rounds: [],
last_inference_round: null,
entropy: 0.86,
representative_time: "05:00",
credible_range: ["05:00", "05:07"],
holdout_passed: null,
};
}
function openEngineReceipt(overrides: Record<string, unknown> = {}) {
return {
acceptance_allowed: true,
accept_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
diagnostic_quality: { passed: false, margin_percent: 19.94 },
event_fit_rate: { band: "high", rate: 0.8 },
oos_blind_prompts: [
{ domain: "family", user_meaning: "家里有没有结婚、添丁或住院这类记得住时间的事?", used_for_scoring: false },
{ domain: "finance", user_meaning: "有没有记得住时间的收入变化?", used_for_scoring: false },
{ domain: "health_pressure", user_meaning: "有没有记得住时间的健康压力事件?", used_for_scoring: false },
],
...overrides,
};
}
function incidentDossier(input: {
occupationCovered?: "declined" | "confirmed_note" | false;
evidence?: DecisionDossier["evidence"];
receipt?: Record<string, unknown>;
} = {}): DecisionDossier {
const occupationCovered = input.occupationCovered ?? "declined";
const evidence = [
...(input.evidence ?? INCIDENT_EVIDENCE),
...(occupationCovered === "confirmed_note"
? [{
id: "e-occupation",
status: "confirmed",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
}]
: []),
];
const declined = [
{ target_domain: "family", status: "declined" },
...(occupationCovered === "declined"
? [{ target_domain: "occupation", status: "declined", question_id: "collect:occupation:collect_method_evidence" }]
: []),
];
const state = incidentInference(evidence);
return {
evidence,
conversationSummary: {
activeFocus: null,
declinedSkippedTopics: declined,
},
latestResult: {
resultId: "55555555-5555-4555-8555-555555555555",
selectionAllowed: true,
confirmationAllowed: false,
evidenceLedgerFingerprint: evidenceLedgerFingerprint(evidence as never),
candidates: state.candidates.map((candidate) => ({
candidateId: INCIDENT_CANDIDATE_IDS[candidate.time as keyof typeof INCIDENT_CANDIDATE_IDS],
time: candidate.time,
rank: candidate.rank,
relativeSupport: candidate.posterior_score,
tiedMinuteCount: 1,
})),
representativeTime: "05:00",
decisionReceipt: {
...openEngineReceipt(input.receipt),
inference_state: state,
},
},
case: { acceptedTime: null },
};
}
function publicFields(dossier: DecisionDossier) {
const parsed = dossier;
const decision = decideFromDossier(parsed, { birthDate: "1997-08-08" });
const overlaid = overlayPublicDecision(parsed.latestResult ?? {}, decision);
return { decision, overlaid, fields: publicDecisionFields(decision) };
}
function rpcEvidenceRows(decision: DecisionDossier) {
return decision.evidence.map((item) => ({
id: item.id,
source_turn_id: TURN_ID,
subject: "self",
event_kind: item.eventKind ?? "event",
domain: item.domain,
occurred_from: item.occurredFrom,
occurred_to: item.occurredTo,
date_precision: item.datePrecision,
summary: item.summary ?? `${item.occurredFrom ?? ""} ${item.eventKind ?? "event"}`.trim(),
status: item.status,
supersedes_evidence_id: null,
created_at: "2026-09-01T00:00:00.000Z",
}));
}
function rpcDossier(decision: DecisionDossier, activeFocus: Record<string, unknown> | null = null) {
const evidence = rpcEvidenceRows(decision);
const evidenceFingerprint = evidenceLedgerFingerprint(evidence.map((item) => ({
id: item.id,
eventKind: item.event_kind,
domain: item.domain,
occurredFrom: item.occurred_from,
occurredTo: item.occurred_to,
datePrecision: item.date_precision,
summary: item.summary,
status: item.status,
})) as never);
return dossierFixture({
evidence,
evidenceCount: decision.evidence.length,
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: decision.latestResult?.representativeTime ?? null,
evidenceLedgerFingerprint: evidenceFingerprint,
candidates: decision.latestResult?.candidates?.map((item) => ({
candidate_id: item.candidateId,
time: item.time,
rank: item.rank,
relative_support: item.relativeSupport,
tied_minute_count: item.tiedMinuteCount ?? 1,
})),
decisionReceipt: { ...(decision.latestResult?.decisionReceipt ?? {}) },
}),
conversationSummary: {
confirmed_evidence_summary: [],
pending_revisions: [],
active_focus: activeFocus,
declined_skipped_topics: decision.conversationSummary.declinedSkippedTopics,
candidate_divergence_summary: null,
missing_evidence_categories: [],
last_result_policy: null,
summary_version: 1,
updated_at: "2026-09-01T00:00:00.000Z",
},
});
}
test("incident shape 1a: occupation coverage keeps collecting until every line is asked", () => {
for (const occupationCovered of ["declined", "confirmed_note"] as const) {
const { decision, overlaid } = publicFields(incidentDossier({ occupationCovered }));
assert.equal(decision.separation.lead, 5, occupationCovered);
assert.equal(decision.separation.sufficient, false, occupationCovered);
// 原值: can_adopt=true / session_outcome=adopt_representative
// 新值: can_adopt=false / session_outcome=collect_evidence
// 原因: D5 让七条线全部轮到 + D6「所有线问完才交付」;财务等线还没问(BUG-751)
assert.equal(overlaid.can_adopt, false, occupationCovered);
assert.equal(overlaid.session_outcome, "collect_evidence", occupationCovered);
assert.equal(overlaid.can_confirm_exact_minute, false, occupationCovered);
assert.equal(decision.canConfirmExactMinute, false, occupationCovered);
}
});
test("incident shape 1b: exact-minute confirm stays closed on the accident case", () => {
for (const occupationCovered of ["declined", "confirmed_note", false] as const) {
const { overlaid } = publicFields(incidentDossier({ occupationCovered }));
assert.equal(overlaid.can_confirm_exact_minute, false, String(occupationCovered));
}
});
test("incident shape 1c: uncovered occupation still collects remaining dated events", async () => {
const dossier = incidentDossier({ occupationCovered: false });
const { decision, overlaid } = publicFields(dossier);
// 原值: can_adopt=true / session_outcome=adopt_representative
// 新值: can_adopt=false / session_outcome=collect_evidence
// 原因: 同 1a——D5 + D6BUG-751
assert.equal(overlaid.can_adopt, false);
assert.equal(overlaid.session_outcome, "collect_evidence");
assert.equal(overlaid.can_confirm_exact_minute, false);
let writtenDomain: string | null = null;
const raw = rpcDossier(dossier);
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => ({
...raw,
conversation_summary: {
...(raw.conversation_summary as Record<string, unknown>),
active_focus: writtenDomain
? {
id: FOCUS_ID,
case_id: CASE_ID,
question_id: `collect:${writtenDomain}:collect_method_evidence`,
intent: "collect_method_evidence",
target_domain: writtenDomain,
status: "active",
}
: null,
},
}),
get_agentic_rectification_case_compute: () => computeFixture(),
set_agentic_rectification_conversation_focus: (_fn, args) => {
writtenDomain = typeof args.p_target_domain === "string" ? args.p_target_domain : null;
return {
focus: {
id: FOCUS_ID,
case_id: CASE_ID,
question_id: args.p_question_id,
intent: args.p_intent,
target_evidence_id: args.p_target_evidence_id,
target_domain: args.p_target_domain,
target_kind: args.p_target_kind,
expected_answer_schema: args.p_expected_answer_schema,
status: "active",
asked_at: "2026-09-01T00:00:00.000Z",
resolved_at: null,
},
idempotent: false,
};
},
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, idempotent: false }),
});
const persisted = await persistNextInterviewIfIdle({
accounting: accounting.client,
userId: USER_ID,
caseId: CASE_ID,
});
// 原值: 财务口述
// 新值: 训练门开后不轮转财务;职业层未覆盖仍可收职业口述
// 原因: BUG-648
assert.notEqual(writtenDomain, "finance");
assert.notEqual(persisted.hostNarration, USER_COLLECT_QUESTION.finance);
assert.equal(decision.canConfirmExactMinute, false);
assert.equal(canRenderRectificationSelectionCards(parseRectificationCandidateResult({
resultId: dossier.latestResult?.resultId,
candidates: overlaid.candidates,
selectionAllowed: overlaid.selection_allowed,
selection_allowed: overlaid.selection_allowed,
canAdopt: overlaid.can_adopt,
can_adopt: overlaid.can_adopt,
representativeTime: overlaid.representative_time,
decisionReceipt: dossier.latestResult?.decisionReceipt ?? null,
// 原值: true
// 新值: false
// 原因: D6——线没问完不渲染可点卡(BUG-751)
})), false);
});
test("after-choice persist collects remaining dated events after family denial", async () => {
const dossier = incidentDossier({ occupationCovered: false });
const decision = decideFromDossier(dossier);
// 原值: canAdopt=true
// 新值: canAdopt=false
// 原因: D6——线没问完时交付能力位按 waitToNarrow 收起(BUG-751
assert.equal(decision.canAdopt, false);
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => rpcDossier(dossier),
get_agentic_rectification_case_compute: () => computeFixture(),
set_agentic_rectification_conversation_focus: (_fn, args) => {
return {
focus: {
id: FOCUS_ID,
case_id: CASE_ID,
question_id: args.p_question_id,
intent: args.p_intent,
target_evidence_id: args.p_target_evidence_id,
target_domain: args.p_target_domain,
target_kind: args.p_target_kind,
expected_answer_schema: args.p_expected_answer_schema,
status: "active",
asked_at: "2026-09-01T00:00:00.000Z",
resolved_at: null,
},
idempotent: false,
};
},
});
const next = await persistNextInterviewAfterChoice({
accounting: accounting.client,
userId: USER_ID,
caseId: CASE_ID,
dossier,
decisionState: null,
nextAction: publicNextAction(decision),
});
// 原值: 财务口述
// 新值: 训练门开后不轮转财务;职业未覆盖可收职业口述
// 原因: BUG-648
assert.notEqual(next.followup?.domain, "finance");
});
test("incident shape 2: evidence floor still blocks adopt", () => {
const twoEvents = publicFields(incidentDossier({
occupationCovered: "declined",
evidence: INCIDENT_EVIDENCE.slice(0, 2),
}));
assert.equal(twoEvents.overlaid.can_adopt, false);
assert.equal(twoEvents.decision.nextAction, "ask_fact_collection");
const oneDomain = publicFields(incidentDossier({
occupationCovered: "declined",
evidence: INCIDENT_EVIDENCE.filter((item) => item.domain === "career"),
}));
assert.equal(oneDomain.overlaid.can_adopt, false);
assert.equal(oneDomain.decision.nextAction, "ask_fact_collection");
});
test("incident shape 3: engine ceiling still fail-closes adopt", () => {
const closed = publicFields(incidentDossier({
occupationCovered: "declined",
receipt: { acceptance_allowed: false },
}));
assert.equal(closed.overlaid.can_adopt, false);
const inconsistent = publicFields(incidentDossier({
occupationCovered: "declined",
receipt: { accept_allowed: false },
}));
assert.equal(inconsistent.overlaid.can_adopt, false);
assert.deepEqual(
engineCapabilityCeilingFromReceipt(inconsistent.decision && incidentDossier({
occupationCovered: "declined",
receipt: { accept_allowed: false },
}).latestResult?.decisionReceipt),
{
acceptanceAllowed: false,
selectionAllowed: false,
proposeAllowed: false,
confirmationAllowed: false,
},
);
});
test("incident shape 4: exact-minute confirm stays fail-closed even when local gates pass", () => {
const separated = [
{ time: "05:00", score: 58 },
{ time: "05:07", score: 42 },
];
const engineFalse = decideRectification({
methodCoverageAll: true,
trainingGateOpen: true,
snapshotCurrent: true,
candidateScores: separated,
holdoutValidation: "passed",
confirmationAllowed: true,
datedEventCount: 5,
datedDomainCount: 3,
engineCeiling: {
acceptanceAllowed: true,
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
},
});
assert.equal(engineFalse.separation.sufficient, true);
assert.equal(engineFalse.canConfirmExactMinute, false);
const syntheticOpen = decideRectification({
methodCoverageAll: true,
trainingGateOpen: true,
snapshotCurrent: true,
candidateScores: separated,
holdoutValidation: "passed",
confirmationAllowed: true,
datedEventCount: 5,
datedDomainCount: 3,
engineCeiling: ENGINE_OPEN,
});
assert.equal(syntheticOpen.canConfirmExactMinute, true);
});
test("delivery B1: GET overlay after occupation coverage renders representative cards", () => {
const dossier = incidentDossier({ occupationCovered: "declined" });
const { decision, overlaid } = publicFields(dossier);
// 原值: sessionOutcome=adopt_representative / nextAction!=ask_fact_collection
// 新值: sessionOutcome=collect_evidence / nextAction=ask_fact_collection
// 原因: D5 + D6——职业拒答只关一条线,其余线仍要问(BUG-751)
assert.equal(decision.sessionOutcome, "collect_evidence");
assert.equal(decision.nextAction, "ask_fact_collection");
const parsed = parseRectificationCandidateResult({
resultId: dossier.latestResult?.resultId,
candidates: overlaid.candidates,
selectionAllowed: overlaid.selection_allowed,
selection_allowed: overlaid.selection_allowed,
canAdopt: overlaid.can_adopt,
can_adopt: overlaid.can_adopt,
representativeTime: overlaid.representative_time,
decisionReceipt: dossier.latestResult?.decisionReceipt ?? null,
});
assert.ok(parsed);
// 原值: 渲染可点卡(BUG-648 训练门开即公开)
// 新值: 不渲染可点卡
// 原因: D6——线没问完不出交付卡(BUG-751)
assert.equal(canRenderRectificationSelectionCards(parsed), false);
assert.deepEqual(parsed.candidates.map((item) => item.time), ["05:00", "05:07", "04:53"]);
assert.equal(parsed.representativeTime, "05:00");
});
test("delivery B2: remaining dated collect stays in front of the adopt early-exit", async () => {
const dossier = incidentDossier({ occupationCovered: "declined" });
const { decision } = publicFields(dossier);
const plan = buildMethodFollowupPlan({
evidence: dossier.evidence,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
closedCollectFocuses: dossier.conversationSummary.declinedSkippedTopics,
sessionOutcome: decision.sessionOutcome,
});
// 原值: plan.session_outcome != collect_evidence
// 新值: plan.session_outcome == collect_evidence
// 原因: D6——采集线未问完,计划层保持采集(BUG-751)
assert.equal(plan.session_outcome, "collect_evidence");
assert.notEqual(plan.next_followup?.domain, "finance");
const action = buildNextUserAction({
scorableCount: 5,
evidenceCount: 5,
hasLatestResult: true,
selectionAllowed: decision.selectionAllowed,
sessionOutcome: decision.sessionOutcome,
nextFollowup: plan.next_followup,
workingTime: "05:00",
});
// 原值: adopt_representativeBUG-648 训练门开即交付)
// 新值: ask_method_followup
// 原因: D6——采集线没问完,下一动作仍是问(BUG-751)
assert.equal(action.id, "ask_method_followup");
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => rpcDossier(dossier),
get_agentic_rectification_case_compute: () => computeFixture(),
set_agentic_rectification_conversation_focus: (_fn, args) => {
return {
focus: {
id: FOCUS_ID,
case_id: CASE_ID,
question_id: args.p_question_id,
intent: args.p_intent,
target_evidence_id: args.p_target_evidence_id,
target_domain: args.p_target_domain,
target_kind: args.p_target_kind,
expected_answer_schema: args.p_expected_answer_schema,
status: "active",
asked_at: "2026-09-01T00:00:00.000Z",
resolved_at: null,
},
idempotent: false,
};
},
});
const idle = await persistNextInterviewIfIdle({
accounting: accounting.client,
userId: USER_ID,
caseId: CASE_ID,
});
// 原值: 写入财务采集
// 新值: 训练门开后不落财务轮转
// 原因: BUG-648
assert.notEqual(idle.hostNarration, USER_COLLECT_QUESTION.finance);
});
test("delivery B3: accept projection stays open from engine receipt, not exact-minute confirm", () => {
const { decision, overlaid } = publicFields(incidentDossier({ occupationCovered: "declined" }));
// 原值: selectionAllowed / overlaid.selection_allowed = true
// 新值: false
// 原因: D6——线没问完时不开放选择(BUG-751)
assert.equal(decision.selectionAllowed, false);
assert.equal(overlaid.selection_allowed, false);
assert.equal(overlaid.can_confirm_exact_minute, false);
assert.equal(decision.canConfirmExactMinute, false);
});
test("capability is computed only in deliveryCapability and spread into every exit", () => {
const source = readFileSync(new URL("../src/lib/rectification-agentic/core/rectification-decision.ts", import.meta.url), "utf8");
assert.match(source, /function deliveryCapability\(/);
assert.equal((source.match(/\.\.\.capability/g) ?? []).length >= 5, true);
assert.doesNotMatch(source, /canAdopt:\s*true/);
assert.doesNotMatch(source, /canConfirmExactMinute:\s*true/);
});