@@ -61,10 +61,10 @@ vi.mock('@/lib/execution/payloads/large-value-metadata', () => largeValueMetadat
6161
6262import {
6363 createResumeAttemptTimeoutController ,
64+ getFailedResumeOutcome ,
6465 PauseResumeManager ,
6566 requireResumeDeploymentVersion ,
6667 updateResumeOutputInAggregationBuffers ,
67- wasPausedExecutionRetained ,
6868} from '@/lib/workflows/executor/human-in-the-loop-manager'
6969import { getAutomaticResumeWaitingMetadata } from '@/lib/workflows/executor/paused-execution-metadata'
7070import { AUTOMATIC_RESUME_WAITING_REASON_MAX_LENGTH } from '@/lib/workflows/executor/resume-policy'
@@ -85,7 +85,7 @@ if (!humanInTheLoopLogger) {
8585}
8686
8787interface PauseResumeManagerInternals {
88- markResumeFailed : ( ...args : unknown [ ] ) => Promise < void >
88+ markResumeFailed : ( ...args : unknown [ ] ) => Promise < boolean >
8989 runResumeExecution : ( ...args : unknown [ ] ) => Promise < unknown >
9090}
9191
@@ -211,7 +211,7 @@ describe('queued resume attempt deadlines', () => {
211211 } )
212212} )
213213
214- describe ( 'which failed resumes keep the pause resumable ' , ( ) => {
214+ describe ( 'what a failed resume did to its paused execution ' , ( ) => {
215215 type StartResumeArgs = Parameters < typeof PauseResumeManager . startResumeExecution > [ 0 ]
216216 const pausedExecution = {
217217 id : 'paused-execution-1' ,
@@ -231,50 +231,87 @@ describe('which failed resumes keep the pause resumable', () => {
231231 resumeInput : { approved : true } ,
232232 userId : 'user-1' ,
233233 }
234+ const attemptArgs = {
235+ resumeEntryId : 'resume-entry-1' ,
236+ pausedExecutionId : 'paused-execution-1' ,
237+ parentExecutionId : 'parent-execution-1' ,
238+ contextId : 'context-1' ,
239+ failureReason : 'Execution can no longer be resumed' ,
240+ preserveForRetry : true ,
241+ }
234242
235243 beforeEach ( ( ) => {
236244 resetDbChainMock ( )
237245 } )
238246
239- it ( 'keeps the pause resumable when the paused log can no longer be claimed' , async ( ) => {
240- const markResumeAttemptFailedSpy = vi
241- . spyOn ( PauseResumeManager , 'markResumeAttemptFailed' )
242- . mockResolvedValueOnce ( )
247+ it ( 'reports the pause still resumable when a refused attempt leaves it paused' , async ( ) => {
248+ queueTableRows ( workflowExecutionLogs , [ { status : 'paused' } ] )
249+ queueTableRows ( pausedExecutions , [ { automaticResumeRetryCount : 0 , status : 'paused' } ] )
243250
244- try {
245- const thrown = await PauseResumeManager . startResumeExecution ( resumeArgs ) . catch (
246- ( error : unknown ) => error
247- )
251+ await expect ( PauseResumeManager . markResumeAttemptFailed ( attemptArgs ) ) . resolves . toBe ( true )
252+ } )
248253
249- expect ( thrown ) . toMatchObject ( { name : 'ResumeAdmissionError' } )
250- expect ( wasPausedExecutionRetained ( thrown ) ) . toBe ( true )
251- } finally {
252- markResumeAttemptFailedSpy . mockRestore ( )
254+ it . each ( [ 'completed' , 'failed' , 'cancelled' ] ) (
255+ 'reports the pause not resumable when the execution is already %s' ,
256+ async ( logStatus ) => {
257+ queueTableRows ( workflowExecutionLogs , [ { status : logStatus } ] )
258+ queueTableRows ( pausedExecutions , [ { automaticResumeRetryCount : 0 , status : 'paused' } ] )
259+
260+ await expect ( PauseResumeManager . markResumeAttemptFailed ( attemptArgs ) ) . resolves . toBe ( false )
253261 }
254- } )
262+ )
255263
256- it ( 'does not keep the pause when the resumed run itself failed' , async ( ) => {
257- const rawError = new Error ( 'Block failed' )
258- const managerInternals = PauseResumeManager as unknown as PauseResumeManagerInternals
259- const runResumeExecutionSpy = vi
260- . spyOn ( managerInternals , 'runResumeExecution' )
261- . mockRejectedValueOnce ( rawError )
262- const markResumeFailedSpy = vi
263- . spyOn ( managerInternals , 'markResumeFailed' )
264- . mockResolvedValueOnce ( )
265- const processQueuedResumesSpy = vi
266- . spyOn ( PauseResumeManager , 'processQueuedResumes' )
267- . mockResolvedValueOnce ( )
264+ it . each ( [
265+ { stillResumable : true , outcome : 'pause_retained' } ,
266+ { stillResumable : false , outcome : undefined } ,
267+ ] ) (
268+ 'records a refused attempt as $outcome when the pause is resumable: $stillResumable' ,
269+ async ( { stillResumable, outcome } ) => {
270+ const markResumeAttemptFailedSpy = vi
271+ . spyOn ( PauseResumeManager , 'markResumeAttemptFailed' )
272+ . mockResolvedValueOnce ( stillResumable )
273+
274+ try {
275+ const thrown = await PauseResumeManager . startResumeExecution ( resumeArgs ) . catch (
276+ ( error : unknown ) => error
277+ )
268278
269- try {
270- await expect ( PauseResumeManager . startResumeExecution ( resumeArgs ) ) . rejects . toBe ( rawError )
271- expect ( wasPausedExecutionRetained ( rawError ) ) . toBe ( false )
272- } finally {
273- runResumeExecutionSpy . mockRestore ( )
274- markResumeFailedSpy . mockRestore ( )
275- processQueuedResumesSpy . mockRestore ( )
279+ expect ( thrown ) . toMatchObject ( { name : 'ResumeAdmissionError' } )
280+ expect ( getFailedResumeOutcome ( thrown ) ) . toBe ( outcome )
281+ } finally {
282+ markResumeAttemptFailedSpy . mockRestore ( )
283+ }
276284 }
277- } )
285+ )
286+
287+ it . each ( [
288+ { executionFailed : true , outcome : 'execution_failed' } ,
289+ { executionFailed : false , outcome : undefined } ,
290+ ] ) (
291+ 'records a failed run as $outcome when it failed the execution: $executionFailed' ,
292+ async ( { executionFailed, outcome } ) => {
293+ const rawError = new Error ( 'Block failed' )
294+ const managerInternals = PauseResumeManager as unknown as PauseResumeManagerInternals
295+ const runResumeExecutionSpy = vi
296+ . spyOn ( managerInternals , 'runResumeExecution' )
297+ . mockRejectedValueOnce ( rawError )
298+ const markResumeFailedSpy = vi
299+ . spyOn ( managerInternals , 'markResumeFailed' )
300+ . mockResolvedValueOnce ( executionFailed )
301+ const processQueuedResumesSpy = vi
302+ . spyOn ( PauseResumeManager , 'processQueuedResumes' )
303+ . mockResolvedValueOnce ( )
304+
305+ try {
306+ await expect ( PauseResumeManager . startResumeExecution ( resumeArgs ) ) . rejects . toBe ( rawError )
307+ expect ( getFailedResumeOutcome ( rawError ) ) . toBe ( outcome )
308+ } finally {
309+ runResumeExecutionSpy . mockRestore ( )
310+ markResumeFailedSpy . mockRestore ( )
311+ processQueuedResumesSpy . mockRestore ( )
312+ }
313+ }
314+ )
278315} )
279316
280317describe ( 'resume failure diagnostic projection' , ( ) => {
0 commit comments