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>
596 lines
29 KiB
TypeScript
596 lines
29 KiB
TypeScript
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 个月/);
|
||
});
|