@@ -5015,6 +5015,9 @@ describe("update-cli", () => {
50155015 } ) ,
50165016 } ) ;
50175017 serviceLoaded . mockResolvedValue ( true ) ;
5018+ serviceReadCommand . mockResolvedValue ( {
5019+ programArguments : [ "node" , updatedEntrypoint , "gateway" , "run" ] ,
5020+ } ) ;
50185021 vi . mocked ( runCommandWithTimeout ) . mockImplementation ( async ( argv ) => ( {
50195022 stdout : "" ,
50205023 stderr :
@@ -5026,39 +5029,22 @@ describe("update-cli", () => {
50265029 killed : false ,
50275030 termination : "exit" ,
50285031 } ) ) ;
5029- probeGateway
5030- . mockResolvedValueOnce ( {
5031- ok : true ,
5032- close : null ,
5033- server : {
5034- version : "2026.4.23" ,
5035- connId : "old-gateway" ,
5036- } ,
5037- auth : { role : "operator" , scopes : [ "operator.read" ] , capability : "read_only" } ,
5038- health : null ,
5039- status : null ,
5040- presence : null ,
5041- configSnapshot : null ,
5042- connectLatencyMs : 1 ,
5043- error : null ,
5044- url : "ws://127.0.0.1:18789" ,
5045- } )
5046- . mockResolvedValue ( {
5047- ok : true ,
5048- close : null ,
5049- server : {
5050- version : "2026.4.24" ,
5051- connId : "updated-gateway" ,
5052- } ,
5053- auth : { role : "operator" , scopes : [ "operator.read" ] , capability : "read_only" } ,
5054- health : null ,
5055- status : null ,
5056- presence : null ,
5057- configSnapshot : null ,
5058- connectLatencyMs : 1 ,
5059- error : null ,
5060- url : "ws://127.0.0.1:18789" ,
5061- } ) ;
5032+ probeGateway . mockResolvedValue ( {
5033+ ok : true ,
5034+ close : null ,
5035+ server : {
5036+ version : "2026.4.24" ,
5037+ connId : "updated-gateway" ,
5038+ } ,
5039+ auth : { role : "operator" , scopes : [ "operator.read" ] , capability : "read_only" } ,
5040+ health : null ,
5041+ status : null ,
5042+ presence : null ,
5043+ configSnapshot : null ,
5044+ connectLatencyMs : 1 ,
5045+ error : null ,
5046+ url : "ws://127.0.0.1:18789" ,
5047+ } ) ;
50625048
50635049 await updateCommand ( { yes : true } ) ;
50645050
@@ -5076,6 +5062,152 @@ describe("update-cli", () => {
50765062 ) . toContain ( "Gateway: restarted and verified." ) ;
50775063 } ) ;
50785064
5065+ it ( "accepts same-version refresh failure recovery when the managed service restarts" , async ( ) => {
5066+ const updatedRoot = createCaseDir ( "openclaw-updated-root" ) ;
5067+ const updatedEntrypoint = path . join ( updatedRoot , "dist" , "entry.js" ) ;
5068+ const updatedPackageJson = path . join ( updatedRoot , "package.json" ) ;
5069+ await fs . mkdir ( updatedRoot , { recursive : true } ) ;
5070+ await fs . writeFile (
5071+ updatedPackageJson ,
5072+ JSON . stringify ( { name : "openclaw" , version : "2026.4.24" } ) ,
5073+ "utf8" ,
5074+ ) ;
5075+ setupUpdatedRootRefresh ( {
5076+ entrypoints : [ updatedEntrypoint ] ,
5077+ gatewayUpdateImpl : async ( ) =>
5078+ makeOkUpdateResult ( {
5079+ mode : "npm" ,
5080+ root : updatedRoot ,
5081+ before : { version : "2026.4.24" } ,
5082+ after : { version : "2026.4.24" } ,
5083+ } ) ,
5084+ } ) ;
5085+ pathExists . mockImplementation (
5086+ async ( candidate : string ) =>
5087+ candidate === updatedEntrypoint || candidate === updatedPackageJson ,
5088+ ) ;
5089+ serviceLoaded . mockResolvedValue ( true ) ;
5090+ serviceReadCommand . mockResolvedValue ( {
5091+ programArguments : [ "node" , updatedEntrypoint , "gateway" , "run" ] ,
5092+ } ) ;
5093+ vi . mocked ( runCommandWithTimeout ) . mockImplementation ( async ( argv ) => ( {
5094+ stdout : "" ,
5095+ stderr :
5096+ argv [ 1 ] === updatedEntrypoint && argv [ 2 ] === "gateway" && argv [ 3 ] === "install"
5097+ ? "launchctl bootstrap failed"
5098+ : "" ,
5099+ code : argv [ 1 ] === updatedEntrypoint && argv [ 2 ] === "gateway" && argv [ 3 ] === "install" ? 1 : 0 ,
5100+ signal : null ,
5101+ killed : false ,
5102+ termination : "exit" ,
5103+ } ) ) ;
5104+ probeGateway . mockResolvedValue ( {
5105+ ok : true ,
5106+ close : null ,
5107+ server : {
5108+ version : "2026.4.24" ,
5109+ connId : "matching-old-gateway" ,
5110+ } ,
5111+ auth : { role : "operator" , scopes : [ "operator.read" ] , capability : "read_only" } ,
5112+ health : null ,
5113+ status : null ,
5114+ presence : null ,
5115+ configSnapshot : null ,
5116+ connectLatencyMs : 1 ,
5117+ error : null ,
5118+ url : "ws://127.0.0.1:18789" ,
5119+ } ) ;
5120+
5121+ await updateCommand ( { yes : true } ) ;
5122+
5123+ expect ( gatewayCommandCall ( updatedEntrypoint , "install" ) ) . toBeDefined ( ) ;
5124+ expect ( gatewayCommandCall ( updatedEntrypoint , "restart" ) ) . toBeDefined ( ) ;
5125+ expect ( runRestartScript ) . not . toHaveBeenCalled ( ) ;
5126+ expect ( defaultRuntime . exit ) . not . toHaveBeenCalledWith ( 1 ) ;
5127+ } ) ;
5128+
5129+ it ( "rejects same-version refresh failure recovery from a stale service definition" , async ( ) => {
5130+ const oldRoot = createCaseDir ( "openclaw-old-root" ) ;
5131+ const updatedRoot = createCaseDir ( "openclaw-updated-root" ) ;
5132+ const oldEntrypoint = path . join ( oldRoot , "dist" , "entry.js" ) ;
5133+ const updatedEntrypoint = path . join ( updatedRoot , "dist" , "entry.js" ) ;
5134+ const oldPackageJson = path . join ( oldRoot , "package.json" ) ;
5135+ const updatedPackageJson = path . join ( updatedRoot , "package.json" ) ;
5136+ await Promise . all ( [
5137+ fs . mkdir ( oldRoot , { recursive : true } ) ,
5138+ fs . mkdir ( updatedRoot , { recursive : true } ) ,
5139+ ] ) ;
5140+ await Promise . all ( [
5141+ fs . writeFile (
5142+ oldPackageJson ,
5143+ JSON . stringify ( { name : "openclaw" , version : "2026.4.24" } ) ,
5144+ "utf8" ,
5145+ ) ,
5146+ fs . writeFile (
5147+ updatedPackageJson ,
5148+ JSON . stringify ( { name : "openclaw" , version : "2026.4.24" } ) ,
5149+ "utf8" ,
5150+ ) ,
5151+ ] ) ;
5152+ setupUpdatedRootRefresh ( {
5153+ entrypoints : [ oldEntrypoint , updatedEntrypoint ] ,
5154+ gatewayUpdateImpl : async ( ) =>
5155+ makeOkUpdateResult ( {
5156+ mode : "npm" ,
5157+ root : updatedRoot ,
5158+ before : { version : "2026.4.24" } ,
5159+ after : { version : "2026.4.24" } ,
5160+ } ) ,
5161+ } ) ;
5162+ pathExists . mockImplementation ( async ( candidate : string ) =>
5163+ [ oldEntrypoint , updatedEntrypoint , oldPackageJson , updatedPackageJson ] . includes ( candidate ) ,
5164+ ) ;
5165+ serviceLoaded . mockResolvedValue ( true ) ;
5166+ serviceReadCommand . mockResolvedValue ( {
5167+ programArguments : [ "node" , oldEntrypoint , "gateway" , "run" ] ,
5168+ } ) ;
5169+ vi . mocked ( runCommandWithTimeout ) . mockImplementation ( async ( argv ) => ( {
5170+ stdout : "" ,
5171+ stderr :
5172+ argv [ 1 ] === updatedEntrypoint && argv [ 2 ] === "gateway" && argv [ 3 ] === "install"
5173+ ? "launchctl bootstrap failed"
5174+ : "" ,
5175+ code : argv [ 1 ] === updatedEntrypoint && argv [ 2 ] === "gateway" && argv [ 3 ] === "install" ? 1 : 0 ,
5176+ signal : null ,
5177+ killed : false ,
5178+ termination : "exit" ,
5179+ } ) ) ;
5180+ probeGateway . mockResolvedValue ( {
5181+ ok : true ,
5182+ close : null ,
5183+ server : {
5184+ version : "2026.4.24" ,
5185+ connId : "matching-old-service" ,
5186+ } ,
5187+ auth : { role : "operator" , scopes : [ "operator.read" ] , capability : "read_only" } ,
5188+ health : null ,
5189+ status : null ,
5190+ presence : null ,
5191+ configSnapshot : null ,
5192+ connectLatencyMs : 1 ,
5193+ error : null ,
5194+ url : "ws://127.0.0.1:18789" ,
5195+ } ) ;
5196+
5197+ await updateCommand ( { yes : true } ) ;
5198+
5199+ expect ( gatewayCommandCall ( updatedEntrypoint , "install" ) ) . toBeDefined ( ) ;
5200+ expect ( gatewayCommandCall ( updatedEntrypoint , "restart" ) ) . toBeDefined ( ) ;
5201+ expect ( runRestartScript ) . not . toHaveBeenCalled ( ) ;
5202+ expect ( defaultRuntime . exit ) . toHaveBeenCalledWith ( 1 ) ;
5203+ expect (
5204+ vi
5205+ . mocked ( defaultRuntime . log )
5206+ . mock . calls . map ( ( call ) => String ( call [ 0 ] ) )
5207+ . join ( "\n" ) ,
5208+ ) . toContain ( "did not point at the updated install" ) ;
5209+ } ) ;
5210+
50795211 it ( "fails a JSON package update when fallback restart leaves the old gateway running" , async ( ) => {
50805212 const updatedRoot = createCaseDir ( "openclaw-updated-root" ) ;
50815213 const updatedEntrypoint = path . join ( updatedRoot , "dist" , "entry.js" ) ;
0 commit comments