import assert from "node:assert/strict"; import test from "node:test"; import { confirmV10Evidence, recordV10EvidenceBatch, resolveV10ConversationFocus, reviseV10Evidence, setV10ConversationFocus, type V10EvidenceBatchItem, } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts"; import { CASE_ID, EVIDENCE_ID, FOCUS_ID, RESULT_ID, TURN_ID, USER_ID, activeFocusFixture, candidateSnapshotFixture, conversationSummaryFixture, dossierFixture, fakeAccounting, receiptHandlers, } from "./rectification-v9-test-support.ts"; import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts"; type ExecutableTool = { execute(input: unknown): Promise; }; const SECOND_EVIDENCE_ID = "45454545-4545-4454-8454-454545454545"; function toolSet(accounting: ReturnType) { return createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never, }); } test("set/resolve focus services preserve the exact durable RPC contract", async () => { const accounting = fakeAccounting({ set_agentic_rectification_conversation_focus: () => ({ focus: activeFocusFixture({ targetEvidenceId: EVIDENCE_ID, expectedAnswerSchema: { required: ["month"] }, }), idempotent: false, }), resolve_agentic_rectification_conversation_focus: () => ({ focus_id: FOCUS_ID, status: "declined", evidence_id: EVIDENCE_ID, idempotent: true, }), }); const setResult = await setV10ConversationFocus(accounting.client, USER_ID, CASE_ID, { questionId: "career-month-question", intent: "clarify_event_date", targetEvidenceId: EVIDENCE_ID, targetDomain: "career", targetKind: "career_entry", expectedAnswerSchema: { required: ["month"] }, }); assert.equal(setResult.focus.id, FOCUS_ID); assert.equal(setResult.focus.targetEvidenceId, EVIDENCE_ID); assert.deepEqual(setResult.focus.expectedAnswerSchema, { required: ["month"] }); assert.equal(setResult.idempotent, false); assert.deepEqual(accounting.calls[0], { fn: "set_agentic_rectification_conversation_focus", args: { p_user_id: USER_ID, p_case_id: CASE_ID, p_question_id: "career-month-question", p_intent: "clarify_event_date", p_target_evidence_id: EVIDENCE_ID, p_target_domain: "career", p_target_kind: "career_entry", p_expected_answer_schema: { required: ["month"] }, }, }); const resolved = await resolveV10ConversationFocus(accounting.client, USER_ID, CASE_ID, { focusId: FOCUS_ID, status: "declined", evidenceId: EVIDENCE_ID, }); assert.deepEqual(resolved, { focusId: FOCUS_ID, status: "declined", evidenceId: EVIDENCE_ID, idempotent: true, }); assert.deepEqual(accounting.calls[1], { fn: "resolve_agentic_rectification_conversation_focus", args: { p_user_id: USER_ID, p_case_id: CASE_ID, p_focus_id: FOCUS_ID, p_status: "declined", p_evidence_id: EVIDENCE_ID, }, }); }); test("set/resolve focus tools return safe projections and never infer the active focus", async () => { const accounting = fakeAccounting({ ...receiptHandlers, set_agentic_rectification_conversation_focus: () => ({ focus: activeFocusFixture({ targetEvidenceId: EVIDENCE_ID }), idempotent: false, }), resolve_agentic_rectification_conversation_focus: (_fn, args) => ({ focus_id: args.p_focus_id, status: args.p_status, evidence_id: args.p_evidence_id, idempotent: false, }), }); const tools = toolSet(accounting); const setResult = await (tools["rectification-set-focus"] as unknown as ExecutableTool>).execute({ caseId: CASE_ID, questionId: "career-month-question", intent: "clarify_event_date", targetEvidenceId: EVIDENCE_ID, targetDomain: "career", targetKind: "career_entry", }); assert.deepEqual(setResult, { focus_id: FOCUS_ID, question_id: "question-1", intent: "confirm_revision", target_evidence_id: EVIDENCE_ID, target_domain: "career", target_kind: "career_entry", expected_answer_schema: {}, status: "active", asked_at: "2026-08-12T10:00:05.000Z", idempotent: false, }); const resolved = await (tools["rectification-resolve-focus"] as unknown as ExecutableTool>).execute({ caseId: CASE_ID, focusId: FOCUS_ID, status: "skipped", evidenceId: EVIDENCE_ID, }); assert.deepEqual(resolved, { focus_id: FOCUS_ID, evidence_id: EVIDENCE_ID, status: "skipped", idempotent: false, }); const resolveCall = accounting.calls.find((call) => call.fn === "resolve_agentic_rectification_conversation_focus"); assert.equal(resolveCall?.args.p_focus_id, FOCUS_ID); assert.equal(resolveCall?.args.p_evidence_id, EVIDENCE_ID); }); test("confirm and revise services bind both focusId and evidenceId", async () => { const accounting = fakeAccounting({ confirm_agentic_rectification_evidence_v10: () => ({ focus_id: FOCUS_ID, evidence_id: EVIDENCE_ID, status: "confirmed", idempotent: false, }), revise_agentic_rectification_evidence_v10: () => ({ focus_id: FOCUS_ID, evidence_id: SECOND_EVIDENCE_ID, supersedes_evidence_id: EVIDENCE_ID, idempotent: true, }), }); const confirmed = await confirmV10Evidence(accounting.client, USER_ID, CASE_ID, FOCUS_ID, EVIDENCE_ID); assert.deepEqual(confirmed, { focusId: FOCUS_ID, evidenceId: EVIDENCE_ID, status: "confirmed", idempotent: false, }); const revised = await reviseV10Evidence(accounting.client, USER_ID, CASE_ID, { focusId: FOCUS_ID, evidenceId: EVIDENCE_ID, quote: "不是9月,是10月", occurredFrom: "2016-10-01", occurredTo: null, datePrecision: "month", summary: "更正为2016年10月去北京工作", }); assert.deepEqual(revised, { focusId: FOCUS_ID, evidenceId: SECOND_EVIDENCE_ID, supersedesEvidenceId: EVIDENCE_ID, idempotent: true, }); const confirmCall = accounting.calls.find((call) => call.fn === "confirm_agentic_rectification_evidence_v10"); assert.equal(confirmCall?.args.p_focus_id, FOCUS_ID); assert.equal(confirmCall?.args.p_evidence_id, EVIDENCE_ID); const reviseCall = accounting.calls.find((call) => call.fn === "revise_agentic_rectification_evidence_v10"); assert.equal(reviseCall?.args.p_focus_id, FOCUS_ID); assert.equal(reviseCall?.args.p_evidence_id, EVIDENCE_ID); }); test("confirm/revise tools forward the durable focus/evidence binding", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture(), confirm_agentic_rectification_evidence_v10: (_fn, args) => ({ focus_id: args.p_focus_id, evidence_id: args.p_evidence_id, status: "confirmed", idempotent: false, }), revise_agentic_rectification_evidence_v10: (_fn, args) => ({ focus_id: args.p_focus_id, evidence_id: SECOND_EVIDENCE_ID, supersedes_evidence_id: args.p_evidence_id, idempotent: false, }), }); const tools = toolSet(accounting); await (tools["rectification-confirm-evidence"] as unknown as ExecutableTool).execute({ caseId: CASE_ID, focusId: FOCUS_ID, evidenceId: EVIDENCE_ID, }); await (tools["rectification-revise-evidence"] as unknown as ExecutableTool).execute({ caseId: CASE_ID, focusId: FOCUS_ID, evidenceId: EVIDENCE_ID, quote: "不是9月,是10月", datePrecision: "month", occurredFrom: "2016-10", summary: "更正为2016年10月去北京工作", }); const writes = accounting.calls.filter((call) => call.fn === "confirm_agentic_rectification_evidence_v10" || call.fn === "revise_agentic_rectification_evidence_v10"); assert.deepEqual(writes.map((call) => ({ fn: call.fn, focusId: call.args.p_focus_id, evidenceId: call.args.p_evidence_id, })), [ { fn: "confirm_agentic_rectification_evidence_v10", focusId: FOCUS_ID, evidenceId: EVIDENCE_ID }, { fn: "revise_agentic_rectification_evidence_v10", focusId: FOCUS_ID, evidenceId: EVIDENCE_ID }, ]); }); test("batch preserves three independent items, parses all outcomes, and keeps item idempotency stable", async () => { let invocation = 0; const accounting = fakeAccounting({ record_agentic_rectification_evidence_batch: () => { invocation += 1; return { items: [ { index: 0, outcome: "accepted", evidence_id: EVIDENCE_ID, status: "confirmed", idempotent: invocation > 1, clarification_fields: [], error_code: null, }, { index: 1, outcome: "needs_clarification", evidence_id: null, status: "pending_confirmation", idempotent: invocation > 1, clarification_fields: ["occurred_from"], error_code: null, }, { index: 2, outcome: "rejected", evidence_id: null, status: "rejected", idempotent: invocation > 1, clarification_fields: [], error_code: "quote_not_grounded", }, ], accepted_count: 1, needs_clarification_count: 1, rejected_count: 1, focus_id: FOCUS_ID, }; }, }); const items: V10EvidenceBatchItem[] = [ { quote: "2016年9月去北京工作", subject: "self" as const, eventKind: "career_entry" as const, domain: "career", occurredFrom: "2016-09-01", occurredTo: null, datePrecision: "month", summary: "2016年9月去北京工作", }, { quote: "后来搬过一次家", subject: "self" as const, eventKind: "relocation" as const, domain: "relocation", occurredFrom: null, occurredTo: null, datePrecision: "unknown", summary: "后来搬过一次家", }, { quote: "助手猜测我2020年结婚", subject: "self" as const, eventKind: "relationship_commitment", domain: "relationship", occurredFrom: "2020-01-01", occurredTo: null, datePrecision: "year", summary: "助手猜测的婚姻事件", }, ]; const first = await recordV10EvidenceBatch(accounting.client, USER_ID, CASE_ID, TURN_ID, FOCUS_ID, items); const second = await recordV10EvidenceBatch(accounting.client, USER_ID, CASE_ID, TURN_ID, FOCUS_ID, items); assert.deepEqual(first.items.map((item) => item.outcome), [ "accepted", "needs_clarification", "rejected", ]); assert.deepEqual(first.items.map((item) => item.idempotent), [false, false, false]); assert.deepEqual(second.items.map((item) => item.idempotent), [true, true, true]); assert.deepEqual({ accepted: first.acceptedCount, needsClarification: first.needsClarificationCount, rejected: first.rejectedCount, focusId: first.focusId, }, { accepted: 1, needsClarification: 1, rejected: 1, focusId: FOCUS_ID, }); assert.deepEqual(first.items[1]?.clarificationFields, ["occurred_from"]); assert.equal(first.items[2]?.errorCode, "quote_not_grounded"); const calls = accounting.calls.filter((call) => call.fn === "record_agentic_rectification_evidence_batch"); assert.equal(calls.length, 2); const firstItems = calls[0]!.args.p_items as Array>; const secondItems = calls[1]!.args.p_items as Array>; assert.deepEqual(firstItems.map((item) => item.quote), items.map((item) => item.quote)); assert.deepEqual(firstItems.map((item) => item.event_kind), items.map((item) => item.eventKind)); assert.equal(new Set(firstItems.map((item) => item.idempotency_key)).size, 3); assert.deepEqual( secondItems.map((item) => item.idempotency_key), firstItems.map((item) => item.idempotency_key), ); }); test("resolve-focus C without new evidence appends an inference transition", async () => { const inference = buildInferenceState({ range_start: "04:50", range_end: "05:10", candidates: [ { id: "05:00", time: "05:00", relative_support: 10 }, { id: "05:10", time: "05:10", relative_support: 10 }, ], events: [ { id: "e1", domain: "education", year: 2016, precision: "month" }, { id: "e2", domain: "career", year: 2018, precision: "year" }, { id: "e3", domain: "relationship", year: 2021, precision: "year" }, { id: "e4", domain: "family", year: 2023, precision: "year" }, ], probes: [{ id: "p-cd", semantic_key: "career.2019", candidate_split_hash: "05:00|05:10", domain: "career", year: 2019, question: "2019 年前后有没有入职或职责加重?", 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.4, source: "dasha_boundary", }], }); const snapshot = candidateSnapshotFixture(); Object.assign(snapshot.decision_receipt, { inference_state: inference }); let appendedState: unknown = null; const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ latestResult: snapshot, conversationSummary: conversationSummaryFixture({ activeFocus: activeFocusFixture({ expectedAnswerSchema: { choice: { prompt: "2019 年前后有没有入职或职责加重?", option_a: "是,大概就在那段时间", option_b: "有类似,但年份不对或不够重大", option_c: "没有明显发生", option_d: "不记得 / 不确定", options: [ { key: "A", label: "是,大概就在那段时间", answer_class: "yes" }, { key: "B", label: "有类似,但年份不对或不够重大", answer_class: "weak_yes" }, { key: "C", label: "没有明显发生", answer_class: "no" }, { key: "D", label: "不记得 / 不确定", answer_class: "unsure" }, ], }, probe_id: "p-cd", semantic_key: "career.2019", }, }), }), }), append_agentic_rectification_inference_transition: (_fn, args) => { appendedState = args.p_inference_state; return { transition_id: "99999999-9999-4999-8999-999999999999", result_id: RESULT_ID, revision: (args.p_expected_revision as number) + 1, idempotent: false, decision_state_fingerprint: args.p_decision_state_fingerprint, decision_receipt: { ...snapshot.decision_receipt, inference_state: args.p_inference_state, decision_state_fingerprint: args.p_decision_state_fingerprint, }, }; }, resolve_agentic_rectification_conversation_focus: (_fn, args) => ({ focus_id: args.p_focus_id, status: args.p_status, evidence_id: args.p_evidence_id, idempotent: false, }), }); const tools = toolSet(accounting); const resolved = await (tools["rectification-resolve-focus"] as unknown as ExecutableTool>).execute({ caseId: CASE_ID, focusId: FOCUS_ID, status: "declined", choiceKey: "C", }); assert.equal(resolved.status, "declined"); assert.equal(resolved.evidence_id, null); assert.ok(resolved.inference_state); assert.ok(appendedState); const answers = (appendedState as { answered_probes?: Array<{ answer_class?: string; semantic_key?: string }> }).answered_probes ?? []; assert.equal(answers.some((item) => item.semantic_key === "career.2019" && item.answer_class === "no"), true); const appendCall = accounting.calls.find((call) => call.fn === "append_agentic_rectification_inference_transition"); assert.ok(appendCall); assert.equal(appendCall.args.p_probe_id, "p-cd"); assert.equal(appendCall.args.p_reason, "choice"); assert.equal( accounting.calls.some((call) => call.fn === "persist_agentic_rectification_candidate_v2"), false, ); assert.equal( accounting.calls.some((call) => call.fn === "patch_agentic_rectification_inference_state"), false, ); });