import assert from "node:assert/strict"; import test from "node:test"; import { isToolInputRejection } from "../src/lib/rectification-agentic/v9/host-fallback.ts"; import { mapStreamChunkToActivity } from "../src/lib/rectification-agentic/v9/stream-mapping.ts"; import { engineMessageForReceipt, parseToolActivityDetail, RectificationToolServiceError, safeToolErrorCode, } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts"; import { CASE_ID, FOCUS_ID, TURN_ID, USER_ID, activeFocusFixture, conversationSummaryFixture, dossierFixture, fakeAccounting, receiptHandlers, } from "./rectification-v9-test-support.ts"; type ExecutableTool = { execute(input: unknown): Promise; inputSchema?: { safeParse(value: unknown): { success: boolean } }; }; function asExecutable(tool: unknown): ExecutableTool { return tool as ExecutableTool; } const INVITE_QUESTION_ID = "collect:invite:more"; const INVITE_PROMPT = "还有吗?把能想起来的都说一声。"; const INVITE_SPOKEN = "还有别的记得起来的事情吗?"; const OTHER_CASE_ID = "21111111-1111-4111-8111-111111111111"; function toolsFor(accounting: ReturnType) { return createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never, }); } function inviteFocus() { return { ...activeFocusFixture({ questionId: INVITE_QUESTION_ID, intent: "collect_method_evidence", targetDomain: "other", expectedAnswerSchema: { prompt: INVITE_PROMPT, collect: true, collect_kind: "invite_more", }, }), target_kind: null, }; } function inviteDossier() { return dossierFixture({ evidenceCount: 2, evidence: [ { id: "44444444-4444-4444-8444-444444444445", source_turn_id: TURN_ID, subject: "self", event_kind: "education_start", domain: "education", occurred_from: "2016-09-01", occurred_to: "2016-09-01", date_precision: "month", summary: "2016年9月上大学", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-09-12T05:00:00.000Z", }, { id: "44444444-4444-4444-8444-444444444446", source_turn_id: TURN_ID, subject: "self", event_kind: "education_completion", domain: "education", occurred_from: "2020-06-01", occurred_to: "2020-06-01", date_precision: "month", summary: "2020年6月毕业", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-09-12T05:00:01.000Z", }, ], conversationSummary: conversationSummaryFixture({ activeFocus: inviteFocus() }), }); } function failedReceipts(accounting: ReturnType) { return accounting.calls.filter((call) => call.fn === "insert_agentic_rectification_tool_receipt" && call.args.p_status === "failed" ); } function setFocusArgs(overrides: Record = {}) { return { caseId: CASE_ID, questionId: INVITE_QUESTION_ID, intent: "collect_method_evidence", spokenPrompt: INVITE_SPOKEN, targetKind: "invite_more", ...overrides, }; } async function invoke(tool: ExecutableTool, input: unknown) { try { return { result: await tool.execute(input), error: null }; } catch (error) { return { result: null, error }; } } test("T0/T2: same invite focus is idempotent even when the RPC would conflict", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => inviteDossier(), set_agentic_rectification_conversation_focus: () => { throw new Error("agentic_rectification_focus_idempotency_conflict"); }, }); const execute = asExecutable>(toolsFor(accounting)["rectification-set-focus"]).execute; const result = await execute(setFocusArgs()); assert.equal(result.error, undefined); assert.equal(result.status, "active"); assert.equal(result.idempotent, true); assert.equal(result.question_id, INVITE_QUESTION_ID); assert.equal(result.focus_id, FOCUS_ID); assert.equal(result.target_kind, null); assert.equal(failedReceipts(accounting).length, 0); }); test("targetKind kind_hint is accepted and clamped to null", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => inviteDossier(), set_agentic_rectification_conversation_focus: () => { throw new Error("set-focus must not reject invite_more by writing a new row"); }, }); const tools = toolsFor(accounting); const setFocus = asExecutable>(tools["rectification-set-focus"]); assert.equal(setFocus.inputSchema?.safeParse(setFocusArgs()).success, true); assert.equal(setFocus.inputSchema?.safeParse(setFocusArgs({ targetKind: "targeted:family" })).success, true); const result = await setFocus.execute(setFocusArgs({ targetKind: "invite_more" })); assert.equal(result.idempotent, true); assert.equal(result.target_kind, null); }); test("tool-internal throws write distinguishable failed receipts; unknown stays tool_failed", async () => { assert.equal( safeToolErrorCode(new RectificationToolServiceError("invalid_case_id")), "invalid_case_id", ); assert.equal( safeToolErrorCode(new RectificationToolServiceError("invalid_domain")), "invalid_domain", ); assert.equal( safeToolErrorCode(new RectificationToolServiceError("invalid_event_kind")), "invalid_event_kind", ); assert.equal(safeToolErrorCode(new Error("connection refused")), "tool_failed"); assert.equal( engineMessageForReceipt(new Error(`boom ${CASE_ID} user@example.com`)), "boom [id] [email]", ); const warns: string[] = []; const original = console.warn; console.warn = (...args: unknown[]) => { warns.push(args.map(String).join(" ")); }; try { const caseAccounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => inviteDossier(), }); const setFocus = asExecutable(toolsFor(caseAccounting)["rectification-set-focus"]); const schemaRejected = await invoke(setFocus, setFocusArgs({ caseId: "not-a-uuid" })); assert.equal(isToolInputRejection(schemaRejected.result), true); assert.equal(failedReceipts(caseAccounting).length, 0); const wrongCase = await invoke(setFocus, setFocusArgs({ caseId: OTHER_CASE_ID })); assert.equal( wrongCase.error instanceof RectificationToolServiceError && wrongCase.error.code, "invalid_case_id", ); const caseFailed = failedReceipts(caseAccounting); assert.equal(caseFailed[0]?.args.p_safe_error_code, "invalid_case_id"); assert.match(String(caseFailed[0]?.args.p_result_fingerprint), /invalid_case_id/); assert.match(String(caseFailed[0]?.args.p_result_fingerprint), /engine_message/); assert.match(warns.join("\n"), /rectification_tool_failed/); assert.match(warns.join("\n"), /invalid_case_id/); const domainAccounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => inviteDossier(), }); const domainFocus = asExecutable(toolsFor(domainAccounting)["rectification-set-focus"]); const domainResult = await invoke(domainFocus, setFocusArgs({ targetDomain: "not_a_domain" })); assert.equal( domainResult.error instanceof RectificationToolServiceError && domainResult.error.code, "invalid_domain", ); assert.equal(failedReceipts(domainAccounting)[0]?.args.p_safe_error_code, "invalid_domain"); const kindAccounting = fakeAccounting({ ...receiptHandlers }); const propose = asExecutable(toolsFor(kindAccounting)["rectification-propose-evidence"]); const kindRejected = await invoke(propose, { caseId: CASE_ID, quote: "2016年9月上大学", proposedKind: "invite_more", subject: "self", domain: "education", datePrecision: "month", occurredFrom: "2016-09", summary: "2016年9月上大学", }); assert.equal(isToolInputRejection(kindRejected.result), true); } finally { console.warn = original; } const detail = parseToolActivityDetail({ error: "tool_failed", result_fingerprint: JSON.stringify({ safe_error_code: "tool_failed", engine_message: "connection refused", }), }); assert.equal(detail?.safe_error_code, "tool_failed"); assert.equal(detail?.engine_message, "connection refused"); }); test("tool-error activity keeps invalid_case_id instead of collapsing to tool_failed", () => { assert.deepEqual( mapStreamChunkToActivity({ type: "tool-error", payload: { toolName: "rectification-set-focus", error: new Error("Rectification tool service error: invalid_case_id"), }, } as never), { type: "tool.activity", tool: "rectification-set-focus", status: "failed", code: "invalid_case_id", }, ); assert.equal( mapStreamChunkToActivity({ type: "tool-error", payload: { toolName: "rectification-set-focus", error: new Error("Rectification tool service error: invalid_event_kind"), }, } as never)?.code, "invalid_event_kind", ); });