@@ -3,8 +3,9 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
33type RestartHealthSnapshot = {
44 healthy : boolean ;
55 staleGatewayPids : number [ ] ;
6- runtime : { status ?: string } ;
6+ runtime : { status ?: string ; pid ?: number } ;
77 portUsage : { port : number ; status : string ; listeners : [ ] ; hints : [ ] ; errors ?: string [ ] } ;
8+ listenerKinds ?: { gateway : number [ ] ; unknown : number [ ] ; other : number [ ] } ;
89} ;
910
1011type RestartPostCheckContext = {
@@ -21,6 +22,7 @@ type RestartParams = {
2122
2223const service = {
2324 readCommand : vi . fn ( ) ,
25+ readRuntime : vi . fn ( ) ,
2426 restart : vi . fn ( ) ,
2527} ;
2628
@@ -128,6 +130,7 @@ describe("runDaemonRestart health checks", () => {
128130
129131 beforeEach ( ( ) => {
130132 service . readCommand . mockReset ( ) ;
133+ service . readRuntime . mockReset ( ) ;
131134 service . restart . mockReset ( ) ;
132135 runServiceRestart . mockReset ( ) ;
133136 runServiceStop . mockReset ( ) ;
@@ -148,6 +151,7 @@ describe("runDaemonRestart health checks", () => {
148151 programArguments : [ "openclaw" , "gateway" , "--port" , "18789" ] ,
149152 environment : { } ,
150153 } ) ;
154+ service . readRuntime . mockResolvedValue ( { status : "running" , pid : 1500 } ) ;
151155 service . restart . mockResolvedValue ( { outcome : "completed" } ) ;
152156
153157 runServiceRestart . mockImplementation ( async ( params : RestartParams ) => {
@@ -194,6 +198,7 @@ describe("runDaemonRestart health checks", () => {
194198 staleGatewayPids : [ ] ,
195199 runtime : { status : "running" } ,
196200 portUsage : { port : 18789 , status : "busy" , listeners : [ ] , hints : [ ] } ,
201+ listenerKinds : { gateway : [ ] , unknown : [ ] , other : [ ] } ,
197202 } ;
198203 waitForGatewayHealthyRestart . mockResolvedValueOnce ( unhealthy ) . mockResolvedValueOnce ( healthy ) ;
199204 terminateStaleGatewayPids . mockResolvedValue ( [ 1993 ] ) ;
@@ -204,6 +209,9 @@ describe("runDaemonRestart health checks", () => {
204209 expect ( terminateStaleGatewayPids ) . toHaveBeenCalledWith ( [ 1993 ] ) ;
205210 expect ( service . restart ) . toHaveBeenCalledTimes ( 1 ) ;
206211 expect ( waitForGatewayHealthyRestart ) . toHaveBeenCalledTimes ( 2 ) ;
212+ for ( const [ params ] of waitForGatewayHealthyRestart . mock . calls ) {
213+ expect ( params ) . not . toHaveProperty ( "includeUnknownListenersAsStale" ) ;
214+ }
207215 } ) ;
208216
209217 it ( "skips stale-pid retry health checks when the retry restart is only scheduled" , async ( ) => {
@@ -242,6 +250,47 @@ describe("runDaemonRestart health checks", () => {
242250 expect ( renderRestartDiagnostics ) . toHaveBeenCalledTimes ( 1 ) ;
243251 } ) ;
244252
253+ it ( "warns on Windows when restart health recovers without a clear pid change" , async ( ) => {
254+ const originalPlatform = process . platform ;
255+ Object . defineProperty ( process , "platform" , { value : "win32" , configurable : true } ) ;
256+ const healthy : RestartHealthSnapshot & {
257+ listenerKinds : { gateway : number [ ] ; unknown : number [ ] ; other : number [ ] } ;
258+ } = {
259+ healthy : true ,
260+ staleGatewayPids : [ ] ,
261+ runtime : { status : "running" , pid : 1500 } ,
262+ portUsage : { port : 18789 , status : "busy" , listeners : [ ] , hints : [ ] } ,
263+ listenerKinds : { gateway : [ 1500 ] , unknown : [ ] , other : [ ] } ,
264+ } ;
265+ waitForGatewayHealthyRestart . mockResolvedValue ( healthy ) ;
266+ const capturedWarnings : string [ ] [ ] = [ ] ;
267+ runServiceRestart . mockImplementationOnce ( async ( params : RestartParams ) => {
268+ const warnings : string [ ] = [ ] ;
269+ capturedWarnings . push ( warnings ) ;
270+ await params . postRestartCheck ?.( {
271+ json : true ,
272+ stdout : process . stdout ,
273+ warnings,
274+ fail : ( message : string , hints ?: string [ ] ) => {
275+ const err = new Error ( message ) as Error & { hints ?: string [ ] } ;
276+ err . hints = hints ;
277+ throw err ;
278+ } ,
279+ } ) ;
280+ return true ;
281+ } ) ;
282+
283+ try {
284+ await expect ( runDaemonRestart ( { json : true } ) ) . resolves . toBe ( true ) ;
285+ } finally {
286+ Object . defineProperty ( process , "platform" , { value : originalPlatform , configurable : true } ) ;
287+ }
288+
289+ expect ( capturedWarnings [ 0 ] ) . toContain (
290+ "Gateway restart became healthy, but process identity did not clearly change from pid 1500; runtime may be lagging or the old process may still own the listener." ,
291+ ) ;
292+ } ) ;
293+
245294 it ( "signals an unmanaged gateway process on stop" , async ( ) => {
246295 findVerifiedGatewayListenerPidsOnPortSync . mockReturnValue ( [ 4200 , 4200 , 4300 ] ) ;
247296 runServiceStop . mockImplementation ( async ( params : { onNotLoaded ?: ( ) => Promise < unknown > } ) => {
0 commit comments