@@ -12,6 +12,7 @@ const {
1212 mockExecute,
1313 mockExecuteFromBlock,
1414 mockFetch,
15+ mockHandleExecutionErrorConsole,
1516 mockResolveStartCandidates,
1617 mockRunUploadStrategy,
1718 mockSelectBestTrigger,
@@ -35,7 +36,7 @@ const {
3536 type : 'starter' ,
3637 name : 'Start' ,
3738 enabled : true ,
38- subBlocks : { } ,
39+ subBlocks : { inputFormat : { value : 'persisted-state' } } ,
3940 } ,
4041 }
4142 const idleExecution = {
@@ -76,12 +77,13 @@ const {
7677 finishRunningEntries : vi . fn ( ) ,
7778 clearExecutionEntries : vi . fn ( ) ,
7879 }
80+ const workflowEdges : Array < { source : string ; target : string } > = [ ]
7981 const workflowStoreState = {
8082 blocks : workflowBlocks ,
81- edges : [ ] ,
83+ edges : workflowEdges ,
8284 getWorkflowState : vi . fn ( ( ) => ( {
8385 blocks : workflowBlocks ,
84- edges : [ ] ,
86+ edges : workflowEdges ,
8587 loops : { } ,
8688 parallels : { } ,
8789 } ) ) ,
@@ -93,6 +95,7 @@ const {
9395 mockExecute : vi . fn ( ) ,
9496 mockExecuteFromBlock : vi . fn ( ) ,
9597 mockFetch : vi . fn ( ) ,
98+ mockHandleExecutionErrorConsole : vi . fn ( ) ,
9699 mockResolveStartCandidates : vi . fn ( ) ,
97100 mockRunUploadStrategy : vi . fn ( ) ,
98101 mockSelectBestTrigger : vi . fn ( ) ,
@@ -179,7 +182,7 @@ vi.mock('@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-u
179182 reconcileFinalBlockLogs : vi . fn ( ) ,
180183 addExecutionErrorConsoleEntry : vi . fn ( ) ,
181184 handleExecutionCancelledConsole : vi . fn ( ) ,
182- handleExecutionErrorConsole : vi . fn ( ) ,
185+ handleExecutionErrorConsole : mockHandleExecutionErrorConsole ,
183186} ) )
184187
185188vi . mock ( '@/blocks' , ( ) => ( {
@@ -283,7 +286,7 @@ vi.mock('@/stores/workflows/registry/store', () => ({
283286} ) )
284287
285288vi . mock ( '@/stores/workflows/utils' , ( ) => ( {
286- mergeSubblockState : ( ) => workflowBlocks ,
289+ mergeSubblockState : ( blocks : Record < string , unknown > ) => blocks ,
287290} ) )
288291
289292vi . mock ( '@/stores/workflows/workflow/store' , ( ) => ( {
@@ -633,6 +636,22 @@ describe('useWorkflowExecution attachment uploads', () => {
633636 }
634637 executionStoreState . getLastExecutionSnapshot . mockReturnValueOnce ( sourceSnapshot )
635638 workflowStoreState . edges . push ( { source : 'start' , target : 'function-1' } as never )
639+ const currentBlocks = {
640+ ...workflowBlocks ,
641+ 'function-1' : {
642+ id : 'function-1' ,
643+ type : 'function' ,
644+ name : 'Function 1' ,
645+ enabled : true ,
646+ subBlocks : { code : { value : 'return "current editor state"' } } ,
647+ } ,
648+ }
649+ workflowStoreState . getWorkflowState . mockReturnValueOnce ( {
650+ blocks : currentBlocks ,
651+ edges : workflowStoreState . edges ,
652+ loops : { } ,
653+ parallels : { } ,
654+ } )
636655
637656 const { result, unmount } = renderWorkflowExecutionHook ( )
638657
@@ -646,6 +665,132 @@ describe('useWorkflowExecution attachment uploads', () => {
646665 startBlockId : 'function-1' ,
647666 sourceExecutionId : 'source-execution-1' ,
648667 sourceSnapshot,
668+ useDraftState : true ,
669+ isClientSession : true ,
670+ workflowStateOverride : {
671+ blocks : currentBlocks ,
672+ edges : workflowStoreState . edges ,
673+ loops : { } ,
674+ parallels : { } ,
675+ } ,
676+ } )
677+ )
678+
679+ unmount ( )
680+ } )
681+
682+ it ( 'uses fresh execution for trigger block runs instead of restoring an empty snapshot' , async ( ) => {
683+ const currentBlocks = {
684+ ...workflowBlocks ,
685+ start : {
686+ ...workflowBlocks . start ,
687+ subBlocks : { inputFormat : { value : 'current-editor-state' } } ,
688+ } ,
689+ }
690+ workflowStoreState . getWorkflowState . mockReturnValueOnce ( {
691+ blocks : currentBlocks ,
692+ edges : [ ] ,
693+ loops : { } ,
694+ parallels : { } ,
695+ } )
696+ const { result, unmount } = renderWorkflowExecutionHook ( )
697+
698+ await act ( async ( ) => {
699+ await result ( ) . handleRunFromBlock ( 'start' , 'workflow-1' )
700+ } )
701+
702+ expect ( mockExecute ) . toHaveBeenCalledWith (
703+ expect . objectContaining ( {
704+ workflowId : 'workflow-1' ,
705+ startBlockId : 'start' ,
706+ triggerType : 'manual' ,
707+ useDraftState : true ,
708+ isClientSession : true ,
709+ workflowStateOverride : {
710+ blocks : currentBlocks ,
711+ edges : [ ] ,
712+ loops : { } ,
713+ parallels : { } ,
714+ } ,
715+ } )
716+ )
717+ expect ( mockExecute . mock . calls [ 0 ] ?. [ 0 ] ) . not . toHaveProperty ( 'sourceSnapshot' )
718+ expect ( mockExecuteFromBlock ) . not . toHaveBeenCalled ( )
719+
720+ unmount ( )
721+ } )
722+
723+ it ( 'fails closed when a legacy run-from-block error has no display projection' , async ( ) => {
724+ mockExecute . mockImplementationOnce ( async ( options ) => {
725+ executionStoreState . getCurrentExecutionId . mockReturnValue ( 'execution-1' )
726+ options . onExecutionId ?.( 'execution-1' )
727+ await options . callbacks ?. onExecutionError ?.( {
728+ error : 'raw-secret-value caused the failure' ,
729+ duration : 8 ,
730+ finalBlockLogs : [ ] ,
731+ } )
732+ } )
733+
734+ const { result, unmount } = renderWorkflowExecutionHook ( )
735+
736+ await act ( async ( ) => {
737+ await result ( ) . handleRunFromBlock ( 'start' , 'workflow-1' )
738+ } )
739+
740+ expect ( mockHandleExecutionErrorConsole ) . toHaveBeenCalledWith (
741+ expect . objectContaining ( {
742+ addConsole : terminalStoreState . addConsole ,
743+ } ) ,
744+ expect . objectContaining ( {
745+ workflowId : 'workflow-1' ,
746+ executionId : 'execution-1' ,
747+ error : 'raw-secret-value caused the failure' ,
748+ hasDisplayProjection : true ,
749+ durationMs : 8 ,
750+ } )
751+ )
752+ expect ( mockHandleExecutionErrorConsole . mock . calls [ 0 ] ?. [ 1 ] ) . not . toHaveProperty ( 'displayError' )
753+
754+ unmount ( )
755+ } )
756+
757+ it ( 'shows one safe error when run-from-block fails before receiving an execution ID' , async ( ) => {
758+ const sourceSnapshot = {
759+ blockStates : { start : { output : { value : 'ready' } } } ,
760+ executedBlocks : [ 'start' ] ,
761+ blockLogs : [ ] ,
762+ decisions : { router : { } , condition : { } } ,
763+ completedLoops : [ ] ,
764+ activeExecutionPath : [ 'start' ] ,
765+ sourceExecutionId : 'source-execution-1' ,
766+ }
767+ executionStoreState . getLastExecutionSnapshot . mockReturnValueOnce ( sourceSnapshot )
768+ workflowStoreState . edges . push ( { source : 'start' , target : 'function-1' } as never )
769+ mockExecuteFromBlock . mockImplementationOnce ( async ( options ) => {
770+ await options . callbacks ?. onExecutionError ?.( {
771+ error : 'raw pre-execution failure' ,
772+ duration : 0 ,
773+ } )
774+ throw new Error ( 'raw pre-execution failure' )
775+ } )
776+
777+ const { result, unmount } = renderWorkflowExecutionHook ( )
778+
779+ await act ( async ( ) => {
780+ await result ( ) . handleRunFromBlock ( 'function-1' , 'workflow-1' )
781+ } )
782+
783+ expect ( mockHandleExecutionErrorConsole ) . toHaveBeenCalledTimes ( 1 )
784+ expect ( mockHandleExecutionErrorConsole ) . toHaveBeenCalledWith (
785+ expect . objectContaining ( {
786+ addConsole : terminalStoreState . addConsole ,
787+ } ) ,
788+ expect . objectContaining ( {
789+ workflowId : 'workflow-1' ,
790+ error : 'raw pre-execution failure' ,
791+ hasDisplayProjection : true ,
792+ durationMs : 0 ,
793+ blockLogs : [ ] ,
649794 } )
650795 )
651796
0 commit comments