@@ -30,6 +30,7 @@ import type {
3030 ConnectOrSpawnRuntimeHostInput ,
3131 RuntimeHostConnection ,
3232} from '@maka/runtime-host/client' ;
33+ import { RuntimeHostOperationError } from '@maka/runtime-host/client' ;
3334import {
3435 SESSION_CONTINUITY_SCHEMA_VERSION ,
3536 type ClientCapabilityCallFrame ,
@@ -895,6 +896,69 @@ test('drops a stale shared Session observation when Guest access is gone', async
895896 await observations . close ( ) ;
896897} ) ;
897898
899+ test ( 'forgets an observed Session the Host no longer serves instead of blocking every reconnect' , async ( ) => {
900+ const observations = new RuntimeHostSessionObservationRegistry ( ) ;
901+ const firstIpc = ipcHarness ( ) ;
902+ const firstHost = connectionHarness ( 'missing-session-source' , {
903+ sessionId : 'session-1' ,
904+ subscriptionSnapshot : continuitySnapshot ( ) ,
905+ } ) ;
906+ const firstCandidate = await createDesktopRuntimeHostCandidate (
907+ firstHost . connection ,
908+ deps ( firstIpc ) ,
909+ observations ,
910+ ) ;
911+ await firstIpc . invoke ( 'sessions:observe' , 'session-1' , 'observer-1' ) ;
912+ await firstCandidate . close ( ) ;
913+
914+ // The replacement Host no longer serves session-1: subscription.open
915+ // deterministically answers not_found.
916+ const changes : Array < { reason : string ; sessionId ?: string } > = [ ] ;
917+ const missingHost = connectionHarness ( 'missing-session-host' , {
918+ sessionId : 'session-1' ,
919+ subscriptionError : new RuntimeHostOperationError (
920+ 'subscription.open' ,
921+ 'not_found' ,
922+ 'Runtime Host Session was not found' ,
923+ ) ,
924+ } ) ;
925+ const secondCandidate = await createDesktopRuntimeHostCandidate (
926+ missingHost . connection ,
927+ {
928+ ...deps ( ipcHarness ( ) ) ,
929+ emitSessionsChanged : ( _scope , reason , sessionId ) => {
930+ changes . push ( { reason, ...( sessionId === undefined ? { } : { sessionId } ) } ) ;
931+ } ,
932+ } ,
933+ observations ,
934+ ) ;
935+
936+ // The stale active registration is forgotten instead of failing the
937+ // candidate start, and the renderer is told to drop the Session view.
938+ assert . deepEqual ( observations . observedSessionIds ( ) , [ ] ) ;
939+ assert . ok (
940+ changes . some (
941+ ( { reason, sessionId } ) => reason === 'deleted' && sessionId === 'session-1' ,
942+ ) ,
943+ ) ;
944+ await secondCandidate . close ( ) ;
945+
946+ // A later reconnect observes new Sessions on the same registry.
947+ const thirdIpc = ipcHarness ( ) ;
948+ const thirdHost = connectionHarness ( 'missing-session-recovered' , {
949+ sessionId : 'session-2' ,
950+ } ) ;
951+ const thirdCandidate = await createDesktopRuntimeHostCandidate (
952+ thirdHost . connection ,
953+ deps ( thirdIpc ) ,
954+ observations ,
955+ ) ;
956+ await thirdIpc . invoke ( 'sessions:observe' , 'session-2' , 'observer-2' ) ;
957+ assert . deepEqual ( observations . observedSessionIds ( ) , [ 'session-2' ] ) ;
958+ await thirdCandidate . close ( ) ;
959+ await observations . close ( ) ;
960+ } ) ;
961+
898962type IpcHandler = Parameters < Pick < IpcMain , 'handle' > [ 'handle' ] > [ 1 ] ;
899963
900964function ipcHarness ( onSend ?: ( channel : string , payload : unknown ) => void ) {
0 commit comments