@@ -743,6 +743,88 @@ describe("cron service timer regressions", () => {
743743 expect ( job ?. state . lastStatus ) . toBe ( "ok" ) ;
744744 } ) ;
745745
746+ it ( "cancels timeout-disabled cron task runs without waiting for the runner" , async ( ) => {
747+ vi . useFakeTimers ( ) ;
748+ try {
749+ resetTaskRegistryForTests ( ) ;
750+ resetActiveCronTaskRunsForTests ( ) ;
751+ const store = timerRegressionFixtures . makeStorePath ( ) ;
752+ const scheduledAt = Date . parse ( "2026-02-15T13:10:00.000Z" ) ;
753+ const cronJob = createIsolatedRegressionJob ( {
754+ id : "no-timeout-cancel" ,
755+ name : "no timeout cancel" ,
756+ scheduledAt,
757+ schedule : { kind : "at" , at : new Date ( scheduledAt ) . toISOString ( ) } ,
758+ payload : { kind : "agentTurn" , message : "work" , timeoutSeconds : 0 } ,
759+ state : { nextRunAtMs : scheduledAt } ,
760+ } ) ;
761+ await saveCronStore ( store . storePath , { version : 1 , jobs : [ cronJob ] } ) ;
762+
763+ let now = scheduledAt ;
764+ let abortObserved = false ;
765+ let timerSettled = false ;
766+ const runnerStarted = createDeferred < void > ( ) ;
767+ const state = createCronServiceState ( {
768+ cronEnabled : true ,
769+ storePath : store . storePath ,
770+ log : noopLogger ,
771+ nowMs : ( ) => now ,
772+ enqueueSystemEvent : vi . fn ( ) ,
773+ requestHeartbeat : vi . fn ( ) ,
774+ runIsolatedAgentJob : vi . fn ( async ( { abortSignal, onExecutionStarted } ) => {
775+ onExecutionStarted ?.( ) ;
776+ runnerStarted . resolve ( ) ;
777+ abortSignal ?. addEventListener (
778+ "abort" ,
779+ ( ) => {
780+ abortObserved = true ;
781+ } ,
782+ { once : true } ,
783+ ) ;
784+ return await new Promise < never > ( ( ) => { } ) ;
785+ } ) ,
786+ } ) ;
787+
788+ const timerPromise = onTimer ( state ) . then ( ( ) => {
789+ timerSettled = true ;
790+ } ) ;
791+ await runnerStarted . promise ;
792+
793+ const runId = `cron:no-timeout-cancel:${ scheduledAt } ` ;
794+ const task = listTaskRecords ( ) . find (
795+ ( entry ) => entry . runtime === "cron" && entry . runId === runId ,
796+ ) ;
797+ if ( ! task ) {
798+ throw new Error ( "Expected timeout-disabled cron task row" ) ;
799+ }
800+
801+ const cancelResult = await cancelTaskById ( {
802+ cfg : { } as never ,
803+ taskId : task . taskId ,
804+ } ) ;
805+ expect ( cancelResult . found ) . toBe ( true ) ;
806+ expect ( cancelResult . cancelled ) . toBe ( true ) ;
807+ expect ( abortObserved ) . toBe ( true ) ;
808+
809+ for ( let attempt = 0 ; attempt < 5 && ! timerSettled ; attempt += 1 ) {
810+ await vi . advanceTimersByTimeAsync ( 0 ) ;
811+ await Promise . resolve ( ) ;
812+ }
813+ expect ( timerSettled ) . toBe ( true ) ;
814+ await timerPromise ;
815+
816+ const finalTask = listTaskRecords ( ) . find ( ( entry ) => entry . taskId === task . taskId ) ;
817+ const job = requireJob ( state , "no-timeout-cancel" ) ;
818+ expect ( finalTask ?. status ) . toBe ( "cancelled" ) ;
819+ expect ( job . state . lastStatus ) . toBe ( "error" ) ;
820+ expect ( job . state . lastError ) . toBe ( "Cancelled by operator." ) ;
821+ } finally {
822+ vi . useRealTimers ( ) ;
823+ resetActiveCronTaskRunsForTests ( ) ;
824+ resetTaskRegistryForTests ( ) ;
825+ }
826+ } ) ;
827+
746828 it ( "does not time out agentTurn jobs at the default 10-minute safety window" , async ( ) => {
747829 const store = timerRegressionFixtures . makeStorePath ( ) ;
748830 const scheduledAt = Date . parse ( "2026-02-15T13:00:00.000Z" ) ;
0 commit comments