Files
Jyotisha/frontend/tests/rectification-event-date-bounds-20261002.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

126 lines
6.6 KiB
TypeScript

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<Record<string, unknown>> : [];
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<Record<string, unknown>>;
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 }), "记下了。");
});