@@ -31,15 +31,23 @@ const adapterKey = (session: string): string => leaseKey('adapter', session);
3131class MockWebSocket {
3232 static OPEN = 1 ;
3333 static CONNECTING = 0 ;
34+ static CLOSED = 3 ;
35+ static instances : MockWebSocket [ ] = [ ] ;
3436 readyState = MockWebSocket . CONNECTING ;
37+ sent : string [ ] = [ ] ;
3538 onopen : ( ( ) => void ) | null = null ;
3639 onmessage : ( ( event : { data : string } ) => void ) | null = null ;
3740 onclose : ( ( ) => void ) | null = null ;
3841 onerror : ( ( ) => void ) | null = null ;
3942
40- constructor ( _url : string ) { }
41- send ( _data : string ) : void { }
43+ constructor ( _url : string ) {
44+ MockWebSocket . instances . push ( this ) ;
45+ }
46+ send ( data : string ) : void {
47+ this . sent . push ( data ) ;
48+ }
4249 close ( ) : void {
50+ this . readyState = MockWebSocket . CLOSED ;
4351 this . onclose ?.( ) ;
4452 }
4553}
@@ -194,6 +202,7 @@ describe('background tab isolation', () => {
194202 beforeEach ( ( ) => {
195203 vi . resetModules ( ) ;
196204 vi . useRealTimers ( ) ;
205+ MockWebSocket . instances = [ ] ;
197206 vi . stubGlobal ( 'WebSocket' , MockWebSocket ) ;
198207 } ) ;
199208
@@ -649,6 +658,75 @@ describe('background tab isolation', () => {
649658 } ) ;
650659 } ) ;
651660
661+ it ( 'keeps the active daemon connection when a superseded WebSocket closes later' , async ( ) => {
662+ const { chrome } = createChromeMock ( ) ;
663+ vi . stubGlobal ( 'chrome' , chrome ) ;
664+ vi . stubGlobal ( 'fetch' , vi . fn ( async ( ) => ( { ok : true } ) ) ) ;
665+
666+ await import ( './background' ) ;
667+ await vi . waitFor ( ( ) => {
668+ expect ( MockWebSocket . instances ) . toHaveLength ( 1 ) ;
669+ } ) ;
670+ const firstWs = MockWebSocket . instances [ 0 ] ;
671+ firstWs . readyState = 3 ;
672+
673+ const onAlarmListener = chrome . alarms . onAlarm . addListener . mock . calls [ 0 ] [ 0 ] ;
674+ await onAlarmListener ( { name : 'keepalive' } ) ;
675+ await vi . waitFor ( ( ) => {
676+ expect ( MockWebSocket . instances ) . toHaveLength ( 2 ) ;
677+ } ) ;
678+ const secondWs = MockWebSocket . instances [ 1 ] ;
679+ secondWs . readyState = MockWebSocket . OPEN ;
680+
681+ firstWs . onclose ?.( ) ;
682+ secondWs . onmessage ?.( {
683+ data : JSON . stringify ( {
684+ id : 'sessions-after-stale-close' ,
685+ action : 'tabs' ,
686+ op : 'list' ,
687+ session : 'work' ,
688+ surface : 'browser' ,
689+ } ) ,
690+ } ) ;
691+
692+ await vi . waitFor ( ( ) => {
693+ expect ( secondWs . sent . some ( ( entry ) => entry . includes ( 'sessions-after-stale-close' ) ) ) . toBe ( true ) ;
694+ } ) ;
695+ } ) ;
696+
697+ it ( 'ignores daemon commands delivered to a superseded WebSocket' , async ( ) => {
698+ const { chrome } = createChromeMock ( ) ;
699+ vi . stubGlobal ( 'chrome' , chrome ) ;
700+ vi . stubGlobal ( 'fetch' , vi . fn ( async ( ) => ( { ok : true } ) ) ) ;
701+
702+ await import ( './background' ) ;
703+ await vi . waitFor ( ( ) => {
704+ expect ( MockWebSocket . instances ) . toHaveLength ( 1 ) ;
705+ } ) ;
706+ const firstWs = MockWebSocket . instances [ 0 ] ;
707+ firstWs . readyState = MockWebSocket . OPEN ;
708+
709+ const onAlarmListener = chrome . alarms . onAlarm . addListener . mock . calls [ 0 ] [ 0 ] ;
710+ firstWs . readyState = MockWebSocket . CLOSED ;
711+ await onAlarmListener ( { name : 'keepalive' } ) ;
712+ await vi . waitFor ( ( ) => {
713+ expect ( MockWebSocket . instances ) . toHaveLength ( 2 ) ;
714+ } ) ;
715+ firstWs . readyState = MockWebSocket . OPEN ;
716+
717+ await firstWs . onmessage ?.( {
718+ data : JSON . stringify ( {
719+ id : 'stale-command' ,
720+ action : 'tabs' ,
721+ op : 'list' ,
722+ session : 'work' ,
723+ surface : 'browser' ,
724+ } ) ,
725+ } ) ;
726+
727+ expect ( firstWs . sent . some ( ( entry ) => entry . includes ( 'stale-command' ) ) ) . toBe ( false ) ;
728+ } ) ;
729+
652730 it ( 'can execute concurrently on two pages in the same session' , async ( ) => {
653731 const { chrome, tabs } = createChromeMock ( ) ;
654732 tabs . push ( {
0 commit comments