@@ -185,6 +185,7 @@ function createHost(): TestGatewayHost {
185185 localMediaPreviewRoots : [ ] ,
186186 serverVersion : null ,
187187 pendingUpdateExpectedVersion : null ,
188+ pendingUpdateHandoff : false ,
188189 updateStatusBanner : null ,
189190 sessionKey : "main" ,
190191 chatMessages : [ ] ,
@@ -532,6 +533,92 @@ describe("connectGateway", () => {
532533 await vi . waitFor ( ( ) => {
533534 expect ( host . pendingUpdateExpectedVersion ) . toBeNull ( ) ;
534535 } ) ;
536+ expect ( host . pendingUpdateHandoff ) . toBe ( false ) ;
537+ expect ( host . updateStatusBanner ) . toBeNull ( ) ;
538+ } ) ;
539+
540+ it ( "clears managed-service handoff verification when update status completes" , async ( ) => {
541+ const host = createHost ( ) ;
542+ host . pendingUpdateHandoff = true ;
543+
544+ connectGateway ( host ) ;
545+ const client = requireGatewayClient ( ) ;
546+ client . request . mockImplementation ( async ( method : string ) => {
547+ if ( method === "update.status" ) {
548+ return {
549+ sentinel : {
550+ kind : "update" ,
551+ status : "ok" ,
552+ stats : {
553+ after : { version : "2.0.0" } ,
554+ } ,
555+ } ,
556+ } ;
557+ }
558+ return { } ;
559+ } ) ;
560+
561+ client . emitHello ( {
562+ type : "hello-ok" ,
563+ protocol : 4 ,
564+ server : { version : "2.0.0" } ,
565+ auth : { role : "operator" , scopes : [ ] } ,
566+ snapshot : { } ,
567+ } ) ;
568+
569+ await vi . waitFor ( ( ) => {
570+ expect ( host . pendingUpdateHandoff ) . toBe ( false ) ;
571+ } ) ;
572+ expect ( host . pendingUpdateExpectedVersion ) . toBeNull ( ) ;
573+ expect ( host . updateStatusBanner ) . toBeNull ( ) ;
574+ } ) ;
575+
576+ it ( "keeps polling while managed-service handoff restart health is pending" , async ( ) => {
577+ const host = createHost ( ) ;
578+ host . pendingUpdateHandoff = true ;
579+
580+ connectGateway ( host ) ;
581+ const client = requireGatewayClient ( ) ;
582+ let updateStatusCalls = 0 ;
583+ client . request . mockImplementation ( async ( method : string ) => {
584+ if ( method === "update.status" ) {
585+ updateStatusCalls += 1 ;
586+ if ( updateStatusCalls === 1 ) {
587+ return {
588+ sentinel : {
589+ kind : "update" ,
590+ status : "skipped" ,
591+ stats : {
592+ reason : "restart-health-pending" ,
593+ } ,
594+ } ,
595+ } ;
596+ }
597+ return {
598+ sentinel : {
599+ kind : "update" ,
600+ status : "ok" ,
601+ stats : {
602+ after : { version : "2.0.0" } ,
603+ } ,
604+ } ,
605+ } ;
606+ }
607+ return { } ;
608+ } ) ;
609+
610+ client . emitHello ( {
611+ type : "hello-ok" ,
612+ protocol : 4 ,
613+ server : { version : "2.0.0" } ,
614+ auth : { role : "operator" , scopes : [ ] } ,
615+ snapshot : { } ,
616+ } ) ;
617+
618+ await vi . waitFor ( ( ) => {
619+ expect ( updateStatusCalls ) . toBeGreaterThanOrEqual ( 2 ) ;
620+ expect ( host . pendingUpdateHandoff ) . toBe ( false ) ;
621+ } ) ;
535622 expect ( host . updateStatusBanner ) . toBeNull ( ) ;
536623 } ) ;
537624
@@ -566,6 +653,7 @@ describe("connectGateway", () => {
566653
567654 await vi . waitFor ( ( ) => {
568655 expect ( host . pendingUpdateExpectedVersion ) . toBeNull ( ) ;
656+ expect ( host . pendingUpdateHandoff ) . toBe ( false ) ;
569657 expect ( host . updateStatusBanner ) . toEqual ( {
570658 tone : "danger" ,
571659 text : "Update installed but running version did not change — restart may have been blocked. Expected v2.0.0, running v1.0.0." ,
@@ -605,13 +693,52 @@ describe("connectGateway", () => {
605693
606694 await vi . waitFor ( ( ) => {
607695 expect ( host . pendingUpdateExpectedVersion ) . toBeNull ( ) ;
696+ expect ( host . pendingUpdateHandoff ) . toBe ( false ) ;
608697 expect ( host . updateStatusBanner ) . toEqual ( {
609698 tone : "danger" ,
610699 text : "Update error: restart-unhealthy. The replacement process never became healthy and the previous process stayed up." ,
611700 } ) ;
612701 } ) ;
613702 } ) ;
614703
704+ it ( "surfaces managed-service handoff failures even without a final version" , async ( ) => {
705+ const host = createHost ( ) ;
706+ host . pendingUpdateHandoff = true ;
707+
708+ connectGateway ( host ) ;
709+ const client = requireGatewayClient ( ) ;
710+ client . request . mockImplementation ( async ( method : string ) => {
711+ if ( method === "update.status" ) {
712+ return {
713+ sentinel : {
714+ kind : "update" ,
715+ status : "error" ,
716+ stats : {
717+ reason : "managed-service-handoff-failed" ,
718+ } ,
719+ } ,
720+ } ;
721+ }
722+ return { } ;
723+ } ) ;
724+
725+ client . emitHello ( {
726+ type : "hello-ok" ,
727+ protocol : 4 ,
728+ server : { version : "1.0.0" } ,
729+ auth : { role : "operator" , scopes : [ ] } ,
730+ snapshot : { } ,
731+ } ) ;
732+
733+ await vi . waitFor ( ( ) => {
734+ expect ( host . pendingUpdateHandoff ) . toBe ( false ) ;
735+ expect ( host . updateStatusBanner ) . toEqual ( {
736+ tone : "danger" ,
737+ text : "Update error: managed-service-handoff-failed. Check the gateway logs for the replacement failure." ,
738+ } ) ;
739+ } ) ;
740+ } ) ;
741+
615742 it ( "ignores stale client onClose callbacks after reconnect" , ( ) => {
616743 const host = createHost ( ) ;
617744
0 commit comments