Files
Jyotisha/frontend/tests/rectification-set-focus-tool-failed-20260912.test.ts
T
Jesse_ChenandCursor 073a45b73e
Independent Staging Quality Gate / validate (push) Failing after 13m36s
Independent Staging Quality Gate / publish (push) Skipped
fix(rectification): keep set-focus idempotent and record tool failures (BUG-659/660)
Same-id focus returns instead of making the model retry; kind_hint targetKind is ignored. Failed receipts keep a redacted original error and known codes. Host fallback recap no longer duplicates dates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 00:23:11 +08:00

276 lines
9.3 KiB
TypeScript

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<T = unknown> = {
execute(input: unknown): Promise<T>;
inputSchema?: { safeParse(value: unknown): { success: boolean } };
};
function asExecutable<T = unknown>(tool: unknown): ExecutableTool<T> {
return tool as ExecutableTool<T>;
}
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<typeof fakeAccounting>) {
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<typeof fakeAccounting>) {
return accounting.calls.filter((call) =>
call.fn === "insert_agentic_rectification_tool_receipt"
&& call.args.p_status === "failed"
);
}
function setFocusArgs(overrides: Record<string, unknown> = {}) {
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<Record<string, unknown>>(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<Record<string, unknown>>(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",
);
});