@@ -36,6 +36,7 @@ import {
3636 responseStatePersistPendingForTests ,
3737} from "../src/responses/state" ;
3838import { clearCursorThreadContinuityForTests } from "../src/adapters/cursor/thread-continuity" ;
39+ import { COMPACT_PROMPT , encodeCompactionSummary } from "../src/responses/compaction" ;
3940
4041// Full-suite Windows load: startServer + combo rename/delete management flows exceed the
4142// default 5s per-test budget (same flake class as 810fa115 / claude-management-api).
@@ -112,6 +113,7 @@ mock.module("../src/lib/upstream-retry", () => ({
112113} ) ) ;
113114
114115const { handleResponses } = await import ( "../src/server/responses" ) ;
116+ const { handleResponsesCompact } = await import ( "../src/server/responses/compact" ) ;
115117type HandleOptions = NonNullable < Parameters < typeof handleResponses > [ 3 ] > ;
116118
117119const TOKEN_ENDPOINT = "https://auth.x.ai/oauth/token" ;
@@ -2299,7 +2301,7 @@ describe("server combo failover 030 activation matrix", () => {
22992301 let backupHits = 0 ;
23002302 const auth : string [ ] = [ ] ;
23012303 globalThis . fetch = ( async ( input , init ) => {
2302- const url = input instanceof Request ? input . url : String ( input ) ;
2304+ const url = typeof input === "object" && input !== null && "url" in input ? String ( ( input as Request ) . url ) : String ( input ) ;
23032305 if ( url === XAI_OAUTH_DISCOVERY_URL ) {
23042306 return Response . json ( { authorization_endpoint : "https://auth.x.ai/oauth/authorize" , token_endpoint : TOKEN_ENDPOINT } ) ;
23052307 }
@@ -2946,3 +2948,131 @@ describe("cursor conversation continuity across store:false chains", () => {
29462948 expect ( seen [ 1 ] ) . toBe ( seen [ 0 ] ) ;
29472949 } ) ;
29482950} ) ;
2951+
2952+ describe ( "combo compact failover" , ( ) => {
2953+ function compactRequest ( body : Record < string , unknown > ) : Request {
2954+ return new Request ( "http://localhost/v1/responses" , {
2955+ method : "POST" ,
2956+ headers : { "content-type" : "application/json" } ,
2957+ body : JSON . stringify ( body ) ,
2958+ } ) ;
2959+ }
2960+
2961+ async function postCompactLogged ( config : OcxConfig ) : Promise < Response > {
2962+ const logCtx : RequestLogContext = { model : "" , provider : "" } ;
2963+ const start = Date . now ( ) ;
2964+ const response = await handleResponsesCompact ( compactRequest ( {
2965+ model : "combo/free" ,
2966+ stream : false ,
2967+ input : [ { type : "message" , role : "user" , content : [ { type : "input_text" , text : "earlier turn" } ] } ] ,
2968+ } ) , config , logCtx ) ;
2969+ loggedRequestSequence += 1 ;
2970+ return responseWithDeferredRequestLog ( response , `combo-compact-${ loggedRequestSequence } ` , start , logCtx ) ;
2971+ }
2972+
2973+ function canonicalPoolConfig (
2974+ targets : Array < { provider : string ; model : string } > ,
2975+ backupUrl ?: string ,
2976+ ) : { config : OcxConfig } {
2977+ const config = comboConfig ( {
2978+ openai : {
2979+ adapter : "openai-responses" ,
2980+ baseUrl : "https://chatgpt.com/backend-api/codex" ,
2981+ authMode : "key" ,
2982+ apiKey : "combo-compact-key" ,
2983+ } ,
2984+ backup : provider ( "openai-chat" , backupUrl ?? "http://127.0.0.1:9" , "key-b" ) ,
2985+ } , targets ) ;
2986+ return { config } ;
2987+ }
2988+
2989+ test ( "native-capable first target 429 hops compact to the backup target" , async ( ) => {
2990+ const childBodies : Array < Record < string , unknown > > = [ ] ;
2991+ const b = serve ( async request => {
2992+ childBodies . push ( JSON . parse ( await request . text ( ) ) as Record < string , unknown > ) ;
2993+ return chatStream ( "compact backup" ) ;
2994+ } ) ;
2995+ const { config } = canonicalPoolConfig ( [
2996+ { provider : "openai" , model : "gpt-5.4" } ,
2997+ { provider : "backup" , model : "m1" } ,
2998+ ] , baseUrl ( b ) ) ;
2999+ globalThis . fetch = ( async ( input : unknown , init ?: RequestInit ) => {
3000+ const url = typeof input === "object" && input !== null && "url" in input ? String ( ( input as Request ) . url ) : String ( input ) ;
3001+ if ( url . includes ( "chatgpt.com" ) ) {
3002+ return Response . json ( { error : { message : "rate limited" } } , { status : 429 } ) ;
3003+ }
3004+ return originalFetch ( input as RequestInfo , init ) ;
3005+ } ) as typeof fetch ;
3006+
3007+ const response = await postCompactLogged ( config ) ;
3008+ expect ( response . status ) . toBe ( 200 ) ;
3009+ const json = await response . json ( ) as { output ?: unknown [ ] } ;
3010+ expect ( JSON . stringify ( json . output ) ) . toContain ( "compact backup" ) ;
3011+
3012+ // The backup child received the synthetic summarizer turn as SSE, with the
3013+ // summarizer prompt present in its chat wire body.
3014+ expect ( childBodies ) . toHaveLength ( 1 ) ;
3015+ expect ( childBodies [ 0 ] ! . stream ) . toBe ( true ) ;
3016+ expect ( JSON . stringify ( childBodies [ 0 ] ! . messages ) ) . toContain ( "CONTEXT CHECKPOINT COMPACTION" ) ;
3017+
3018+ const { log } = await latestAttemptReceipts ( config ) ;
3019+ const attempts = log . attempts as Array < Record < string , unknown > > ;
3020+ expect ( attempts ) . toHaveLength ( 2 ) ;
3021+ expect ( attempts [ 0 ] ) . toMatchObject ( {
3022+ provider : "openai" ,
3023+ adapter : "openai-responses" ,
3024+ status : 429 ,
3025+ } ) ;
3026+ expect ( attempts [ 1 ] ) . toMatchObject ( { provider : "backup" , adapter : "openai-chat" , status : 200 } ) ;
3027+ } ) ;
3028+
3029+ test ( "combo compact runs the synthetic turn as SSE so a canonical child can serve it" , async ( ) => {
3030+ const bodies : Array < Record < string , unknown > > = [ ] ;
3031+ const { config } = canonicalPoolConfig ( [ { provider : "openai" , model : "gpt-5.4" } ] ) ;
3032+ globalThis . fetch = ( async ( input : unknown , init ?: RequestInit ) => {
3033+ const url = input instanceof Request ? input . url : String ( input ) ;
3034+ if ( ! url . includes ( "chatgpt.com" ) ) {
3035+ return originalFetch ( input as RequestInfo , init ) ;
3036+ }
3037+ // Only the codex/responses child turn is under test; side probes (e.g. the
3038+ // wham/usage quota check) just get a tolerated non-2xx.
3039+ if ( ! url . includes ( "backend-api/codex/responses" ) ) {
3040+ return Response . json ( { error : { message : "probe not under test" } } , { status : 403 } ) ;
3041+ }
3042+ const body = JSON . parse ( String ( init ?. body ?? "{}" ) ) as Record < string , unknown > ;
3043+ bodies . push ( body ) ;
3044+ // Canonical ChatGPT Responses rejects non-streaming turns; a stream:false child
3045+ // request would strand every canonical-only combo here before the SSE coercion.
3046+ if ( body . stream !== true ) {
3047+ return Response . json ( { error : { message : "non-streaming turns are rejected" } } , { status : 400 } ) ;
3048+ }
3049+ const completed = {
3050+ type : "response.completed" ,
3051+ response : {
3052+ id : "resp_compact" ,
3053+ status : "completed" ,
3054+ output : [ { type : "compaction" , encrypted_content : encodeCompactionSummary ( "compact summary" ) } ] ,
3055+ } ,
3056+ } ;
3057+ return new Response ( [
3058+ "event: response.created" ,
3059+ 'data: {"type":"response.created","response":{"id":"resp_compact","status":"in_progress"}}' ,
3060+ "" ,
3061+ `event: ${ completed . type } ` ,
3062+ `data: ${ JSON . stringify ( completed ) } ` ,
3063+ "" ,
3064+ "" ,
3065+ ] . join ( "\n" ) , { headers : { "content-type" : "text/event-stream" } } ) ;
3066+ } ) as typeof fetch ;
3067+
3068+ const response = await postCompactLogged ( config ) ;
3069+ expect ( response . status ) . toBe ( 200 ) ;
3070+ const json = await response . json ( ) as { output ?: unknown [ ] } ;
3071+ expect ( JSON . stringify ( json . output ) ) . toContain ( "compact summary" ) ;
3072+ expect ( bodies ) . toHaveLength ( 1 ) ;
3073+ expect ( bodies [ 0 ] ! . stream ) . toBe ( true ) ;
3074+ // Canonical children keep the native compaction_trigger item — only
3075+ // non-native targets get the summarizer prompt injected (covered above).
3076+ expect ( JSON . stringify ( bodies [ 0 ] ! . input ) ) . toContain ( "compaction_trigger" ) ;
3077+ } ) ;
3078+ } ) ;
0 commit comments