Files
Jyotisha/frontend/tests/rectification-credible-range-projection.test.ts
T
Jesse_Chen ca4e2408d8 fix(rectification): keep collecting while dated evidence domains remain open
Separated candidates were treated as adoptable while finance, relocation, health, or occupation collect was still pending, so clickable cards and a verification report appeared before those questions were asked.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 21:06:38 +08:00

277 lines
9.4 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 test from "node:test";
import { buildInferenceState, type EngineEventInput } from "../src/lib/rectification-agentic/core/build-state.ts";
import { unionStillValidRange } from "../src/lib/rectification-agentic/core/credible-range.ts";
import { decideFromDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import {
authoritativeCandidateProjection,
buildCaseInferenceState,
} from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
const HOLDOUT_EVENTS: readonly EngineEventInput[] = [
{ id: "e-edu", domain: "education", year: 2016, precision: "year" },
{ id: "e-career", domain: "career", year: 2018, precision: "year" },
{ id: "e-rel", domain: "relationship", year: 2021, precision: "year" },
{ id: "e-fam", domain: "family", year: 2023, precision: "year" },
];
const COVERAGE_EVIDENCE = [
{
id: "e-edu",
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
eventKind: "education_milestone",
},
{
id: "e-career",
status: "confirmed",
domain: "career",
datePrecision: "year",
occurredFrom: "2018-01-01",
occurredTo: null,
eventKind: "career_entry",
},
{
id: "e-rel",
status: "confirmed",
domain: "relationship",
datePrecision: "year",
occurredFrom: "2021-01-01",
occurredTo: null,
eventKind: "relationship_start",
},
{
id: "e-fam",
status: "confirmed",
domain: "family",
datePrecision: "year",
occurredFrom: "2023-01-01",
occurredTo: null,
eventKind: "family_event",
},
{
id: "e-occ",
status: "confirmed",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
},
{
id: "e-fin",
status: "confirmed",
domain: "finance",
datePrecision: "year",
occurredFrom: "2017-01-01",
occurredTo: null,
eventKind: "income_change",
},
{
id: "e-reloc",
status: "confirmed",
domain: "relocation",
datePrecision: "year",
occurredFrom: "2019-01-01",
occurredTo: null,
eventKind: "home_change",
},
{
id: "e-health",
status: "confirmed",
domain: "health_pressure",
datePrecision: "year",
occurredFrom: "2021-01-01",
occurredTo: null,
eventKind: "self_health_event",
},
] as const;
function producedSnapshot(
rows: readonly Readonly<{ time: string; relativeSupport: number }>[],
events: readonly EngineEventInput[] = [],
) {
const times = rows.map((item) => item.time).sort();
const state = buildInferenceState({
range_start: times[0]!,
range_end: times[times.length - 1]!,
candidates: rows.map((item) => ({
id: item.time,
time: item.time,
relative_support: item.relativeSupport,
})),
events,
probes: [],
});
const persisted = state.candidates.map((item) => ({
candidateId: item.id,
time: item.time,
rank: item.rank,
relativeSupport: Math.round(item.posterior_score),
}));
return {
state,
latest: {
resultId: "55555555-5555-4555-8555-555555555555",
candidates: persisted,
representativeTime: state.representative_time,
evidenceLedgerFingerprint: evidenceLedgerFingerprint(COVERAGE_EVIDENCE as never),
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: state,
},
},
};
}
function dossierFor(latest: ReturnType<typeof producedSnapshot>["latest"]) {
return {
evidence: COVERAGE_EVIDENCE,
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
latestResult: latest,
case: { acceptedTime: null },
};
}
test("producer 58/42 lead keeps both candidates and can close without opening the minute gate", () => {
const { state, latest } = producedSnapshot([
{ time: "05:02", relativeSupport: 58 },
{ time: "04:55", relativeSupport: 42 },
], HOLDOUT_EVENTS);
assert.deepEqual(state.credible_range, unionStillValidRange(state.candidates));
assert.notDeepEqual(state.credible_range, ["04:55", "05:02"]);
const projection = authoritativeCandidateProjection(latest);
assert.equal(projection.consistent, true);
assert.equal(projection.candidates.length, 2);
assert.deepEqual(projection.credibleRange, state.credible_range);
const decision = decideFromDossier(dossierFor(latest));
assert.equal(decision.separation.sufficient, true);
// 原断言 nextAction=ask_holdout_validation → 新断言 ready_to_adopt。
// 为什么:分离充分仍不够开确认门;holdout 只挡唯一分钟,provisional 采用应直接出牌。
assert.equal(decision.nextAction, "ready_to_adopt");
assert.equal(decision.canAdopt, true);
assert.equal(decision.canConfirmExactMinute, false);
assert.notEqual(decision.sessionOutcome, "exact_minute_confirmed");
});
test("producer 58/42 with no holdout remains a review-only range", () => {
const { latest } = producedSnapshot([
{ time: "05:02", relativeSupport: 58 },
{ time: "04:55", relativeSupport: 42 },
]);
const decision = decideFromDossier(dossierFor(latest));
assert.equal(authoritativeCandidateProjection(latest).consistent, true);
assert.equal(decision.separation.sufficient, true);
// 原断言 canAdopt=falseholdout unavailable 不放行)→ 新断言 canAdopt=true。
// 为什么:无 holdout 只挡唯一分钟确认,不挡代表性采用。
assert.equal(decision.nextAction, "offer_provisional_range");
assert.equal(decision.canAdopt, true);
assert.equal(decision.canConfirmExactMinute, false);
assert.notEqual(decision.sessionOutcome, "exact_minute_confirmed");
});
test("producer 50/30/20 lead keeps all three candidates and still-valid range", () => {
const { state, latest } = producedSnapshot([
{ time: "05:00", relativeSupport: 50 },
{ time: "05:20", relativeSupport: 30 },
{ time: "05:40", relativeSupport: 20 },
]);
const projection = authoritativeCandidateProjection(latest);
assert.equal(projection.consistent, true);
assert.equal(projection.candidates.length, 3);
assert.deepEqual(projection.credibleRange, unionStillValidRange(state.candidates));
assert.deepEqual(projection.credibleRange, state.credible_range);
});
test("producer sole candidate stays consistent", () => {
const { state, latest } = producedSnapshot([
{ time: "05:02", relativeSupport: 100 },
]);
const projection = authoritativeCandidateProjection(latest);
assert.equal(projection.consistent, true);
assert.equal(projection.candidates.length, 1);
assert.deepEqual(projection.credibleRange, unionStillValidRange(state.candidates));
});
test("missing inference_state still fail-closes to an empty projection", () => {
const projection = authoritativeCandidateProjection({
candidates: [
{ candidateId: "05:02", time: "05:02", relativeSupport: 58 },
{ candidateId: "04:55", time: "04:55", relativeSupport: 42 },
],
decisionReceipt: {},
});
assert.equal(projection.consistent, false);
assert.deepEqual(projection.candidates, []);
assert.deepEqual(projection.scores, []);
assert.equal(projection.representativeTime, null);
assert.equal(projection.credibleRange, null);
});
test("a corrupted receipt range outside the window still fail-closes", () => {
const { state, latest } = producedSnapshot([
{ time: "05:02", relativeSupport: 58 },
{ time: "04:55", relativeSupport: 42 },
]);
const broken = {
...latest,
decisionReceipt: {
inference_state: { ...state, credible_range: ["04:00", "04:10"] },
},
};
const projection = authoritativeCandidateProjection(broken);
assert.equal(projection.consistent, false);
assert.deepEqual(projection.candidates, []);
assert.equal(projection.credibleRange, null);
});
test("quarter-precision evidence stays dated in the inference ledger", () => {
const state = buildCaseInferenceState({
range: { start_time: "05:00", end_time: "05:10" },
candidates: [
{ candidateId: "05:00", time: "05:00", relativeSupport: 50 },
{ candidateId: "05:10", time: "05:10", relativeSupport: 50 },
],
evidence: [
{ id: "q-career", domain: "career", occurredFrom: "2018-04-01", datePrecision: "quarter" },
{ id: "y-edu", domain: "education", occurredFrom: "2016-01-01", datePrecision: "year" },
],
probes: [],
});
const quarter = state.events.find((item) => item.id === "q-career");
assert.equal(quarter?.precision, "year");
assert.notEqual(quarter?.usage, "unused");
});
test("buildCaseInferenceState receipt is the producer of the projected range", () => {
const state = buildCaseInferenceState({
range: { start_time: "04:55", end_time: "05:02" },
candidates: [
{ candidateId: "05:02", time: "05:02", relativeSupport: 58 },
{ candidateId: "04:55", time: "04:55", relativeSupport: 42 },
],
evidence: [],
probes: [],
});
const projection = authoritativeCandidateProjection({
candidates: state.candidates.map((item) => ({
candidateId: item.id,
time: item.time,
relativeSupport: Math.round(item.posterior_score),
})),
decisionReceipt: { inference_state: state },
});
assert.equal(projection.consistent, true);
assert.deepEqual(projection.credibleRange, unionStillValidRange(state.candidates));
});