Files
Jyotisha/frontend/tests/rectification-fewer-probes-card-20260926.test.ts
T
Jesse_ChenandClaude Opus 5.5 7203d94e9c
Independent Staging Quality Gate / validate (push) Successful in 11m49s
Independent Staging Quality Gate / publish (push) Successful in 3m44s
fix(rectification): 区分不开时不显示「最可能」副标题(验收补改)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 14:45:11 +08:00

303 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 { buildRangeDelivery } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
import {
RANGE_DELIVERY_PERCENT_MIN_GAP,
RECTIFICATION_USER_COPY,
listUserVisibleCopy,
rangeDeliveryMostLikely,
rangeDeliveryShowsPercents,
} from "../src/lib/rectification-agentic/user-copy.ts";
import {
GUIDED_WINDOW_CASE_LIMIT,
cardHoldingLinesExhausted,
guidedCollectExhausted,
guidedWindowPool,
type CollectionEvidence,
type GuidedCollectWindow,
} from "../src/lib/rectification-agentic/v9/collection-question-pool.ts";
import { buildMethodFollowupPlan, targetedCollectFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { RectificationRangeDelivery } from "../src/components/rectification-range-delivery.tsx";
import type { RectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts";
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
// Fictional data only. Three columns inside 05:00–05:15.
function cand(time: string, probability: number): InferenceCandidate {
return {
id: time,
time,
cluster_range: [time, time],
prior_score: probability * 100,
posterior_score: probability * 100,
probability,
status: "active",
rank: 1,
strong_conflict_count: 0,
};
}
function inference(probabilities: readonly [number, number, number]): InferenceState {
return {
algorithm_version: INFERENCE_ALGORITHM_VERSION,
candidate_set_id: "05:00-05:15:05:02,05:07,05:13",
revision: 1,
phase: "discrimination",
result_status: "credible_range",
range_start: "05:00",
range_end: "05:15",
candidates: [
cand("05:07", probabilities[0]),
cand("05:02", probabilities[1]),
cand("05:13", probabilities[2]),
],
events: [
{ id: "e1", domain: "career", year: 2018, precision: "month", usage: "training" },
{ id: "e2", domain: "education", year: 2012, precision: "month", usage: "training" },
],
probes: [],
answered_probes: [],
rounds: [],
entropy: 1,
representative_time: "05:07",
credible_range: ["05:00", "05:15"],
};
}
const PUBLIC = [
{ candidateId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", time: "05:07" },
{ candidateId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2", time: "05:02" },
{ candidateId: "cccccccc-cccc-4ccc-8ccc-ccccccccccc3", time: "05:13" },
];
function result(probabilities: readonly [number, number, number]): RectificationCandidateResult {
const delivery = buildRangeDelivery({
inference: inference(probabilities),
publicCandidates: PUBLIC,
credibleRange: ["05:00", "05:15"],
representativeTime: "05:07",
});
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: null,
selectionKind: 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: null,
};
}
function render(probabilities: readonly [number, number, number]): string {
return renderToStaticMarkup(createElement(RectificationRangeDelivery, {
result: result(probabilities),
acceptingCandidateId: null,
readonly: false,
onAccept: () => undefined,
}));
}
test("D4 threshold is 5 points between the first and second column", () => {
assert.equal(RANGE_DELIVERY_PERCENT_MIN_GAP, 5);
assert.equal(rangeDeliveryShowsPercents([{ probability_percent: 40 }, { probability_percent: 35 }]), true);
assert.equal(rangeDeliveryShowsPercents([{ probability_percent: 30 }, { probability_percent: 26 }]), false);
assert.equal(rangeDeliveryShowsPercents([
{ probability_percent: 26 },
{ probability_percent: 26 },
{ probability_percent: 20 },
]), false);
// Order-independent: the gap is first − second after ranking.
assert.equal(rangeDeliveryShowsPercents([{ probability_percent: 30 }, { probability_percent: 45 }]), true);
// One column has no second place to be clearly ahead of.
assert.equal(rangeDeliveryShowsPercents([{ probability_percent: 100 }]), false);
});
test("D3 range is the title and the representative minute is the subtitle", () => {
const html = render([0.45, 0.3, 0.25]);
assert.match(html, /<strong>目前范围 05:00–05:15(对照了 2 件经历)<\/strong>/);
assert.match(html, /rectification-range-delivery__most-likely">最可能 05:07</);
assert.ok(html.indexOf("最可能 05:07") < html.indexOf("rectification-range-delivery__columns"));
assert.equal(rangeDeliveryMostLikely("05:07"), "最可能 05:07");
});
test("D4 gap ≥ 5 shows every column's percentage and no indistinct line", () => {
const html = render([0.45, 0.3, 0.25]);
assert.match(html, /相对可能性 45%/);
assert.match(html, /相对可能性 30%/);
assert.match(html, /相对可能性 25%/);
assert.doesNotMatch(html, new RegExp(RECTIFICATION_USER_COPY.rangeDeliveryIndistinct));
});
test("D4 gap < 5 shows no numbers and one indistinct sentence; columns and order unchanged", () => {
const html = render([0.35, 0.33, 0.32]);
assert.doesNotMatch(html, /相对可能性/);
assert.doesNotMatch(html, /\d+%/);
assert.equal(html.split(RECTIFICATION_USER_COPY.rangeDeliveryIndistinct).length - 1, 1);
assert.equal(
RECTIFICATION_USER_COPY.rangeDeliveryIndistinct,
"这几个时刻目前区分不开,补一件带年月的经历能帮助分开。",
);
assert.equal([...html.matchAll(/<button\b/g)].length, 3);
const order = ["05:07", "05:02", "05:13"].map((time) => html.indexOf(`__time">${time}`));
assert.deepEqual([...order].sort((left, right) => left - right), order);
assert.ok(order.every((index) => index > 0));
// Acceptance 2026-09-26: 「最可能」 beside 「区分不开」 contradicts itself, so
// the subtitle is hidden when the columns are indistinct.
assert.doesNotMatch(html, /最可能/);
});
test("D3/D4 copy is registered as user-visible copy", () => {
const visible = listUserVisibleCopy();
assert.ok(visible.includes(RECTIFICATION_USER_COPY.rangeDeliveryIndistinct));
assert.ok(visible.includes(rangeDeliveryMostLikely("05:07")));
const voice = readFileSync(new URL("../docs/VOICE.md", import.meta.url), "utf8");
assert.ok(voice.includes(RECTIFICATION_USER_COPY.rangeDeliveryIndistinct));
assert.ok(voice.includes("最可能 HH:MM"));
});
const FRESH_WINDOWS: readonly GuidedCollectWindow[] = [
{ year: 2019, month_lo: 4, month_hi: 6, domain: "any", split: { left: 2, right: 1 } },
{ year: 2015, month_lo: 9, month_hi: 9, domain: "any", split: { left: 1, right: 2 } },
];
function askedWindow(year: number, lo: number, hi: number, status = "declined") {
return {
questionId: `collect:guided:window:${year}:${lo}:${hi}:any`,
target_domain: "any",
status,
intent: "collect_method_evidence",
target_kind: "guided:window:any",
};
}
test("D1 at most two guided windows per Case even when a new receipt brings fresh ones", () => {
assert.equal(GUIDED_WINDOW_CASE_LIMIT, 2);
const oneAsked = [askedWindow(2021, 1, 3)];
assert.equal(guidedWindowPool(FRESH_WINDOWS, [], oneAsked).length, 2);
const twoAsked = [askedWindow(2021, 1, 3), askedWindow(2017, 5, 5)];
assert.equal(guidedWindowPool(FRESH_WINDOWS, [], twoAsked).length, 0);
// The same window re-asked under `:next` is still one window.
const sameTwice = [askedWindow(2021, 1, 3), { ...askedWindow(2021, 1, 3), questionId: "collect:guided:window:2021:1:3:any:next" }];
assert.equal(guidedWindowPool(FRESH_WINDOWS, [], sameTwice).length, 2);
// An active (being asked) window does not count as asked yet.
assert.equal(guidedWindowPool(FRESH_WINDOWS, [], [askedWindow(2021, 1, 3), askedWindow(2017, 5, 5, "active")]).length, 2);
// With the cap reached the next question falls through to the targeted lines.
const followup = targetedCollectFollowup(["d9"], [], twoAsked, ["05:00", "05:15"], 3, ["05:00", "05:15"], FRESH_WINDOWS);
assert.ok(followup?.collection_key?.startsWith("collect:targeted:"), followup?.collection_key);
});
const ALL_SEVEN: CollectionEvidence[] = [
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2012-09-01", occurredTo: "2012-09-01" },
{ status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2018-07-01", occurredTo: "2018-07-01" },
{ status: "confirmed", domain: "relocation", datePrecision: "month", occurredFrom: "2016-08-01", occurredTo: "2016-08-01" },
{ status: "confirmed", domain: "relationship", datePrecision: "month", occurredFrom: "2021-08-01", occurredTo: "2021-08-01" },
{ status: "confirmed", domain: "family", datePrecision: "month", occurredFrom: "2019-01-01", occurredTo: "2019-01-01" },
{ status: "confirmed", domain: "finance", datePrecision: "month", occurredFrom: "2020-03-01", occurredTo: "2020-03-01" },
{ status: "confirmed", domain: "health_pressure", datePrecision: "month", occurredFrom: "2022-04-01", occurredTo: "2022-04-01" },
];
const LAYERS = ["d9", "d10", "d4", "d5", "d7", "d2", "d30"];
test("D2 unasked guided windows no longer hold the card once the targeted seven are asked", () => {
// The whole guided pool still reports a window left…
assert.equal(guidedCollectExhausted(LAYERS, ALL_SEVEN, [], FRESH_WINDOWS, ["05:00", "05:15"], 3), false);
// …but the lines that may hold the card are asked out.
assert.equal(cardHoldingLinesExhausted(LAYERS, ALL_SEVEN, [], ["05:00", "05:15"], 3), true);
});
test("D2 the skip re-ask and a pending year answer still hold the card", () => {
const skipped = [{
questionId: "collect:targeted:family",
target_domain: "family",
status: "skipped",
intent: "collect_method_evidence",
target_kind: "targeted:family",
}];
const withoutFamily = ALL_SEVEN.filter((row) => row.domain !== "family");
assert.equal(cardHoldingLinesExhausted(LAYERS, withoutFamily, skipped, ["05:00", "05:15"], 3), false);
const yesOnWindow = [askedWindow(2019, 4, 6, "resolved")];
assert.equal(cardHoldingLinesExhausted(LAYERS, ALL_SEVEN, yesOnWindow, ["05:00", "05:15"], 3), false);
// Targeted lines still open: keep asking.
assert.equal(cardHoldingLinesExhausted(LAYERS, ALL_SEVEN.slice(0, 3), [], ["05:00", "05:15"], 3), false);
});
test("D2 gate unmet with every card-holding line asked delivers; D5 range rules unchanged", () => {
const decision = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: [
{ time: "05:02", score: 13 },
{ time: "05:07", score: 13.4 },
{ time: "05:13", score: 13.1 },
],
holdoutValidation: "unavailable",
datedEventCount: 7,
datedDomainCount: 7,
discriminatorProbe: null,
targetedCollectExhausted: true,
refreshExhausted: true,
inferenceCredibleRange: ["05:00", "05:15"],
openingCandidateRange: ["04:45", "05:15"],
precisionGateMet: false,
guidedCollectExhausted: cardHoldingLinesExhausted(LAYERS, ALL_SEVEN, [], ["05:00", "05:15"], 3),
});
assert.equal(decision.canOfferRange, true);
assert.deepEqual(decision.credibleRange, ["05:00", "05:15"]);
assert.equal(decision.precisionGateMet, false);
assert.equal(decision.canConfirmExactMinute, false);
});
test("D2 a delivered card does not carry an unasked guided window as the next question", () => {
const base = {
evidence: ALL_SEVEN,
declinedTopics: [],
candidatesSeparated: false,
remainingLayers: LAYERS,
remainingSplitTimes: ["05:00", "05:15"] as const,
remainingCandidateCount: 3,
remainingCredibleRange: ["05:00", "05:15"] as const,
guidedWindows: FRESH_WINDOWS,
};
for (const sessionOutcome of ["completed_with_range", "provisional_range", "adopt_representative"] as const) {
const plan = buildMethodFollowupPlan({ ...base, sessionOutcome });
const key = plan.next_followup?.collection_key ?? "";
assert.equal(key.startsWith("collect:guided:window:"), false, `${sessionOutcome}: ${key}`);
}
// While still collecting, the same windows are asked (at most two per Case).
const collecting = buildMethodFollowupPlan({ ...base, sessionOutcome: "collect_evidence" });
assert.match(collecting.next_followup?.collection_key ?? "", /^collect:guided:window:/);
});