Files
Jyotisha/frontend/tests/rectification-range-delivery-20260907.test.ts
T
66f087b588 feat(rectification): submit varga-resolution implementation for review
Review-only snapshot for BUG-1115 through BUG-1117; not merge-ready. New opening and append-turn PostgreSQL permission failures remain blocked. Persisted joint replay has zero completed questions; segment ordering remains off by default. The existing offline replay JSON is retained stale and unchanged after a denied overwrite, including its CRLF line endings. Browser/provider validation and final serial gates remain pending. No deployment, role permission changes, or staging/main push.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-01 08:04:04 +08:00

596 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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> = {}): 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<typeof buildRangeDelivery>,
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(/<button\b/g)].length, delivery.columns.length);
assert.match(html, /aria-pressed="true"/);
assert.match(html, /查看验证报告/);
assert.match(html, /更像这个|已采用/);
assert.doesNotMatch(html, / open[=> ]/);
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(/<button\b/g)].length, delivery.columns.length);
const locked = renderToStaticMarkup(createElement(RectificationRangeDelivery, {
result: deliveryResult({ ...delivery, tie_break_available: true }, "04:50"),
acceptingCandidateId: null,
readonly: false,
onAccept: () => 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, /<RectificationRangeDelivery/);
assert.match(source, /rangeDeliveryAdopting/);
assert.match(source, /rangeDeliveryMoreLikeThis/);
assert.match(source, /RectificationVerificationReport/);
});
test("chat pins the card on the offering message without a dismissed-range continue hint", () => {
// 原值: 读 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, /<h3/);
assert.doesNotMatch(html, /经历对照/);
assert.doesNotMatch(html, /往后 12 个月/);
});