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 { INFERENCE_ALGORITHM_VERSION, type InferenceCandidate, type InferenceState } from "../src/lib/rectification-agentic/core/types.ts"; import { buildRangeDelivery, d10DivergenceLabel, d9DivergenceLabel, } from "../src/lib/rectification-agentic/v9/divergence-panel.ts"; import { D10_TYPE_TABLE, D9_TYPE_TABLE } from "../src/lib/rectification-agentic/v9/varga-type-tables.ts"; import { RECTIFICATION_USER_COPY, REPRESENTATIVE_MINUTE_DISCLAIMER, deliveryTurnNarration, } from "../src/lib/rectification-agentic/user-copy.ts"; import { appendPostAdoptProspectiveWindows } from "../src/lib/rectification-agentic/v9/answer-choice.ts"; import { RectificationRangeDelivery } from "../src/components/rectification-range-delivery.tsx"; import { deliveryWindowMinutes, protectVargaReport } from "../src/lib/rectification-agentic/v9/delivery-window.ts"; import { buildSkillVerificationReport } from "../src/lib/rectification-agentic/v9/skill-verification-report.ts"; import { RectificationSegmentDelivery } from "../src/components/rectification-segment-delivery.tsx"; import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts"; import { targetChartsForDomain } from "../src/lib/rectification-agentic/core/segment-summary.ts"; import { latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts"; import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts"; import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts"; import { buildRectificationTimeline } from "../src/lib/rectification-timeline-scale.ts"; import type { RectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts"; import { rectificationChatSurface } from "./rectification-chat-surface.ts"; const ID_A = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1"; const ID_B = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2"; function cand( time: string, range: readonly [string, string], posterior = 20, ): InferenceCandidate { return { id: time, time, cluster_range: range, prior_score: posterior, posterior_score: posterior, probability: 0.5, status: "active", rank: 1, strong_conflict_count: 0, }; } function inference(overrides: Partial = {}): InferenceState { return { algorithm_version: INFERENCE_ALGORITHM_VERSION, candidate_set_id: "04:48-04:59:04:50,04:56", revision: 1, phase: "discrimination", result_status: "credible_range", range_start: "04:48", range_end: "04:59", candidates: [ cand("04:50", ["04:48", "04:53"]), cand("04:56", ["04:54", "04:59"]), ], events: [ { id: "e1", domain: "career", year: 2018, precision: "month", usage: "training" }, { id: "e2", domain: "education", year: 2016, precision: "year", usage: "training" }, ], probes: [], answered_probes: [], rounds: [], entropy: 1, representative_time: "04:50", credible_range: ["04:48", "04:59"], ...overrides, }; } function d9Probe() { return { id: "probe:varga.d9", semantic_key: "varga.d9.天秤座/天蝎座", candidate_split_hash: "varga.d9", domain: "relationship", year: 0, question: "相处更像哪一种?", candidate_ids: ["04:50", "04:56"], expected_outcomes: [ { answer_class: "yes" as const, supports: ["04:50"], conflicts: ["04:56"] }, { answer_class: "weak_yes" as const, supports: ["04:56"], conflicts: ["04:50"] }, ], information_gain: 1, source: "varga_contrast", choice_kind: "varga_style" as const, style_options: [ { sign: "天秤座", label: D9_TYPE_TABLE.天秤座.userChoice, answer_class: "yes" as const }, { sign: "天蝎座", label: D9_TYPE_TABLE.天蝎座.userChoice, answer_class: "weak_yes" as const }, ], }; } function d10Probe() { return { id: "probe:varga.d10", semantic_key: "varga.d10.巨蟹座/狮子座", candidate_split_hash: "varga.d10", domain: "career", year: 0, question: "做事更像哪一种?", candidate_ids: ["04:50", "04:56"], expected_outcomes: [ { answer_class: "yes" as const, supports: ["04:50"], conflicts: ["04:56"] }, { answer_class: "weak_yes" as const, supports: ["04:56"], conflicts: ["04:50"] }, ], information_gain: 1, source: "varga_contrast", choice_kind: "varga_style" as const, style_options: [ { sign: "巨蟹座", label: D10_TYPE_TABLE.巨蟹座.userChoice, answer_class: "yes" as const }, { sign: "狮子座", label: D10_TYPE_TABLE.狮子座.userChoice, answer_class: "weak_yes" as const }, ], }; } const publicCandidates = [ { candidateId: ID_A, time: "04:50" }, { candidateId: ID_B, time: "04:56" }, ]; function deliveryResult( delivery: ReturnType, selectedTime: string | null = null, ): RectificationCandidateResult { return { resultId: "result-1", candidates: delivery.columns.map((column, index) => ({ candidateId: column.candidate_id, rank: index + 1, time: column.time, relativeSupport: 10, tiedMinuteCount: 1, })), overallConfidence: "medium", selectionAllowed: true, canAdopt: true, confirmationAllowed: false, decisionReceipt: null, representativeTime: delivery.representative_time, selectedTime, selectionKind: selectedTime ? "accepted" : 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("wide legacy fallback uses the full score window rather than a narrowed credible range", () => { const delivery = buildRangeDelivery({ inference: inference({ probes: [d9Probe(), d10Probe()] }), publicCandidates }); const result = deliveryResult({ ...delivery, window_minutes: 121, shared_traits: ["关系盘共享推荐"], verification_markdown: "## D9 推荐\n不得泄漏" }); const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, { result: { ...result, decisionReceipt: { candidate_intervals: [{ start_at: "2000-06-14T04:00", end_at: "2000-06-14T06:00" }] } }, acceptingCandidateId: null, readonly: false, onAccept: () => {}, })); assert.ok(html.includes("分盘解读需要更准的出生时间")); assert.ok(!html.includes("关系盘共享推荐")); assert.ok(!html.includes("不得泄漏")); assert.ok(!html.includes(d9DivergenceLabel("天秤座")!)); assert.ok(!html.includes(d10DivergenceLabel("巨蟹座")!)); assert.equal(deliveryWindowMinutes({ start_time: "23:50", end_time: "00:10" }), 21); assert.equal(deliveryWindowMinutes({ candidate_intervals: [{ start_at: "2000-06-14T00:00", end_at: "2000-06-15T00:00" }] }), 1441); }); test("wide report suppresses whole nested varga sections and preserves event permission and blocked audit", () => { const report = buildSkillVerificationReport({ representativeTime: "04:50", widthMinutes: 121, candidates: [{ time: "04:50", rank: 1, relativeSupport: 50 }], signByCandidate: { "04:50": { d9: "天秤座", d10: "巨蟹座" } }, eventFitRate: { matched: 1, total: 2, percent: 50, label: "事件核对保留", user_meaning: "事件证据保留", unique_minute_claim: false }, techniqueAuditTable: [{ technique: "D12", status: "blocked", note: "精度不足,不作解读" }] }); const protectedReport = protectVargaReport(report, true)!; assert.match(protectedReport, /事件证据保留/); assert.match(protectedReport, /不是已确认的唯一出生分钟/); assert.match(protectedReport, /\| D12 \| blocked \| 精度不足,不作解读/); assert.doesNotMatch(protectedReport, /允许事业类型表|家人事件对照|sign_by_candidate|D9 上升在当前窗较稳/); const nested = protectVargaReport("## D9 推荐\n泄漏一\n### 无盘号子标题\n泄漏二\n#### 更深\n泄漏三\n## 事件\n事件保留\n### D24 推荐\n泄漏四\n#### 子标题\n泄漏五\n### 权限\n权限保留", true)!; assert.doesNotMatch(nested, /泄漏/); assert.match(nested, /事件保留/); assert.match(nested, /权限保留/); assert.equal(protectVargaReport(protectedReport, true), protectedReport, "protection is idempotent across projection and render"); }); test("unknown full window fails closed without deleting non-varga report", () => { const delivery = buildRangeDelivery({ inference: null, publicCandidates, verificationMarkdown: "## 事件\n事件保留\n## D10 推荐\n不应显示" }); const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, { result: deliveryResult(delivery), acceptingCandidateId: null, readonly: false, onAccept: () => {}, })); assert.match(html, /完整窗口范围暂不可用/); assert.match(html, /事件保留/); assert.doesNotMatch(html, /不应显示/); }); test("segment report explicit null does not restore cached markdown", () => { const state = buildInferenceState({ range_start: "04:50", range_end: "04:50", candidates: [{ id: "04:50", time: "04:50", relative_support: 50, raw_score: 10, cluster_times: ["04:50"] }], events: [], probes: [], segment_minutes: [{ offset: 0, date: "2000-06-14", time: "04:50", signs: { D1: 1 } }], segment_targets: ["D1"], segment_scan_complete: true }); const delivery = buildRangeDelivery({ inference: state, publicCandidates, verificationMarkdown: "## 事件\n缓存报告不得恢复" }); const html = renderToStaticMarkup(createElement(RectificationSegmentDelivery, { summary: state.segment_summary!, delivery, rangeLabel: null, resultId: "result-1", busy: false, accepting: false, onAccept: () => {}, markdown: null, })); assert.doesNotMatch(html, /缓存报告不得恢复|rectification-verification-report/); }); for (const windowMinutes of [21, 61, 121]) { for (const domain of ["relationship", "career", "general"]) { test(`delivery targets across card report bubble timeline ${domain} ${windowMinutes} minutes`, () => { // Fictional complete scan for the nine rendering boundaries, not calibrated engine evidence. const targets = targetChartsForDomain(domain); const end = windowMinutes === 21 ? "09:20" : windowMinutes === 61 ? "10:00" : "11:00"; const minutes = Array.from({ length: windowMinutes }, (_, offset) => ({ offset, date: "2000-06-14", time: `${String(9 + Math.floor(offset / 60)).padStart(2, "0")}:${String(offset % 60).padStart(2, "0")}`, signs: { D1: 1, D9: offset < 10 ? 2 : 3, D10: offset < 12 ? 4 : 5 } })); const state = buildInferenceState({ range_start: "09:00", range_end: end, candidates: [{ id: "09:00", time: "09:00", relative_support: 58, raw_score: 10, cluster_times: ["09:00"] }, { id: end, time: end, relative_support: 42, raw_score: 10, cluster_times: [end] }], events: [], probes: [], segment_targets: targets, segment_minutes: minutes, segment_scan_complete: true }); const report = buildSkillVerificationReport({ representativeTime: "09:00", widthMinutes: windowMinutes, candidates: [{ time: "09:00", rank: 1, relativeSupport: 58 }], signByCandidate: { "09:00": { d9: "天秤座", d10: "巨蟹座" } }, eventFitRate: { matched: 1, total: 2, percent: 50, label: "核对", user_meaning: "事件证据保留", unique_minute_claim: false }, techniqueAuditTable: [{ technique: "D12", status: "blocked", note: "精度不足,不作解读" }] }); const delivery = buildRangeDelivery({ inference: state, publicCandidates: [{ candidateId: ID_A, time: "09:00" }, { candidateId: ID_B, time: end }], verificationMarkdown: report }); const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, { result: deliveryResult(delivery), acceptingCandidateId: null, readonly: false, onAccept: () => {}, })); assert.match(html, /事件证据保留/); assert.match(delivery.verification_markdown!, /\| D12 \| blocked \|/); assert.doesNotMatch(html, /家人事件对照|sign_by_candidate|允许事业类型表/); const bubble = deliveryTurnNarration({ segmentSummary: state.segment_summary, credibleRange: state.credible_range }); const timeline = buildRectificationTimeline({ searchWindow: ["09:00", end], credibleRange: state.credible_range, candidateTimes: ["09:00", end], stage: "minute", segmentSummary: state.segment_summary }); const candidates = [{ candidateId: ID_A, time: "09:00", rank: 1, relativeSupport: 58, tiedMinuteCount: windowMinutes }, { candidateId: ID_B, time: end, rank: 2, relativeSupport: 42, tiedMinuteCount: windowMinutes }]; const projection = latestResultToolProjection({ resultId: "fictional-render-result", candidates, selectionAllowed: true, confirmationAllowed: false, representativeTime: "09:00", selectedTime: "09:03", selectionKind: "adopted", algorithmVersion: "rectification-v5", decisionReceipt: { inference_state: state, acceptance_allowed: true, selection_allowed: true, propose_allowed: true, confirmation_allowed: false, technique_audit_table: [{ technique: "D12", status: "blocked", note: "精度不足,不作解读" }] }, }, decideRectification({ engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING, methodCoverageAll: true, trainingGateOpen: true, candidateScores: candidates.map(candidate => ({ time: candidate.time, score: candidate.relativeSupport })) })); const packet = projection.skill_verification_report as { width_minutes: number; markdown: string; sign_by_candidate_scope: string; segment_summary: unknown }; assert.equal(packet.width_minutes, windowMinutes); assert.deepEqual(packet.segment_summary, state.segment_summary); assert.equal(packet.sign_by_candidate_scope, "representative_diagnostic_not_segment_evidence_not_adopted_chart"); assert.match(packet.markdown, /不是实际采用分钟的盘/); assert.doesNotMatch(packet.markdown, /采用只会把代表性时间写入/); assert.match(packet.markdown, /服务端核验的实际日期和分钟/); assert.match(packet.markdown, /\| D12 \| blocked \|/); assert.doesNotMatch(packet.markdown, /家人事件对照|各候选分盘|类型表|只抄本表/); for (const chart of ["D9", "D10"]) { const visible = windowMinutes <= 61 && targets.includes(chart as "D9" | "D10"); for (const surface of [html, bubble, timeline?.workingLabel ?? "", packet.markdown]) { assert.equal(surface.includes(`${chart} 上升`), visible, `${chart} target recommendation scope`); } if (!visible) assert.doesNotMatch(delivery.verification_markdown!, new RegExp(`(?:^|\\n)(?!\\s*\\|)[^\\n]*${chart}`)); } }); } } test("scan failure without inference retains original case window and blocks wide varga traits", () => { const delivery = buildRangeDelivery({ inference: null, windowMinutes: 121, publicCandidates, credibleRange: ["04:48", "04:59"], verificationMarkdown: "D10 narrow recommendation" }); assert.equal(delivery.window_minutes, 121); assert.equal(delivery.verification_markdown?.includes("D10 narrow recommendation"), false); assert.ok(delivery.columns.every(column => !column.traits.d9 && !column.traits.d10)); assert.equal(delivery.narrow_hint, "分盘解读需要更准的出生时间"); }); test("delivery columns rank by posterior and write D9/D10 on every column", () => { // 原值: rows 代表分钟第一、差异句来自类型表、相同则空 // 新值: 至多三列,按后验排序,每列写全 D9/D10,不预标排盘用 // 原因: BUG-597 决策 1–2 const delivery = buildRangeDelivery({ inference: inference({ probes: [d9Probe(), d10Probe()], candidates: [ { ...cand("04:50", ["04:48", "04:53"]), probability: 0.55, posterior_score: 30 }, { ...cand("04:56", ["04:54", "04:59"]), probability: 0.35, posterior_score: 20 }, ], }), publicCandidates, credibleRange: ["04:48", "04:59"], representativeTime: "04:50", }); assert.equal(delivery.columns[0]?.time, "04:50"); assert.equal(delivery.columns[0]?.probability_percent, 55); assert.ok(delivery.columns[0]?.traits.d9.includes(d9DivergenceLabel("天秤座") ?? "missing")); assert.ok(delivery.columns[0]?.traits.d10.includes(d10DivergenceLabel("巨蟹座") ?? "missing")); assert.equal(delivery.columns[1]?.time, "04:56"); assert.ok(delivery.columns[1]?.traits.d9.includes(d9DivergenceLabel("天蝎座") ?? "missing")); assert.ok(delivery.columns[1]?.traits.d10.includes(d10DivergenceLabel("狮子座") ?? "missing")); assert.equal(delivery.event_count, 2); assert.equal(delivery.boundary, REPRESENTATIVE_MINUTE_DISCLAIMER); assert.equal(delivery.representative_candidate_id, ID_A); assert.equal(delivery.more_count, 0); }); test("same D9/D10 lifts the shared sentences to the card top", () => { const sharedD9 = { ...d9Probe(), candidate_ids: ["04:50", "04:51"], expected_outcomes: [ { answer_class: "yes" as const, supports: ["04:50", "04:51"], conflicts: [] }, { answer_class: "weak_yes" as const, supports: [], conflicts: ["04:50", "04:51"] }, ], }; const sharedD10 = { ...d10Probe(), candidate_ids: ["04:50", "04:51"], expected_outcomes: [ { answer_class: "yes" as const, supports: ["04:50", "04:51"], conflicts: [] }, { answer_class: "weak_yes" as const, supports: [], conflicts: ["04:50", "04:51"] }, ], }; const delivery = buildRangeDelivery({ inference: inference({ candidates: [ { ...cand("04:50", ["04:48", "04:53"]), probability: 0.6 }, { ...cand("04:51", ["04:48", "04:53"]), probability: 0.4 }, ], probes: [sharedD9, sharedD10], representative_time: "04:50", }), publicCandidates: [ { candidateId: ID_A, time: "04:50" }, { candidateId: ID_B, time: "04:51" }, ], credibleRange: ["04:50", "04:51"], representativeTime: "04:50", }); assert.equal(delivery.columns.length, 2); assert.ok(delivery.shared_traits.some((line) => line.includes("两个时间的关系盘都在"))); assert.ok(delivery.shared_traits.some((line) => line.includes("两个时间的事业盘都在"))); assert.equal(delivery.columns[0]?.traits.d9, ""); assert.equal(delivery.columns[1]?.traits.d9, ""); assert.equal(delivery.columns[0]?.traits.d10, ""); assert.equal(delivery.columns[1]?.traits.d10, ""); }); test("range-delivery DOM is one button per row and adopted is aria-pressed", () => { const delivery = buildRangeDelivery({ inference: inference({ probes: [d9Probe()] }), publicCandidates, credibleRange: ["04:48", "04:59"], representativeTime: "04:50", verificationMarkdown: "| 方法 | 结果 |\n| --- | --- |\n| 1 | 吻合 |", }); const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, { result: deliveryResult(delivery, "04:50"), acceptingCandidateId: null, readonly: false, onAccept: () => undefined, })); assert.equal([...html.matchAll(/ ]/); assert.doesNotMatch(html, /按这个范围用/); assert.doesNotMatch(html, /再补一件经历/); assert.doesNotMatch(html, /都不像/); assert.doesNotMatch(html, /说不好/); assert.doesNotMatch(html, /排盘用/); const source = readFileSync(new URL("../src/components/rectification-range-delivery.tsx", import.meta.url), "utf8"); assert.match(source, /onAccept\(column\.candidate_id\)/); assert.match(source, /aria-pressed=\{adopted\}/); }); test("range-delivery never shows a post-card tie-break button", () => { const delivery = buildRangeDelivery({ inference: inference({ probes: [d9Probe()] }), publicCandidates, credibleRange: ["04:48", "04:59"], representativeTime: "04:50", tieBreakAvailable: true, }); const shown = renderToStaticMarkup(createElement(RectificationRangeDelivery, { result: deliveryResult({ ...delivery, tie_break_available: true }, "04:50"), acceptingCandidateId: null, readonly: false, onAccept: () => undefined, })); // 原值: tie_break_available + onTieBreak 时出现「再答两道参考题微调排序」 // 新值: 卡上不再渲染该入口,参考题只在出卡前问 // 原因: BUG-706,产品决定删掉卡上第二条路 assert.doesNotMatch(shown, /再答两道参考题微调排序/); assert.doesNotMatch(shown, /onTieBreak/); assert.equal([...shown.matchAll(/ undefined, adoptLocked: true, })); assert.match(locked, /先答完上面这道,再选时间/); }); test("the range-delivery component does not render relative support or the old two actions", () => { const source = readFileSync(new URL("../src/components/rectification-range-delivery.tsx", import.meta.url), "utf8"); // 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。 const chat = rectificationChatSurface; assert.doesNotMatch(source, /相对支持度/); assert.doesNotMatch(source, /当前推荐/); assert.doesNotMatch(source, /采用此时间/); assert.doesNotMatch(source, /排盘用/); // 原值: 不含 rangeDeliveryAdopt / rangeDeliveryContinue;`Adopt[^e]` 会误伤 rangeDeliveryAdopting // 新值: 只禁旧标识符单词本身 // 原因: BUG-595 决策 1 assert.doesNotMatch(source, /\brangeDeliveryAdopt\b|\brangeDeliveryContinue\b/); assert.doesNotMatch(chat, /相对支持度/); assert.doesNotMatch(chat, /RectificationCandidateCards/); assert.match(chat, / { // 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。 const chat = rectificationChatSurface; assert.match(chat, /showSelectionCards && candidateResult && message\.renderKey === selectionCardMessageKey/); assert.doesNotMatch(chat, /dismissedRangeResultId/); assert.doesNotMatch(chat, /RangeDeliveryContinueHint/); assert.doesNotMatch(chat, /rectification-composer-meta/); }); const CAREER_WINDOW_PROBE = { candidate_label: "A", domain: "career", window_label: "2027 年 3 月附近", user_meaning: "候选 A 预测下一次事业变动更可能在 2027 年 3 月附近。这是预测窗口,不是承诺;下次发生时回来补一条,可进一步分辨。", used_for_scoring: false, }; test("post-adopt first-round narration appends prospective windows when present", () => { const base = "2013 年前后,钱上有没有明显变化?"; const withWindows = appendPostAdoptProspectiveWindows(base, { prospective_probes: [CAREER_WINDOW_PROBE], }); assert.match(withWindows, /预测下一次事业变动/); assert.match(withWindows, /2013 年前后/); const without = appendPostAdoptProspectiveWindows(base, { prospective_probes: [] }); assert.equal(without, base); const idle = readFileSync( new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url), "utf8", ); const start = idle.indexOf("export async function persistNextInterviewIfIdle"); const slice = idle.slice(start, start + 1800); assert.match(slice, /appendPostAdoptProspectiveWindows/); assert.match(slice, /dossier\.case\.acceptedTime/); }); test("delivery turn narration is three sentences without eight-method tables", () => { const text = deliveryTurnNarration({ credibleRange: ["04:51", "04:59"], representativeTime: "04:53", eventCount: 7, fitPercent: 80, }); const sentences = text.split(/(?<=。)/).filter(Boolean); // 原值: 3 句(范围、对照了 7 件经历与吻合率、边界句) // 新值: 2 句(范围、边界句);有计分选择题时第二句「用了 N 道选择题」,见下 // 原因: BUG-1085 交付正文不数口述经历、不写吻合率(2026-09-29 D2) assert.equal(sentences.length, 2); assert.equal(deliveryTurnNarration({ credibleRange: ["04:51", "04:59"], choiceCount: 4 }).split(/(?<=。)/).filter(Boolean).length, 3); assert.doesNotMatch(text, /方法1|Technique Audit/); }); function ledgerRow( match: "strong" | "medium" | "weak" | "none", yearMonth: string, domain: string, ) { return { summary: `${domain} ${yearMonth}`, match, match_label: match, user_meaning: `${domain} ${yearMonth}`, gochara: null, domain, year_month: yearMonth, }; } test("posterior top three still get fit when engine score order differs", () => { const ID_C = "cccccccc-cccc-4ccc-8ccc-ccccccccccc3"; const delivery = buildRangeDelivery({ inference: inference({ candidates: [ { ...cand("04:49", ["04:47", "04:53"]), probability: 0.51, posterior_score: 51 }, { ...cand("04:47", ["04:47", "04:53"]), probability: 0.24, posterior_score: 24 }, { ...cand("04:53", ["04:47", "04:53"]), probability: 0.24, posterior_score: 24 }, ], representative_time: "04:49", range_start: "04:47", range_end: "04:53", credible_range: ["04:47", "04:53"], }), publicCandidates: [ { candidateId: ID_A, time: "04:49" }, { candidateId: ID_B, time: "04:47" }, { candidateId: ID_C, time: "04:53" }, ], credibleRange: ["04:47", "04:53"], representativeTime: "04:49", eventDashaLedgerByTime: { "04:47": [ledgerRow("strong", "2016-09", "education"), ledgerRow("none", "2024-05", "relationship")], "04:49": [ledgerRow("strong", "2016-09", "education"), ledgerRow("medium", "2018-03", "career")], "04:53": [ledgerRow("weak", "2016-09", "education"), ledgerRow("none", "2024-05", "relationship")], }, prospectiveWindowsByTime: { "04:47": [{ domain: "career", from: "2027-03", to: "2027-03" }], "04:49": [{ domain: "career", from: "2027-03", to: "2027-03" }], "04:53": [], }, }); assert.equal(delivery.columns.map((column) => column.time).join(","), "04:49,04:47,04:53"); assert.ok(delivery.columns.every((column) => column.fit != null)); assert.ok(delivery.columns.every((column) => column.windows != null)); assert.match(delivery.columns[0]?.fit_line ?? "", /2 件经历里 2 件对得上/); assert.match(delivery.columns[1]?.fit_line ?? "", /最不合的是 2024 年 5 月那段感情/); assert.equal(delivery.columns[2]?.window_line, RECTIFICATION_USER_COPY.rangeDeliveryNoWindow); }); test("missing by_time keys stay null and render 还没对照, never 0 · 0 · 0", () => { const delivery = buildRangeDelivery({ inference: inference({ probes: [d9Probe()], candidates: [ { ...cand("04:50", ["04:48", "04:59"]), probability: 0.6 }, { ...cand("04:56", ["04:48", "04:59"]), probability: 0.4 }, ], }), publicCandidates, credibleRange: ["04:48", "04:59"], representativeTime: "04:50", eventDashaLedgerByTime: { "04:50": [ledgerRow("strong", "2016-09", "education")], }, prospectiveWindowsByTime: { "04:50": [{ domain: "career", from: "2027-03", to: "2027-03" }], }, }); assert.ok(delivery.columns[0]?.fit != null); assert.equal(delivery.columns[1]?.fit, null); assert.equal(delivery.columns[1]?.windows, null); assert.equal(delivery.columns[1]?.fit_line, RECTIFICATION_USER_COPY.rangeDeliveryNotCompared); const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, { result: deliveryResult(delivery), acceptingCandidateId: null, readonly: false, onAccept: () => undefined, })); assert.match(html, /这一分钟还没对照/); assert.doesNotMatch(html, /0 · 0 · 0/); assert.doesNotMatch(html, /强相关/); assert.doesNotMatch(html, /