Files
Jyotisha/frontend/tests/rectification-range-delivery-20260907.test.ts
T
Jesse_ChenandCursor 7e3b6cdc7a
Independent Staging Quality Gate / validate (push) Successful in 25m9s
Independent Staging Quality Gate / publish (push) Successful in 37m19s
fix(rectification): simplify delivery card and guard double turns (BUG-595, BUG-596)
Make the range card row-select, drop the composer 先这样 control and adopt status bar, keep delivery copy to three sentences with a folded verification report, and skip a second no-message agent run after a terminal delivery turn.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 19:56:48 +08:00

292 lines
11 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.rows.map((row, index) => ({
candidateId: row.candidate_id,
rank: index + 1,
time: row.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 rows put the representative first and label D9/D10 differences", () => {
// 原值: buildDivergencePanel 两列 + 稳定/敏感主题
// 新值: rows 代表分钟第一、差异句来自类型表、相同则空
// 原因: BUG-595 决策 1,时间卡直接选
const delivery = buildRangeDelivery({
inference: inference({ probes: [d9Probe(), d10Probe()] }),
publicCandidates,
credibleRange: ["04:48", "04:59"],
representativeTime: "04:50",
});
assert.equal(delivery.rows[0]?.time, "04:50");
assert.equal(delivery.rows[0]?.representative, true);
assert.equal(delivery.rows[0]?.difference_label, "");
assert.equal(delivery.rows[1]?.time, "04:56");
assert.equal(delivery.rows[1]?.representative, false);
assert.ok(delivery.rows[1]?.difference_label.includes(d9DivergenceLabel("天蝎座") ?? "missing"));
assert.ok(delivery.rows[1]?.difference_label.includes(d10DivergenceLabel("狮子座") ?? "missing"));
assert.equal(delivery.event_count, 2);
assert.equal(delivery.boundary, REPRESENTATIVE_MINUTE_DISCLAIMER);
assert.equal(delivery.representative_candidate_id, ID_A);
});
test("same D9/D10 as the representative leaves the difference empty", () => {
const delivery = buildRangeDelivery({
inference: inference({
candidates: [cand("04:50", ["04:48", "04:53"]), cand("04:51", ["04:48", "04:53"])],
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.rows.every((row) => row.difference_label === ""), true);
});
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.rows.length);
assert.match(html, /aria-pressed="true"/);
assert.match(html, /查看验证报告/);
assert.doesNotMatch(html, / open[=> ]/);
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\(row\.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, /采用此时间/);
// 原值: 不含 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, /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/);
});