import assert from "node:assert/strict"; import test from "node:test"; import { readFileSync } from "node:fs"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { RectificationRangeDelivery } from "../src/components/rectification-range-delivery.tsx"; import { parseRectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts"; import { buildRangeDelivery } from "../src/lib/rectification-agentic/v9/divergence-panel.ts"; import { buildCaseInferenceState } from "../src/lib/rectification-agentic/v9/inference-adapter.ts"; import { engineRequestBody, runV9CandidateScore } from "../src/lib/rectification-agentic/v9/engine-client.ts"; import { unionStillValidRange } from "../src/lib/rectification-agentic/core/credible-range.ts"; import { indistinguishableWidthMinutes } from "../src/lib/rectification-agentic/v9/candidate-plateau.ts"; import type { InferenceCandidate } from "../src/lib/rectification-agentic/core/types.ts"; import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts"; import { nakshatraBoundaryProbe } from "../src/lib/rectification-agentic/v9/inference-adapter.ts"; import { requestCandidateIntervals, parseCandidateIntervals } from "../src/lib/rectification-agentic/core/candidate-window.ts"; import { midnightSideChoice } from "../src/lib/rectification-agentic/v9/block-scan.ts"; import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts"; import { applyRectificationChoice } from "../src/lib/rectification-agentic/v9/answer-choice.ts"; import { expectedAnswerSchemaFor } from "../src/lib/rectification-agentic/v9/server-focus.ts"; import { CHOICE_ACTION } from "../src/lib/rectification-agentic/v9/choice-action.ts"; import { CASE_ID, FOCUS_ID, SESSION_ID, TURN_ID, USER_ID, activeFocusFixture, dossierFixture, fakeAccounting, receiptHandlers } from "./rectification-v9-test-support.ts"; const intervals = [{ start_index: 0, end_index: 2, start_offset_minutes: 0, end_offset_minutes: 2, start_at: "2000-06-14T23:58", end_at: "2000-06-15T00:00", segment_index: 0 }]; test("983 request explicitly anchors midnight center on declared date", () => { const body = engineRequestBody({ baselineBirthSnapshot: { birth_date: "2000-06-15", reported_birth_time: "00:10", birth_time_source: "approximate", latitude: 0, longitude: 0, timezone_offset: 0, }, candidateRange: { start_time: "23:55", end_time: "00:25" }, events: [{ id: "00000000-0000-4000-8000-000000000001", domain: "career", event_kind: "career_entry", date_start: "2020-01-01", date_end: "2020-01-01", precision: "day", summary: "fictional", }] }); assert.deepEqual(body.candidate_intervals, [{ start_at: "2000-06-14T23:55", end_at: "2000-06-15T00:25" }]); }); test("982 credible union uses ordinal endpoints, not clock min/max", () => { const row: InferenceCandidate = { id: "a", time: "23:59", cluster_range: ["23:58", "00:00"], cluster_intervals: intervals, window_index: 1, window_offset_minutes: 1, candidate_date: "2000-06-14", prior_score: 10, posterior_score: 10, probability: 1, status: "active", rank: 1, strong_conflict_count: 0 }; assert.deepEqual(unionStillValidRange([row as InferenceCandidate]), ["23:58", "00:00"]); }); test("nakshatra earlier/later groups follow civil ordinals across midnight", () => { const state = buildInferenceState({ range_start: "23:58", range_end: "00:00", events: [], probes: [], candidates: ["23:58", "23:59", "00:00"].map((time, index) => ({ id: time, time, relative_support: 50, candidate_date: index === 2 ? "2000-06-15" : "2000-06-14", window_index: index, window_offset_minutes: index, segment_index: 0, cluster_intervals: intervals })) }); const probe = nakshatraBoundaryProbe(state, { near_boundary: true, user_meaning: null, options: [ { key: "A", time_bias: "earlier", traits: ["直接"] }, { key: "B", time_bias: "later", traits: ["谨慎"] }, ] }); assert.deepEqual(probe?.expected_outcomes[0]?.supports, ["23:58", "23:59"]); assert.deepEqual(probe?.expected_outcomes[1]?.supports, ["00:00"]); }); test("narrowed previous-day window never reanchors to declaration day", () => { assert.deepEqual(requestCandidateIntervals({ birth_date: "2000-03-01", reported_birth_time: "00:10" }, { start_time: "23:55", end_time: "23:59" }), [{ start_at: "2000-02-29T23:55", end_at: "2000-02-29T23:59" }]); assert.equal(parseCandidateIntervals([{ start_at: "2000-02-30T00:00", end_at: "2000-03-01T00:00" }]), null); }); for (const option of ["A", "B", "C", "skip_probe"] as const) test(`D1 initial no-result action ${option} reaches dated RPC once`, async () => { const range = { start_time: "23:00", end_time: "03:59", midnight_side_pending: true, candidate_intervals: [ { start_at: "2000-06-15T00:00", end_at: "2000-06-15T03:59" }, { start_at: "2000-06-15T23:00", end_at: "2000-06-15T23:59" }, ] }; const midnight = midnightSideChoice(range)!; const plan = buildMethodFollowupPlan({ evidence: [], candidateRange: range, caseStage: "block_scan" }); assert.equal(plan.next_followup?.midnight_side, true); const schema = expectedAnswerSchemaFor(midnight.frame, midnight.frame.question_id, null, plan.next_followup!); assert.ok(schema, "initial followup must produce a real renderable focus schema"); let current = dossierFixture({ stage: "block_scan", candidateRange: range, latestResult: null, evidence: [], conversationSummary: { active_focus: activeFocusFixture({ intent: "choose_birth_block", questionId: midnight.frame.question_id, expectedAnswerSchema: schema }), declined_skipped_topics: [] } }); const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => current, advance_agentic_rectification_dated_window: (_fn, args) => { const narrowed = { start_time: String(args.p_start_time), end_time: String(args.p_end_time), candidate_intervals: args.p_candidate_intervals, midnight_side_pending: false }; current = { ...current, case: { ...current.case, candidate_range: narrowed } }; return { candidate_range: narrowed }; }, apply_agentic_rectification_choice_action: (_fn, args) => ({ 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 }), append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, idempotent: false }), set_agentic_rectification_conversation_focus: () => ({ focus: null, idempotent: false }), }); await applyRectificationChoice(accounting.client, { userId: USER_ID, caseId: CASE_ID, sessionId: SESSION_ID, actionId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", action: CHOICE_ACTION, focusId: FOCUS_ID, optionId: option, expectedRevision: 0 }); const calls = accounting.calls.filter((item) => item.fn === "advance_agentic_rectification_dated_window"); assert.equal(calls.length, 1); assert.equal((calls[0].args.p_candidate_intervals as unknown[]).length, option === "A" || option === "B" ? 1 : 2); assert.equal(midnightSideChoice(current.case.candidate_range), null); }); test("native golden crosses engine parsing, inference and dated delivery without reanchoring", async () => { const golden = JSON.parse(readFileSync(new URL("./fixtures/rectification-midnight-date-anchor.delivery.native.json", import.meta.url), "utf8")); const originalFetch = globalThis.fetch; globalThis.fetch = (async () => new Response(JSON.stringify(golden.response), { status: 200 })) as typeof fetch; try { const request = golden.request; const range = { start_time: request.start_time, end_time: request.end_time, candidate_intervals: request.candidate_intervals }; const scored = await runV9CandidateScore({ baselineBirthSnapshot: { birth_date: request.birth_date, latitude: request.lat, longitude: request.lon, timezone_offset: request.tz }, candidateRange: range, events: request.events }); const inference = buildCaseInferenceState({ range, candidates: scored.candidates, evidence: [], probes: [], transitions: scored.windowScan?.transitions }); const delivery = buildRangeDelivery({ inference, publicCandidates: scored.candidates, windowScan: scored.windowScan }); const result = parseRectificationCandidateResult({ ...scored, resultId: scored.engineResultId, decisionReceipt: { ...scored.decisionReceipt, inference_state: inference }, rangeDelivery: delivery }); assert.ok(result); assert.equal(result.declaredBirthDate, request.birth_date); assert.deepEqual(result.candidates.map((row) => row.candidate_date), scored.candidates.map((row) => row.candidate_date)); assert.equal(result.credibleIntervals?.[0]?.start_at, "2000-02-29T23:58"); assert.ok(delivery.columns.length > 0, "cross-midnight range must not filter out every candidate"); const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, { result, acceptingCandidateId: null, readonly: true, onAccept: () => undefined, })); assert.match(html, /前一天/); assert.match(html, /2000-02-29/); assert.match(html, /原填报日期保留/); assert.doesNotMatch(html, /2000-03-02/); } finally { globalThis.fetch = originalFetch; } }); for (const invalid of [null, [], {}, "", [{ start_at: "2000-03-01T00:00" }]]) test(`explicit invalid intervals never fall back: ${JSON.stringify(invalid)}`, () => { assert.throws(() => engineRequestBody({ baselineBirthSnapshot: { birth_date: "2000-03-01", latitude: 0, longitude: 0, timezone_offset: 0 }, candidateRange: { start_time: "23:58", end_time: "00:02", candidate_intervals: invalid } as never, events: [{ id: "00000000-0000-4000-8000-000000000001", domain: "career", event_kind: "career_entry", date_start: "2020-01-01", date_end: "2020-01-01", precision: "day", summary: "fictional", }] }), /invalid_candidate_intervals/); }); test("982 plateau width retains actual minutes across midnight", () => { assert.equal(indistinguishableWidthMinutes([{ time: "23:59", rank: 1, tiedMinuteCount: 1, clusterStart: "23:58", clusterEnd: "00:00", clusterIntervals: intervals }]), 3); });