84e6191fc2
Co-authored-by: Cursor <cursoragent@cursor.com>
231 lines
9.7 KiB
TypeScript
231 lines
9.7 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import { fileURLToPath } from "node:url";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
DATE_PRECISIONS,
|
||
EVIDENCE_DOMAINS,
|
||
EVIDENCE_KINDS,
|
||
displayDateLabel,
|
||
isEvidenceDomain,
|
||
isEvidenceKind,
|
||
quoteIsGroundedInMessage,
|
||
} from "../src/lib/rectification-agentic/v9/evidence-model.ts";
|
||
import {
|
||
confirmationAllowedForWidth,
|
||
indistinguishableWidthMinutes,
|
||
} from "../src/lib/rectification-agentic/v9/candidate-plateau.ts";
|
||
import { RECTIFICATION_POLICY } from "../src/lib/rectification-policy.ts";
|
||
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||
import { createRectificationV9Tools, latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
|
||
import {
|
||
CASE_ID,
|
||
CANDIDATE_ID,
|
||
EVIDENCE_ID,
|
||
FOCUS_ID,
|
||
RESULT_ID,
|
||
SECOND_CANDIDATE_ID,
|
||
TURN_ID,
|
||
USER_ID,
|
||
candidateSnapshotFixture,
|
||
computeFixture,
|
||
dossierFixture,
|
||
fakeAccounting,
|
||
receiptHandlers,
|
||
} from "./rectification-v9-test-support.ts";
|
||
|
||
const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883";
|
||
const ingestMigration = readFileSync(
|
||
new URL("../supabase/migrations/20260819010000_rectification_ingest_precision_plateau.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const occupationHoraryMigration = readFileSync(
|
||
new URL("../supabase/migrations/20260820040000_rectification_occupation_horary.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const agentSource = readFileSync(
|
||
new URL("../src/mastra/agentic-rectification.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const skill = readFileSync(
|
||
new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const evidenceModel = readFileSync(
|
||
new URL("../../skills/jyotish-birth-time-rectification/references/evidence-model.md", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const candidateComparison = readFileSync(
|
||
new URL("../../skills/jyotish-birth-time-rectification/references/candidate-comparison.md", import.meta.url),
|
||
"utf8",
|
||
);
|
||
|
||
function quotedSqlValues(source: string, pattern: RegExp): string[] {
|
||
const match = source.match(pattern);
|
||
assert.ok(match?.[1], `missing SQL allowlist: ${pattern}`);
|
||
return [...match[1].matchAll(/'([^']+)'/g)].map((value) => value[1]);
|
||
}
|
||
|
||
test("SQL kind and domain helpers cover the TypeScript evidence allowlists", () => {
|
||
const sqlKinds = quotedSqlValues(
|
||
occupationHoraryMigration,
|
||
/create or replace function public\.agentic_rectification_evidence_kinds\(\)[\s\S]*?select array\[([\s\S]*?)\]::text\[\]/,
|
||
);
|
||
const sqlDomains = quotedSqlValues(
|
||
occupationHoraryMigration,
|
||
/create or replace function public\.agentic_rectification_evidence_domains\(\)[\s\S]*?select array\[([\s\S]*?)\]::text\[\]/,
|
||
);
|
||
const sqlPrecisions = quotedSqlValues(
|
||
ingestMigration,
|
||
/create or replace function public\.agentic_rectification_date_precisions\(\)[\s\S]*?select array\[([\s\S]*?)\]::text\[\]/,
|
||
);
|
||
assert.deepEqual(sqlKinds, [...EVIDENCE_KINDS]);
|
||
assert.deepEqual(sqlDomains, [...EVIDENCE_DOMAINS]);
|
||
assert.deepEqual(sqlPrecisions, [...DATE_PRECISIONS]);
|
||
assert.equal(isEvidenceKind("education_milestone"), true);
|
||
assert.equal(isEvidenceKind("occupation_note"), true);
|
||
assert.equal(isEvidenceKind("horary_query"), true);
|
||
assert.equal(isEvidenceDomain("education"), true);
|
||
assert.equal(isEvidenceDomain("occupation"), true);
|
||
assert.equal(isEvidenceDomain("horary"), true);
|
||
assert.equal(isEvidenceDomain("health_pressure"), true);
|
||
});
|
||
|
||
test("day precision labels stay ISO dates and never collapse to a year sentence", () => {
|
||
assert.equal(displayDateLabel("day", "2024-08-08", null), "2024-08-08");
|
||
assert.equal(displayDateLabel("month", "2024-05-01", null), "2024-05");
|
||
assert.equal(displayDateLabel("year", "2024-01-01", null), "2024年");
|
||
assert.equal(displayDateLabel("range", "2024-01-01", "2024-03-31"), "2024-01-01–2024-03-31");
|
||
assert.doesNotMatch(displayDateLabel("day", "2024-08-08", null), /年份|年$/);
|
||
assert.match(skill, /display_date_label/);
|
||
assert.match(evidenceModel, /禁止把日级格式化成“年份已确定为 YYYY”/);
|
||
});
|
||
|
||
test("ASCII punctuation is stripped for quote grounding without allowing paraphrase", () => {
|
||
assert.equal(quoteIsGroundedInMessage("2016年6月高考结束", "2016年6月高考结束,"), true);
|
||
assert.equal(quoteIsGroundedInMessage("2016年6月高考结束", "那年夏天考完了"), false);
|
||
});
|
||
|
||
test("a 25-minute tied plateau projects width and forbids unique-minute confirmation", () => {
|
||
const candidates = [
|
||
{ candidateId: CANDIDATE_ID, time: "04:45", rank: 1, relativeSupport: 40, tiedMinuteCount: 25 },
|
||
{ candidateId: SECOND_CANDIDATE_ID, time: "04:46", rank: 2, relativeSupport: 35, tiedMinuteCount: 25 },
|
||
{ candidateId: THIRD_CANDIDATE_ID, time: "04:47", rank: 3, relativeSupport: 25, tiedMinuteCount: 25 },
|
||
];
|
||
const width = indistinguishableWidthMinutes(candidates);
|
||
assert.ok(width >= 25);
|
||
assert.equal(confirmationAllowedForWidth(true, width), false);
|
||
assert.equal(RECTIFICATION_POLICY.maxConfirmationWidthMinutes, 5);
|
||
const projection = latestResultToolProjection({
|
||
resultId: RESULT_ID,
|
||
candidates,
|
||
selectionAllowed: true,
|
||
confirmationAllowed: true,
|
||
representativeTime: "04:45",
|
||
selectedTime: null,
|
||
selectionKind: null,
|
||
algorithmVersion: "rectification-v5",
|
||
});
|
||
assert.equal(projection.indistinguishable_width_minutes, width);
|
||
assert.equal(projection.confirmation_allowed, false);
|
||
assert.equal(
|
||
(projection.confirmation_gate as { confirmation_allowed: boolean }).confirmation_allowed,
|
||
false,
|
||
);
|
||
assert.match(candidateComparison, /一段不可分区间/);
|
||
assert.match(candidateComparison, /代表性候选/);
|
||
assert.match(agentSource, /不得说已定位到唯一分钟/);
|
||
});
|
||
|
||
test("read-case evidence context keeps day labels and confirm does not rewrite dates", async () => {
|
||
const accounting = fakeAccounting({
|
||
...receiptHandlers,
|
||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||
evidence: [{
|
||
id: EVIDENCE_ID,
|
||
source_turn_id: TURN_ID,
|
||
subject: "self",
|
||
event_kind: "relationship_end",
|
||
domain: "relationship",
|
||
occurred_from: "2024-08-08",
|
||
occurred_to: null,
|
||
date_precision: "day",
|
||
summary: "2024-08-08 的一件关系变化",
|
||
status: "pending_confirmation",
|
||
supersedes_evidence_id: null,
|
||
created_at: "2026-08-12T10:00:06.000Z",
|
||
}],
|
||
latestResult: {
|
||
...candidateSnapshotFixture({
|
||
confirmationAllowed: true,
|
||
representativeTime: "04:45",
|
||
candidates: [
|
||
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:45", relative_support: 40, tied_minute_count: 25 },
|
||
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:46", relative_support: 35, tied_minute_count: 25 },
|
||
{ candidate_id: THIRD_CANDIDATE_ID, rank: 3, time: "04:47", relative_support: 25, tied_minute_count: 25 },
|
||
],
|
||
}),
|
||
selection_allowed: true,
|
||
confirmation_allowed: true,
|
||
},
|
||
}),
|
||
get_agentic_rectification_case_compute: () => computeFixture(),
|
||
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 projection = await (tools["rectification-read-case"] as unknown as {
|
||
execute(input: unknown): Promise<{
|
||
evidence_context: Array<{ date_precision: string; display_date_label: string }>;
|
||
latest_result: { confirmation_allowed: boolean; indistinguishable_width_minutes: number };
|
||
}>;
|
||
}).execute({ caseId: CASE_ID });
|
||
assert.equal(projection.evidence_context[0]?.date_precision, "day");
|
||
assert.equal(projection.evidence_context[0]?.display_date_label, "2024-08-08");
|
||
assert.doesNotMatch(projection.evidence_context[0]?.display_date_label ?? "", /年份已确定为/);
|
||
assert.ok(projection.latest_result.indistinguishable_width_minutes >= 25);
|
||
assert.equal(projection.latest_result.confirmation_allowed, false);
|
||
|
||
await (tools["rectification-confirm-evidence"] as unknown as {
|
||
execute(input: unknown): Promise<unknown>;
|
||
}).execute({ caseId: CASE_ID, evidenceId: EVIDENCE_ID });
|
||
const confirmCall = accounting.calls.find((call) => call.fn === "confirm_agentic_rectification_evidence_v10");
|
||
assert.ok(confirmCall);
|
||
assert.equal(confirmCall.args.p_focus_id, null);
|
||
assert.equal("p_date_precision" in confirmCall.args, false);
|
||
assert.equal("p_occurred_from" in confirmCall.args, false);
|
||
});
|
||
|
||
test("new-case skill identity is 10.0.10 and the prompt prefers batch ingest", () => {
|
||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.10");
|
||
assert.match(skill, /^version: 10\.0\.10$/m);
|
||
assert.match(skill, /不要对同一句用户消息里的多件事件逐条 propose\+confirm/);
|
||
assert.match(agentSource, /当前轮新事件一律走 rectification-record-evidence-batch/);
|
||
assert.doesNotMatch(agentSource, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
|
||
});
|
||
|
||
test("receipt allowlist includes D5, D7 and D3 public methods", () => {
|
||
const d5d7Migration = readFileSync(
|
||
new URL("../supabase/migrations/20260820020000_rectification_d5_d7_precision.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(d5d7Migration, /'d5-panchamsha'/);
|
||
assert.match(d5d7Migration, /'d7-saptamsha'/);
|
||
assert.match(d5d7Migration, /'d12-dwadashamsha'/);
|
||
const d3Migration = readFileSync(
|
||
new URL("../supabase/migrations/20260820030000_rectification_d3_family.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(d3Migration, /'d3-drekkana'/);
|
||
});
|