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, TURN_ID, USER_ID, activeFocusFixture, dossierFixture, fakeAccounting, receiptHandlers, } from "./rectification-v9-test-support.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), ); });