Files
Jyotisha/frontend/tests/rectification-range-delivery-20260907.test.ts
T
Jesse_ChenandCursor a43a6db884
Independent Staging Quality Gate / validate (push) Successful in 14m8s
Independent Staging Quality Gate / publish (push) Successful in 10m21s
fix(rectification): three-column candidate compare card (BUG-597, BUG-598)
Replace the minute-row delivery card with up to three compare columns so users can pick the time that fits, and apply the adult-year floor on inspect fallbacks.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 00:12:20 +08:00

325 lines
12 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 type { RectificationCandidateResult } from "../src/lib/rectification-candidate-result.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("delivery columns rank by posterior and write D9/D10 on every column", () => {
// 原值: rows 代表分钟第一、差异句来自类型表、相同则空
// 新值: 至多三列,按后验排序,每列写全 D9/D10,不预标排盘用
// 原因: BUG-597 决策 12
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 still writes the type-table sentences on both columns", () => {
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.equal(delivery.columns[0]?.traits.d9, delivery.columns[1]?.traits.d9);
assert.ok((delivery.columns[0]?.traits.d9 ?? "").length > 0);
});
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("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");
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
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", () => {
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
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);
assert.equal(sentences.length, 3);
assert.doesNotMatch(text, /方法1|Technique Audit/);
});