@@ -54,6 +54,18 @@ const getInspectableActiveTaskRestartBlockers = vi.fn(
5454const markGatewayDraining = vi . fn ( ) ;
5555const waitForActiveTasks = vi . fn ( async ( _timeoutMs ?: number ) => ( { drained : true } ) ) ;
5656const resetAllLanes = vi . fn ( ) ;
57+ const advanceCronActiveJobGeneration = vi . fn ( ) ;
58+ const resetCronActiveJobs = vi . fn ( ) ;
59+ const abortActiveCronTaskRuns = vi . fn ( ( _reason ?: string ) => 0 ) ;
60+ const retireActiveCronTaskRunTracking = vi . fn ( ) ;
61+ const waitForActiveCronTaskRuns = vi . fn ( async ( _timeoutMs ?: number ) => ( {
62+ drained : true ,
63+ active : 0 ,
64+ } ) ) ;
65+ const waitForActiveCronJobs = vi . fn ( async ( _timeoutMs ?: number ) => ( {
66+ drained : true ,
67+ active : 0 ,
68+ } ) ) ;
5769const reloadTaskRegistryFromStore = vi . fn ( ) ;
5870const rotateAgentEventLifecycleGeneration = vi . fn ( ) ;
5971const clearRuntimeConfigSnapshot = vi . fn ( ) ;
@@ -151,6 +163,18 @@ vi.mock("../../process/command-queue.js", () => ({
151163 resetAllLanes : ( ) => resetAllLanes ( ) ,
152164} ) ) ;
153165
166+ vi . mock ( "../../cron/active-jobs.js" , ( ) => ( {
167+ advanceCronActiveJobGeneration : ( ) => advanceCronActiveJobGeneration ( ) ,
168+ resetCronActiveJobs : ( ) => resetCronActiveJobs ( ) ,
169+ waitForActiveCronJobs : ( timeoutMs : number ) => waitForActiveCronJobs ( timeoutMs ) ,
170+ } ) ) ;
171+
172+ vi . mock ( "../../tasks/cron-task-cancel.js" , ( ) => ( {
173+ abortActiveCronTaskRuns : ( reason ?: string ) => abortActiveCronTaskRuns ( reason ) ,
174+ retireActiveCronTaskRunTracking : ( ) => retireActiveCronTaskRunTracking ( ) ,
175+ waitForActiveCronTaskRuns : ( timeoutMs : number ) => waitForActiveCronTaskRuns ( timeoutMs ) ,
176+ } ) ) ;
177+
154178vi . mock ( "../../tasks/runtime-internal.js" , ( ) => ( {
155179 reloadTaskRegistryFromStore : ( ) => reloadTaskRegistryFromStore ( ) ,
156180} ) ) ;
@@ -876,14 +900,32 @@ describe("runGatewayLoop", () => {
876900 expect ( gatewayLog . warn ) . toHaveBeenCalledWith ( DRAIN_TIMEOUT_LOG ) ;
877901 expectRestartCloseCall ( closeFirst , 1_234 ) ;
878902 expect ( markGatewaySigusr1RestartHandled ) . toHaveBeenCalledTimes ( 1 ) ;
903+ expect ( abortActiveCronTaskRuns ) . toHaveBeenCalledWith ( "Gateway restarting." ) ;
904+ expect ( waitForActiveCronTaskRuns ) . toHaveBeenCalledWith ( 1_000 ) ;
905+ expect ( waitForActiveCronJobs ) . toHaveBeenCalledWith ( 1_000 ) ;
879906 expect ( resetAllLanes ) . toHaveBeenCalledTimes ( 1 ) ;
907+ expect ( advanceCronActiveJobGeneration ) . toHaveBeenCalledTimes ( 1 ) ;
908+ expect ( retireActiveCronTaskRunTracking ) . toHaveBeenCalledTimes ( 1 ) ;
909+ expect ( resetCronActiveJobs ) . toHaveBeenCalledTimes ( 1 ) ;
880910 expect ( clearRuntimeConfigSnapshot ) . toHaveBeenCalledTimes ( 1 ) ;
881911 expect ( resetGatewayRestartStateForInProcessRestart ) . toHaveBeenCalledTimes ( 1 ) ;
882912 expect ( rotateAgentEventLifecycleGeneration ) . toHaveBeenCalledTimes ( 1 ) ;
883913 expect ( reloadTaskRegistryFromStore ) . toHaveBeenCalledTimes ( 1 ) ;
884914 expect (
885915 rotateAgentEventLifecycleGeneration . mock . invocationCallOrder [ 0 ] ?? Infinity ,
886916 ) . toBeLessThan ( resetAllLanes . mock . invocationCallOrder [ 0 ] ?? Infinity ) ;
917+ expect ( advanceCronActiveJobGeneration . mock . invocationCallOrder [ 0 ] ?? Infinity ) . toBeLessThan (
918+ abortActiveCronTaskRuns . mock . invocationCallOrder [ 0 ] ?? Infinity ,
919+ ) ;
920+ expect ( waitForActiveCronJobs . mock . invocationCallOrder [ 0 ] ?? Infinity ) . toBeLessThan (
921+ retireActiveCronTaskRunTracking . mock . invocationCallOrder [ 0 ] ?? Infinity ,
922+ ) ;
923+ expect ( retireActiveCronTaskRunTracking . mock . invocationCallOrder [ 0 ] ?? Infinity ) . toBeLessThan (
924+ resetCronActiveJobs . mock . invocationCallOrder [ 0 ] ?? Infinity ,
925+ ) ;
926+ expect ( resetCronActiveJobs . mock . invocationCallOrder [ 0 ] ?? Infinity ) . toBeLessThan (
927+ resetAllLanes . mock . invocationCallOrder [ 0 ] ?? Infinity ,
928+ ) ;
887929
888930 sigusr1 ( ) ;
889931
@@ -894,7 +936,13 @@ describe("runGatewayLoop", () => {
894936 expectRestartCloseCall ( closeSecond , 1_234 ) ;
895937 expect ( markGatewaySigusr1RestartHandled ) . toHaveBeenCalledTimes ( 2 ) ;
896938 expect ( markGatewayDraining ) . toHaveBeenCalledTimes ( 2 ) ;
939+ expect ( abortActiveCronTaskRuns ) . toHaveBeenCalledTimes ( 2 ) ;
940+ expect ( waitForActiveCronTaskRuns ) . toHaveBeenCalledTimes ( 2 ) ;
941+ expect ( waitForActiveCronJobs ) . toHaveBeenCalledTimes ( 2 ) ;
897942 expect ( resetAllLanes ) . toHaveBeenCalledTimes ( 2 ) ;
943+ expect ( advanceCronActiveJobGeneration ) . toHaveBeenCalledTimes ( 2 ) ;
944+ expect ( retireActiveCronTaskRunTracking ) . toHaveBeenCalledTimes ( 2 ) ;
945+ expect ( resetCronActiveJobs ) . toHaveBeenCalledTimes ( 2 ) ;
898946 expect ( clearRuntimeConfigSnapshot ) . toHaveBeenCalledTimes ( 2 ) ;
899947 expect ( resetGatewayRestartStateForInProcessRestart ) . toHaveBeenCalledTimes ( 2 ) ;
900948 expect ( rotateAgentEventLifecycleGeneration ) . toHaveBeenCalledTimes ( 2 ) ;
@@ -910,6 +958,42 @@ describe("runGatewayLoop", () => {
910958 } ) ;
911959 } ) ;
912960
961+ it ( "advances stale cron active markers after bounded restart cron-run drain" , async ( ) => {
962+ vi . clearAllMocks ( ) ;
963+ waitForActiveCronJobs . mockResolvedValueOnce ( { drained : false , active : 1 } ) ;
964+ peekGatewaySigusr1RestartReason . mockReturnValue ( undefined ) ;
965+ respawnGatewayProcessForUpdate . mockReturnValue ( {
966+ mode : "disabled" ,
967+ detail : "OPENCLAW_NO_RESPAWN" ,
968+ } ) ;
969+
970+ await withIsolatedSignals ( async ( { captureSignal } ) => {
971+ const { start, exited } = await createSignaledLoopHarness ( ) ;
972+ const sigusr1 = captureSignal ( "SIGUSR1" ) ;
973+ const sigint = captureSignal ( "SIGINT" ) ;
974+
975+ sigusr1 ( ) ;
976+ await waitForLoopCondition (
977+ ( ) => start . mock . calls . length >= 2 ,
978+ "expected SIGUSR1 to trigger restart" ,
979+ ) ;
980+
981+ expect ( abortActiveCronTaskRuns ) . toHaveBeenCalledWith ( "Gateway restarting." ) ;
982+ expect ( waitForActiveCronTaskRuns ) . toHaveBeenCalledWith ( 1_000 ) ;
983+ expect ( waitForActiveCronJobs ) . toHaveBeenCalledWith ( 1_000 ) ;
984+ expect ( resetAllLanes ) . toHaveBeenCalledTimes ( 1 ) ;
985+ expect ( advanceCronActiveJobGeneration ) . toHaveBeenCalledTimes ( 1 ) ;
986+ expect ( retireActiveCronTaskRunTracking ) . toHaveBeenCalledTimes ( 1 ) ;
987+ expect ( resetCronActiveJobs ) . toHaveBeenCalledTimes ( 1 ) ;
988+ expect ( gatewayLog . warn ) . toHaveBeenCalledWith (
989+ "cron run drain timed out during restart lifecycle reset after retiring old cron admission; 0 task handle(s) and 1 active marker(s) remain after aborting old cron runs" ,
990+ ) ;
991+
992+ sigint ( ) ;
993+ await expect ( exited ) . resolves . toBe ( 0 ) ;
994+ } ) ;
995+ } ) ;
996+
913997 it ( "queues SIGUSR1 received before the run-loop installs its restart waiter" , async ( ) => {
914998 vi . clearAllMocks ( ) ;
915999 peekGatewaySigusr1RestartReason . mockReturnValue ( undefined ) ;
0 commit comments