Files
Jyotisha/frontend/tests/rectification-v10-tool-contract.test.ts
T
Jesse_ChenandClaude Opus 5.5 3bbe65a215
Independent Staging Quality Gate / validate (push) Successful in 13m37s
Independent Staging Quality Gate / publish (push) Successful in 3m37s
fix(rectification): out-of-window event dates no longer fail every rescore; future dates refused at intake; corrections revise without a focus (BUG-1178, BUG-1179)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-10-02 09:03:22 +08:00

266 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { PUBLIC_RECTIFICATION_TOOLS } from "../src/lib/rectification-agentic/v9/public-receipt.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { createRectificationV9AgentTools, createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts";
import {
OPEN_ENGINE_CAPABILITY_CEILING,
CASE_ID,
EVIDENCE_ID,
FOCUS_ID,
RESULT_ID,
CANDIDATE_ID,
TURN_ID,
USER_ID,
fakeAccounting,
} from "./rectification-v9-test-support.ts";
type ToolSchema = {
inputSchema: {
safeParse(value: unknown): { success: boolean };
};
};
const EXACT_TOOL_KEYS = [
"rectification-read-case",
"rectification-set-focus",
"rectification-resolve-focus",
"rectification-record-evidence-batch",
"rectification-propose-evidence",
"rectification-confirm-evidence",
"rectification-revise-evidence",
"rectification-compare-candidates",
"rectification-read-diagnostics",
"rectification-offer-candidates",
"rectification-accept-candidate",
"rectification-confirm-birth-time",
"rectification-stop-and-review",
"rectification-close-case",
] as const;
function toolsUnderTest() {
return createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: fakeAccounting({}).client as never,
});
}
const validInputs: Record<(typeof EXACT_TOOL_KEYS)[number], Record<string, unknown>> = {
"rectification-read-case": { caseId: CASE_ID },
"rectification-set-focus": {
caseId: CASE_ID,
questionId: "career-month-question",
intent: "clarify_event_date",
spokenPrompt: "2016年前后,你是哪年上的大学?",
targetEvidenceId: EVIDENCE_ID,
targetDomain: "career",
targetKind: "career_entry",
expectedAnswerSchema: { required: ["month"] },
},
"rectification-resolve-focus": {
caseId: CASE_ID,
focusId: FOCUS_ID,
status: "resolved",
evidenceId: EVIDENCE_ID,
},
"rectification-record-evidence-batch": {
caseId: CASE_ID,
focusId: FOCUS_ID,
items: [{
quote: "2016年9月去北京工作",
proposedKind: "career_entry",
subject: "self",
domain: "career",
datePrecision: "month",
occurredFrom: "2016-09",
summary: "2016年9月去北京工作",
}],
},
"rectification-propose-evidence": {
caseId: CASE_ID,
quote: "2016年9月去北京工作",
proposedKind: "career_entry",
subject: "self",
domain: "career",
datePrecision: "month",
occurredFrom: "2016-09",
summary: "2016年9月去北京工作",
},
"rectification-confirm-evidence": {
caseId: CASE_ID,
focusId: FOCUS_ID,
evidenceId: EVIDENCE_ID,
},
"rectification-revise-evidence": {
caseId: CASE_ID,
focusId: FOCUS_ID,
evidenceId: EVIDENCE_ID,
quote: "不是9月,是10月",
datePrecision: "month",
occurredFrom: "2016-10",
summary: "更正为2016年10月去北京工作",
},
"rectification-compare-candidates": { caseId: CASE_ID },
"rectification-read-diagnostics": { caseId: CASE_ID },
"rectification-offer-candidates": { caseId: CASE_ID },
"rectification-accept-candidate": {
caseId: CASE_ID,
resultId: RESULT_ID,
candidateId: CANDIDATE_ID,
},
"rectification-confirm-birth-time": {
caseId: CASE_ID,
resultId: RESULT_ID,
candidateId: CANDIDATE_ID,
consentQuote: "就用05:02",
},
"rectification-stop-and-review": { caseId: CASE_ID },
"rectification-close-case": {
caseId: CASE_ID,
reason: "completed_by_user",
},
};
test("V10 exposes exactly the 14 allowlisted rectification tool keys", () => {
const tools = toolsUnderTest();
assert.deepEqual(Object.keys(tools), [...EXACT_TOOL_KEYS]);
assert.deepEqual([...PUBLIC_RECTIFICATION_TOOLS], [...EXACT_TOOL_KEYS]);
assert.equal(new Set(Object.keys(tools)).size, 14);
});
test("all 14 tool input schemas accept their minimal contract and reject unknown fields", () => {
const tools = toolsUnderTest() as unknown as Record<string, ToolSchema>;
for (const toolName of EXACT_TOOL_KEYS) {
const schema = tools[toolName]?.inputSchema;
assert.ok(schema, `${toolName} must expose inputSchema`);
assert.equal(
schema.safeParse(validInputs[toolName]).success,
true,
`${toolName} must accept its documented input`,
);
assert.equal(
schema.safeParse({ ...validInputs[toolName], userId: USER_ID }).success,
false,
`${toolName} must be strict and reject server-owned userId`,
);
assert.equal(
schema.safeParse({ ...validInputs[toolName], candidate_range: { start_time: "04:50", end_time: "05:10" } }).success,
false,
`${toolName} must reject server-owned candidate ranges`,
);
}
});
test("focus/evidence mutation schemas require durable focus and evidence references", () => {
const tools = toolsUnderTest() as unknown as Record<string, ToolSchema>;
const confirm = tools["rectification-confirm-evidence"].inputSchema;
const revise = tools["rectification-revise-evidence"].inputSchema;
const resolve = tools["rectification-resolve-focus"].inputSchema;
assert.equal(confirm.safeParse({ caseId: CASE_ID, evidenceId: EVIDENCE_ID }).success, true);
assert.equal(confirm.safeParse({ caseId: CASE_ID, focusId: FOCUS_ID }).success, false);
// BUG-1179 — 原值: revise 缺 focusId 时 schema 拒绝(false) / 新值: 允许省略 focusId(true),但仍必须带
// evidenceId / 原因: 产品 10-02 选 A,用户随口更正打错的年份时没有对应的问题焦点;「改动必须来自用户
// 原话」改由服务端校验 quote 落在本轮用户消息里来保证(见 rectification-v9-evidence 用例)。
assert.equal(revise.safeParse({
...validInputs["rectification-revise-evidence"],
focusId: undefined,
}).success, true);
assert.equal(revise.safeParse({
...validInputs["rectification-revise-evidence"],
evidenceId: undefined,
}).success, false);
assert.equal(resolve.safeParse({ caseId: CASE_ID, status: "declined" }).success, false);
});
test("batch item schemas are independently strict and bounded", () => {
const tools = toolsUnderTest() as unknown as Record<string, ToolSchema>;
const schema = tools["rectification-record-evidence-batch"].inputSchema;
const valid = validInputs["rectification-record-evidence-batch"];
const item = (valid.items as Record<string, unknown>[])[0]!;
assert.equal(schema.safeParse({ ...valid, items: [] }).success, false);
assert.equal(schema.safeParse({
...valid,
items: [{ ...item, evidenceId: EVIDENCE_ID }],
}).success, false);
assert.equal(schema.safeParse({
...valid,
items: Array.from({ length: 13 }, () => item),
}).success, false);
});
test("evidence kind and domain schemas enumerate legal values so education is not a proposedKind", () => {
const tools = toolsUnderTest() as unknown as Record<string, ToolSchema>;
const propose = tools["rectification-propose-evidence"].inputSchema;
const batch = tools["rectification-record-evidence-batch"].inputSchema;
const validPropose = validInputs["rectification-propose-evidence"];
const validBatch = validInputs["rectification-record-evidence-batch"];
const item = (validBatch.items as Record<string, unknown>[])[0]!;
assert.equal(propose.safeParse(validPropose).success, true);
assert.equal(propose.safeParse({ ...validPropose, proposedKind: "education" }).success, false);
assert.equal(propose.safeParse({ ...validPropose, proposedKind: "education_start" }).success, true);
assert.equal(batch.safeParse({
...validBatch,
items: [{ ...item, proposedKind: "education" }],
}).success, false);
assert.equal(batch.safeParse({
...validBatch,
items: [{ ...item, proposedKind: "education_start", domain: "education" }],
}).success, true);
});
test("agent tools omit offer-candidates when proposeAllowed is false", () => {
const tools = createRectificationV9AgentTools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: fakeAccounting({}).client as never,
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: [],
}));
assert.equal("rectification-set-focus" in tools, true);
assert.equal("rectification-read-case" in tools, true);
assert.equal("rectification-offer-candidates" in tools, false);
assert.equal("rectification-accept-candidate" in tools, false);
assert.equal("rectification-confirm-birth-time" in tools, false);
});
test("agent tools include set-focus so spokenPrompt can be written", () => {
const tools = createRectificationV9AgentTools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: fakeAccounting({}).client as never,
});
assert.equal("rectification-set-focus" in tools, true);
assert.deepEqual(
Object.keys(tools).sort(),
[...EXACT_TOOL_KEYS].slice().sort(),
);
const source = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
assert.match(source, /error: "invalid_spoken_prompt"/);
assert.match(source, /retryable: spokenPromptFailures < 2/);
assert.match(source, /askedTurnId: turnId/);
});
test("evidence batch items may use source offsets instead of a rewritten quote", () => {
const tools = toolsUnderTest() as unknown as Record<string, ToolSchema>;
const schema = tools["rectification-record-evidence-batch"].inputSchema;
const valid = validInputs["rectification-record-evidence-batch"];
const item = { ...(valid.items as Record<string, unknown>[])[0]! };
delete item.quote;
assert.equal(schema.safeParse({
...valid,
items: [{ ...item, quoteStart: 0, quoteEnd: 11 }],
}).success, true);
});