@@ -525,6 +525,90 @@ describe("BoardWidgetSandboxHost", () => {
525525 } ) ;
526526 } ) ;
527527
528+ it ( "drops in-flight responses from a replaced Gateway client" , async ( ) => {
529+ const frame = document . createElement ( "iframe" ) ;
530+ document . body . append ( frame ) ;
531+ vi . stubGlobal (
532+ "fetch" ,
533+ vi . fn ( async ( ) => new Response ( "<!doctype html><p>weather</p>" ) ) ,
534+ ) ;
535+ let resolveOldRequest : ( value : unknown ) => void = ( ) => { } ;
536+ const oldClient = {
537+ request : vi . fn (
538+ async ( ) =>
539+ await new Promise < unknown > ( ( resolve ) => {
540+ resolveOldRequest = resolve ;
541+ } ) ,
542+ ) ,
543+ } ;
544+ const newClient = { request : vi . fn ( async ( ) => ( { ok : true } ) ) } ;
545+ const baseOptions = {
546+ frame,
547+ widget : widget ( ) ,
548+ sandboxOrigin : "https://sandbox.example" ,
549+ sandboxUrl : SANDBOX_URL ,
550+ sourceOrigin : "https://gateway.example" ,
551+ client : oldClient ,
552+ resolveFrameUrl : ( ) => "/widget?bt=ticket" ,
553+ confirmPrompt : ( ) => true ,
554+ onFrameUrl : vi . fn ( ) ,
555+ onUnauthorized : vi . fn ( ) ,
556+ onReadyTimeout : vi . fn ( ) ,
557+ onLoaded : vi . fn ( ) ,
558+ onError : vi . fn ( ) ,
559+ } ;
560+ const host = new BoardWidgetSandboxHost ( baseOptions ) ;
561+ host . handleMessage (
562+ new MessageEvent ( "message" , {
563+ source : frame . contentWindow ,
564+ origin : "https://sandbox.example" ,
565+ data : {
566+ method : "ui/notifications/sandbox-proxy-ready" ,
567+ params : { sandboxUrl : SANDBOX_URL } ,
568+ } ,
569+ } ) ,
570+ ) ;
571+ await vi . waitFor ( ( ) => expect ( baseOptions . onLoaded ) . toHaveBeenCalledOnce ( ) ) ;
572+ const bridgePort = await offerBridgePort ( host , frame ) ;
573+ const responses : unknown [ ] = [ ] ;
574+ bridgePort . addEventListener ( "message" , ( event ) => {
575+ if ( event . data ?. type === "openclaw:widget-bridge-response" ) {
576+ responses . push ( event . data ) ;
577+ }
578+ } ) ;
579+
580+ bridgePort . postMessage (
581+ {
582+ type : "openclaw:widget-bridge-request" ,
583+ id : "old-client" ,
584+ method : "data.read" ,
585+ params : { bindingId : "health" } ,
586+ ticket : "ticket" ,
587+ } ,
588+ [ ] ,
589+ ) ;
590+ await vi . waitFor ( ( ) => expect ( oldClient . request ) . toHaveBeenCalledOnce ( ) ) ;
591+ host . update ( { ...baseOptions , client : newClient } ) ;
592+ resolveOldRequest ( { private : "old-context" } ) ;
593+ await Promise . resolve ( ) ;
594+ await Promise . resolve ( ) ;
595+ expect ( responses ) . not . toContainEqual ( expect . objectContaining ( { id : "old-client" } ) ) ;
596+
597+ await expect (
598+ sendBridgeRequest ( bridgePort , {
599+ type : "openclaw:widget-bridge-request" ,
600+ id : "new-client" ,
601+ method : "state.emit" ,
602+ params : { payload : { phase : "reconnected" } } ,
603+ ticket : "ticket" ,
604+ } ) ,
605+ ) . resolves . toMatchObject ( { id : "new-client" , ok : true } ) ;
606+ expect ( newClient . request ) . toHaveBeenCalledWith ( "board.event" , {
607+ ticket : "ticket" ,
608+ payload : { phase : "reconnected" } ,
609+ } ) ;
610+ } ) ;
611+
528612 it ( "reloads equal-name revisions when their board session identity changes" , async ( ) => {
529613 const frame = document . createElement ( "iframe" ) ;
530614 document . body . append ( frame ) ;
0 commit comments