Files
Jyotisha/frontend/tests/rectification-v9-regenerate.test.ts
T

259 lines
7.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,
RECTIFICATION_SKILL_SHA256,
RECTIFICATION_SKILL_SOURCE_COMMIT,
dossierFixture,
fakeAccounting,
} from "./rectification-v9-test-support.ts";
const REQUEST_ID = "88888888-8888-4888-8888-888888888888";
const OLD_REPLY = "旧的 Agent 正文";
const NEW_REPLY = "新的自然回复";
const SKILL_PACKAGE = {
name: "jyotish-birth-time-rectification",
version: "9.0.0",
sha256: RECTIFICATION_SKILL_SHA256,
sourceCommit: RECTIFICATION_SKILL_SOURCE_COMMIT,
packagePath: "skills/jyotish-birth-time-rectification",
status: "active" as const,
resolvedPath: "/repo/skills/jyotish-birth-time-rectification",
};
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() }),
insert_agentic_rectification_skill_run_receipt: () => ({
receipt_id: "99999999-9999-4999-8999-999999999999",
}),
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,
skillPackage: SKILL_PACKAGE,
...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",
"insert_agentic_rectification_skill_run_receipt",
"regenerate_agentic_rectification_turn",
]);
const receipt = accounting.calls.find((call) => call.fn === "insert_agentic_rectification_skill_run_receipt");
assert.deepEqual(receipt?.args, {
p_user_id: USER_ID,
p_case_id: CASE_ID,
p_turn_id: TURN_ID,
p_request_id: REQUEST_ID,
p_run_kind: "regeneration",
p_skill_name: SKILL_PACKAGE.name,
p_skill_version: SKILL_PACKAGE.version,
p_skill_sha256: SKILL_PACKAGE.sha256,
p_source_commit: SKILL_PACKAGE.sourceCommit,
});
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);
});