Files
Jyotisha/frontend/tests/rectification-range-delivery-20260907.test.ts
T
Jesse_Chen a00b069dcc
Independent Staging Quality Gate / validate (push) Successful in 13m23s
Independent Staging Quality Gate / publish (push) Successful in 30m54s
fix(rectification): unblock range-card tsc and restore post-adopt windows (BUG-583)
Null-check the candidate result before reading resultId so next build can ship. Adopt reconnects prospective windows on the first verify turn, and dismissing the range card now shows the collect hint.

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

346 lines
13 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 {
buildDivergencePanel,
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 } from "../src/lib/rectification-agentic/user-copy.ts";
import type { NakshatraBoundary } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import { appendPostAdoptProspectiveWindows } from "../src/lib/rectification-agentic/v9/answer-choice.ts";
import { RangeDeliveryContinueHint } from "../src/components/rectification-range-delivery.tsx";
import { STEP_STATE_COPY } from "../src/lib/rectification-agentic/v9/step-state.ts";
const ID_A = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1";
const ID_B = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2";
const ID_C = "cccccccc-cccc-4ccc-8ccc-ccccccccccc3";
const ID_D = "dddddddd-dddd-4ddd-8ddd-ddddddddddd4";
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" },
] as const;
test("two clusters with unasked D9 yield two D9 columns", () => {
const panel = buildDivergencePanel({
inference: inference({ probes: [d9Probe()] }),
publicCandidates,
});
assert.equal(panel.eligible, true);
assert.equal(panel.columns.length, 2);
assert.deepEqual(panel.columns.map((column) => column.rows.map((row) => row.layer)), [["d9"], ["d9"]]);
assert.equal(panel.columns[0]?.rows[0]?.sign, "天秤座");
assert.equal(panel.columns[1]?.rows[0]?.sign, "天蝎座");
assert.equal(panel.columns[0]?.rows[0]?.label, d9DivergenceLabel("天秤座"));
assert.equal(panel.columns[1]?.rows[0]?.label, d9DivergenceLabel("天蝎座"));
assert.equal(panel.columns[0]?.representative_candidate_id, ID_A);
assert.equal(panel.columns[1]?.representative_candidate_id, ID_B);
});
test("asked D9 with unasked D10 only lists D10", () => {
const panel = buildDivergencePanel({
inference: inference({
probes: [d9Probe(), d10Probe()],
answered_probes: [{
probe_id: "probe:varga.d9",
semantic_key: "varga.d9.天秤座/天蝎座",
candidate_split_hash: "varga.d9",
answer_class: "yes",
classified_from: "choice",
}],
}),
publicCandidates,
});
assert.equal(panel.eligible, true);
assert.deepEqual(panel.columns.map((column) => column.rows.map((row) => row.layer)), [["d10"], ["d10"]]);
assert.equal(panel.columns[0]?.rows[0]?.sign, "巨蟹座");
assert.equal(panel.columns[1]?.rows[0]?.sign, "狮子座");
assert.equal(panel.columns[0]?.rows[0]?.label, d10DivergenceLabel("巨蟹座"));
});
test("asked D9 and D10 make the panel ineligible", () => {
const panel = buildDivergencePanel({
inference: inference({
probes: [d9Probe(), d10Probe()],
answered_probes: [
{
probe_id: "probe:varga.d9",
semantic_key: "varga.d9.天秤座/天蝎座",
candidate_split_hash: "varga.d9",
answer_class: "yes",
classified_from: "choice",
},
{
probe_id: "probe:varga.d10",
semantic_key: "varga.d10.巨蟹座/狮子座",
candidate_split_hash: "varga.d10",
answer_class: "yes",
classified_from: "choice",
},
],
}),
publicCandidates,
});
assert.equal(panel.eligible, false);
assert.equal(panel.columns.length, 0);
assert.equal(panel.reason, "layers_asked");
});
test("four clusters make the panel ineligible", () => {
const panel = buildDivergencePanel({
inference: inference({
candidates: [
cand("04:48", ["04:48", "04:49"]),
cand("04:51", ["04:50", "04:52"]),
cand("04:54", ["04:53", "04:55"]),
cand("04:58", ["04:56", "04:59"]),
],
probes: [d9Probe()],
}),
publicCandidates: [
{ candidateId: ID_A, time: "04:48" },
{ candidateId: ID_B, time: "04:51" },
{ candidateId: ID_C, time: "04:54" },
{ candidateId: ID_D, time: "04:58" },
],
});
assert.equal(panel.eligible, false);
assert.equal(panel.reason, "cluster_count");
assert.equal(panel.columns.length, 0);
});
test("panel labels hit the D9/D10 type tables and never invent copy", () => {
const panel = buildDivergencePanel({
inference: inference({ probes: [d9Probe(), d10Probe()] }),
publicCandidates,
});
assert.equal(panel.eligible, true);
for (const column of panel.columns) {
for (const row of column.rows) {
if (row.layer === "d9") {
assert.ok(row.label.includes(D9_TYPE_TABLE[row.sign]!.userChoice));
assert.equal(row.label, d9DivergenceLabel(row.sign));
}
if (row.layer === "d10") {
assert.ok(row.label.includes(D10_TYPE_TABLE[row.sign]!.userChoice));
assert.equal(row.label, d10DivergenceLabel(row.sign));
}
}
}
});
test("nakshatra traits appear only when unasked and near a boundary", () => {
const boundary: NakshatraBoundary = {
near_boundary: true,
user_meaning: null,
options: [
{ key: "A", time_bias: "earlier", traits: ["做事偏急", "话少"] },
{ key: "B", time_bias: "later", traits: ["做事偏稳", "话多"] },
],
};
const panel = buildDivergencePanel({
inference: inference(),
nakshatraBoundary: boundary,
publicCandidates,
});
assert.equal(panel.eligible, true);
assert.equal(panel.columns[0]?.rows.some((row) => row.layer === "nakshatra"), true);
assert.ok(panel.columns[0]?.rows.some((row) => row.label === "做事偏急、话少"));
assert.ok(panel.columns[1]?.rows.some((row) => row.label === "做事偏稳、话多"));
const asked = buildDivergencePanel({
inference: inference({
answered_probes: [{
probe_id: "probe:nakshatra",
semantic_key: "nakshatra-boundary:04:48-04:59:04:50,04:56",
candidate_split_hash: "n",
answer_class: "yes",
classified_from: "choice",
}],
}),
nakshatraBoundary: boundary,
publicCandidates,
});
assert.equal(asked.eligible, false);
});
test("range delivery keeps the representative-minute boundary and event count", () => {
const delivery = buildRangeDelivery({
inference: inference({ probes: [d9Probe()] }),
rangeReading: { stable_themes: ["career"], sensitive_themes: ["marriage"] },
publicCandidates,
credibleRange: ["04:48", "04:59"],
representativeTime: "04:50",
});
assert.deepEqual(delivery.range, ["04:48", "04:59"]);
assert.equal(delivery.representative_time, "04:50");
assert.equal(delivery.representative_candidate_id, ID_A);
assert.equal(delivery.event_count, 2);
assert.deepEqual(delivery.stable_themes, ["事业方向"]);
assert.deepEqual(delivery.sensitive_themes, ["婚恋(D9"]);
assert.equal(delivery.boundary, REPRESENTATIVE_MINUTE_DISCLAIMER);
assert.equal(delivery.divergence.eligible, true);
});
test("the range-delivery component does not render relative support", () => {
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(chat, /相对支持度/);
assert.doesNotMatch(chat, /RectificationCandidateCards/);
assert.match(chat, /<RectificationRangeDelivery/);
assert.match(chat, /mode: "range"/);
assert.match(source, /rangeDeliveryAdopt/);
assert.match(source, /rangeDeliveryContinue/);
});
test("chat null-checks candidateResult before reading dismissedRangeResultId", () => {
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
const start = chat.indexOf("showSelectionCards && candidateResult");
assert.ok(start >= 0);
const slice = chat.slice(start, start + 280);
assert.match(slice, /candidateResult && dismissedRangeResultId !== candidateResult\.resultId/);
assert.ok(slice.indexOf("candidateResult &&") < slice.indexOf("dismissedRangeResultId"));
});
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);
assert.doesNotMatch(without, /预测下一次事业变动/);
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("dismissing the range card hides it and shows the collect-continue hint", () => {
const hidden = renderToStaticMarkup(createElement(RangeDeliveryContinueHint, { visible: false }));
assert.equal(hidden, "");
const shown = renderToStaticMarkup(createElement(RangeDeliveryContinueHint, { visible: true }));
assert.ok(shown.includes(STEP_STATE_COPY.collect.headline));
assert.ok(shown.includes(RECTIFICATION_USER_COPY.continueCollectFallback));
assert.doesNotMatch(shown, /rectification-step-state/);
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
assert.match(chat, /RangeDeliveryContinueHint/);
assert.match(chat, /setDismissedRangeResultId\(candidateResult\.resultId\)/);
assert.match(chat, /rangeCardDismissed/);
});