Files
Jyotisha/frontend/tests/rectification-v9-regenerate.test.ts
T
Jesse_Chen 2e17ccad64
Independent Staging Quality Gate / validate (push) Failing after 30m22s
Independent Staging Quality Gate / publish (push) Has been skipped
fix(rectification): restore message actions and retry receipts
2026-08-14 00:10:22 +08:00

230 lines
6.7 KiB
TypeScript

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<RectificationRegenerationAgent> = {}) {
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<typeof fakeAccounting>["client"],
agent: RectificationRegenerationAgent,
overrides: Partial<Parameters<typeof regenerateV9AssistantTurn>[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<unknown>, 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);
});