import assert from "node:assert/strict"; import test from "node:test"; import { applyRectificationChoice } from "../src/lib/rectification-agentic/v9/answer-choice.ts"; import { CHOICE_ACTION } from "../src/lib/rectification-agentic/v9/choice-action.ts"; import { decideFromDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts"; import { buildWidenWindowFrame, shouldSuggestWindowWiden, } from "../src/lib/rectification-agentic/v9/window-widen.ts"; import { clockSpanMinutes, splitWindowIntoThirds } from "../src/lib/rectification-agentic/v9/search-window.ts"; import { evidenceLedgerFingerprint, parseV9CaseDossier } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { WIDEN_WINDOW_INTENT, WIDEN_WINDOW_KIND } from "../src/lib/rectification-agentic/v9/block-scan.ts"; import { CASE_ID, FOCUS_ID, SESSION_ID, TURN_ID, USER_ID, activeFocusFixture, candidateSnapshotFixture, dossierFixture, fakeAccounting, receiptHandlers, } from "./rectification-v9-test-support.ts"; const TRAINING_EVIDENCE = [ { id: "e-edu", source_turn_id: TURN_ID, subject: "self", event_kind: "education_start", domain: "education", occurred_from: "2016-09-01", occurred_to: "2016-09-30", date_precision: "month", summary: "education start", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-09-07T00:00:00.000Z", }, { id: "e-rel", source_turn_id: TURN_ID, subject: "self", event_kind: "relationship_end", domain: "relationship", occurred_from: "2024-08-08", occurred_to: "2024-08-08", date_precision: "day", summary: "relationship end", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-09-07T00:00:01.000Z", }, { id: "e-move", source_turn_id: TURN_ID, subject: "self", event_kind: "relocation", domain: "relocation", occurred_from: "2023-07-01", occurred_to: "2023-07-31", date_precision: "month", summary: "relocation", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-09-07T00:00:02.000Z", }, { id: "e-job", source_turn_id: TURN_ID, subject: "self", event_kind: "career_entry", domain: "career", occurred_from: "2018-07-01", occurred_to: "2018-07-31", date_precision: "month", summary: "career entry", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-09-07T00:00:03.000Z", }, ] as const; function widenSchema(reportedTime = "05:00", window = { start_time: "04:45", end_time: "05:15" }) { const frame = buildWidenWindowFrame({ reportedTime, currentWindow: window }); assert.ok(frame); const windows = { A: { radius: 30, start_time: "04:30", end_time: "05:30" }, B: { radius: 60, start_time: "04:00", end_time: "06:00" }, }; return { choice: { prompt: frame.prompt, option_a: frame.option_a_hint, option_b: frame.option_b_hint, option_c: frame.neither_label, option_d: frame.unsure_label, options: [ { key: "A", label: frame.option_a_hint, answer_class: frame.option_a_answer_class, role: "primary" }, { key: "B", label: frame.option_b_hint, answer_class: frame.option_b_answer_class, role: "primary" }, { key: "C", label: frame.neither_label, answer_class: frame.option_c_answer_class, role: "primary" }, { key: "D", label: frame.unsure_label, answer_class: frame.option_d_answer_class, role: "primary" }, ], }, choice_kind: WIDEN_WINDOW_KIND, scoring: false, widen_windows: windows, }; } function widenDossier(extra: { representativeTime?: string; range?: { start_time: string; end_time: string }; declinedFingerprint?: string | null; birthTimeSource?: string; evidence?: unknown[]; withFocus?: boolean; } = {}) { const range = extra.range ?? { start_time: "04:45", end_time: "05:15" }; const schema = widenSchema("05:00", range); return dossierFixture({ candidateRange: range, stage: "minute", reportedBirthTime: "05:00", birthTimeSource: extra.birthTimeSource ?? "approximate", widenDeclinedAtFingerprint: extra.declinedFingerprint ?? null, evidence: extra.evidence ?? [...TRAINING_EVIDENCE], latestResult: candidateSnapshotFixture({ representativeTime: extra.representativeTime ?? "04:45", decisionReceipt: { event_fit_rate: { matched: 1, total: 4, percent: 25, band: "low", label: "事件吻合率", user_meaning: "这是相对拟合,不是已确认唯一出生分钟。", unique_minute_claim: false, }, }, }), conversationSummary: { confirmed_evidence_summary: [], pending_revisions: [], active_focus: extra.withFocus === false ? null : activeFocusFixture({ intent: WIDEN_WINDOW_INTENT, targetDomain: null, targetKind: null, questionId: "widen_window:search_range:holdout", expectedAnswerSchema: schema, }), declined_skipped_topics: [], candidate_divergence_summary: null, missing_evidence_categories: [], last_result_policy: null, summary_version: 1, updated_at: "2026-09-07T00:00:00.000Z", }, }); } function choiceHandlers(current: { value: ReturnType }) { return { ...receiptHandlers, get_agentic_rectification_case_dossier: () => current.value, get_agentic_rectification_case_compute: () => ({ case_id: CASE_ID, skill_version: "9.0.0", baseline_profile_fingerprint: "a".repeat(64), baseline_birth_snapshot: { birth_date: "1997-08-08", latitude: 36.42, longitude: 114.2, timezone_id: "Asia/Shanghai", timezone_offset: 8, birth_time_source: current.value.case.birth_time_source, reported_birth_time: "05:00", }, candidate_range: current.value.case.candidate_range, }), append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, idempotent: false }), apply_agentic_rectification_choice_action: (_fn: string, args: Record) => ({ action_id: args.p_action_id, status: "applied", idempotent: false, question_id: args.p_question_id, option_id: args.p_option_id, revision: 1, narration: args.p_narration, focus_status: args.p_focus_status, }), set_agentic_rectification_conversation_focus: (_fn: string, args: Record) => ({ focus: { id: FOCUS_ID, case_id: CASE_ID, question_id: args.p_question_id, intent: args.p_intent, expected_answer_schema: args.p_expected_answer_schema, status: "active", asked_at: "2026-09-07T00:00:00.000Z", resolved_at: null, }, idempotent: false, }), persist_agentic_rectification_inference_transition: () => { throw new Error("widen must not rewrite inference"); }, }; } test("exclusive span 120 stays minute; 240 splits into three non-overlapping thirds", () => { assert.equal(clockSpanMinutes("04:00", "06:00"), 120); assert.equal(clockSpanMinutes("03:00", "07:00"), 240); const thirds = splitWindowIntoThirds({ start_time: "03:00", end_time: "07:00" }); assert.equal(thirds.length, 3); assert.equal(thirds[0]?.start_time, "03:00"); assert.equal(thirds[2]?.end_time, "07:00"); assert.notEqual(thirds[0]?.end_time, thirds[1]?.start_time); }); test("fit low plus a representative on the edge asks a four-option widen card", () => { const parsed = parseV9CaseDossier(widenDossier({ withFocus: false })); assert.ok(parsed); const decision = decideFromDossier(parsed); assert.equal(decision.nextAction, "ask_window_widen"); assert.equal(decision.sessionOutcome, "widen_window"); assert.equal(decision.canAdopt, false); const frame = buildWidenWindowFrame({ reportedTime: "05:00", currentWindow: { start_time: "04:45", end_time: "05:15" }, }); assert.ok(frame); assert.equal(frame.choice_kind, WIDEN_WINDOW_KIND); assert.match(frame.option_a_hint, /半小时/); assert.match(frame.option_b_hint, /一小时/); assert.match(frame.neither_label, /不放宽/); assert.ok(frame.unsure_label.length >= 4); const labels = [frame.option_a_hint, frame.option_b_hint, frame.neither_label, frame.unsure_label]; assert.equal(new Set(labels).size, 4); }); test("fit low with a representative in the middle of the window does not suggest widen", () => { const parsed = parseV9CaseDossier(widenDossier({ representativeTime: "05:00", withFocus: false, })); assert.ok(parsed); assert.notEqual(decideFromDossier(parsed).nextAction, "ask_window_widen"); assert.equal( shouldSuggestWindowWiden({ trainingGateOpen: true, fitBand: "low", fitTotal: 4, representativeNearEdge: false, stage: "minute", accepted: false, reportedTime: "05:00", declinedForFingerprint: false, currentRadius: 15, }), false, ); }); test("answering A widens the window, patches approximate radius, and does not rewrite answered probes", async () => { const current = { value: widenDossier() }; const accounting = fakeAccounting({ ...choiceHandlers(current), widen_agentic_rectification_case_window: (_fn, args) => { current.value = widenDossier({ range: { start_time: String(args.p_start_time), end_time: String(args.p_end_time) }, withFocus: false, }); return { case_id: CASE_ID, stage: "minute", candidate_range: { start_time: args.p_start_time, end_time: args.p_end_time }, }; }, }); const askedBefore = accounting.calls.filter((item) => item.fn.includes("inference")).length; const receipt = await applyRectificationChoice(accounting.client, { userId: USER_ID, caseId: CASE_ID, sessionId: SESSION_ID, actionId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa3", action: CHOICE_ACTION, focusId: FOCUS_ID, optionId: "A", expectedRevision: 0, }); assert.equal(receipt.snapshotCurrent, false); assert.ok(accounting.calls.some((item) => item.fn === "widen_agentic_rectification_case_window")); const widenCall = accounting.calls.find((item) => item.fn === "widen_agentic_rectification_case_window"); assert.equal(widenCall?.args.p_start_time, "04:30"); assert.equal(widenCall?.args.p_end_time, "05:30"); assert.deepEqual(accounting.profilePatches, [{ birth_time_source: "approximate", uncertainty_before_minutes: 30, uncertainty_after_minutes: 30, }]); assert.equal( accounting.calls.filter((item) => item.fn.includes("inference")).length, askedBefore, ); }); test("answering C records the fingerprint and does not ask again until evidence changes", async () => { const current = { value: widenDossier() }; const parsedBefore = parseV9CaseDossier(current.value); assert.ok(parsedBefore); const fingerprint = evidenceLedgerFingerprint(parsedBefore.evidence); const accounting = fakeAccounting({ ...choiceHandlers(current), set_agentic_rectification_widen_declined: (_fn, args) => { current.value = widenDossier({ declinedFingerprint: String(args.p_fingerprint), withFocus: false, }); return { case_id: CASE_ID, widen_declined_at_fingerprint: args.p_fingerprint, }; }, }); await applyRectificationChoice(accounting.client, { userId: USER_ID, caseId: CASE_ID, sessionId: SESSION_ID, actionId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa4", action: CHOICE_ACTION, focusId: FOCUS_ID, optionId: "C", expectedRevision: 0, }); const declined = parseV9CaseDossier(current.value); assert.ok(declined); assert.equal(declined.case.widenDeclinedAtFingerprint, fingerprint); assert.notEqual(decideFromDossier(declined, { currentEvidenceFingerprint: fingerprint }).nextAction, "ask_window_widen"); const freshEvidence = [ ...TRAINING_EVIDENCE, { ...TRAINING_EVIDENCE[3], id: "e-job-2", occurred_from: "2019-03-01", occurred_to: "2019-03-31", summary: "later career", }, ]; const later = parseV9CaseDossier(widenDossier({ declinedFingerprint: fingerprint, evidence: freshEvidence, withFocus: false, })); assert.ok(later); assert.equal(decideFromDossier(later).nextAction, "ask_window_widen"); }); test("already ±120 does not suggest a widen card", () => { assert.equal( shouldSuggestWindowWiden({ trainingGateOpen: true, fitBand: "low", fitTotal: 4, representativeNearEdge: true, stage: "minute", accepted: false, reportedTime: "05:00", declinedForFingerprint: false, currentRadius: 120, }), false, ); }); test("hospital_record answering A widens the window but does not rewrite the archive source", async () => { const current = { value: widenDossier({ birthTimeSource: "hospital_record" }) }; const accounting = fakeAccounting({ ...choiceHandlers(current), widen_agentic_rectification_case_window: (_fn, args) => { current.value = widenDossier({ birthTimeSource: "hospital_record", range: { start_time: String(args.p_start_time), end_time: String(args.p_end_time) }, withFocus: false, }); return { case_id: CASE_ID, stage: "minute", candidate_range: { start_time: args.p_start_time, end_time: args.p_end_time }, }; }, }); await applyRectificationChoice(accounting.client, { userId: USER_ID, caseId: CASE_ID, sessionId: SESSION_ID, actionId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa5", action: CHOICE_ACTION, focusId: FOCUS_ID, optionId: "A", expectedRevision: 0, }); assert.ok(accounting.calls.some((item) => item.fn === "widen_agentic_rectification_case_window")); assert.deepEqual(accounting.profilePatches, []); });