@@ -588,4 +588,47 @@ describe.runIf(Boolean(redisUrl))('taking over a run whose controller died', ()
588588 await db . update ( copilotRuns ) . set ( { status : 'cancelled' } ) . where ( eq ( copilotRuns . id , runId ) )
589589 }
590590 } )
591+
592+ it ( 're-claims a server workflow claim whose claimant died before starting it, once' , async ( ) => {
593+ const chatId = generateId ( )
594+ chatIds . push ( chatId )
595+ const runId = generateId ( )
596+ const streamId = generateId ( )
597+ await db . insert ( copilotChats ) . values ( { id : chatId , userId, workspaceId, type : 'mothership' } )
598+ await db . insert ( copilotRuns ) . values ( {
599+ id : runId ,
600+ executionId : generateId ( ) ,
601+ chatId,
602+ userId,
603+ workspaceId,
604+ streamId,
605+ status : 'paused_waiting_for_tool' ,
606+ toolExecutionVersion : 2 ,
607+ } )
608+ const toolCallId = scopeProviderToolCallId (
609+ 'workflow-call' ,
610+ createProviderToolCallIdentity ( runId )
611+ )
612+ await upsertAsyncToolCall ( { runId, toolCallId, toolName : 'run_workflow' , args : { } } )
613+ await db
614+ . update ( copilotAsyncToolCalls )
615+ . set ( { status : 'running' } )
616+ . where ( eq ( copilotAsyncToolCalls . toolCallId , toolCallId ) )
617+ try {
618+ /** The dead controller's server fallback claimed the call, then died before starting it. */
619+ expect ( await claimWorkflowToolExecution ( toolCallId , generateId ( ) , 'sim' ) ) . not . toBeNull ( )
620+
621+ expect ( await claimWorkflowToolExecution ( toolCallId , generateId ( ) , 'client' ) ) . toBeNull ( )
622+ expect ( await claimWorkflowToolExecution ( toolCallId , generateId ( ) , 'sim' ) ) . not . toBeNull ( )
623+ const successor = { toolCallId, runId, userId, ownerToken : 'successor' }
624+ expect ( await claimToolExecution ( successor ) ) . toEqual ( { outcome : 'claimed' } )
625+ expect ( await claimToolExecution ( { ...successor , ownerToken : 'late-claimant' } ) ) . toEqual ( {
626+ outcome : 'existing' ,
627+ } )
628+ expect ( await claimWorkflowToolExecution ( toolCallId , generateId ( ) , 'sim' ) ) . toBeNull ( )
629+ expect ( await storedTool ( toolCallId ) ) . toMatchObject ( { attempts : 1 , ownerToken : 'successor' } )
630+ } finally {
631+ await db . update ( copilotRuns ) . set ( { status : 'cancelled' } ) . where ( eq ( copilotRuns . id , runId ) )
632+ }
633+ } )
591634} )
0 commit comments