2026-03-14 13:47:32 +09:00
import { describe , expect , it } from "bun:test"
import type { HookDeps , RuntimeFallbackPluginInput } from "./types"
import type { AutoRetryHelpers } from "./auto-retry"
import { createFallbackState } from "./fallback-state"
import { createSessionStatusHandler } from "./session-status-handler"
import { SessionCategoryRegistry } from "../../shared/session-category-registry"
function createContext ( ) : RuntimeFallbackPluginInput {
return {
client : {
session : {
abort : async ( ) = > ( { } ) ,
messages : async ( ) = > ( { data : [ ] } ) ,
promptAsync : async ( ) = > ( { } ) ,
} ,
tui : {
showToast : async ( ) = > ( { } ) ,
} ,
} ,
directory : "/test/dir" ,
}
}
function createDeps ( ) : HookDeps {
return {
ctx : createContext ( ) ,
config : {
enabled : true ,
retry_on_errors : [ 429 , 503 , 529 ] ,
max_fallback_attempts : 4 ,
cooldown_seconds : 60 ,
timeout_seconds : 30 ,
notify_on_fallback : false ,
} ,
options : undefined ,
pluginConfig : {
categories : {
test : {
fallback_models : [ "openai/gpt-5.4" , "google/gemini-2.5-pro" ] ,
} ,
} ,
} ,
sessionStates : new Map ( ) ,
sessionLastAccess : new Map ( ) ,
sessionRetryInFlight : new Set ( ) ,
sessionAwaitingFallbackResult : new Set ( ) ,
sessionFallbackTimeouts : new Map ( ) ,
sessionStatusRetryKeys : new Map ( ) ,
}
}
function createHelpers ( abortCalls : string [ ] , retryCalls : Array < { sessionID : string ; model : string ; source : string } > ) : AutoRetryHelpers {
return {
abortSessionRequest : async ( sessionID : string ) = > {
abortCalls . push ( sessionID )
} ,
clearSessionFallbackTimeout : ( ) = > { } ,
scheduleSessionFallbackTimeout : ( ) = > { } ,
autoRetryWithFallback : async ( sessionID : string , model : string , _resolvedAgent : string | undefined , source : string ) = > {
retryCalls . push ( { sessionID , model , source } )
} ,
resolveAgentForSessionFromContext : async ( ) = > undefined ,
cleanupStaleSessions : ( ) = > { } ,
}
}
describe ( "createSessionStatusHandler" , ( ) = > {
2026-05-20 11:42:46 +09:00
it ( "#given pending fallback prompt may already be accepted #when provider retry status arrives #then it keeps waiting for that accepted prompt" , async ( ) = > {
// given
SessionCategoryRegistry . clear ( )
const sessionID = "session-status-ambiguous-pending"
SessionCategoryRegistry . register ( sessionID , "test" )
const deps = createDeps ( )
const abortCalls : string [ ] = [ ]
const retryCalls : Array < { sessionID : string ; model : string ; source : string } > = [ ]
const state = createFallbackState ( "anthropic/claude-opus-4-7" )
state . currentModel = "openai/gpt-5.4"
state . fallbackIndex = 0
state . attemptCount = 1
state . pendingFallbackModel = "openai/gpt-5.4"
state . pendingFallbackPromptMayHaveBeenAccepted = true
deps . sessionStates . set ( sessionID , state )
const handler = createSessionStatusHandler ( deps , createHelpers ( abortCalls , retryCalls ) , deps . sessionStatusRetryKeys )
// when
await handler ( {
sessionID ,
model : "openai/gpt-5.4" ,
status : {
type : "retry" ,
attempt : 2 ,
message : "All credentials for model gpt-5.4 are cooling down [retrying in 7m 56s attempt #2]" ,
} ,
} )
// then
expect ( abortCalls ) . toEqual ( [ ] )
expect ( retryCalls ) . toEqual ( [ ] )
expect ( state . currentModel ) . toBe ( "openai/gpt-5.4" )
expect ( state . pendingFallbackModel ) . toBe ( "openai/gpt-5.4" )
expect ( state . pendingFallbackPromptMayHaveBeenAccepted ) . toBe ( true )
SessionCategoryRegistry . clear ( )
} )
2026-03-14 13:47:32 +09:00
it ( "#given a pending fallback model #when a new provider cooldown retry arrives #then the handler overrides the pending fallback and advances the chain" , async ( ) = > {
// given
SessionCategoryRegistry . clear ( )
const sessionID = "session-status-pending-fallback"
SessionCategoryRegistry . register ( sessionID , "test" )
const deps = createDeps ( )
const abortCalls : string [ ] = [ ]
const retryCalls : Array < { sessionID : string ; model : string ; source : string } > = [ ]
2026-04-17 14:51:52 +09:00
const state = createFallbackState ( "anthropic/claude-opus-4-7" )
2026-03-14 13:47:32 +09:00
state . currentModel = "openai/gpt-5.4"
state . fallbackIndex = 0
state . attemptCount = 1
state . pendingFallbackModel = "openai/gpt-5.4"
2026-04-17 14:51:52 +09:00
state . failedModels . set ( "anthropic/claude-opus-4-7" , Date . now ( ) )
2026-03-14 13:47:32 +09:00
deps . sessionStates . set ( sessionID , state )
const handler = createSessionStatusHandler ( deps , createHelpers ( abortCalls , retryCalls ) , deps . sessionStatusRetryKeys )
// when
await handler ( {
sessionID ,
model : "openai/gpt-5.4" ,
status : {
type : "retry" ,
attempt : 2 ,
message : "All credentials for model gpt-5.4 are cooling down [retrying in 7m 56s attempt #2]" ,
} ,
} )
// then
expect ( abortCalls ) . toEqual ( [ sessionID ] )
expect ( retryCalls ) . toEqual ( [
{
sessionID ,
model : "google/gemini-2.5-pro" ,
source : "session.status" ,
} ,
] )
expect ( state . currentModel ) . toBe ( "google/gemini-2.5-pro" )
expect ( state . pendingFallbackModel ) . toBe ( "google/gemini-2.5-pro" )
SessionCategoryRegistry . clear ( )
} )
} )