@@ -2939,6 +2939,94 @@ describe("cron service timer regressions", () => {
29392939 }
29402940 } ) ;
29412941
2942+ it ( "keeps the pre-execution watchdog armed for bare runner entry (#93912)" , async ( ) => {
2943+ vi . useFakeTimers ( ) ;
2944+ try {
2945+ const store = timerRegressionFixtures . makeStorePath ( ) ;
2946+ const scheduledAt = Date . parse ( "2026-05-10T09:06:00.000Z" ) ;
2947+ const cronJob = createIsolatedRegressionJob ( {
2948+ id : "isolated-runner-entry-only-93912" ,
2949+ name : "runner entry only regression" ,
2950+ scheduledAt,
2951+ schedule : { kind : "at" , at : new Date ( scheduledAt ) . toISOString ( ) } ,
2952+ payload : { kind : "agentTurn" , message : "work" , timeoutSeconds : 1_200 } ,
2953+ state : { nextRunAtMs : scheduledAt } ,
2954+ } ) ;
2955+ await saveCronStore ( store . storePath , { version : 1 , jobs : [ cronJob ] } ) ;
2956+
2957+ vi . setSystemTime ( scheduledAt ) ;
2958+ let now = scheduledAt ;
2959+ const started = createDeferred < void > ( ) ;
2960+ let abortObserved = false ;
2961+ let abortReason : unknown ;
2962+ const cleanupTimedOutAgentRun = vi . fn ( async ( ) => { } ) ;
2963+ const onIsolatedAgentSetupTimeout = vi . fn ( ) ;
2964+ const state = createCronServiceState ( {
2965+ cronEnabled : true ,
2966+ storePath : store . storePath ,
2967+ log : noopLogger ,
2968+ nowMs : ( ) => now ,
2969+ enqueueSystemEvent : vi . fn ( ) ,
2970+ requestHeartbeat : vi . fn ( ) ,
2971+ cleanupTimedOutAgentRun,
2972+ onIsolatedAgentSetupTimeout,
2973+ runIsolatedAgentJob : vi . fn (
2974+ async ( {
2975+ abortSignal,
2976+ onExecutionStarted,
2977+ } : {
2978+ abortSignal ?: AbortSignal ;
2979+ onExecutionStarted ?: ( info ?: CronAgentExecutionStarted ) => void ;
2980+ } ) => {
2981+ onExecutionStarted ?.( {
2982+ jobId : "isolated-runner-entry-only-93912" ,
2983+ agentId : "main" ,
2984+ sessionId : "cron-run-session" ,
2985+ sessionKey : "agent:main:cron:isolated-runner-entry-only-93912:run:cron-run-session" ,
2986+ phase : "runner_entered" ,
2987+ } ) ;
2988+ started . resolve ( ) ;
2989+ abortSignal ?. addEventListener (
2990+ "abort" ,
2991+ ( ) => {
2992+ abortObserved = true ;
2993+ abortReason = abortSignal . reason ;
2994+ } ,
2995+ { once : true } ,
2996+ ) ;
2997+ return await new Promise < never > ( ( ) => { } ) ;
2998+ } ,
2999+ ) ,
3000+ } ) ;
3001+
3002+ const timerPromise = onTimer ( state ) ;
3003+ await started . promise ;
3004+ await vi . advanceTimersByTimeAsync ( 60_100 ) ;
3005+ now += 60_100 ;
3006+ await timerPromise ;
3007+
3008+ const job = requireJob ( state , "isolated-runner-entry-only-93912" ) ;
3009+ expect ( abortObserved ) . toBe ( true ) ;
3010+ expect ( job . state . lastStatus ) . toBe ( "error" ) ;
3011+ expect ( job . state . lastError ) . toContain ( "stalled before execution start" ) ;
3012+ expect ( job . state . lastError ) . toContain ( "runner-entered" ) ;
3013+ expect ( abortReason ) . toMatchObject ( {
3014+ name : "TimeoutError" ,
3015+ message : expect . stringContaining ( "runner-entered" ) ,
3016+ } ) ;
3017+ expect ( cleanupTimedOutAgentRun ) . toHaveBeenCalledTimes ( 1 ) ;
3018+ const cleanupArgs = requireRecord ( firstMockArg ( cleanupTimedOutAgentRun ) ) ;
3019+ expect ( requireRecord ( cleanupArgs . job ) . id ) . toBe ( "isolated-runner-entry-only-93912" ) ;
3020+ expect ( cleanupArgs . timeoutMs ) . toBe ( 1_200_000 ) ;
3021+ const execution = requireRecord ( cleanupArgs . execution ) ;
3022+ expect ( execution . jobId ) . toBe ( "isolated-runner-entry-only-93912" ) ;
3023+ expect ( execution . phase ) . toBe ( "runner_entered" ) ;
3024+ expect ( onIsolatedAgentSetupTimeout ) . not . toHaveBeenCalled ( ) ;
3025+ } finally {
3026+ vi . useRealTimers ( ) ;
3027+ }
3028+ } ) ;
3029+
29423030 it ( "keeps isolated runs alive when setup phases keep progressing (#93530)" , async ( ) => {
29433031 vi . useFakeTimers ( ) ;
29443032 try {
0 commit comments