@@ -22,6 +22,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2222import { createTimeoutAbortController , getExecutionDeadlineAt } from '@/lib/core/execution-limits'
2323import { abortManualExecution } from '@/lib/execution/manual-cancellation'
2424import { terminalExecutionLogFields } from '@/lib/logs/execution/cancellation'
25+ import { LoggingSession } from '@/lib/logs/execution/logging-session'
2526
2627const {
2728 mockExecuteWorkflowCore,
@@ -265,6 +266,8 @@ describe('what a failed resume did to its paused execution', () => {
265266 { logStatus : 'running' , pauseStatus : 'paused' , executionFailed : true } ,
266267 { logStatus : 'cancelled' , pauseStatus : 'paused' , executionFailed : false } ,
267268 { logStatus : 'running' , pauseStatus : 'cancelling' , executionFailed : false } ,
269+ { logStatus : 'failed' , pauseStatus : 'paused' , executionFailed : true } ,
270+ { logStatus : 'completed' , pauseStatus : 'paused' , executionFailed : false } ,
268271 ] ) (
269272 'reports the execution failed: $executionFailed for a $logStatus log and $pauseStatus pause' ,
270273 async ( { logStatus, pauseStatus, executionFailed } ) => {
@@ -276,6 +279,23 @@ describe('what a failed resume did to its paused execution', () => {
276279 }
277280 )
278281
282+ it . each ( [
283+ { logStatus : 'completed' , updated : [ resumeQueue ] } ,
284+ { logStatus : 'failed' , updated : [ resumeQueue , pausedExecutions ] } ,
285+ { logStatus : 'running' , updated : [ resumeQueue , pausedExecutions , workflowExecutionLogs ] } ,
286+ ] ) (
287+ 'leaves a $logStatus execution log as it is when a resume fails late' ,
288+ async ( { logStatus, updated } ) => {
289+ queueTableRows ( workflowExecutionLogs , [ { status : logStatus } ] )
290+ queueTableRows ( pausedExecutions , [ { status : 'paused' } ] )
291+ const managerInternals = PauseResumeManager as unknown as PauseResumeManagerInternals
292+
293+ await managerInternals . markResumeFailed ( attemptArgs )
294+
295+ expect ( dbChainMockFns . update . mock . calls . map ( ( [ table ] ) => table ) ) . toEqual ( updated )
296+ }
297+ )
298+
279299 /** Resume args that collect every outcome the manager reports. */
280300 function argsReportingOutcomes ( onAttemptFailed ?: ( ) => Promise < void > ) {
281301 const outcomes : FailedResumeOutcome [ ] = [ ]
@@ -313,6 +333,59 @@ describe('what a failed resume did to its paused execution', () => {
313333 }
314334 )
315335
336+ describe ( 'when the resumed run pauses but its pause cannot be saved' , ( ) => {
337+ const spies : { mockRestore : ( ) => void } [ ] = [ ]
338+
339+ function pauseRun ( options : { snapshotSeed ?: unknown ; persistError ?: Error } ) {
340+ const managerInternals = PauseResumeManager as unknown as PauseResumeManagerInternals
341+ spies . push (
342+ vi . spyOn ( managerInternals , 'runResumeExecution' ) . mockResolvedValueOnce ( {
343+ success : true ,
344+ status : 'paused' ,
345+ output : { } ,
346+ logs : [ ] ,
347+ pausePoints : [ ] ,
348+ snapshotSeed : options . snapshotSeed ,
349+ metadata : { executionId : 'parent-execution-1' , duration : 1 , startTime : 'start' } ,
350+ } ) ,
351+ vi . spyOn ( managerInternals , 'markResumeFailed' ) . mockResolvedValueOnce ( true ) ,
352+ vi . spyOn ( LoggingSession , 'markExecutionAsFailed' ) . mockResolvedValueOnce ( ) ,
353+ vi . spyOn ( PauseResumeManager , 'processQueuedResumes' ) . mockResolvedValueOnce ( )
354+ )
355+ if ( options . persistError ) {
356+ spies . push (
357+ vi
358+ . spyOn ( PauseResumeManager , 'persistPauseResult' )
359+ . mockRejectedValueOnce ( options . persistError )
360+ )
361+ }
362+ }
363+
364+ afterEach ( ( ) => {
365+ for ( const spy of spies . splice ( 0 ) ) spy . mockRestore ( )
366+ } )
367+
368+ it ( 'fails the attempt when the pause state cannot be persisted' , async ( ) => {
369+ pauseRun ( { snapshotSeed : createSnapshotSeed ( ) , persistError : new Error ( 'lock timeout' ) } )
370+ const { outcomes, args } = argsReportingOutcomes ( )
371+
372+ await expect ( PauseResumeManager . startResumeExecution ( args ) ) . rejects . toThrow (
373+ 'Failed to persist pause state: lock timeout'
374+ )
375+ expect ( outcomes ) . toEqual ( [ 'execution_failed' ] )
376+ } )
377+
378+ it ( 'fails the attempt when the paused run has no snapshot seed' , async ( ) => {
379+ pauseRun ( { } )
380+ const { outcomes, args } = argsReportingOutcomes ( )
381+
382+ await expect ( PauseResumeManager . startResumeExecution ( args ) ) . rejects . toThrow (
383+ 'Missing snapshot seed for paused execution'
384+ )
385+ expect ( outcomes ) . toEqual ( [ 'execution_failed' ] )
386+ } )
387+ } )
388+
316389 describe ( 'when the resumed run fails' , ( ) => {
317390 const rawError = new Error ( 'Block failed' )
318391 const spies : { mockRestore : ( ) => void } [ ] = [ ]
0 commit comments