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 { buildChoiceFrame, parseAgentChoiceCopy, serverOwnedChoiceCopy } from "../src/lib/rectification-agentic/v9/choice-card.ts"; import { buildMethodFollowupPlan, type MethodFollowup, } from "../src/lib/rectification-agentic/v9/method-followup.ts"; import { decideFromDossier, rectificationFollowupCatalog, } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts"; import { expectedAnswerSchemaFor, serverOwnedExpectedAnswerSchema, stableFollowupQuestionId, } from "../src/lib/rectification-agentic/v9/server-focus.ts"; import { projectCurrentQuestion } from "../src/lib/rectification-agentic/v9/turn-decision.ts"; import { USER_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.ts"; import { attachQuestionsToTurns, parseTurnQuestion, } from "../src/lib/rectification-agentic/v9/turn-question.ts"; import { listV10ConversationFocuses, parseV9CaseDossier, questionSourceFromFocusList, } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { internalObservationsFromWindowScan, windowScanFromDecisionReceipt, } from "../src/lib/rectification-agentic/v9/varga-observations.ts"; import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts"; import { CASE_ID, EVIDENCE_ID, FOCUS_ID, TURN_ID, USER_ID, activeFocusFixture, candidateSnapshotFixture, conversationSummaryFixture, dossierFixture, fakeAccounting, receiptHandlers, } from "./rectification-v9-test-support.ts"; type ExecutableTool = { execute(input: unknown): Promise; }; const STYLE_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 RELOCATION_2015 = { id: "p-reloc-2015", semantic_key: "relocation.2015.05.dasha_boundary", candidate_split_hash: "reloc-2015-split", domain: "relocation", year: 2015, month: 5, question: "2015 年 5 月前后有没有搬家或长期住到外地?", candidate_ids: ["05:00", "05:10"], expected_outcomes: [ { answer_class: "yes" as const, supports: ["05:00"], conflicts: ["05:10"] }, { answer_class: "no" as const, supports: ["05:10"], conflicts: ["05:00"] }, { answer_class: "unsure" as const, supports: [], conflicts: [] }, ], information_gain: 0.87, source: "dasha_boundary", }; function eventProbeFromConflict(probe: typeof RELOCATION_2015) { return { year: probe.year, year_label: `${probe.year} 年 ${probe.month} 月前后`, month: probe.month, domain: probe.domain, event_family: "搬家或长期住到外地", source: probe.source, tracks: ["vimshottari", "narayana"], tracks_agree: false, unique_minute_claim: false, user_meaning: probe.question, role: "distinguish", phase: "candidate_discriminator", information_gain: probe.information_gain, semantic_key: probe.semantic_key, candidate_split_hash: probe.candidate_split_hash, candidate_ids: probe.candidate_ids, expected_outcomes: probe.expected_outcomes, style_options: STYLE_OPTIONS, choice_kind: "existence" as const, }; } function coverageEvidence() { return [ evidenceRow(EVIDENCE_ID, "career_entry", "career", "2020-04-01", "month", "2020 年 4 月开始实习"), evidenceRow("44444444-4444-4444-8444-444444444445", "education_start", "education", "2016-09-01", "month", "2016 年入学"), evidenceRow("44444444-4444-4444-8444-444444444446", "relationship_start", "relationship", "2024-05-01", "month", "2024 年 5 月认识一位女生"), evidenceRow("44444444-4444-4444-8444-444444444447", "family_event", "family", "2021-03-01", "month", "2021 年家里有人住院"), ]; } function evidenceRow( id: string, eventKind: string, domain: string, occurredFrom: string, datePrecision: string, summary: string, ) { return { id, source_turn_id: TURN_ID, subject: "self", event_kind: eventKind, domain, occurred_from: occurredFrom, occurred_to: occurredFrom, date_precision: datePrecision, summary, status: "confirmed", supersedes_evidence_id: null, created_at: "2026-08-28T07:35:45.000Z", }; } function distinguishDossier(activeFocus: unknown = null) { const inference = buildInferenceState({ range_start: "04:45", range_end: "05:15", candidates: [ { id: "05:00", time: "05:00", relative_support: 16 }, { id: "05:10", time: "05:10", relative_support: 14 }, ], events: [ { id: "e1", domain: "career", year: 2020, precision: "month" }, { id: "e2", domain: "education", year: 2016, precision: "month" }, { id: "e3", domain: "relationship", year: 2024, precision: "month" }, { id: "e4", domain: "family", year: 2021, precision: "month" }, ], probes: [RELOCATION_2015], }); return dossierFixture({ evidenceCount: 4, evidence: coverageEvidence(), latestResult: candidateSnapshotFixture({ decisionReceipt: { inference_state: inference, discriminating_event_probes: [eventProbeFromConflict(RELOCATION_2015)], }, }), conversationSummary: conversationSummaryFixture({ activeFocus }), }); } function nextFollowupFromRaw(raw: unknown): MethodFollowup | null { const parsed = parseV9CaseDossier(raw); assert.ok(parsed); const decision = decideFromDossier(parsed); const catalog = rectificationFollowupCatalog(parsed.latestResult, parsed.evidence); const observations = internalObservationsFromWindowScan( windowScanFromDecisionReceipt(parsed.latestResult?.decisionReceipt ?? null), ); return buildMethodFollowupPlan({ evidence: parsed.evidence, activeFocus: parsed.conversationSummary.activeFocus, declinedTopics: parsed.conversationSummary.declinedSkippedTopics, closedCollectFocuses: parsed.conversationSummary.declinedSkippedTopics, observations, sessionOutcome: decision.sessionOutcome, ...catalog, birthDate: null, accepted: Boolean(parsed.case.acceptedTime), candidatesSeparated: decision.separation.sufficient, holdoutValidation: decision.holdoutValidation, }).next_followup; } function identityMinusPrompt(schema: Record) { const next = { ...schema }; delete next.prompt; if (next.choice && typeof next.choice === "object" && !Array.isArray(next.choice)) { const choice = { ...(next.choice as Record) }; delete choice.prompt; next.choice = choice; } return next; } function setFocusHandler(store: { focus: ReturnType | null }) { return (_fn: string, args: Record) => { const incoming = (args.p_expected_answer_schema as Record) ?? {}; const existing = store.focus; const sameIdentity = Boolean( existing && existing.question_id === args.p_question_id && existing.intent === args.p_intent && JSON.stringify(identityMinusPrompt(existing.expected_answer_schema as Record)) === JSON.stringify(identityMinusPrompt(incoming)), ); store.focus = { ...activeFocusFixture({ id: existing && sameIdentity ? existing.id : FOCUS_ID, questionId: String(args.p_question_id), intent: String(args.p_intent), targetEvidenceId: args.p_target_evidence_id ? String(args.p_target_evidence_id) : null, targetDomain: typeof args.p_target_domain === "string" ? args.p_target_domain : null, targetKind: args.p_target_kind == null ? null : String(args.p_target_kind), expectedAnswerSchema: incoming, askedTurnId: args.p_asked_turn_id ? String(args.p_asked_turn_id) : null, }), }; return { focus: store.focus, idempotent: sameIdentity }; }; } function toolsFor(accounting: ReturnType) { return createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never, }); } function discriminatorFollowup(): MethodFollowup { const frame = buildChoiceFrame({ method_id: "dasha_events", ask_theme: "dated_event", domain: "education", user_prompt_hint: "ask", }, { probes: [{ year: 2016, year_label: "2016 年前后", domain: "education", event_family: "升学结果或学习环境出现明显变化", source: "dasha_activation", tracks: ["vimshottari", "narayana"], tracks_agree: true, unique_minute_claim: false, user_meaning: "2016 年前后升学结果或学习环境出现明显变化", role: "distinguish", information_gain: 0.4, semantic_key: "education:2016", style_options: STYLE_OPTIONS, candidate_ids: ["05:00", "05:20"], expected_outcomes: [ { answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] }, { answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] }, ], }], }); assert.ok(frame); return { method_id: "dasha_events", intent: "distinguish_candidates", ask_theme: "dated_event", domain: "education", kind_hint: null, user_prompt_hint: "ask", must_not_label: false, choice_frame: frame, source: "event_probe", information_gain: 0.4, semantic_key: "education:2016", probe_year: 2016, candidate_ids: ["05:00", "05:20"], expected_outcomes: [ { answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] }, { answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] }, ], }; } test("server-owned schema matches choice_frame copy and ignores Agent choice", () => { const followup = discriminatorFollowup(); const copy = serverOwnedChoiceCopy(followup.choice_frame!); assert.ok(copy); const schema = expectedAnswerSchemaFor( followup.choice_frame!, stableFollowupQuestionId(followup), null, followup, ); assert.ok(schema?.choice); const parsed = parseAgentChoiceCopy(schema); assert.deepEqual(parsed?.options, copy.options); const viaWrapper = serverOwnedExpectedAnswerSchema(followup, null); assert.deepEqual( parseAgentChoiceCopy(viaWrapper)?.options, copy.options, ); }); test("Agent set-focus with only spokenPrompt persists server choice options", async () => { const raw = distinguishDossier(); const followup = nextFollowupFromRaw(raw); assert.ok(followup?.choice_frame, JSON.stringify({ intent: followup?.intent, source: followup?.source, method_id: followup?.method_id, })); const questionId = stableFollowupQuestionId(followup); const copy = serverOwnedChoiceCopy(followup.choice_frame); assert.ok(copy); const store: { focus: ReturnType | null } = { focus: null }; const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => raw, set_agentic_rectification_conversation_focus: setFocusHandler(store), }); const tools = toolsFor(accounting); const spokenPrompt = "记下了。2015 年前后,有没有搬家或长期住到外地?"; const first = await (tools["rectification-set-focus"] as unknown as ExecutableTool>).execute({ caseId: CASE_ID, questionId, intent: "distinguish_candidates", spokenPrompt, }) as Record; const withAgentChoice = await (tools["rectification-set-focus"] as unknown as ExecutableTool>).execute({ caseId: CASE_ID, questionId, intent: "distinguish_candidates", spokenPrompt, expectedAnswerSchema: { choice: { prompt: "伪造题干", option_a: "模型自编A", option_b: "模型自编B", option_c: "模型自编C", option_d: "模型自编D", options: [ { key: "A", label: "模型自编A", answer_class: "no" }, { key: "B", label: "模型自编B", answer_class: "yes" }, { key: "C", label: "模型自编C", answer_class: "unsure" }, { key: "D", label: "模型自编D", answer_class: "weak_yes" }, ], }, }, }) as Record; assert.equal(first.error, undefined); assert.equal(withAgentChoice.error, undefined); const firstSchema = first.expected_answer_schema as Record; const secondSchema = withAgentChoice.expected_answer_schema as Record; const firstCopy = parseAgentChoiceCopy(firstSchema); assert.ok(firstCopy); assert.deepEqual(firstCopy.options, copy.options); assert.equal(typeof firstSchema.probe_id, "string"); assert.ok(String(firstSchema.probe_id).length > 0); assert.equal(projectCurrentQuestion({ id: String(first.focus_id), questionId: String(first.question_id), intent: String(first.intent), targetDomain: typeof first.target_domain === "string" ? first.target_domain : null, expectedAnswerSchema: firstSchema, })?.kind, "choice"); assert.deepEqual(identityMinusPrompt(firstSchema), identityMinusPrompt(secondSchema)); assert.equal(firstCopy.option_a, copy.option_a); assert.notEqual(firstCopy.option_a, "模型自编A"); const attached = attachQuestionsToTurns( [{ id: TURN_ID, role: "assistant", text: "记下了。", status: "completed" }], [{ id: String(first.focus_id), caseId: CASE_ID, questionId: String(first.question_id), intent: String(first.intent), targetEvidenceId: null, targetDomain: typeof first.target_domain === "string" ? first.target_domain : null, targetKind: null, expectedAnswerSchema: firstSchema, status: "active", askedAt: "2026-09-02T00:00:00.000Z", resolvedAt: null, askedTurnId: TURN_ID, answerOption: null, }], ); assert.equal(attached[0]?.question?.kind, "choice"); assert.equal(attached[0]?.question?.options?.length, 4); }); test("collect set-focus writes collect:true even if Agent sends choice", async () => { const raw = dossierFixture({ latestResult: candidateSnapshotFixture() }); const followup = nextFollowupFromRaw(raw); assert.equal(followup?.choice_frame ?? null, null); const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => raw, set_agentic_rectification_conversation_focus: setFocusHandler({ focus: null }), }); const result = await (toolsFor(accounting)["rectification-set-focus"] as unknown as ExecutableTool>).execute({ caseId: CASE_ID, questionId: followup ? stableFollowupQuestionId(followup) : "collect:relationship:collect_method_evidence", intent: "collect_method_evidence", // 原值: USER_COLLECT_QUESTION.family(3847e9c9;45bdb63e 为「哪年上的大学」) // 新值: USER_COLLECT_QUESTION[followup.domain](默认 dossier 一件事业 → 感情) // 原因: dated 分支移到家人之后,前两问恢复感情;BUG-529 仍要求题干命中领域词,大学不命中感情。 spokenPrompt: USER_COLLECT_QUESTION[followup?.domain ?? "relationship"] ?? USER_COLLECT_QUESTION.relationship, expectedAnswerSchema: { choice: { prompt: "伪造采集", option_a: "模型自编A", option_b: "模型自编B", option_c: "模型自编C", option_d: "模型自编D", }, }, }) as Record; assert.equal(result.error, undefined); const schema = result.expected_answer_schema as Record; assert.equal(schema.collect, true); assert.equal(schema.choice, undefined); assert.equal(projectCurrentQuestion({ id: String(result.focus_id), questionId: String(result.question_id), intent: String(result.intent), expectedAnswerSchema: schema, })?.kind, "collect_spoken"); }); test("set-focus returns no_pending_question when there is no next followup", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture(), }); const result = await (toolsFor(accounting)["rectification-set-focus"] as unknown as ExecutableTool>).execute({ caseId: CASE_ID, questionId: "career-month-question", intent: "clarify_event_date", spokenPrompt: "你还记得哪年上的大学吗?", }) as Record; assert.deepEqual(result, { ok: false, error: "no_pending_question" }); assert.equal( accounting.calls.some((call) => call.fn === "set_agentic_rectification_conversation_focus"), false, ); }); test("second set-focus on the same choice probe is idempotent and keeps options", async () => { const raw = distinguishDossier(); const followup = nextFollowupFromRaw(raw); assert.ok(followup?.choice_frame); const questionId = stableFollowupQuestionId(followup); const store: { focus: ReturnType | null } = { focus: null }; const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => raw, set_agentic_rectification_conversation_focus: setFocusHandler(store), }); const execute = (toolsFor(accounting)["rectification-set-focus"] as unknown as ExecutableTool>).execute; const first = await execute({ caseId: CASE_ID, questionId, intent: "distinguish_candidates", spokenPrompt: "记下了。2015 年前后,有没有搬家或长期住到外地?", }) as Record; const second = await execute({ caseId: CASE_ID, questionId, intent: "distinguish_candidates", spokenPrompt: "工作记下了。2015 年 5 月前后,还记不记得搬过家?", }) as Record; assert.equal(first.focus_id, second.focus_id); assert.equal(second.idempotent, true); assert.deepEqual( parseAgentChoiceCopy(first.expected_answer_schema as Record)?.options, parseAgentChoiceCopy(second.expected_answer_schema as Record)?.options, ); }); test("list focuses RPC error marks GET questions unavailable and warns", async () => { const warns: string[] = []; const original = console.warn; console.warn = (...args: unknown[]) => { warns.push(args.map(String).join(" ")); }; try { const accounting = fakeAccounting({ list_agentic_rectification_conversation_focuses: () => { throw new Error("agentic_rectification_focus_not_found"); }, }); const listed = await listV10ConversationFocuses(accounting.client, USER_ID, CASE_ID); assert.equal(listed.available, false); assert.deepEqual(listed.focuses, []); assert.equal(questionSourceFromFocusList(listed), "unavailable"); assert.match(warns.join("\n"), /list focuses failed/); assert.match(warns.join("\n"), /focus_not_found/); const attached = attachQuestionsToTurns( [ { id: TURN_ID, role: "assistant", text: "你好。", status: "completed" }, { id: "44444444-4444-4444-8444-444444444444", role: "assistant", text: "记下了。", status: "completed" }, ], listed.focuses, ); assert.ok(attached.every((turn) => turn.question === null)); } finally { console.warn = original; } const casesRoute = readFileSync(new URL("../src/app/api/rectification/cases/[caseId]/route.ts", import.meta.url), "utf8"); assert.match(casesRoute, /question_source: questionSourceFromFocusList\(listed\)/); assert.match(casesRoute, /return NextResponse\.json\(dossierResponse/); }); test("walkthrough-shaped chain rebuilds each assistant question from asked_turn_id", () => { const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8"); assert.match(chat, /question: parseTurnQuestion\(turn\.question\)/); const collect = (id: string, prompt: string, status: "active" | "resolved") => ({ id, caseId: CASE_ID, questionId: `collect:${id}:collect_method_evidence`, intent: "collect_method_evidence", targetEvidenceId: null, targetDomain: "relationship", targetKind: null, expectedAnswerSchema: { prompt, collect: true }, status, askedAt: "2026-09-02T00:00:00.000Z", resolvedAt: status === "resolved" ? "2026-09-02T00:01:00.000Z" : null, askedTurnId: id, answerOption: null, }); const choice = ( id: string, prompt: string, year: string, status: "active" | "resolved", answer: "A" | "B" | "C" | "D" | null, ) => ({ id, caseId: CASE_ID, questionId: `probe:${year}`, intent: "distinguish_candidates", targetEvidenceId: null, targetDomain: "relocation", targetKind: null, expectedAnswerSchema: { prompt, probe_id: `probe:${year}`, choice: { prompt, option_a: STYLE_OPTIONS[0]!.label, option_b: STYLE_OPTIONS[1]!.label, option_c: STYLE_OPTIONS[2]!.label, option_d: STYLE_OPTIONS[3]!.label, options: STYLE_OPTIONS.map((option, index) => ({ key: (["A", "B", "C", "D"] as const)[index]!, label: option.label, answer_class: option.answer_class, })), }, }, status, askedAt: "2026-09-02T00:00:00.000Z", resolvedAt: status === "resolved" ? "2026-09-02T00:01:00.000Z" : null, askedTurnId: id, answerOption: answer, }); const opening = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1"; const collectTwo = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2"; const collectThree = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa3"; const tapOne = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa4"; const tapTwo = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa5"; const tapThree = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa6"; const typed = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa7"; const reverse = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa8"; const turns = [ { id: opening, role: "assistant" as const, text: "从你最容易想起来的开始。", status: "completed" }, { id: collectTwo, role: "assistant" as const, text: "工作记下了。", status: "completed" }, { id: collectThree, role: "assistant" as const, text: "感情这块也记下了。", status: "completed" }, { id: tapOne, role: "assistant" as const, text: "接下来对一下这段经历。", status: "completed" }, { id: tapTwo, role: "assistant" as const, text: "候选比较更新了。", status: "completed" }, { id: tapThree, role: "assistant" as const, text: "再看一件相近的。", status: "completed" }, { id: typed, role: "assistant" as const, text: "也可以打字回答。", status: "completed" }, { id: reverse, role: "assistant" as const, text: "采用之后再核对一件前事。", status: "completed" }, ]; const attached = attachQuestionsToTurns(turns, [ collect(opening, "从你最容易想起来的开始就好,记得大概年份就行。", "resolved"), collect(collectTwo, "钱的方面,还记得哪年收入明显变过吗?", "resolved"), collect(collectThree, "家里有没有结婚、添丁或住院这类事?", "resolved"), choice(tapOne, "2015 年前后,有没有搬家或长期住到外地?", "2015", "resolved", "A"), choice(tapTwo, "2014 年前后,有没有升学、转学或换学习环境?", "2014", "resolved", "B"), choice(tapThree, "2018 年前后,有没有换过工作?", "2018", "resolved", "C"), choice(typed, "2023 年前后,有没有一段认真开始或结束的关系?", "2023", "resolved", null), { id: reverse, caseId: CASE_ID, questionId: "reverse:family:2021", intent: "reverse_verify", targetEvidenceId: null, targetDomain: "family", targetKind: null, expectedAnswerSchema: { prompt: "2021 年前后,家里有没有结婚、添丁或住院?", collect: true, }, status: "active", askedAt: "2026-09-02T00:00:00.000Z", resolvedAt: null, askedTurnId: reverse, answerOption: null, }, ]); const shapes = attached.map((turn) => { const question = parseTurnQuestion(turn.question); return { id: turn.id, kind: question?.kind ?? null, focus_id: question?.focus_id ?? null, answer_option: question?.answer_option ?? null, status: question?.status ?? null, options: question?.options?.length ?? 0, }; }); assert.deepEqual(shapes, [ { id: opening, kind: "collect_spoken", focus_id: opening, answer_option: null, status: "resolved", options: 0 }, { id: collectTwo, kind: "collect_spoken", focus_id: collectTwo, answer_option: null, status: "resolved", options: 0 }, { id: collectThree, kind: "collect_spoken", focus_id: collectThree, answer_option: null, status: "resolved", options: 0 }, { id: tapOne, kind: "choice", focus_id: tapOne, answer_option: "A", status: "resolved", options: 4 }, { id: tapTwo, kind: "choice", focus_id: tapTwo, answer_option: "B", status: "resolved", options: 4 }, { id: tapThree, kind: "choice", focus_id: tapThree, answer_option: "C", status: "resolved", options: 4 }, { id: typed, kind: "choice", focus_id: typed, answer_option: null, status: "resolved", options: 4 }, { id: reverse, kind: "reverse_verify", focus_id: reverse, answer_option: null, status: "active", options: 0 }, ]); assert.ok(attached[3]?.question?.prompt.includes("2015")); }); test("set-focus description no longer tells the Agent to write option_a", () => { const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8"); const hints = readFileSync(new URL("../src/lib/rectification-agentic/v9/method-followup.ts", import.meta.url), "utf8"); assert.match(tools, /选项、计分由服务端按 choice_frame 写入,你只写 spokenPrompt/); assert.match(tools, /题干必须写出服务端给你的年份\/期间/); assert.doesNotMatch(tools, /写入 option_a/); assert.doesNotMatch(tools, /parseAgentChoiceCopy\(expectedAnswerSchemaInput\)/); assert.match(hints, /选项、计分由服务端按 choice_frame 写入,你只写 spokenPrompt/); assert.doesNotMatch(hints, /不要写 expectedAnswerSchema\.choice/); }); test("adopt continuation set-focus keeps the persisted reverse_verify question id", async () => { const questionId = "reverse_verify:education_style:score"; const schema = { prompt: "2016 年前后,升学结果或学习环境有没有明显变化?", probe_year: 2016, choice: { prompt: "2016 年前后,升学结果或学习环境有没有明显变化?", option_a: STYLE_OPTIONS[0].label, option_b: STYLE_OPTIONS[1].label, option_c: STYLE_OPTIONS[2].label, option_d: STYLE_OPTIONS[3].label, options: STYLE_OPTIONS.map((option, index) => ({ key: (["A", "B", "C", "D"] as const)[index]!, label: option.label, answer_class: option.answer_class, })), }, }; const raw = distinguishDossier(activeFocusFixture({ questionId, intent: "reverse_verify", targetDomain: "education", targetKind: "education_milestone", expectedAnswerSchema: schema, })); (raw.case as { accepted_time: string | null }).accepted_time = "05:06:00"; const store: { focus: ReturnType | null } = { focus: activeFocusFixture({ questionId, intent: "reverse_verify", targetDomain: "education", targetKind: "education_milestone", expectedAnswerSchema: schema, }), }; const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => raw, set_agentic_rectification_conversation_focus: setFocusHandler(store), }); const result = await (toolsFor(accounting)["rectification-set-focus"] as unknown as ExecutableTool>).execute({ caseId: CASE_ID, questionId, intent: "reverse_verify", spokenPrompt: "2016 年前后升学或学习环境有没有明显变化?", }) as Record; assert.equal(result.error, undefined); assert.equal(result.question_id, questionId); assert.equal(result.focus_id, FOCUS_ID); const copy = parseAgentChoiceCopy(result.expected_answer_schema); assert.equal(copy?.options.length, 4); const schemaRow = result.expected_answer_schema && typeof result.expected_answer_schema === "object" ? result.expected_answer_schema as Record : {}; assert.notEqual(schemaRow.semantic_key, RELOCATION_2015.semantic_key); assert.equal(schemaRow.probe_year, 2016); });