import assert from "node:assert/strict"; import test from "node:test"; import { boundEngineEventDates, engineRequestBody, type V9EngineEvent } from "../src/lib/rectification-agentic/v9/engine-client.ts"; import { composeSpokenWithServerFacts } from "../src/lib/rectification-agentic/v9/agent-run-support.ts"; import { batchHasFutureDatedItem } from "../src/lib/rectification-agentic/v9/host-fallback.ts"; import { RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts"; import { createRectificationV9Tools, evidenceStartsInFuture } from "../src/mastra/rectification-v9-tools.ts"; import { CASE_ID, FOCUS_ID, TURN_ID, USER_ID, computeFixture, dossierFixture, fakeAccounting, receiptHandlers, } from "./rectification-v9-test-support.ts"; // Real staging case 2026-10-02 (BUG-1178): a typo'd 「2028 年」 event entered the ledger and the // engine (scripts/rectification/contracts.py) rejected every later rescore with // 「events[i] dates must be between birth_date and today」; the card never came. 「今年 / 这个月」 // events (period end after today) failed the same way. const NOW = new Date("2026-10-02T03:00:00Z"); const event = (id: string, start: string, end: string): V9EngineEvent => ({ id, domain: "finance", event_kind: "finance_change", date_start: start, date_end: end, precision: "year", summary: "fictional", date_source: null, date_reliability: null, date_corroboration: null, date_conflict_status: null, source_turn_id: null, subject: "self", } as V9EngineEvent); test("engine events stay inside [birth_date, yesterday]: future dropped, open periods clipped", () => { const bounded = boundEngineEventDates([ event("past", "2014-01-01", "2014-12-31"), event("future", "2028-01-01", "2028-12-31"), event("this-year", "2026-01-01", "2026-12-31"), event("this-month", "2026-10-01", "2026-10-31"), event("before-birth", "1980-01-01", "1980-12-31"), event("straddles-birth", "1995-01-01", "1995-12-31"), ], "1995-06-14", NOW); assert.deepEqual(bounded.map((row) => [row.id, row.date_start, row.date_end]), [ ["past", "2014-01-01", "2014-12-31"], ["this-year", "2026-01-01", "2026-10-01"], ["this-month", "2026-10-01", "2026-10-01"], ["straddles-birth", "1995-06-14", "1995-12-31"], ]); }); test("the engine request body carries only bounded events", () => { const body = engineRequestBody({ baselineBirthSnapshot: { birth_date: "1995-06-14", latitude: 30, longitude: 120, timezone_offset: 8, timezone_id: "Asia/Shanghai" }, candidateRange: { start_time: "14:34", end_time: "15:04" } as never, events: [event("past", "2014-01-01", "2014-12-31"), event("future", "2099-01-01", "2099-12-31")], }); const events = body.events as V9EngineEvent[]; assert.deepEqual(events.map((row) => row.id), ["past"]); assert.throws(() => engineRequestBody({ baselineBirthSnapshot: { birth_date: "1995-06-14", latitude: 30, longitude: 120, timezone_offset: 8 }, candidateRange: { start_time: "14:34", end_time: "15:04" } as never, events: [event("future", "2099-01-01", "2099-12-31")], }), /no scorable evidence/); }); test("intake: an item starting after today is future-dated; this year is not", () => { assert.equal(evidenceStartsInFuture({ occurredFrom: "2028" }, NOW), true); assert.equal(evidenceStartsInFuture({ occurredFrom: "2026-11" }, NOW), true); assert.equal(evidenceStartsInFuture({ occurredFrom: "2026" }, NOW), false); assert.equal(evidenceStartsInFuture({ occurredFrom: "2014-12" }, NOW), false); assert.equal(evidenceStartsInFuture({ occurredFrom: null, occurredTo: null }, NOW), false); }); test("batch: the future item is not written, the rest keep their own index and recap", async () => { const quote = "2099年借出一笔钱,2014年年底拔牙"; const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ latestResult: null }), get_agentic_rectification_case_compute: () => computeFixture(), record_agentic_rectification_evidence_batch: (_fn, args) => { const items = Array.isArray(args.p_items) ? args.p_items as Array> : []; return { items: items.map((_item, index) => ({ index, outcome: "accepted", evidence_id: "77777777-7777-4777-8777-777777777777", status: "confirmed", idempotent: false, clarification_fields: [], error_code: null, })), accepted_count: items.length, needs_clarification_count: 0, rejected_count: 0, focus_id: FOCUS_ID, }; }, }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, userMessage: quote, accounting: accounting.client as never, }); const result = await (tools["rectification-record-evidence-batch"] as unknown as { execute(input: unknown): Promise<{ items: Array<{ index: number; outcome: string; error_code: string | null }>; accepted_recaps: Array<{ display_date_label?: string; event_phrase?: string }>; accepted_count: number; }>; }).execute({ caseId: CASE_ID, items: [ { quote: "2099年借出一笔钱", proposedKind: "finance_change", subject: "self", domain: "finance", datePrecision: "year", occurredFrom: "2099", summary: "借出一笔钱" }, { quote: "2014年年底拔牙", proposedKind: "self_health_event", subject: "self", domain: "health", datePrecision: "month", occurredFrom: "2014-12", summary: "2014年年底拔牙" }, ], }); const write = accounting.calls.find((call) => call.fn === "record_agentic_rectification_evidence_batch"); const written = (write?.args.p_items ?? []) as Array>; assert.equal(written.length, 1, "the 2099 item is never written"); assert.equal(written[0]?.occurred_from, "2014-12-01"); assert.deepEqual(result.items.map((item) => [item.index, item.outcome, item.error_code]), [ [0, "rejected", "future_date"], [1, "accepted", null], ]); assert.equal(result.accepted_count, 1); assert.equal(result.accepted_recaps.length, 1, "the recap reads the recorded row, not the rejected slot"); assert.equal(batchHasFutureDatedItem(result), true); }); test("the reply says the future-dated item was not counted, once", () => { const facts = { compareFailed: false, rescoreSkipped: false, futureDated: true, rangeBefore: null, rangeAfter: null }; const spoken = composeSpokenWithServerFacts("记下了:2014年年底拔牙。", facts); assert.ok(spoken.endsWith(RECTIFICATION_USER_COPY.futureDatedEvent)); assert.equal(spoken.split(RECTIFICATION_USER_COPY.futureDatedEvent).length, 2); assert.equal(composeSpokenWithServerFacts("记下了。", { ...facts, futureDated: false }), "记下了。"); });