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; }).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'/); });