@@ -47,7 +47,12 @@ const TEST_CONFIG: Pick<ServerConfig, "auth" | "appVersion"> = {
4747
4848const createMockSocket = ( ) : MockSocket => ( {
4949 readyState : WebSocket . OPEN ,
50- send : vi . fn ( ) ,
50+ send : vi . fn ( ( ...args : unknown [ ] ) => {
51+ const callback = args . find ( ( arg ) => typeof arg === "function" ) as
52+ | ( ( error ?: Error ) => void )
53+ | undefined ;
54+ callback ?.( ) ;
55+ } ) ,
5156 ping : vi . fn ( ) ,
5257 close : vi . fn ( ) ,
5358 on : vi . fn ( ) ,
@@ -246,14 +251,88 @@ describe("WsHub", () => {
246251 const claim = activationMgr . claim ( "client-b" , clientB , createMockRequest ( ) ) ;
247252 expect ( claim . displacedWsClientId ) . toBe ( clientA ) ;
248253
254+ socketA . send . mockClear ( ) ;
249255 hub . revokeAndCloseClient ( clientA , claim . generation ) ;
250256
251- expect ( socketA . send ) . toHaveBeenCalledWith (
252- expect . stringContaining ( '"topic":"activation.revoked"' )
253- ) ;
257+ expect ( parseSentEvents ( socketA ) ) . toEqual ( [
258+ expect . objectContaining ( {
259+ kind : "event" ,
260+ topic : "activation.revoked" ,
261+ data : {
262+ reason : "displaced" ,
263+ generation : claim . generation ,
264+ } ,
265+ } ) ,
266+ ] ) ;
254267 expect ( socketA . close ) . toHaveBeenCalledWith ( 4001 , "single_active_displaced" ) ;
255268 } ) ;
256269
270+ it ( "no-ops when revoking a client that is already disconnected" , ( ) => {
271+ const socket = createMockSocket ( ) ;
272+ hub . handleConnection ( socket as never , createMockRequest ( ) ) ;
273+ socket . send . mockClear ( ) ;
274+
275+ hub . revokeAndCloseClient ( "missing-client" , 7 ) ;
276+
277+ expect ( socket . send ) . not . toHaveBeenCalled ( ) ;
278+ expect ( socket . close ) . not . toHaveBeenCalled ( ) ;
279+ } ) ;
280+
281+ it ( "cleans up activation and request metadata after a revoked client closes" , ( ) => {
282+ const activationMgr = createActivationManager ( ) ;
283+ const onSocketClosedSpy = vi . spyOn ( activationMgr , "onSocketClosed" ) ;
284+ mockCommandContext = createCommandContext ( eventBus , { activationMgr } ) ;
285+ hub . destroy ( ) ;
286+ hub = createHub ( eventBus , mockCommandContext ) ;
287+
288+ const socketA = createMockSocket ( ) ;
289+ const socketB = createMockSocket ( ) ;
290+ hub . handleConnection ( socketA as never , createMockRequest ( ) ) ;
291+ hub . handleConnection ( socketB as never , createMockRequest ( ) ) ;
292+
293+ const [ clientA , clientB ] = getConnectedClientIds ( [ socketA , socketB ] ) ;
294+ activationMgr . claim ( "client-a" , clientA , createMockRequest ( ) ) ;
295+ const claim = activationMgr . claim ( "client-b" , clientB , createMockRequest ( ) ) ;
296+ const closeHandler = getCloseHandler ( socketA ) ;
297+
298+ expect ( hub . getRequestMetadata ( clientA ) ) . toBeDefined ( ) ;
299+
300+ hub . revokeAndCloseClient ( clientA , claim . generation ) ;
301+ closeHandler ?.( ) ;
302+
303+ expect ( onSocketClosedSpy ) . toHaveBeenCalledWith ( clientA ) ;
304+ expect ( hub . getRequestMetadata ( clientA ) ) . toBeUndefined ( ) ;
305+ } ) ;
306+
307+ it ( "sends activation.revoked before starting the close handshake" , ( ) => {
308+ let sendCallback : ( ( error ?: Error ) => void ) | undefined ;
309+ const socket = createMockSocket ( ) ;
310+ socket . send = vi . fn ( ( ...args : unknown [ ] ) => {
311+ sendCallback = args . find ( ( arg ) => typeof arg === "function" ) as
312+ | ( ( error ?: Error ) => void )
313+ | undefined ;
314+ } ) ;
315+ hub . handleConnection ( socket as never , createMockRequest ( ) ) ;
316+
317+ const [ clientId ] = getConnectedClientIds ( [ socket ] ) ;
318+ socket . send . mockClear ( ) ;
319+
320+ hub . revokeAndCloseClient ( clientId , 9 ) ;
321+
322+ expect ( parseSentEvents ( socket ) ) . toEqual ( [
323+ expect . objectContaining ( {
324+ kind : "event" ,
325+ topic : "activation.revoked" ,
326+ data : { reason : "displaced" , generation : 9 } ,
327+ } ) ,
328+ ] ) ;
329+ expect ( socket . close ) . not . toHaveBeenCalled ( ) ;
330+
331+ sendCallback ?.( ) ;
332+
333+ expect ( socket . close ) . toHaveBeenCalledWith ( 4001 , "single_active_displaced" ) ;
334+ } ) ;
335+
257336 it ( "should handle domain events" , ( ) => {
258337 const socket = createMockSocket ( ) ;
259338 hub . handleConnection ( socket as never , createMockRequest ( ) ) ;
0 commit comments