import assert from "node:assert/strict"; import test from "node:test"; import { slimDecisionReceipt } from "../src/lib/rectification-agentic/v9/case-receipt-projection.ts"; import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts"; import { TARGETED_YEAR_PROMPT } from "../src/lib/rectification-agentic/v9/collection-question-pool.ts"; import { parseV9CaseDossier } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { projectCurrentQuestion } from "../src/lib/rectification-agentic/v9/turn-decision.ts"; import { attachQuestionsToTurns, questionIsAnswered, } from "../src/lib/rectification-agentic/v9/turn-question.ts"; import { interviewChoiceCardUnavailable, rectificationQuestionGapState, } from "../src/lib/rectification-surface-state.ts"; import { CASE_ID, FOCUS_ID, TURN_ID, activeFocusFixture, candidateSnapshotFixture, conversationSummaryFixture, dossierFixture, } from "./rectification-v9-test-support.ts"; const YEAR_FOCUS_ID = "acacacac-acac-4cac-8cac-acacacacacac"; const CHOICE_FOCUS_ID = "adadadad-adad-4dad-8dad-adadadadadad"; function spokenYearFocus() { return { id: FOCUS_ID, caseId: CASE_ID, questionId: "collect:targeted:finance:year", intent: "collect_method_evidence", targetEvidenceId: null, targetDomain: "finance", targetKind: "targeted:finance:year", expectedAnswerSchema: { collect: true, prompt: TARGETED_YEAR_PROMPT, collect_kind: "targeted:finance:year", }, status: "active" as const, askedAt: "2026-09-14T00:00:00.000Z", resolvedAt: null, askedTurnId: null, answerOption: null, }; } test("spoken year follow-up without asked_turn_id hangs on the last assistant turn", () => { const focus = spokenYearFocus(); const current = projectCurrentQuestion(focus); assert.equal(current?.kind, "collect_spoken"); assert.equal(current?.prompt, "大概哪年几月?"); assert.equal(current?.question_id, "collect:targeted:finance:year"); assert.equal( interviewChoiceCardUnavailable({ questionKind: current?.kind, hasChoiceCard: false, questionId: current?.question_id, }), false, ); const attached = attachQuestionsToTurns( [{ id: TURN_ID, role: "assistant" as const, text: "记下了。" }], [focus], ); assert.equal(attached[0]?.question?.kind, "collect_spoken"); assert.equal(attached[0]?.question?.prompt, "大概哪年几月?"); assert.equal(attached[0]?.question?.question_id, "collect:targeted:finance:year"); assert.equal(attached[0]?.question?.focus_id, FOCUS_ID); const live = Boolean( attached[0]?.question && current && attached[0].question.focus_id === current.focus_id && !questionIsAnswered(attached[0].question), ); assert.equal(live, true); assert.equal( rectificationQuestionGapState({ liveQuestionVisible: live, questionMissing: false, questionLoadFailed: false, questionPersisted: true, busy: false, readonly: false, regenerating: false, snapshotLoaded: true, resumableCase: true, retryAttempts: 0, }), "idle", ); }); test("hanging spoken focus does not replace a question already on the last assistant turn", () => { const prompt = "结过婚或订过婚吗?"; const attached = attachQuestionsToTurns( [{ id: TURN_ID, role: "assistant" as const, text: "记下了,这方面先跳过。" }], [ { id: CHOICE_FOCUS_ID, caseId: CASE_ID, questionId: "collect:targeted:relationship", intent: "collect_method_evidence", targetEvidenceId: null, targetDomain: "relationship", targetKind: "targeted:relationship", expectedAnswerSchema: { targeted_collect: true, prompt, choice: { prompt, option_a: "有过这件事", option_b: "没有发生过", option_c: "记不太清楚", option_d: "这条先跳过", options: [ { key: "A", label: "有过这件事", answer_class: "yes" }, { key: "B", label: "没有发生过", answer_class: "no" }, { key: "C", label: "记不太清楚", answer_class: "unsure" }, { key: "D", label: "这条先跳过", answer_class: "weak_yes" }, ], }, }, status: "active", askedAt: "2026-09-14T00:00:00.000Z", resolvedAt: null, askedTurnId: TURN_ID, answerOption: null, }, { ...spokenYearFocus(), id: YEAR_FOCUS_ID, }, ], ); assert.equal(attached[0]?.question?.kind, "choice"); assert.equal(attached[0]?.question?.focus_id, CHOICE_FOCUS_ID); assert.equal(attached[0]?.question?.question_id, "collect:targeted:relationship"); }); test("engine representative time of an eliminated minute is not kept in model house tables", () => { const inference = buildInferenceState({ range_start: "04:48", range_end: "05:14", candidates: [ { id: "04:53", time: "04:53", relative_support: 16 }, { id: "05:06", time: "05:06", relative_support: 15 }, { id: "05:14", time: "05:14", relative_support: 7 }, ], events: [], probes: [], }); const fullInference = { ...inference, representative_time: "04:53", credible_range: ["04:48", "05:07"] as const, candidates: inference.candidates.map((candidate) => ( candidate.time === "05:14" ? { ...candidate, status: "eliminated" as const } : candidate )), }; const houseTables = { "04:53": { time: "04:53", houses: Array(12).fill({ sign: "x" }) }, "05:06": { time: "05:06", houses: Array(12).fill({ sign: "x" }) }, "05:14": { time: "05:14", houses: Array(12).fill({ sign: "x" }) }, }; const receipt = { inference_state: fullInference, house_tables_by_time: houseTables, }; const dropped = parseV9CaseDossier(dossierFixture({ latestResult: candidateSnapshotFixture({ representativeTime: "05:14", decisionReceipt: { inference_state: fullInference }, }), conversationSummary: conversationSummaryFixture({ activeFocus: activeFocusFixture({ expectedAnswerSchema: { collect: true, prompt: TARGETED_YEAR_PROMPT }, }), }), })); assert.ok(dropped); const slim = slimDecisionReceipt(receipt, dropped!); assert.deepEqual( Object.keys(slim.house_tables_by_time as Record).sort(), ["04:53", "05:06"], ); const kept = parseV9CaseDossier(dossierFixture({ latestResult: candidateSnapshotFixture({ representativeTime: "05:14", selectedTime: "05:14", decisionReceipt: { inference_state: fullInference }, }), conversationSummary: conversationSummaryFixture({ activeFocus: activeFocusFixture({ expectedAnswerSchema: { collect: true, prompt: TARGETED_YEAR_PROMPT }, }), }), })); assert.ok(kept); const slimKept = slimDecisionReceipt(receipt, kept!); assert.deepEqual( Object.keys(slimKept.house_tables_by_time as Record).sort(), ["04:53", "05:06", "05:14"], ); });