@@ -6,7 +6,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
66import { createRoot , type Root } from 'react-dom/client'
77import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
88
9- const { mockRequestJson } = vi . hoisted ( ( ) => ( {
9+ const { mockFetch, mockRequestJson } = vi . hoisted ( ( ) => ( {
10+ mockFetch : vi . fn ( ) ,
1011 mockRequestJson : vi . fn ( ) ,
1112} ) )
1213
@@ -16,7 +17,7 @@ vi.mock('@/lib/api/client/request', () => ({
1617
1718import { getLogByExecutionIdContract } from '@/lib/api/contracts/logs'
1819import { cancelWorkflowExecutionContract } from '@/lib/api/contracts/workflows'
19- import { useCancelExecution } from '@/hooks/queries/logs'
20+ import { useCancelExecution , useRetryExecution } from '@/hooks/queries/logs'
2021
2122function renderHookWithClient < T > ( useHook : ( ) => T ) : {
2223 result : ( ) => T
@@ -198,3 +199,99 @@ describe('useCancelExecution', () => {
198199 unmount ( )
199200 } )
200201} )
202+
203+ function failedLogDetail (
204+ children = [
205+ {
206+ id : 'failed-span' ,
207+ name : 'Failed block' ,
208+ type : 'function' ,
209+ status : 'error' ,
210+ blockId : 'failed-block' ,
211+ } ,
212+ ]
213+ ) {
214+ return {
215+ data : {
216+ executionData : {
217+ workflowInput : { prompt : 'original input' } ,
218+ traceSpans : [
219+ {
220+ id : 'workflow-execution' ,
221+ name : 'Workflow Execution' ,
222+ type : 'workflow' ,
223+ status : 'error' ,
224+ children,
225+ } ,
226+ ] ,
227+ } ,
228+ } ,
229+ }
230+ }
231+
232+ describe ( 'useRetryExecution' , ( ) => {
233+ beforeEach ( ( ) => {
234+ vi . clearAllMocks ( )
235+ vi . stubGlobal ( 'fetch' , mockFetch )
236+ } )
237+
238+ afterEach ( ( ) => {
239+ vi . unstubAllGlobals ( )
240+ } )
241+
242+ it ( 'starts the retry from the failed block using the source execution state' , async ( ) => {
243+ const cancel = vi . fn ( )
244+ mockRequestJson . mockResolvedValue ( failedLogDetail ( ) )
245+ mockFetch . mockResolvedValue ( {
246+ ok : true ,
247+ body : {
248+ getReader : ( ) => ( { read : vi . fn ( ) . mockResolvedValue ( { done : false } ) , cancel } ) ,
249+ } ,
250+ } )
251+
252+ const { result, unmount } = renderHookWithClient ( ( ) => useRetryExecution ( 'workspace-1' ) )
253+
254+ await act ( async ( ) => {
255+ await result ( ) . mutateAsync ( { workflowId : 'workflow-1' , executionId : 'execution-1' } )
256+ } )
257+
258+ expect ( mockRequestJson ) . toHaveBeenCalledWith ( getLogByExecutionIdContract , {
259+ params : { executionId : 'execution-1' } ,
260+ query : { workspaceId : 'workspace-1' } ,
261+ signal : undefined ,
262+ } )
263+ expect ( mockFetch ) . toHaveBeenCalledWith ( '/api/workflows/workflow-1/execute' , {
264+ method : 'POST' ,
265+ headers : { 'Content-Type' : 'application/json' } ,
266+ body : JSON . stringify ( {
267+ inputFromExecutionId : 'execution-1' ,
268+ triggerType : 'manual' ,
269+ stream : true ,
270+ runFromBlock : { startBlockId : 'failed-block' , executionId : 'execution-1' } ,
271+ } ) ,
272+ } )
273+ expect ( cancel ) . toHaveBeenCalled ( )
274+
275+ unmount ( )
276+ } )
277+
278+ it ( 'does not execute when the source run has multiple terminating failures' , async ( ) => {
279+ mockRequestJson . mockResolvedValue (
280+ failedLogDetail ( [
281+ { id : 'failure-1' , name : 'One' , type : 'function' , status : 'error' , blockId : 'one' } ,
282+ { id : 'failure-2' , name : 'Two' , type : 'function' , status : 'error' , blockId : 'two' } ,
283+ ] )
284+ )
285+
286+ const { result, unmount } = renderHookWithClient ( ( ) => useRetryExecution ( 'workspace-1' ) )
287+
288+ await act ( async ( ) => {
289+ await expect (
290+ result ( ) . mutateAsync ( { workflowId : 'workflow-1' , executionId : 'execution-1' } )
291+ ) . rejects . toThrow ( 'multiple terminating failures' )
292+ } )
293+ expect ( mockFetch ) . not . toHaveBeenCalled ( )
294+
295+ unmount ( )
296+ } )
297+ } )
0 commit comments