@@ -479,6 +479,149 @@ describe("RunEngine heartbeats", () => {
479479 }
480480 } ) ;
481481
482+ containerTest ( "Suspended" , async ( { prisma, redisOptions } ) => {
483+ const authenticatedEnvironment = await setupAuthenticatedEnvironment ( prisma , "PRODUCTION" ) ;
484+
485+ const heartbeatTimeout = 1000 ;
486+
487+ const engine = new RunEngine ( {
488+ prisma,
489+ worker : {
490+ redis : redisOptions ,
491+ workers : 1 ,
492+ tasksPerWorker : 10 ,
493+ pollIntervalMs : 100 ,
494+ } ,
495+ queue : {
496+ redis : redisOptions ,
497+ } ,
498+ runLock : {
499+ redis : redisOptions ,
500+ } ,
501+ machines : {
502+ defaultMachine : "small-1x" ,
503+ machines : {
504+ "small-1x" : {
505+ name : "small-1x" as const ,
506+ cpu : 0.5 ,
507+ memory : 0.5 ,
508+ centsPerMs : 0.0001 ,
509+ } ,
510+ } ,
511+ baseCostInCents : 0.0001 ,
512+ } ,
513+ heartbeatTimeoutsMs : {
514+ SUSPENDED : heartbeatTimeout ,
515+ } ,
516+ tracer : trace . getTracer ( "test" , "0.0.0" ) ,
517+ } ) ;
518+
519+ try {
520+ const taskIdentifier = "test-task" ;
521+
522+ //create background worker
523+ const backgroundWorker = await setupBackgroundWorker (
524+ engine ,
525+ authenticatedEnvironment ,
526+ taskIdentifier
527+ ) ;
528+
529+ //trigger the run
530+ const run = await engine . trigger (
531+ {
532+ number : 1 ,
533+ friendlyId : "run_1234" ,
534+ environment : authenticatedEnvironment ,
535+ taskIdentifier,
536+ payload : "{}" ,
537+ payloadType : "application/json" ,
538+ context : { } ,
539+ traceContext : { } ,
540+ traceId : "t12345" ,
541+ spanId : "s12345" ,
542+ masterQueue : "main" ,
543+ queue : "task/test-task" ,
544+ isTest : false ,
545+ tags : [ ] ,
546+ } ,
547+ prisma
548+ ) ;
549+
550+ //dequeue the run
551+ const dequeued = await engine . dequeueFromMasterQueue ( {
552+ consumerId : "test_12345" ,
553+ masterQueue : run . masterQueue ,
554+ maxRunCount : 10 ,
555+ } ) ;
556+
557+ //create an attempt
558+ await engine . startRunAttempt ( {
559+ runId : dequeued [ 0 ] . run . id ,
560+ snapshotId : dequeued [ 0 ] . snapshot . id ,
561+ } ) ;
562+
563+ //cancel run
564+ //create a manual waitpoint
565+ const waitpointResult = await engine . createManualWaitpoint ( {
566+ environmentId : authenticatedEnvironment . id ,
567+ projectId : authenticatedEnvironment . projectId ,
568+ } ) ;
569+ expect ( waitpointResult . waitpoint . status ) . toBe ( "PENDING" ) ;
570+
571+ //block the run
572+ const blockedResult = await engine . blockRunWithWaitpoint ( {
573+ runId : run . id ,
574+ waitpoints : waitpointResult . waitpoint . id ,
575+ projectId : authenticatedEnvironment . projectId ,
576+ organizationId : authenticatedEnvironment . organizationId ,
577+ } ) ;
578+
579+ const blockedExecutionData = await engine . getRunExecutionData ( { runId : run . id } ) ;
580+ expect ( blockedExecutionData ?. snapshot . executionStatus ) . toBe ( "EXECUTING_WITH_WAITPOINTS" ) ;
581+
582+ // Create a checkpoint
583+ const checkpointResult = await engine . createCheckpoint ( {
584+ runId : run . id ,
585+ snapshotId : blockedResult . id ,
586+ checkpoint : {
587+ type : "DOCKER" ,
588+ reason : "TEST_CHECKPOINT" ,
589+ location : "test-location" ,
590+ imageRef : "test-image-ref" ,
591+ } ,
592+ } ) ;
593+
594+ expect ( checkpointResult . ok ) . toBe ( true ) ;
595+
596+ const snapshot = checkpointResult . ok ? checkpointResult . snapshot : null ;
597+
598+ assertNonNullable ( snapshot ) ;
599+
600+ // Verify checkpoint creation
601+ expect ( snapshot . executionStatus ) . toBe ( "SUSPENDED" ) ;
602+
603+ // Now wait for the heartbeat to timeout, but it should retry later
604+ await setTimeout ( heartbeatTimeout * 1.5 ) ;
605+
606+ // Simulate a suspended run without any blocking waitpoints by deleting any blocking task run waitpoints
607+ await prisma . taskRunWaitpoint . deleteMany ( {
608+ where : {
609+ taskRunId : run . id ,
610+ } ,
611+ } ) ;
612+
613+ // Now wait for the heartbeat to timeout again
614+ await setTimeout ( heartbeatTimeout * 2 ) ;
615+
616+ // Expect the run to be queued
617+ const executionData2 = await engine . getRunExecutionData ( { runId : run . id } ) ;
618+ assertNonNullable ( executionData2 ) ;
619+ expect ( executionData2 . snapshot . executionStatus ) . toBe ( "QUEUED" ) ;
620+ } finally {
621+ await engine . quit ( ) ;
622+ }
623+ } ) ;
624+
482625 containerTest ( "Heartbeat keeps run alive" , async ( { prisma, redisOptions } ) => {
483626 const authenticatedEnvironment = await setupAuthenticatedEnvironment ( prisma , "PRODUCTION" ) ;
484627
0 commit comments