2026-05-15 11:29:42 +09:00
import { afterEach , describe , expect , test } from "bun:test"
import {
2026-05-15 11:49:44 +09:00
promptAfterSessionIdle ,
2026-05-15 11:29:42 +09:00
promptAsyncAfterSessionIdle ,
2026-05-15 21:52:29 +09:00
releasePromptAsyncReservation ,
2026-05-15 11:29:42 +09:00
releaseAllPromptAsyncReservationsForTesting ,
} from "./prompt-async-gate"
describe ( "promptAsyncAfterSessionIdle" , ( ) = > {
afterEach ( ( ) = > {
// then
releaseAllPromptAsyncReservationsForTesting ( )
} )
test ( "#given two internal promptAsync calls race for one idle session #when they dispatch concurrently #then only one prompt is accepted" , async ( ) = > {
// given
let promptCalls = 0
let releasePrompt : ( ( ) = > void ) | undefined
const promptGate = new Promise < void > ( ( resolve ) = > {
releasePrompt = resolve
} )
const client = {
session : {
status : async ( ) = > ( { data : { ses_race : { type : "idle" } } } ) ,
promptAsync : async ( ) = > {
promptCalls += 1
await promptGate
} ,
} ,
}
// when
const first = promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_race" ,
input : { path : { id : "ses_race" } , body : { parts : [ ] } } ,
source : "test:first" ,
settleMs : 0 ,
postDispatchHoldMs : 0 ,
} )
await Promise . resolve ( )
const second = await promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_race" ,
input : { path : { id : "ses_race" } , body : { parts : [ ] } } ,
source : "test:second" ,
settleMs : 0 ,
postDispatchHoldMs : 0 ,
} )
releasePrompt ? . ( )
const firstResult = await first
// then
expect ( firstResult . status ) . toBe ( "dispatched" )
expect ( second . status ) . toBe ( "reserved" )
expect ( promptCalls ) . toBe ( 1 )
} )
2026-05-15 12:05:03 +09:00
test ( "#given settle is disabled and status is unavailable #when a second promptAsync starts after the first dispatch resolves #then the default dispatch hold keeps the session reserved" , async ( ) = > {
// given
let promptCalls = 0
const client = {
session : {
promptAsync : async ( ) = > {
promptCalls += 1
} ,
} ,
}
// when
const first = promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_hold_after_dispatch" ,
input : { path : { id : "ses_hold_after_dispatch" } , body : { parts : [ ] } } ,
source : "test:hold:first" ,
settleMs : 0 ,
} )
await new Promise ( ( resolve ) = > setTimeout ( resolve , 0 ) )
const second = await promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_hold_after_dispatch" ,
input : { path : { id : "ses_hold_after_dispatch" } , body : { parts : [ ] } } ,
source : "test:hold:second" ,
settleMs : 0 ,
} )
const firstResult = await first
// then
expect ( firstResult . status ) . toBe ( "dispatched" )
expect ( second . status ) . toBe ( "reserved" )
expect ( promptCalls ) . toBe ( 1 )
} )
2026-05-15 11:29:42 +09:00
test ( "#given session.status reports busy #when an internal promptAsync is requested #then no prompt is sent" , async ( ) = > {
// given
let promptCalls = 0
const client = {
session : {
status : async ( ) = > ( { data : { ses_busy : { type : "busy" } } } ) ,
promptAsync : async ( ) = > {
promptCalls += 1
} ,
} ,
}
// when
const result = await promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_busy" ,
input : { path : { id : "ses_busy" } , body : { parts : [ ] } } ,
source : "test:busy" ,
settleMs : 0 ,
postDispatchHoldMs : 0 ,
} )
// then
expect ( result . status ) . toBe ( "active" )
expect ( promptCalls ) . toBe ( 0 )
} )
2026-05-15 11:49:44 +09:00
2026-05-15 13:19:10 +09:00
test ( "#given dispatch hold has expired #when the same session prompts again #then the next promptAsync is accepted" , async ( ) = > {
// given
let promptCalls = 0
const client = {
session : {
promptAsync : async ( ) = > {
promptCalls += 1
} ,
} ,
}
// when
const first = await promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_expired_hold" ,
input : { path : { id : "ses_expired_hold" } , body : { parts : [ ] } } ,
source : "test:expired:first" ,
settleMs : 0 ,
postDispatchHoldMs : 1 ,
} )
await new Promise ( ( resolve ) = > setTimeout ( resolve , 5 ) )
const second = await promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_expired_hold" ,
input : { path : { id : "ses_expired_hold" } , body : { parts : [ ] } } ,
source : "test:expired:second" ,
settleMs : 0 ,
postDispatchHoldMs : 0 ,
} )
// then
expect ( first . status ) . toBe ( "dispatched" )
expect ( second . status ) . toBe ( "dispatched" )
expect ( promptCalls ) . toBe ( 2 )
} )
2026-05-15 21:52:29 +09:00
test ( "#given a peer-message promptAsync hold #when an unrelated route releases the session #then the peer-message hold remains reserved" , async ( ) = > {
// given
let promptCalls = 0
const client = {
session : {
promptAsync : async ( ) = > {
promptCalls += 1
} ,
} ,
}
// when
const first = await promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_release_scope" ,
input : {
path : { id : "ses_release_scope" } ,
body : {
parts : [ { type : "text" , text : '<peer_message from="teammate">hello</peer_message>' } ] ,
} ,
} ,
source : "team-live-delivery" ,
settleMs : 0 ,
} )
releasePromptAsyncReservation ( "ses_release_scope" , "ralph-loop:activity" )
const second = await promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_release_scope" ,
input : {
path : { id : "ses_release_scope" } ,
body : { parts : [ { type : "text" , text : "continue" } ] } ,
} ,
source : "todo-continuation-enforcer" ,
settleMs : 0 ,
postDispatchHoldMs : 0 ,
} )
// then
expect ( first . status ) . toBe ( "dispatched" )
expect ( second ) . toEqual ( { status : "reserved" , reservedBy : "team-live-delivery" } )
expect ( promptCalls ) . toBe ( 1 )
} )
test ( "#given a route family promptAsync hold #when the same family aborts another source #then the reservation is released" , async ( ) = > {
// given
let promptCalls = 0
const client = {
session : {
promptAsync : async ( ) = > {
promptCalls += 1
} ,
} ,
}
// when
const first = await promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_release_family_scope" ,
input : {
path : { id : "ses_release_family_scope" } ,
body : { parts : [ { type : "text" , text : "continue" } ] } ,
} ,
source : "model-fallback:message.updated" ,
settleMs : 0 ,
} )
const released = releasePromptAsyncReservation (
"ses_release_family_scope" ,
"model-fallback-abort:session.error" ,
{ reservedByPrefix : "model-fallback:" } ,
)
const second = await promptAsyncAfterSessionIdle ( {
client ,
sessionID : "ses_release_family_scope" ,
input : {
path : { id : "ses_release_family_scope" } ,
body : { parts : [ { type : "text" , text : "continue again" } ] } ,
} ,
source : "model-fallback:session.error" ,
settleMs : 0 ,
} )
// then
expect ( first . status ) . toBe ( "dispatched" )
expect ( released ) . toBe ( true )
expect ( second . status ) . toBe ( "dispatched" )
expect ( promptCalls ) . toBe ( 2 )
} )
2026-05-15 11:49:44 +09:00
test ( "#given two internal prompt calls race for one idle session #when they dispatch concurrently #then only one prompt is accepted" , async ( ) = > {
// given
let promptCalls = 0
let releasePrompt : ( ( ) = > void ) | undefined
const promptGate = new Promise < void > ( ( resolve ) = > {
releasePrompt = resolve
} )
const client = {
session : {
status : async ( ) = > ( { data : { ses_prompt_race : { type : "idle" } } } ) ,
prompt : async ( ) = > {
promptCalls += 1
await promptGate
} ,
} ,
}
// when
const first = promptAfterSessionIdle ( {
client ,
sessionID : "ses_prompt_race" ,
input : { path : { id : "ses_prompt_race" } , body : { parts : [ ] } } ,
source : "test:prompt:first" ,
settleMs : 0 ,
postDispatchHoldMs : 0 ,
} )
await Promise . resolve ( )
const second = await promptAfterSessionIdle ( {
client ,
sessionID : "ses_prompt_race" ,
input : { path : { id : "ses_prompt_race" } , body : { parts : [ ] } } ,
source : "test:prompt:second" ,
settleMs : 0 ,
postDispatchHoldMs : 0 ,
} )
releasePrompt ? . ( )
const firstResult = await first
// then
expect ( firstResult . status ) . toBe ( "dispatched" )
expect ( second . status ) . toBe ( "reserved" )
expect ( promptCalls ) . toBe ( 1 )
} )
2026-05-15 12:05:03 +09:00
test ( "#given settle is disabled and status is unavailable #when a second prompt starts after the first dispatch resolves #then the default dispatch hold keeps the session reserved" , async ( ) = > {
// given
let promptCalls = 0
const client = {
session : {
prompt : async ( ) = > {
promptCalls += 1
} ,
} ,
}
// when
const first = promptAfterSessionIdle ( {
client ,
sessionID : "ses_prompt_hold_after_dispatch" ,
input : { path : { id : "ses_prompt_hold_after_dispatch" } , body : { parts : [ ] } } ,
source : "test:prompt-hold:first" ,
settleMs : 0 ,
} )
await new Promise ( ( resolve ) = > setTimeout ( resolve , 0 ) )
const second = await promptAfterSessionIdle ( {
client ,
sessionID : "ses_prompt_hold_after_dispatch" ,
input : { path : { id : "ses_prompt_hold_after_dispatch" } , body : { parts : [ ] } } ,
source : "test:prompt-hold:second" ,
settleMs : 0 ,
} )
const firstResult = await first
// then
expect ( firstResult . status ) . toBe ( "dispatched" )
expect ( second . status ) . toBe ( "reserved" )
expect ( promptCalls ) . toBe ( 1 )
} )
2026-05-15 11:29:42 +09:00
} )