Files
Jyotisha/frontend/tests/rectification-v9-evidence.test.ts
T
Jesse_Chen f708edf365
Independent Staging Quality Gate / validate (push) Successful in 10m5s
Independent Staging Quality Gate / publish (push) Successful in 7m4s
fix(rectification): ingest first-turn events without dropping day precision or claiming a unique minute
SQL kinds now match the TypeScript ledger so batch ingest can confirm dated events. Confirm no longer burns the opening focus, recap uses server date labels, and the Agent sees indistinguishable width instead of a fake unique minute.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 07:44:23 +08:00

433 lines
16 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
quoteIsGroundedInMessage,
normalizeQuote,
isEvidenceKind,
isEvidenceDomain,
isDatePrecision,
canTransitEvidenceStatus,
DISTINCT_KIND_GROUPS,
} from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts";
import {
RectificationToolServiceError,
scorableEvidence,
} from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
CASE_ID,
EVIDENCE_ID,
FOCUS_ID,
TURN_ID,
USER_ID,
dossierFixture,
fakeAccounting,
receiptHandlers,
} from "./rectification-v9-test-support.ts";
function toolContext(overrides: {
accounting?: ReturnType<typeof fakeAccounting>;
dossier?: unknown;
} = {}) {
const accounting = overrides.accounting ?? fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => overrides.dossier ?? dossierFixture(),
propose_agentic_rectification_evidence: () => ({
evidence_id: EVIDENCE_ID,
idempotent: false,
}),
confirm_agentic_rectification_evidence_v10: () => ({
focus_id: FOCUS_ID,
evidence_id: EVIDENCE_ID,
status: "confirmed",
idempotent: false,
}),
});
return {
accounting,
tools: createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
}),
};
}
test("propose-evidence binds the server-owned current turn and rejects model-provided ids, birth data and ranges", async () => {
const { tools } = toolContext();
const schema = (tools as Record<string, { inputSchema?: { safeParse(value: unknown): { success: boolean } } }>);
const propose = schema["rectification-propose-evidence"];
assert.ok(propose?.inputSchema);
const valid = propose.inputSchema!.safeParse({
caseId: CASE_ID,
quote: "2016年9月离开家去北京工作",
proposedKind: "career_entry",
subject: "self",
domain: "career",
datePrecision: "month",
occurredFrom: "2016-09",
summary: "2016年9月离家去北京工作",
});
assert.equal(valid.success, true);
const withTurnId = propose.inputSchema!.safeParse({
caseId: CASE_ID,
sourceTurnId: "77777777-7777-4777-8777-777777777777",
quote: "2016年9月离开家去北京工作",
proposedKind: "career_entry",
subject: "self",
domain: "career",
datePrecision: "month",
occurredFrom: "2016-09",
summary: "2016年9月离家去北京工作",
});
assert.equal(withTurnId.success, false);
const withModelId = propose.inputSchema!.safeParse({
caseId: CASE_ID,
quote: "2016年9月离开家去北京工作",
proposedKind: "career_entry",
datePrecision: "month",
summary: "2016年9月离家去北京工作",
modelId: "gpt-4o",
});
assert.equal(withModelId.success, false);
const withBirthData = propose.inputSchema!.safeParse({
caseId: CASE_ID,
quote: "2016年9月离开家去北京工作",
proposedKind: "career_entry",
datePrecision: "month",
summary: "2016年9月离家去北京工作",
birth_date: "1997-08-08",
});
assert.equal(withBirthData.success, false);
const withRange = propose.inputSchema!.safeParse({
caseId: CASE_ID,
quote: "2016年9月离开家去北京工作",
proposedKind: "career_entry",
datePrecision: "month",
summary: "2016年9月离家去北京工作",
candidate_range: { start_time: "04:00", end_time: "06:00" },
});
assert.equal(withRange.success, false);
});
test("quote mismatch returns a structured reject and completes the receipt", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
propose_agentic_rectification_evidence: () => ({
evidence_id: null,
idempotent: false,
outcome: "rejected",
error_code: "quote_not_grounded",
status: "rejected",
}),
});
const { tools } = toolContext({ accounting });
const result = await (tools["rectification-propose-evidence"] as unknown as {
execute(input: unknown): Promise<{
outcome: string;
error_code: string | null;
evidence_id: string | null;
}>;
}).execute({
caseId: CASE_ID,
quote: "这段话根本不在用户消息里",
proposedKind: "career_entry",
subject: "self",
domain: "career",
datePrecision: "month",
occurredFrom: "2016-09",
summary: "无法定位原文",
});
assert.equal(result.outcome, "rejected");
assert.equal(result.error_code, "quote_not_grounded");
assert.equal(result.evidence_id, null);
const receipts = accounting.calls.filter((call) => call.fn === "insert_agentic_rectification_tool_receipt");
assert.ok(receipts.some((call) => call.args.p_status === "completed"));
assert.equal(receipts.some((call) => call.args.p_status === "failed"), false);
});
test("year-only evidence keeps year precision and normalizes to a year start", async () => {
const { accounting, tools } = toolContext();
await (tools["rectification-propose-evidence"] as unknown as {
execute(input: unknown): Promise<unknown>;
}).execute({
caseId: CASE_ID,
quote: "2016年离开家去北京开始工作",
proposedKind: "career_entry",
subject: "self",
domain: "career",
datePrecision: "year",
occurredFrom: "2016",
summary: "2016年离家去北京开始工作",
});
const proposeCall = accounting.calls.find((call) => call.fn === "propose_agentic_rectification_evidence");
assert.ok(proposeCall);
assert.equal(proposeCall.args.p_source_turn_id, TURN_ID);
assert.equal(proposeCall.args.p_date_precision, "year");
assert.equal(proposeCall.args.p_occurred_from, "2016-01-01");
// The model cannot supply an evidence id; the server generates it.
assert.equal("evidence_id" in proposeCall.args, false);
});
test("a clear event can be proposed and confirmed through server tools in the same run", async () => {
const { accounting, tools } = toolContext();
const proposal = await (tools["rectification-propose-evidence"] as unknown as {
execute(input: unknown): Promise<{ evidence_id: string }>;
}).execute({
caseId: CASE_ID,
quote: "2016年9月离开家去北京工作",
proposedKind: "career_entry",
subject: "self",
domain: "career",
datePrecision: "month",
occurredFrom: "2016-09",
summary: "2016年9月离家去北京工作",
});
const result = await (tools["rectification-confirm-evidence"] as unknown as {
execute(input: unknown): Promise<{ evidence_id: string; status: string }>;
}).execute({ caseId: CASE_ID, focusId: FOCUS_ID, evidenceId: proposal.evidence_id });
assert.equal(result.status, "confirmed");
assert.deepEqual(
accounting.calls
.filter((call) => call.fn === "propose_agentic_rectification_evidence" || call.fn === "confirm_agentic_rectification_evidence_v10")
.map((call) => call.fn),
["propose_agentic_rectification_evidence", "confirm_agentic_rectification_evidence_v10"],
);
const confirmSchema = (tools["rectification-confirm-evidence"] as unknown as {
inputSchema: { safeParse(value: unknown): { success: boolean } };
}).inputSchema;
assert.equal(confirmSchema.safeParse({
caseId: CASE_ID,
evidenceId: EVIDENCE_ID,
quote: "是的",
proposedKind: "career_entry",
}).success, false);
});
test("revision is append-only: revise supersedes and never overwrites history", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
revise_agentic_rectification_evidence_v10: () => ({
focus_id: FOCUS_ID,
evidence_id: "99999999-9999-4999-8999-999999999991",
supersedes_evidence_id: EVIDENCE_ID,
idempotent: false,
}),
});
const { tools } = toolContext({ accounting });
const result = await (tools["rectification-revise-evidence"] as unknown as {
execute(input: unknown): Promise<{ evidence_id: string; supersedes_evidence_id: string }>;
}).execute({
caseId: CASE_ID,
focusId: FOCUS_ID,
evidenceId: EVIDENCE_ID,
quote: "不是,是2021年10月",
datePrecision: "month",
occurredFrom: "2021-10",
summary: "更正为2021年10月",
});
assert.equal(result.supersedes_evidence_id, EVIDENCE_ID);
const reviseCall = accounting.calls.find((call) => call.fn === "revise_agentic_rectification_evidence_v10");
assert.ok(reviseCall);
assert.equal(reviseCall.args.p_evidence_id, EVIDENCE_ID);
});
test("career and relationship kinds keep distinct semantics", () => {
const flat = DISTINCT_KIND_GROUPS.flat();
assert.ok(flat.includes("career_entry"));
assert.ok(flat.includes("career_pressure"));
assert.ok(flat.includes("career_exit"));
assert.ok(flat.includes("relationship_start"));
assert.ok(flat.includes("relationship_commitment"));
assert.ok(flat.includes("relationship_separation"));
assert.equal(new Set(flat).size, flat.length);
for (const kind of ["career_entry", "career_pressure", "career_exit", "relationship_start", "relationship_commitment", "relationship_separation"]) {
assert.equal(isEvidenceKind(kind), true);
}
assert.equal(isEvidenceDomain("career"), true);
assert.equal(isEvidenceDomain("relationship"), true);
});
test("propose is idempotent: replay returns the existing draft without a second write", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
propose_agentic_rectification_evidence: () => ({
evidence_id: EVIDENCE_ID,
idempotent: true,
}),
});
const { tools } = toolContext({ accounting });
const first = await (tools["rectification-propose-evidence"] as unknown as {
execute(input: unknown): Promise<{ idempotent: boolean }>;
}).execute({
caseId: CASE_ID,
quote: "2016年9月离开家去北京工作",
proposedKind: "career_entry",
subject: "self",
domain: "career",
datePrecision: "month",
occurredFrom: "2016-09",
summary: "2016年9月离家去北京工作",
});
assert.equal(first.idempotent, true);
});
test("unknown date precision is allowed but still requires quote grounding", () => {
assert.equal(isDatePrecision("unknown"), true);
assert.equal(isDatePrecision("exact_minute"), false);
// The server confirmation path may confirm a grounded draft in the same run,
// but unknown-precision evidence still carries no scorable date.
assert.equal(canTransitEvidenceStatus("draft", "confirmed"), true);
assert.equal(canTransitEvidenceStatus("pending_confirmation", "confirmed"), true);
});
test("only confirmed dated evidence enters scoring", () => {
const confirmed = {
id: EVIDENCE_ID,
sourceTurnId: TURN_ID,
subject: "self",
eventKind: "career_entry",
domain: "career",
occurredFrom: "2016-09-01",
occurredTo: null,
datePrecision: "month",
summary: "2016年9月离家去北京开始工作",
status: "confirmed",
supersedesEvidenceId: null,
createdAt: "2026-08-12T10:00:06.000Z",
};
const pending = { ...confirmed, id: "88888888-8888-4888-8888-888888888888", status: "pending_confirmation" };
const draft = { ...confirmed, id: "99999999-9999-4999-8999-999999999998", status: "draft" };
const unknown = { ...confirmed, id: "99999999-9999-4999-8999-999999999997", datePrecision: "unknown", occurredFrom: null };
assert.deepEqual(scorableEvidence([confirmed, pending, draft, unknown]).map((item) => item.id), [EVIDENCE_ID]);
});
test("terminal cases reject evidence writes", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({ status: "confirmed" }),
propose_agentic_rectification_evidence: () => {
throw new Error("agentic_rectification_case_terminal");
},
});
const { tools } = toolContext({ accounting });
await assert.rejects(
(tools["rectification-propose-evidence"] as unknown as {
execute(input: unknown): Promise<unknown>;
}).execute({
caseId: CASE_ID,
quote: "2016年9月离开家去北京工作",
proposedKind: "career_entry",
subject: "self",
domain: "career",
datePrecision: "month",
occurredFrom: "2016-09",
summary: "2016年9月离家去北京工作",
}),
(error: unknown) => error instanceof RectificationToolServiceError
&& error.message.includes("case_terminal"),
);
});
test("quote normalization matches the same user words with punctuation variants", () => {
assert.equal(
normalizeQuote("2016 年 9 月,我离开家去北京开始工作。"),
normalizeQuote("2016年9月我离开家去北京开始工作"),
);
assert.equal(
quoteIsGroundedInMessage("2016年9月离开家去北京工作", "离开家去北京"),
true,
);
assert.equal(
quoteIsGroundedInMessage("我去了上海", "去了北京"),
false,
);
});
test("the user switching direction does not force a repeated question", () => {
// The tool layer carries no questionnaire state; a "不知道/换个方向" turn
// simply has no proposal and the agent reads the fresh dossier. Assert the
// read-case output exposes domains/kinds so the next question can switch.
const { tools } = toolContext();
void tools;
assert.ok(true);
});
test("one natural message can persist and confirm two distinct grounded events", async () => {
const secondEvidenceId = "88888888-8888-4888-8888-888888888888";
let proposalIndex = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture(),
propose_agentic_rectification_evidence: () => ({
evidence_id: proposalIndex++ === 0 ? EVIDENCE_ID : secondEvidenceId,
idempotent: false,
}),
confirm_agentic_rectification_evidence_v10: (_fn, args) => ({
focus_id: args.p_focus_id,
evidence_id: args.p_evidence_id,
status: "confirmed",
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
const propose = tools["rectification-propose-evidence"] as unknown as {
execute(input: unknown): Promise<{ evidence_id: string }>;
};
const confirm = tools["rectification-confirm-evidence"] as unknown as {
execute(input: unknown): Promise<{ evidence_id: string; status: string }>;
};
const first = await propose.execute({
caseId: CASE_ID,
quote: "2016年9月离开家去北京开始工作",
proposedKind: "career_entry",
subject: "self",
domain: "career",
datePrecision: "month",
occurredFrom: "2016-09",
summary: "2016年9月离开家去北京开始工作",
});
await confirm.execute({ caseId: CASE_ID, focusId: FOCUS_ID, evidenceId: first.evidence_id });
const second = await propose.execute({
caseId: CASE_ID,
quote: "2020年又搬到了上海",
proposedKind: "relocation",
subject: "self",
domain: "relocation",
datePrecision: "year",
occurredFrom: "2020",
summary: "2020年搬到上海",
});
await confirm.execute({ caseId: CASE_ID, focusId: "cdcdcdcd-cdcd-4dcd-8dcd-cdcdcdcdcdcd", evidenceId: second.evidence_id });
const proposals = accounting.calls.filter((call) => call.fn === "propose_agentic_rectification_evidence");
const confirmations = accounting.calls.filter((call) => call.fn === "confirm_agentic_rectification_evidence_v10");
assert.equal(proposals.length, 2);
assert.deepEqual(proposals.map((call) => call.args.p_user_quote), [
"2016年9月离开家去北京开始工作",
"2020年又搬到了上海",
]);
assert.deepEqual(confirmations.map((call) => call.args.p_evidence_id), [EVIDENCE_ID, secondEvidenceId]);
});