Files
Jyotisha/frontend/tests/rectification-v10-conversation-focus.test.ts

366 lines
12 KiB
TypeScript

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<T = unknown> = {
execute(input: unknown): Promise<T>;
};
const SECOND_EVIDENCE_ID = "45454545-4545-4454-8454-454545454545";
function toolSet(accounting: ReturnType<typeof fakeAccounting>) {
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<Record<string, unknown>>).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<Record<string, unknown>>).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<Record<string, unknown>>;
const secondItems = calls[1]!.args.p_items as Array<Record<string, unknown>>;
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),
);
});