import assert from "node:assert/strict"; import test from "node:test"; import { RECTIFICATION_POLICY } from "../src/lib/rectification-policy.ts"; import { decideRectification, mayDeliverOnPrecision, publicCanAdopt, publicNextAction, sessionOutcomeAllowsDelivery, } from "../src/lib/rectification-agentic/core/rectification-decision.ts"; import { computePrecisionGateMet } from "../src/lib/rectification-agentic/core/precision-gate.ts"; import { RANGE_DELIVERY_TIE_PERCENT } from "../src/lib/rectification-agentic/user-copy.ts"; import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts"; const TIED_FIVE = [ { time: "04:51", score: 13 }, { time: "04:53", score: 13 }, { time: "04:56", score: 13 }, { time: "04:58", score: 13 }, { time: "05:11", score: 13 }, ] as const; function input(overrides: Partial[0]> = {}) { return { engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING, methodCoverageAll: true, trainingGateOpen: true, candidateScores: TIED_FIVE.map((item) => ({ time: item.time, score: item.score })), holdoutValidation: "unavailable" as const, datedEventCount: 5, datedDomainCount: 3, discriminatorProbe: null, targetedCollectExhausted: true, refreshExhausted: true, inferenceCredibleRange: ["04:51", "05:11"] as const, openingCandidateRange: ["04:45", "05:15"] as const, ...overrides, }; } test("policy defines deliveryMaxWidthMinutes once", () => { assert.equal(RECTIFICATION_POLICY.deliveryMaxWidthMinutes, 10); assert.equal(RANGE_DELIVERY_TIE_PERCENT, 3); }); test("precision gate requires width, display gap, and no exact tie", () => { assert.equal(computePrecisionGateMet({ range: ["04:51", "05:11"], topTwoPercents: [26, 26], tiedForFirst: true, }), false); assert.equal(computePrecisionGateMet({ range: ["04:51", "04:59"], topTwoPercents: [40, 35], tiedForFirst: false, }), true); assert.equal(computePrecisionGateMet({ range: ["04:51", "04:59"], topTwoPercents: [26, 26], tiedForFirst: true, }), false); }); test("20 minute tied range keeps collecting with a guided question", () => { const decision = decideRectification(input({ precisionGateMet: false, guidedCollectExhausted: false, })); assert.equal(decision.nextAction, "ask_fact_collection"); assert.equal(decision.canOfferRange, false); assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), false); assert.equal(publicCanAdopt(decision), false); }); test("8 minute range with a 5 point lead delivers once every line is asked", () => { const decision = decideRectification(input({ candidateScores: [ { time: "04:51", score: 20 }, { time: "04:53", score: 15 }, { time: "04:56", score: 10 }, ], inferenceCredibleRange: ["04:51", "04:59"], precisionGateMet: true, // 原值: guidedCollectExhausted: false // 新值: guidedCollectExhausted: true // 原因: D6——门槛只是必要条件,引导线没问完不出卡(BUG-751) guidedCollectExhausted: true, targetedCollectExhausted: true, refreshExhausted: true, })); assert.equal(decision.canOfferRange, true); assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true); assert.equal(decision.precisionGateMet, true); }); test("gate met but the targeted line is still open keeps asking", () => { const decision = decideRectification(input({ candidateScores: [ { time: "04:51", score: 20 }, { time: "04:53", score: 15 }, { time: "04:56", score: 10 }, ], inferenceCredibleRange: ["04:51", "04:59"], precisionGateMet: true, guidedCollectExhausted: false, targetedCollectExhausted: false, refreshExhausted: true, })); assert.equal(decision.nextAction, "ask_fact_collection"); assert.equal(decision.canOfferRange, false); assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), false); }); test("empty guided pool with refresh untried does not deliver", () => { const decision = decideRectification(input({ candidateScores: [ { time: "04:51", score: 20 }, { time: "04:53", score: 15 }, { time: "04:56", score: 10 }, ], inferenceCredibleRange: ["04:51", "04:59"], precisionGateMet: true, guidedCollectExhausted: true, targetedCollectExhausted: true, refreshExhausted: false, })); assert.equal(decision.nextAction, "ask_fact_collection"); assert.equal(decision.canOfferRange, false); assert.equal( mayDeliverOnPrecision({ methodCoverageAll: true, candidateScores: [], engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING, precisionGateMet: true, guidedCollectExhausted: true, refreshExhausted: false, targetedCollectExhausted: true, }), false, ); }); test("8 minute exact tie keeps collecting", () => { const decision = decideRectification(input({ candidateScores: [ { time: "04:51", score: 13 }, { time: "04:53", score: 13 }, ], inferenceCredibleRange: ["04:51", "04:59"], precisionGateMet: false, guidedCollectExhausted: false, })); assert.equal(decision.nextAction, "ask_fact_collection"); assert.equal(decision.canOfferRange, false); }); test("every line asked but the gate unmet still delivers under the existing rules", () => { // D2:题源都空了就是「用户确实补不出来」,卡片、采用按钮、三句正文不变, // 也不加「未达门槛」标注。D6 只保证在还有题可问时不会提前出卡。 const decision = decideRectification(input({ precisionGateMet: false, guidedCollectExhausted: true, })); assert.equal(decision.canOfferRange, true); assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true); // 门槛值照常上报,不参与出卡判断。 assert.equal(decision.precisionGateMet, false); }); test("the precision gate is reported on a collect exit too", () => { const collecting = decideRectification(input({ precisionGateMet: false, guidedCollectExhausted: false, })); assert.equal(collecting.nextAction, "ask_fact_collection"); assert.equal(collecting.precisionGateMet, false); assert.equal(publicNextAction(collecting).precision_gate_met, false); const helper = decideRectification(input()); assert.equal(helper.precisionGateMet, null); }); test("userStopped delivers even when the precision gate is unmet", () => { const decision = decideRectification(input({ userStopped: true, precisionGateMet: false, guidedCollectExhausted: false, })); assert.equal(decision.canOfferRange, true); assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true); }); test("omitted precision flags keep the helper path delivering", () => { assert.equal(mayDeliverOnPrecision({ methodCoverageAll: true, candidateScores: [], engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING, }), true); });