import assert from "node:assert/strict"; import test from "node:test"; import { regenerateV9AssistantTurn, type RectificationRegenerationAgent, } from "../src/lib/rectification-agentic/v9/regenerate-turn.ts"; import { RectificationToolServiceError } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { CASE_ID, SESSION_ID, SOURCE_TURN_ID, TURN_ID, USER_ID, dossierFixture, fakeAccounting, } from "./rectification-v9-test-support.ts"; const REQUEST_ID = "88888888-8888-4888-8888-888888888888"; const OLD_REPLY = "旧的 Agent 正文"; const NEW_REPLY = "新的自然回复"; function completedTurns() { return [ { id: TURN_ID, role: "user", text: "2020 年 4 月开始实习", status: "completed", created_at: "2026-08-13T01:00:00.000Z", }, { id: TURN_ID, role: "assistant", text: OLD_REPLY, status: "completed", created_at: "2026-08-13T01:00:00.000Z", }, ]; } function fakeAgent(overrides: Partial = {}) { const calls = { getSkill: 0, generate: 0 }; const agent: RectificationRegenerationAgent = { async getSkill() { calls.getSkill += 1; return { name: "jyotish-birth-time-rectification" }; }, async generate() { calls.generate += 1; return { text: NEW_REPLY }; }, ...overrides, }; return { agent, calls }; } function validAccounting() { return fakeAccounting({ get_agentic_rectification_turn_regeneration: () => null, get_agentic_rectification_case_dossier: () => dossierFixture({ turns: completedTurns() }), regenerate_agentic_rectification_turn: () => ({ ok: true, turn_id: TURN_ID, assistant_message: NEW_REPLY, idempotent: false, }), }); } function options( accounting: ReturnType["client"], agent: RectificationRegenerationAgent, overrides: Partial[0]> = {}, ) { return { userId: USER_ID, caseId: CASE_ID, sessionId: SESSION_ID, turnId: TURN_ID, requestId: REQUEST_ID, accounting, agent, ...overrides, }; } async function rejectsWithCode(promise: Promise, code: string) { await assert.rejects(promise, (error: unknown) => ( error instanceof RectificationToolServiceError && error.code === code )); } test("regeneration replaces the latest completed Assistant text in place", async () => { const accounting = validAccounting(); const { agent, calls } = fakeAgent(); const result = await regenerateV9AssistantTurn(options(accounting.client, agent)); assert.deepEqual(result, { ok: true, turnId: TURN_ID, assistantMessage: NEW_REPLY, idempotent: false, }); assert.equal(calls.getSkill, 1); assert.equal(calls.generate, 1); assert.deepEqual(accounting.calls.map((call) => call.fn), [ "get_agentic_rectification_turn_regeneration", "get_agentic_rectification_case_dossier", "regenerate_agentic_rectification_turn", ]); const update = accounting.calls.at(-1); assert.equal(update?.args.p_assistant_message, NEW_REPLY); assert.equal(accounting.calls.some((call) => call.fn === "append_agentic_rectification_turn"), false); }); test("same request id returns the stored replacement without invoking the Agent again", async () => { const accounting = fakeAccounting({ get_agentic_rectification_turn_regeneration: () => ({ ok: true, turn_id: TURN_ID, assistant_message: NEW_REPLY, idempotent: true, }), }); const { agent, calls } = fakeAgent(); const result = await regenerateV9AssistantTurn(options(accounting.client, agent)); assert.equal(result.idempotent, true); assert.equal(calls.getSkill, 0); assert.equal(calls.generate, 0); assert.deepEqual(accounting.calls.map((call) => call.fn), [ "get_agentic_rectification_turn_regeneration", ]); }); test("terminal cases cannot regenerate replies", async () => { const accounting = fakeAccounting({ get_agentic_rectification_turn_regeneration: () => null, get_agentic_rectification_case_dossier: () => dossierFixture({ status: "closed", turns: completedTurns(), }), }); const { agent, calls } = fakeAgent(); await rejectsWithCode( regenerateV9AssistantTurn(options(accounting.client, agent)), "agentic_rectification_case_terminal", ); assert.equal(calls.generate, 0); }); test("case and session must remain exactly bound", async () => { const accounting = validAccounting(); const { agent, calls } = fakeAgent(); await rejectsWithCode( regenerateV9AssistantTurn(options(accounting.client, agent, { sessionId: "99999999-9999-4999-8999-999999999999", })), "agentic_rectification_case_session_mismatch", ); assert.equal(calls.generate, 0); }); test("only the latest completed Assistant turn can be regenerated", async () => { const turns = [ { id: SOURCE_TURN_ID, role: "assistant", text: "更早的回答", status: "completed", created_at: "2026-08-13T00:00:00.000Z", }, ...completedTurns(), ]; const accounting = fakeAccounting({ get_agentic_rectification_turn_regeneration: () => null, get_agentic_rectification_case_dossier: () => dossierFixture({ turns }), }); const { agent, calls } = fakeAgent(); await rejectsWithCode( regenerateV9AssistantTurn(options(accounting.client, agent, { turnId: SOURCE_TURN_ID })), "agentic_rectification_turn_not_latest", ); assert.equal(calls.generate, 0); assert.equal(accounting.calls.some((call) => call.fn === "regenerate_agentic_rectification_turn"), false); }); test("missing or empty completed Assistant text is rejected", async () => { const accounting = fakeAccounting({ get_agentic_rectification_turn_regeneration: () => null, get_agentic_rectification_case_dossier: () => dossierFixture({ turns: [{ id: TURN_ID, role: "assistant", text: " ", status: "completed", created_at: "2026-08-13T01:00:00.000Z", }], }), }); const { agent, calls } = fakeAgent(); await rejectsWithCode( regenerateV9AssistantTurn(options(accounting.client, agent)), "agentic_rectification_turn_not_found", ); assert.equal(calls.generate, 0); }); test("an empty regenerated body never reaches the update RPC", async () => { const accounting = validAccounting(); const { agent } = fakeAgent({ async generate() { return { text: " " }; }, }); await rejectsWithCode( regenerateV9AssistantTurn(options(accounting.client, agent)), "agentic_rectification_regeneration_empty", ); assert.equal(accounting.calls.some((call) => call.fn === "regenerate_agentic_rectification_turn"), false); });