11import { dbChainMockFns , queueTableRows , resetDbChainMock , schemaMock } from '@sim/testing'
22import { asyncJobsMock , asyncJobsMockFns } from '@sim/testing/mocks/async-jobs.mock'
33import { traceStoreMock , traceStoreMockFns } from '@sim/testing/mocks/trace-store.mock'
4+ import { sleep } from '@sim/utils/helpers'
45import { and } from 'drizzle-orm'
56import { beforeEach , describe , expect , it , vi } from 'vitest'
67
@@ -12,6 +13,7 @@ vi.mock('@/lib/workflows/executor/paused-execution-metadata', () => ({
1213 getAutomaticResumeWaitingMetadata : vi . fn ( ) . mockReturnValue ( null ) ,
1314} ) )
1415
16+ import { FunctionalOutputsUnavailableError } from '@/lib/logs/execution/functional-outputs'
1517import { getWorkflowExecutionStatus } from '@/lib/workflows/executor/execution-status'
1618
1719const mockGetJob = asyncJobsMockFns . mockJobQueue . getJob
@@ -552,3 +554,108 @@ describe('getWorkflowExecutionStatus settled resume attempts', () => {
552554 expect ( status ) . toMatchObject ( { status : 'cancelled' , level : 'info' , error : null } )
553555 } )
554556} )
557+
558+ describe ( 'getWorkflowExecutionStatus polls that read no execution data' , ( ) => {
559+ const STALLED = Symbol ( 'stalled' )
560+
561+ function queueLog ( status : string , pausedRows : unknown [ ] = [ ] ) {
562+ queueTableRows ( schemaMock . workflowExecutionLogs , [
563+ {
564+ executionId : 'execution-1' ,
565+ workflowId : 'workflow-1' ,
566+ workspaceId : 'workspace-1' ,
567+ status,
568+ level : 'info' ,
569+ trigger : 'api' ,
570+ startedAt : new Date ( '2026-08-05T12:00:00.000Z' ) ,
571+ endedAt : new Date ( '2026-08-05T12:00:01.000Z' ) ,
572+ totalDurationMs : 1000 ,
573+ executionData : { finalOutput : { answer : 42 } } ,
574+ costTotal : '0.5' ,
575+ } ,
576+ ] )
577+ queueTableRows ( schemaMock . resumeQueue , [ ] )
578+ queueTableRows ( schemaMock . pausedExecutions , pausedRows )
579+ }
580+
581+ /** Stands in for an archive read that has not come back from object storage yet. */
582+ function stallArchiveRead ( ) {
583+ mockMaterializeForDisplayWithBlockOutputs . mockReturnValue ( new Promise ( ( ) => { } ) )
584+ }
585+
586+ /** Settles with {@link STALLED} instead of hanging when the read waits on the archive. */
587+ function readOrStall ( read : Parameters < typeof getWorkflowExecutionStatus > [ 0 ] ) {
588+ return Promise . race ( [ getWorkflowExecutionStatus ( read ) , sleep ( 0 ) . then ( ( ) => STALLED ) ] )
589+ }
590+
591+ beforeEach ( ( ) => {
592+ resetDbChainMock ( )
593+ mockGetJob . mockResolvedValue ( null )
594+ } )
595+
596+ it . each ( [
597+ [ 'a running run' , 'running' , input ] ,
598+ [ 'a cancelled run' , 'cancelled' , input ] ,
599+ [ 'a completed run polled without includeOutput' , 'completed' , input ] ,
600+ [ 'a running run asked for its output early' , 'running' , { ...input , includeOutput : true } ] ,
601+ ] ) ( 'answers %s without waiting on the run archive' , async ( _ , status , read ) => {
602+ queueLog ( status )
603+ stallArchiveRead ( )
604+
605+ await expect ( readOrStall ( read ) ) . resolves . toMatchObject ( {
606+ status,
607+ cost : { total : 0.5 } ,
608+ error : null ,
609+ finalOutput : null ,
610+ blockOutputs : null ,
611+ } )
612+ } )
613+
614+ it ( 'answers a paused run without waiting on the run archive' , async ( ) => {
615+ queueLog ( 'running' , [
616+ {
617+ id : 'paused-1' ,
618+ status : 'paused' ,
619+ pausePoints : [ { contextId : 'context-1' , blockId : 'block-1' , resumeStatus : 'paused' } ] ,
620+ metadata : { } ,
621+ resumedCount : 0 ,
622+ pausedAt : new Date ( '2026-08-05T12:00:00.500Z' ) ,
623+ nextResumeAt : null ,
624+ } ,
625+ ] )
626+ stallArchiveRead ( )
627+
628+ await expect ( readOrStall ( { ...input , includeOutput : true } ) ) . resolves . toMatchObject ( {
629+ status : 'paused' ,
630+ paused : { contextId : 'context-1' } ,
631+ finalOutput : null ,
632+ } )
633+ } )
634+
635+ it ( 'still reads selected block outputs from a run that is still running' , async ( ) => {
636+ queueLog ( 'running' )
637+ mockMaterializeForDisplayWithBlockOutputs . mockResolvedValue ( {
638+ executionData : { } ,
639+ blockOutputs : new Map ( [ [ 'block-1' , { content : 'partial' } ] ] ) ,
640+ } )
641+
642+ await expect (
643+ getWorkflowExecutionStatus ( { ...input , selectedOutputs : [ 'block-1' ] } )
644+ ) . resolves . toMatchObject ( {
645+ status : 'running' ,
646+ finalOutput : null ,
647+ blockOutputs : { 'block-1' : { content : 'partial' } } ,
648+ } )
649+ } )
650+
651+ it ( 'still rejects selected outputs of a running run whose execution data was truncated' , async ( ) => {
652+ queueLog ( 'running' )
653+ mockMaterializeForDisplayWithBlockOutputs . mockRejectedValue (
654+ new FunctionalOutputsUnavailableError ( )
655+ )
656+
657+ await expect (
658+ getWorkflowExecutionStatus ( { ...input , selectedOutputs : [ 'block-1' ] } )
659+ ) . rejects . toBeInstanceOf ( FunctionalOutputsUnavailableError )
660+ } )
661+ } )
0 commit comments