@@ -0,0 +1,344 @@
import assert from "node:assert/strict" ;
import test from "node:test" ;
import {
decideRectification ,
publicCanAdopt ,
sessionOutcomeAllowsDelivery ,
} from "../src/lib/rectification-agentic/core/rectification-decision.ts" ;
import { rangeNarrowHint } from "../src/lib/rectification-agentic/v9/collection-question-pool.ts" ;
import { rangeDeliveryForSnapshot } from "../src/lib/rectification-agentic/v9/divergence-panel.ts" ;
import {
tieBreakGateInput ,
tieBreakPersonalityAvailable ,
tieBreakPersonalityFollowup ,
} from "../src/lib/rectification-agentic/v9/method-followup.ts" ;
import {
RECTIFICATION_QUESTION_RETRY_LIMIT ,
rectificationQuestionGapState ,
} from "../src/lib/rectification-surface-state.ts" ;
import { applySnapshotTurnsToMessages } from "../src/lib/rectification-snapshot-messages.ts" ;
import type { TurnQuestion } from "../src/lib/rectification-agentic/v9/turn-question.ts" ;
import { RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts" ;
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts" ;
import { alreadyDelivered , markDeliveryTurn , resetDeliveryTurnGuardForTests } from "../src/lib/rectification-agentic/v9/delivery-turn-guard.ts" ;
function contrastProbe ( layer : "d9" | "d10" ) {
const signs = layer === "d9" ? [ "巨蟹座" , "狮子座" ] as const : [ "狮子座" , "处女座" ] ;
return {
probeId : ` contrast:varga. ${ layer } . ${ signs [ 0 ] } / ${ signs [ 1 ] } ` ,
candidateSetVersion : "04:48-05:07" ,
question : layer === "d9"
? "亲密关系里更接近下面哪一种相处方式?"
: "平时做事,你更接近下面哪一种?" ,
expectedOutcomes : [
{ outcomeId : "yes" , supportsCandidateIds : [ "04:53" ] , conflictsCandidateIds : [ "05:06" ] } ,
{ outcomeId : "weak_yes" , supportsCandidateIds : [ "05:06" ] , conflictsCandidateIds : [ "04:53" ] } ,
] ,
candidateSplitHash : ` varga. ${ layer } . ${ signs [ 0 ] } / ${ signs [ 1 ] } ` ,
informationGain : layer === "d9" ? 1.4 : 0.9 ,
sourceFeatures : [ { technique : layer.toUpperCase ( ) , calculationResultId : null } ] ,
domain : layer === "d9" ? "relationship" : "career" ,
year : null ,
semanticKey : ` varga. ${ layer } . ${ signs [ 0 ] } / ${ signs [ 1 ] } ` ,
choiceKind : "varga_style" as const ,
styleOptions : [
{ label : "相处里更在意照顾对方的感受" , answerClass : "yes" as const , sign : signs [ 0 ] } ,
{ label : "习惯带头,也不排斥站到台前" , answerClass : "weak_yes" as const , sign : signs [ 1 ] } ,
] ,
} ;
}
const D9 = contrastProbe ( "d9" ) ;
const D10 = contrastProbe ( "d10" ) ;
function planInput ( askedProbeKeys : readonly string [ ] = [ ] ) {
return {
evidence : [
{
status : "confirmed" ,
domain : "education" ,
datePrecision : "month" ,
occurredFrom : "2016-09-01" ,
occurredTo : "2016-09-30" ,
eventKind : "education_start" ,
summary : "上大学" ,
} ,
{
status : "confirmed" ,
domain : "career" ,
datePrecision : "month" ,
occurredFrom : "2020-04-01" ,
occurredTo : null ,
eventKind : "career_entry" ,
summary : "入职实习" ,
} ,
{
status : "confirmed" ,
domain : "relationship" ,
datePrecision : "month" ,
occurredFrom : "2024-05-01" ,
occurredTo : null ,
eventKind : "relationship_start" ,
summary : "确定关系" ,
} ,
] ,
declinedTopics : [ ] ,
sessionOutcome : "discriminate_candidates" as const ,
candidatesSeparated : false ,
remainingLayers : [ "d9" , "d10" ] ,
remainingSplitTimes : [ "04:48" , "05:07" ] as const ,
remainingCandidateCount : 4 ,
topCandidateTimes : [ "04:48" , "04:53" , "05:06" , "05:07" ] ,
askedProbeKeys ,
contrastPacket : {
candidateSetVersion : "04:48-05:07" ,
vargaDifferences : [ ] ,
probes : [ D9 , D10 ] ,
} ,
} ;
}
function tiedDecision ( overrides : Partial < Parameters < typeof decideRectification > [ 0 ] > = { } ) {
return decideRectification ( {
engineCeiling : OPEN_ENGINE_CAPABILITY_CEILING ,
methodCoverageAll : false ,
trainingGateOpen : true ,
candidateScores : [
{ time : "04:53" , score : 16 } ,
{ time : "05:00" , score : 16 } ,
{ time : "05:06" , score : 15 } ,
{ time : "04:51" , score : 14 } ,
{ time : "04:59" , score : 13 } ,
] ,
discriminatorProbe : null ,
holdoutValidation : "unavailable" ,
datedEventCount : 3 ,
datedDomainCount : 3 ,
targetedCollectExhausted : true ,
refreshExhausted : true ,
. . . overrides ,
} ) ;
}
test ( "a tie-break snapshot turn is merged into messages with its question" , ( ) = > {
type Message = {
role : "assistant" ;
text : string ;
renderKey : string ;
turnId : string ;
question? : TurnQuestion ;
} ;
const current : Message [ ] = [ {
role : "assistant" ,
text : "目前范围 04:48– 05:07" ,
renderKey : "existing" ,
turnId : "turn-1" ,
} ] ;
const turns = [
{ id : "turn-1" , role : "assistant" , text : "目前范围 04:48– 05:07" } ,
{
id : "turn-2" ,
role : "assistant" ,
text : RECTIFICATION_USER_COPY.rangeDeliveryTieBreakAck ,
question : {
focus_id : "focus-d9" ,
question_id : "varga.d9.巨蟹座/狮子座" ,
kind : "choice" ,
prompt : D9.question ,
options : [
{ key : "A" , label : "相处里更在意照顾对方的感受" } ,
{ key : "B" , label : "习惯带头,也不排斥站到台前" } ,
{ key : "C" , label : "一时说不好" } ,
{ key : "D" , label : "先这样" } ,
] ,
status : "active" ,
answer_option : null ,
probe_id : D9.probeId ,
} ,
} ,
] ;
const next = applySnapshotTurnsToMessages ( current , turns , ( incoming ) : Message [ ] = > (
incoming . flatMap ( ( item ) = > {
if ( ! item || typeof item !== "object" ) return [ ] ;
const turn = item as { id? : string ; role? : string ; text? : string ; question? : unknown } ;
if ( turn . role !== "assistant" || typeof turn . id !== "string" ) return [ ] ;
return [ {
role : "assistant" as const ,
text : turn.text ? ? "" ,
renderKey : turn.id ,
turnId : turn.id ,
} ] ;
} )
) ) ;
assert . equal ( next . length , 2 ) ;
assert . equal ( next [ 1 ] ? . turnId , "turn-2" ) ;
assert . equal ( next [ 1 ] ? . text , RECTIFICATION_USER_COPY . rangeDeliveryTieBreakAck ) ;
assert . equal ( next [ 1 ] ? . question ? . prompt , D9 . question ) ;
assert . equal ( next [ 1 ] ? . question ? . kind , "choice" ) ;
} ) ;
test ( "a delivery card plus an unanswered focus is not an idle gap" , ( ) = > {
assert . equal (
rectificationQuestionGapState ( {
liveQuestionVisible : false ,
questionMissing : false ,
questionLoadFailed : false ,
questionPersisted : true ,
offerAwaitingReader : true ,
busy : false ,
readonly : false ,
regenerating : false ,
snapshotLoaded : true ,
resumableCase : true ,
retryAttempts : RECTIFICATION_QUESTION_RETRY_LIMIT ,
sessionOutcome : "completed_with_range" ,
} ) ,
"persisted_question" ,
) ;
} ) ;
test ( "a first-place tie with unused personality questions holds delivery" , ( ) = > {
const decision = tiedDecision ( { pendingTieBreak : true } ) ;
assert . equal ( decision . heldForTieBreak , true ) ;
assert . notEqual ( decision . nextAction , "complete_with_range" ) ;
assert . equal ( sessionOutcomeAllowsDelivery ( decision . sessionOutcome ) , false ) ;
assert . equal ( publicCanAdopt ( decision ) , false ) ;
const followup = tieBreakPersonalityFollowup ( planInput ( ) ) ;
assert . equal ( followup ? . choice_kind , "varga_style" ) ;
assert . match ( followup ? . semantic_key ? ? "" , /varga\.d9/ ) ;
} ) ;
test ( "the same tie delivers after both personality questions are used" , ( ) = > {
const bothAsked = planInput ( [ D9 . semanticKey , D10 . semanticKey ] ) ;
assert . equal ( tieBreakPersonalityAvailable ( bothAsked ) , false ) ;
const decision = tiedDecision ( { pendingTieBreak : false } ) ;
assert . equal ( decision . heldForTieBreak ? ? false , false ) ;
assert . equal ( sessionOutcomeAllowsDelivery ( decision . sessionOutcome ) , true ) ;
assert . equal ( rangeDeliveryForSnapshot ( {
tieBreakAvailable : tieBreakPersonalityAvailable ( bothAsked ) ,
tieBreakUsed : true ,
stillTied : decision.separation.lead <= 1 ,
} ) . tie_break_available , false ) ;
assert . equal (
rangeDeliveryForSnapshot ( {
tieBreakAvailable : false ,
tieBreakUsed : true ,
stillTied : true ,
} ) . tie_break_note ,
RECTIFICATION_USER_COPY . rangeDeliveryTieBreakUsed ,
) ;
} ) ;
test ( "a two-point lead does not insert personality questions before delivery" , ( ) = > {
const decision = tiedDecision ( {
pendingTieBreak : true ,
candidateScores : [
{ time : "04:53" , score : 16 } ,
{ time : "05:00" , score : 14 } ,
{ time : "05:06" , score : 13 } ,
] ,
} ) ;
assert . equal ( decision . heldForTieBreak ? ? false , false ) ;
assert . equal ( sessionOutcomeAllowsDelivery ( decision . sessionOutcome ) , true ) ;
} ) ;
test ( "holding for a tie-break does not mark a delivery turn" , ( ) = > {
resetDeliveryTurnGuardForTests ( ) ;
const decision = tiedDecision ( { pendingTieBreak : true } ) ;
assert . equal ( decision . heldForTieBreak , true ) ;
markDeliveryTurn ( { caseId : "case-1" , resultId : "result-1" } ) ;
assert . equal ( alreadyDelivered ( {
caseId : "case-1" ,
resultId : "result-1" ,
hasUserMessage : false ,
} ) , true ) ;
assert . equal ( alreadyDelivered ( {
caseId : "case-1" ,
resultId : "result-2" ,
hasUserMessage : false ,
} ) , false ) ;
resetDeliveryTurnGuardForTests ( ) ;
} ) ;
test ( "range copy omits declined lines and closes when none remain" , ( ) = > {
const layers = [ "d9" , "d2" , "d7" , "d4" ] ;
const evidence = [
{
status : "confirmed" ,
domain : "education" ,
datePrecision : "month" ,
occurredFrom : "2016-09-01" ,
occurredTo : null ,
eventKind : "education_start" ,
} ,
{
status : "confirmed" ,
domain : "career" ,
datePrecision : "month" ,
occurredFrom : "2020-04-01" ,
occurredTo : null ,
eventKind : "career_entry" ,
} ,
{
status : "confirmed" ,
domain : "relocation" ,
datePrecision : "month" ,
occurredFrom : "2018-07-01" ,
occurredTo : null ,
eventKind : "relocation" ,
} ,
] ;
const declined = [
{ questionId : "collect:targeted:relationship" , status : "declined" , target_kind : "targeted:relationship" } ,
{ questionId : "collect:targeted:finance" , status : "declined" , target_kind : "targeted:finance" } ,
{ questionId : "collect:targeted:family" , status : "declined" , target_kind : "targeted:family" } ,
] ;
const openCopy = rangeNarrowHint ( layers , evidence , [ ] , [ "04:48" , "05:07" ] , 5 , [ "04:48" , "05:07" ] ) ;
assert . match ( openCopy , /结婚/ ) ;
assert . match ( openCopy , /收入/ ) ;
assert . match ( openCopy , /添丁/ ) ;
const copy = rangeNarrowHint ( layers , evidence , declined , [ "04:48" , "05:07" ] , 5 , [ "04:48" , "05:07" ] ) ;
assert . doesNotMatch ( copy , /结婚/ ) ;
assert . doesNotMatch ( copy , /收入/ ) ;
assert . doesNotMatch ( copy , /添丁/ ) ;
assert . match ( copy , /能问的都问完了/ ) ;
} ) ;
test ( "GET availability and the POST gate share one input builder" , ( ) = > {
const openPlan = planInput ( ) ;
const open = tieBreakGateInput ( {
evidence : openPlan.evidence ,
declinedTopics : [ ] ,
catalog : {
remainingLayers : openPlan.remainingLayers ,
remainingSplitTimes : openPlan.remainingSplitTimes ,
remainingCandidateCount : openPlan.remainingCandidateCount ,
contrastPacket : openPlan.contrastPacket ,
askedProbeKeys : openPlan.askedProbeKeys ,
topCandidateTimes : openPlan.topCandidateTimes ,
} ,
accepted : false ,
stage : "minute" ,
candidateRange : { start_time : "04:45" , end_time : "05:15" } ,
} ) ;
const closed = tieBreakGateInput ( {
evidence : openPlan.evidence ,
declinedTopics : [ ] ,
catalog : {
remainingLayers : openPlan.remainingLayers ,
remainingSplitTimes : openPlan.remainingSplitTimes ,
remainingCandidateCount : openPlan.remainingCandidateCount ,
contrastPacket : openPlan.contrastPacket ,
askedProbeKeys : [ D9 . semanticKey , D10 . semanticKey ] ,
topCandidateTimes : openPlan.topCandidateTimes ,
} ,
accepted : false ,
stage : "minute" ,
candidateRange : { start_time : "04:45" , end_time : "05:15" } ,
} ) ;
assert . equal ( tieBreakPersonalityAvailable ( open ) , true ) ;
assert . equal ( Boolean ( tieBreakPersonalityFollowup ( open ) ? . choice_kind === "varga_style" ) , true ) ;
assert . equal ( tieBreakPersonalityAvailable ( closed ) , false ) ;
assert . equal ( tieBreakPersonalityFollowup ( closed ) , null ) ;
} ) ;