Files
Jyotisha/frontend/tests/rectification-tied-first-fix-20260914.test.ts
T
Jesse_ChenandClaude Opus 5.5 84eb23152a feat(rectification): remove the rectification regenerate button and endpoint (BUG-1056)
T2 of TASK-rectification-grounding-20260927 (product decision P1).
The rectification 「重试(重新生成)」 was a blind agent.generate rewrite that
persisted unchecked text (invented range / fit rate reproduced). Removed:
the regenerate route, regenerate-turn.ts, the regeneration agent and its
read-only tool set, the client regenerate action/state and the
regenerating/canRegenerate props. ChatMessageActions renders the regenerate
button only when onRegenerate is passed; ordinary consultation is unchanged.
The DB function regenerate_agentic_rectification_turn is kept (AGENTS §7.6,
retire in a later round). DESIGN.md / VOICE.md updated; source-contract
tests follow with 原值/新值/原因 notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-27 03:28:27 +08:00

219 lines
7.3 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,
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,
snapshotLoaded: true,
resumableCase: true,
retryAttempts: RECTIFICATION_QUESTION_RETRY_LIMIT,
sessionOutcome: "collect_evidence",
}),
"unavailable",
);
assert.equal(RECTIFICATION_QUESTION_UNAVAILABLE_COPY, "没有拿到下一个问题。");
});