@@ -430,3 +430,183 @@ describe('getWorkflowExecutionStatus queue projection', () => {
430430 } )
431431 } )
432432} )
433+
434+ describe ( 'getWorkflowExecutionStatus settled resume attempts' , ( ) => {
435+ const resumeInput = { ...input , executionId : 'resume-run-1' }
436+
437+ function parentLog ( overrides : Record < string , unknown > = { } ) {
438+ return {
439+ executionId : 'execution-1' ,
440+ workflowId : 'workflow-1' ,
441+ workspaceId : 'workspace-1' ,
442+ status : 'completed' ,
443+ level : 'info' ,
444+ trigger : 'api' ,
445+ startedAt : new Date ( '2026-08-05T11:00:00.000Z' ) ,
446+ endedAt : new Date ( '2026-08-05T12:00:05.000Z' ) ,
447+ totalDurationMs : 3605000 ,
448+ executionData : { finalOutput : { answer : 42 } } ,
449+ costTotal : '0.5' ,
450+ ...overrides ,
451+ }
452+ }
453+
454+ function settledAttempt ( overrides : Record < string , unknown > = { } ) {
455+ return {
456+ id : 'resume-entry-1' ,
457+ parentExecutionId : 'execution-1' ,
458+ status : 'completed' ,
459+ queuedAt : new Date ( '2026-08-05T12:00:00.000Z' ) ,
460+ claimedAt : new Date ( '2026-08-05T12:00:01.000Z' ) ,
461+ completedAt : new Date ( '2026-08-05T12:00:05.000Z' ) ,
462+ failureReason : null ,
463+ ...overrides ,
464+ }
465+ }
466+
467+ /** The attempt has no log of its own; the parent run is read second. */
468+ function queueSettledResume (
469+ attempt : Record < string , unknown > ,
470+ log : Record < string , unknown > ,
471+ pausedRows : unknown [ ] = [ ]
472+ ) {
473+ queueTableRows ( schemaMock . workflowExecutionLogs , [ ] )
474+ queueTableRows ( schemaMock . resumeQueue , [ attempt ] )
475+ queueTableRows ( schemaMock . workflowExecutionLogs , [ log ] )
476+ queueTableRows ( schemaMock . resumeQueue , [ ] )
477+ queueTableRows ( schemaMock . pausedExecutions , pausedRows )
478+ }
479+
480+ beforeEach ( ( ) => {
481+ vi . clearAllMocks ( )
482+ resetDbChainMock ( )
483+ mockGetJob . mockResolvedValue ( null )
484+ mockMaterializeForDisplayWithBlockOutputs . mockImplementation ( async ( executionData ) => ( {
485+ executionData,
486+ blockOutputs : new Map ( ) ,
487+ } ) )
488+ } )
489+
490+ it ( 'projects a completed resume from the run it continued, under its own run ID' , async ( ) => {
491+ queueSettledResume ( settledAttempt ( ) , parentLog ( ) )
492+
493+ const status = await getWorkflowExecutionStatus ( { ...resumeInput , includeOutput : true } )
494+
495+ expect ( status ) . toEqual ( {
496+ executionId : 'resume-run-1' ,
497+ workflowId : 'workflow-1' ,
498+ status : 'completed' ,
499+ trigger : 'api' ,
500+ level : 'info' ,
501+ startedAt : '2026-08-05T12:00:01.000Z' ,
502+ endedAt : '2026-08-05T12:00:05.000Z' ,
503+ totalDurationMs : 4000 ,
504+ paused : null ,
505+ cost : { total : 0.5 } ,
506+ error : null ,
507+ finalOutput : { answer : 42 } ,
508+ blockOutputs : null ,
509+ } )
510+ expect ( mockMaterializeForDisplayWithBlockOutputs ) . toHaveBeenCalledWith (
511+ expect . anything ( ) ,
512+ { workspaceId : 'workspace-1' , workflowId : 'workflow-1' , executionId : 'execution-1' } ,
513+ [ ]
514+ )
515+ expect ( mockGetJob ) . toHaveBeenCalledWith ( 'workflow-execution:resume-run-1' )
516+ } )
517+
518+ it ( 'reports the next pause when a completed resume paused the run again' , async ( ) => {
519+ queueSettledResume ( settledAttempt ( ) , parentLog ( { status : 'paused' , executionData : { } } ) , [
520+ {
521+ id : 'paused-1' ,
522+ status : 'partially_resumed' ,
523+ pausePoints : {
524+ 'context-2' : {
525+ contextId : 'context-2' ,
526+ blockId : 'block-2' ,
527+ pauseKind : 'human' ,
528+ resumeStatus : 'paused' ,
529+ } ,
530+ } ,
531+ metadata : { } ,
532+ resumedCount : 1 ,
533+ pausedAt : new Date ( '2026-08-05T12:00:04.000Z' ) ,
534+ nextResumeAt : null ,
535+ } ,
536+ ] )
537+
538+ const status = await getWorkflowExecutionStatus ( resumeInput )
539+
540+ expect ( status ) . toMatchObject ( {
541+ executionId : 'resume-run-1' ,
542+ status : 'paused' ,
543+ endedAt : '2026-08-05T12:00:05.000Z' ,
544+ paused : { contextId : 'context-2' , pausedExecutionId : 'paused-1' , resumedCount : 1 } ,
545+ } )
546+ } )
547+
548+ it ( 'reports no end time while a later resume is still running the run' , async ( ) => {
549+ queueSettledResume ( settledAttempt ( ) , parentLog ( { status : 'running' , endedAt : null } ) )
550+
551+ const status = await getWorkflowExecutionStatus ( resumeInput )
552+
553+ expect ( status ) . toMatchObject ( {
554+ executionId : 'resume-run-1' ,
555+ status : 'running' ,
556+ startedAt : '2026-08-05T12:00:01.000Z' ,
557+ endedAt : null ,
558+ totalDurationMs : null ,
559+ } )
560+ } )
561+
562+ it ( 'reports a failed resume that left the run paused as failed with its reason' , async ( ) => {
563+ queueSettledResume (
564+ settledAttempt ( { status : 'failed' , failureReason : 'Resume execution cancelled' } ) ,
565+ parentLog ( { status : 'paused' , executionData : { } } )
566+ )
567+
568+ const status = await getWorkflowExecutionStatus ( { ...resumeInput , includeOutput : true } )
569+
570+ expect ( status ) . toMatchObject ( {
571+ executionId : 'resume-run-1' ,
572+ status : 'failed' ,
573+ level : 'error' ,
574+ error : 'Resume execution cancelled' ,
575+ endedAt : '2026-08-05T12:00:05.000Z' ,
576+ paused : null ,
577+ finalOutput : null ,
578+ blockOutputs : null ,
579+ } )
580+ } )
581+
582+ it ( "prefers the run's own error when the failed resume failed the run" , async ( ) => {
583+ queueSettledResume (
584+ settledAttempt ( { status : 'failed' , failureReason : 'Unexpected error' } ) ,
585+ parentLog ( { status : 'failed' , level : 'error' , executionData : { error : 'Block 2 timed out' } } )
586+ )
587+
588+ const status = await getWorkflowExecutionStatus ( resumeInput )
589+
590+ expect ( status ) . toMatchObject ( { status : 'failed' , error : 'Block 2 timed out' } )
591+ } )
592+
593+ it ( 'reports a resume that lost to cancellation as cancelled' , async ( ) => {
594+ queueSettledResume (
595+ settledAttempt ( { status : 'failed' , failureReason : 'Paused execution cancelled' } ) ,
596+ parentLog ( { status : 'cancelled' , executionData : { } } )
597+ )
598+
599+ const status = await getWorkflowExecutionStatus ( resumeInput )
600+
601+ expect ( status ) . toMatchObject ( { status : 'cancelled' , level : 'info' , error : null } )
602+ } )
603+
604+ it ( 'returns null when the run a settled resume continued no longer exists' , async ( ) => {
605+ queueTableRows ( schemaMock . workflowExecutionLogs , [ ] )
606+ queueTableRows ( schemaMock . resumeQueue , [ settledAttempt ( ) ] )
607+ queueTableRows ( schemaMock . workflowExecutionLogs , [ ] )
608+ queueTableRows ( schemaMock . resumeQueue , [ ] )
609+
610+ await expect ( getWorkflowExecutionStatus ( resumeInput ) ) . resolves . toBeNull ( )
611+ } )
612+ } )
0 commit comments