f708edf365
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>
204 lines
8.6 KiB
TypeScript
204 lines
8.6 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 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(
|
||
ingestMigration,
|
||
/create or replace function public\.agentic_rectification_evidence_kinds\(\)[\s\S]*?select array\[([\s\S]*?)\]::text\[\]/,
|
||
);
|
||
const sqlDomains = quotedSqlValues(
|
||
ingestMigration,
|
||
/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(isEvidenceDomain("education"), 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.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.1 and the prompt prefers batch ingest", () => {
|
||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.1");
|
||
assert.match(skill, /^version: 10\.0\.1$/m);
|
||
assert.match(skill, /不要对同一句用户消息里的多件事件逐条 propose\+confirm/);
|
||
assert.match(agentSource, /当前轮新事件一律走 rectification-record-evidence-batch/);
|
||
assert.doesNotMatch(agentSource, /分别调用 rectification-propose-evidence 和 rectification-confirm-evidence/);
|
||
});
|