import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { FORBIDDEN_DECLARED_BIRTH_TIME_PHRASE, SWITCH_TO_RECTIFIED_LABEL, boardDeclaredTimeLine, normalizeBirthTimeSource, reportedTimeOffsetCopy, } from "../src/lib/rectification-agentic/birth-time-provenance.ts"; import { publicDecisionFields } from "../src/lib/rectification-agentic/core/rectification-decision.ts"; import { INFERENCE_ALGORITHM_VERSION, type InferenceCandidate, type InferenceState } from "../src/lib/rectification-agentic/core/types.ts"; import { decideFromDossier, type DecisionDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts"; import { buildRangeDelivery } from "../src/lib/rectification-agentic/v9/divergence-panel.ts"; import { RectificationRangeDelivery } from "../src/components/rectification-range-delivery.tsx"; import type { RectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts"; import { RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts"; function dossier(source?: string | null): DecisionDossier { return { evidence: [], conversationSummary: { activeFocus: null, declinedSkippedTopics: [] }, latestResult: null, case: { acceptedTime: null, ...(source === undefined ? {} : { birthTimeSource: source }) }, }; } function cand(time: string, probability: number): InferenceCandidate { return { id: time, time, cluster_range: ["04:48", "05:07"], prior_score: probability * 100, posterior_score: probability * 100, probability, status: "active", rank: 1, strong_conflict_count: 0, }; } function inference(): InferenceState { return { algorithm_version: INFERENCE_ALGORITHM_VERSION, candidate_set_id: "set", revision: 1, phase: "discrimination", result_status: "credible_range", range_start: "04:48", range_end: "05:07", candidates: [cand("04:53", 0.4), cand("04:50", 0.35)], events: [], probes: [], answered_probes: [], rounds: [], entropy: 1, representative_time: "04:53", credible_range: ["04:48", "05:07"], }; } function deliveryResult(delivery: ReturnType): RectificationCandidateResult { return { resultId: "result-provenance", candidates: delivery.columns.map((column, index) => ({ candidateId: column.candidate_id, rank: index + 1, time: column.time, relativeSupport: column.probability_percent, tiedMinuteCount: 1, })), overallConfidence: "medium", selectionAllowed: true, canAdopt: true, confirmationAllowed: false, decisionReceipt: null, representativeTime: delivery.representative_time, selectedTime: null, selectionKind: null, houseTable: null, houseTablesByTime: {}, natalRecast: null, techniqueAudit: [], windowTransitions: [], eventDashaLedger: [], dashaAgreement: null, lagnaContrast: null, nakshatraBoundary: null, precisionStage: null, oosBlindPrompts: [], confirmationGate: { confirmation_allowed: false } as RectificationCandidateResult["confirmationGate"], validated: false, completionStatus: null, sessionOutcome: "adopt_representative", credibleRange: delivery.range, rangeDelivery: delivery, verificationReportMarkdown: delivery.verification_markdown, }; } test("GET and decideFromDossier expose hospital / approximate / period_only, missing becomes approximate", () => { assert.equal(normalizeBirthTimeSource("hospital_record"), "hospital_record"); assert.equal(normalizeBirthTimeSource("approximate"), "approximate"); assert.equal(normalizeBirthTimeSource("period_only"), "period_only"); assert.equal(normalizeBirthTimeSource("family_exact"), "approximate"); assert.equal(normalizeBirthTimeSource(null), "approximate"); assert.equal(normalizeBirthTimeSource(undefined), "approximate"); assert.equal(publicDecisionFields(decideFromDossier(dossier("hospital_record"))).birth_time_source, "hospital_record"); assert.equal(publicDecisionFields(decideFromDossier(dossier("approximate"))).birth_time_source, "approximate"); assert.equal(publicDecisionFields(decideFromDossier(dossier("period_only"))).birth_time_source, "period_only"); assert.equal(publicDecisionFields(decideFromDossier(dossier())).birth_time_source, "approximate"); const response = readFileSync( new URL("../src/lib/rectification-agentic/v9/case-dossier-response.ts", import.meta.url), "utf8", ); assert.match(response, /birth_time_source: birthTimeSource/); }); test("approximate and period_only copy never says 你的出生时间", () => { const approximate = [ boardDeclaredTimeLine("05:00", "approximate"), reportedTimeOffsetCopy({ source: "approximate", reportedTime: "05:00", range: ["04:48", "05:07"], representativeTime: "04:53", }) ?? "", ]; const period = [ boardDeclaredTimeLine("05:00", "period_only"), reportedTimeOffsetCopy({ source: "period_only", reportedTime: "05:00", range: ["04:48", "05:07"], representativeTime: "04:53", }) ?? "", ]; for (const line of [...approximate, ...period]) { assert.doesNotMatch(line, FORBIDDEN_DECLARED_BIRTH_TIME_PHRASE); } assert.match(approximate.join("\n"), /你填的大概时间/); assert.match(period.join("\n"), /你给的时间段/); }); test("hospital_record copy names the record and states the offset without picking a side", () => { const inside = reportedTimeOffsetCopy({ source: "hospital_record", reportedTime: "05:00", range: ["04:48", "05:07"], representativeTime: "04:53", }); assert.match(inside ?? "", /出生记录时间 05:00,落在目前范围内/); const outside = reportedTimeOffsetCopy({ source: "hospital_record", reportedTime: "05:12", range: ["04:48", "05:07"], representativeTime: "04:53", }); assert.match(outside ?? "", /出生记录时间 05:12/); assert.match(outside ?? "", /相差 5 分钟/); assert.doesNotMatch(outside ?? "", /以记录为准|以证据为准|应该信/); const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, { result: deliveryResult(buildRangeDelivery({ inference: inference(), publicCandidates: [ { candidateId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", time: "04:53" }, { candidateId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2", time: "04:50" }, ], credibleRange: ["04:48", "05:07"], representativeTime: "04:53", reportedBirthTime: "05:12", birthTimeSource: "hospital_record", })), acceptingCandidateId: null, readonly: false, onAccept: () => undefined, })); assert.match(html, /出生记录时间 05:12/); assert.match(html, /相差 5 分钟/); // 原值: assert.ok(html.includes(RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis)) // 新值: 记录落在范围外时按钮写 SWITCH_TO_RECTIFIED_LABEL(「改用校正结果」) // 原因: BUG-691 决策 D3——记录与范围冲突时采用入口要说明自己换掉的是什么; // 05:12 对 04:48–05:07 正是冲突态。非冲突态仍走「更像这个」, // 由 rectification-record-conflict-copy-20260916.test.ts 逐态锁住。 assert.ok(html.includes(SWITCH_TO_RECTIFIED_LABEL)); assert.equal(html.includes(RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis), false); });