@@ -34,13 +34,16 @@ function createCapturingClient() {
3434 return { callback, writeFrame, events, frames }
3535}
3636
37- function makeFetcher ( map : Record < string , { permissions ?: unknown [ ] ; questions ?: unknown [ ] } > ) : PendingActionsFetcher {
37+ function makeFetcher (
38+ map : Record < string , { permissions ?: unknown [ ] ; questions ?: unknown [ ] ; statuses ?: Record < string , { type : string } > } > ,
39+ ) : PendingActionsFetcher {
3840 return {
3941 async getJson < T > ( path : string , opts ?: { directory ?: string } ) : Promise < T > {
4042 const directory = opts ?. directory ?? ''
4143 const entry = map [ directory ] ?? { }
4244 if ( path === '/permission' ) return ( entry . permissions ?? [ ] ) as T
4345 if ( path === '/question' ) return ( entry . questions ?? [ ] ) as T
46+ if ( path === '/session/status' ) return ( entry . statuses ?? { } ) as T
4447 throw new Error ( `unexpected path: ${ path } ` )
4548 } ,
4649 }
@@ -197,6 +200,66 @@ describe('SSEAggregator pending replay on connect', () => {
197200 } )
198201} )
199202
203+ describe ( 'SSEAggregator session status replay on upstream reconnect' , ( ) => {
204+ beforeEach ( ( ) => {
205+ sseAggregator . shutdown ( )
206+ sseAggregator . setPendingActionsFetcher ( null )
207+ } )
208+
209+ it ( 're-emits active session statuses to subscribed clients' , async ( ) => {
210+ const fetcher = makeFetcher ( {
211+ '/repo/a' : { statuses : { 'sess-1' : { type : 'busy' } , 'sess-2' : { type : 'idle' } } } ,
212+ } )
213+ sseAggregator . setPendingActionsFetcher ( fetcher )
214+
215+ const clientA = createCapturingClient ( )
216+ sseAggregator . addClient ( 'status-1' , clientA . callback , clientA . writeFrame , [ '/repo/a' ] )
217+
218+ await ( sseAggregator as any ) . replaySessionStatusesForAllClients ( )
219+ await flushReplay ( )
220+
221+ const parsed = clientA . frames . map ( f => JSON . parse ( f . replace ( / ^ e v e n t : m e s s a g e \n d a t a : / , '' ) . trim ( ) ) as {
222+ directory : string
223+ payload : { type : string ; properties : { sessionID : string ; status : { type : string } } }
224+ } )
225+
226+ const statusEvents = parsed . filter ( p => p . payload . type === 'session.status' )
227+ expect ( statusEvents ) . toHaveLength ( 1 )
228+ expect ( statusEvents [ 0 ] ?. payload . properties . sessionID ) . toBe ( 'sess-1' )
229+ expect ( statusEvents [ 0 ] ?. payload . properties . status . type ) . toBe ( 'busy' )
230+ } )
231+
232+ it ( 'emits idle for sessions that finished during the disconnect' , async ( ) => {
233+ const clientA = createCapturingClient ( )
234+ sseAggregator . addClient ( 'status-2' , clientA . callback , clientA . writeFrame , [ '/repo/a' ] )
235+
236+ const busy = JSON . stringify ( { directory : '/repo/a' , payload : { type : 'session.status' , properties : { sessionID : 'sess-9' , status : { type : 'busy' } } } } )
237+ ; ( sseAggregator as any ) . handleUpstreamMessage ( busy )
238+
239+ sseAggregator . setPendingActionsFetcher ( makeFetcher ( { '/repo/a' : { statuses : { } } } ) )
240+
241+ await ( sseAggregator as any ) . replaySessionStatusesForAllClients ( )
242+ await flushReplay ( )
243+
244+ const parsed = clientA . frames . map ( f => JSON . parse ( f . replace ( / ^ e v e n t : m e s s a g e \n d a t a : / , '' ) . trim ( ) ) as {
245+ payload : { type : string ; properties : { sessionID : string ; status : { type : string } } }
246+ } )
247+ const idleEvents = parsed . filter ( p => p . payload . type === 'session.status' && p . payload . properties . status . type === 'idle' )
248+ expect ( idleEvents ) . toHaveLength ( 1 )
249+ expect ( idleEvents [ 0 ] ?. payload . properties . sessionID ) . toBe ( 'sess-9' )
250+ } )
251+
252+ it ( 'does nothing when no fetcher is configured' , async ( ) => {
253+ const clientA = createCapturingClient ( )
254+ sseAggregator . addClient ( 'status-3' , clientA . callback , clientA . writeFrame , [ '/repo/a' ] )
255+
256+ await ( sseAggregator as any ) . replaySessionStatusesForAllClients ( )
257+ await flushReplay ( )
258+
259+ expect ( clientA . frames ) . toHaveLength ( 0 )
260+ } )
261+ } )
262+
200263describe ( 'SSEAggregator directory-indexed broadcast' , ( ) => {
201264 beforeEach ( ( ) => {
202265 sseAggregator . shutdown ( )
0 commit comments