Files
Jyotisha/frontend/tests/rectification-tied-first-fix-20260914.test.ts
T
jesse-ux 9da42c99d2
Independent Staging Quality Gate / validate (push) Successful in 13m48s
Independent Staging Quality Gate / publish (push) Successful in 2m6s
fix(rectification): keep tied-first from delivering while probes remain (BUG-683/684)
tied_first is only terminal when there is no dated probe and targeted collect is exhausted. An unnarrowed opening window cannot announce adopt. Delivered dead choice cards take the delivered gap instead of the repair copy.
2026-09-14 17:06:51 +08:00

221 lines
7.4 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
decideRectification,
publicCanAdopt,
rangeMatchesOpeningWindow,
sessionOutcomeAllowsDelivery,
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import {
RECTIFICATION_DELIVERED_COPY,
RECTIFICATION_QUESTION_RETRY_LIMIT,
RECTIFICATION_QUESTION_UNAVAILABLE_COPY,
interviewDeliveredGap,
rectificationQuestionGapState,
} from "../src/lib/rectification-surface-state.ts";
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
import type { CandidateDiscriminatorProbe } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
const DATED_PROBE: CandidateDiscriminatorProbe = {
probeId: "probe-2023-career",
candidateSetVersion: "set-1",
question: "2023 年 5 月前后,你有没有入职、换工作,或者当时手上的职责明显变重?",
expectedOutcomes: [
{
outcomeId: "yes",
supportsCandidateIds: ["05:06"],
conflictsCandidateIds: ["05:08", "05:14"],
},
{
outcomeId: "no",
supportsCandidateIds: ["05:08", "05:14"],
conflictsCandidateIds: ["05:06"],
},
],
candidateSplitHash: "05:06|05:08|05:14",
informationGain: 0.4,
sourceFeatures: [{ technique: "test", calculationResultId: null }],
domain: "career",
year: 2023,
semanticKey: "career.2023.entry",
};
const TIED_THREE = [
{ time: "05:06", score: 13 },
{ time: "05:08", score: 13 },
{ time: "05:14", score: 13 },
{ time: "05:03", score: 12 },
{ time: "04:59", score: 11 },
{ time: "04:46", score: 10 },
{ time: "04:53", score: 10 },
{ time: "05:15", score: 10 },
{ time: "04:50", score: 8 },
] as const;
const OPENING = ["04:45", "05:15"] as const;
function tiedInput(overrides: Partial<Parameters<typeof decideRectification>[0]> = {}) {
return {
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: TIED_THREE.map((item) => ({ time: item.time, score: item.score })),
holdoutValidation: "unavailable" as const,
datedEventCount: 3,
datedDomainCount: 2,
inferenceCredibleRange: OPENING,
openingCandidateRange: OPENING,
...overrides,
};
}
test("tied first place with a dated probe keeps collecting instead of delivering", () => {
const decision = decideRectification(tiedInput({
discriminatorProbe: DATED_PROBE,
targetedCollectExhausted: true,
refreshExhausted: false,
}));
assert.notEqual(decision.nextAction, "complete_with_range");
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), false);
assert.equal(publicCanAdopt(decision), false);
assert.equal(decision.canAdopt, false);
assert.equal(decision.nextAction, "ask_candidate_discriminator");
});
test("tied first place with no probe still collects while targeted questions remain", () => {
const decision = decideRectification(tiedInput({
discriminatorProbe: null,
targetedCollectExhausted: false,
refreshExhausted: true,
}));
assert.notEqual(decision.nextAction, "complete_with_range");
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), false);
assert.equal(publicCanAdopt(decision), false);
assert.equal(decision.nextAction, "ask_fact_collection");
});
test("tied first place with no probe and targeted collect exhausted delivers a range", () => {
const decision = decideRectification(tiedInput({
discriminatorProbe: null,
targetedCollectExhausted: true,
refreshExhausted: false,
}));
assert.equal(decision.stopReason, "tied_first");
assert.equal(decision.nextAction, "complete_with_range");
assert.equal(decision.canOfferRange, true);
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true);
});
test("a live first-place tie with remaining probes does not announce adopt_representative", () => {
const decision = decideRectification(tiedInput({
discriminatorProbe: DATED_PROBE,
datedEventCount: 3,
targetedCollectExhausted: false,
refreshExhausted: false,
}));
assert.notEqual(decision.sessionOutcome, "adopt_representative");
assert.equal(publicCanAdopt(decision), false);
assert.equal(rangeMatchesOpeningWindow(decision.credibleRange, OPENING), true);
});
test("an unnarrowed delivered range withholds adopt", () => {
const unnarrowed = decideRectification(tiedInput({
discriminatorProbe: null,
targetedCollectExhausted: true,
inferenceCredibleRange: OPENING,
openingCandidateRange: OPENING,
}));
assert.equal(sessionOutcomeAllowsDelivery(unnarrowed.sessionOutcome), true);
assert.equal(unnarrowed.sessionOutcome === "provisional_range"
|| unnarrowed.sessionOutcome === "completed_with_range", true);
assert.equal(unnarrowed.canAdopt, false);
assert.equal(publicCanAdopt(unnarrowed), false);
assert.equal(unnarrowed.canOfferRange, true);
const narrowed = decideRectification(tiedInput({
discriminatorProbe: null,
targetedCollectExhausted: true,
inferenceCredibleRange: ["04:48", "05:07"],
openingCandidateRange: OPENING,
}));
assert.equal(sessionOutcomeAllowsDelivery(narrowed.sessionOutcome), true);
assert.equal(narrowed.canAdopt, true);
assert.equal(narrowed.sessionOutcome, "adopt_representative");
});
test("a delivered dead choice is the delivered gap, not unavailable repair", () => {
const warnings: string[] = [];
const original = console.warn;
console.warn = (value: unknown) => {
warnings.push(String(value));
};
try {
assert.equal(
interviewDeliveredGap({
questionMissing: false,
deadChoice: true,
sessionOutcome: "completed_with_range",
stopReason: "tied_first",
questionId: "distinguish:career.2023.entry",
hasChoiceCard: false,
}),
true,
);
assert.equal(
rectificationQuestionGapState({
liveQuestionVisible: false,
questionMissing: false,
questionLoadFailed: true,
deadChoice: true,
questionId: "distinguish:career.2023.entry",
hasChoiceCard: false,
busy: false,
readonly: false,
regenerating: false,
snapshotLoaded: true,
resumableCase: true,
retryAttempts: RECTIFICATION_QUESTION_RETRY_LIMIT,
sessionOutcome: "completed_with_range",
stopReason: "tied_first",
}),
"delivered",
);
assert.ok(warnings.some((line) => line.includes("rectification_delivered_state_dead_choice")));
} finally {
console.warn = original;
}
assert.equal(RECTIFICATION_DELIVERED_COPY.includes("没有拿到下一个问题"), false);
});
test("a non-delivery dead choice stays on the unavailable repair path", () => {
assert.equal(
interviewDeliveredGap({
questionMissing: false,
deadChoice: true,
sessionOutcome: "collect_evidence",
hasChoiceCard: false,
}),
false,
);
assert.equal(
rectificationQuestionGapState({
liveQuestionVisible: false,
questionMissing: false,
questionLoadFailed: true,
deadChoice: true,
questionPersisted: false,
collectWaiting: false,
busy: false,
readonly: false,
regenerating: false,
snapshotLoaded: true,
resumableCase: true,
retryAttempts: RECTIFICATION_QUESTION_RETRY_LIMIT,
sessionOutcome: "collect_evidence",
}),
"unavailable",
);
assert.equal(RECTIFICATION_QUESTION_UNAVAILABLE_COPY, "没有拿到下一个问题。");
});