@@ -414,151 +414,26 @@ describe('useWorkflowExecution cancellation', () => {
414414 } )
415415} )
416416
417- describe ( 'useWorkflowExecution attachment uploads' , ( ) => {
418- beforeEach ( ( ) => {
419- vi . clearAllMocks ( )
420- mockEndScopedExecution . mockReset ( ) . mockReturnValue ( true )
421- terminalStoreState . _hasHydrated = false
422- executionStoreState . workflowExecutions . set ( 'workflow-1' , idleExecution )
423- executionStoreState . getWorkflowExecution . mockReturnValue ( idleExecution )
424- executionStoreState . getCurrentExecutionId . mockReturnValue ( null )
425- mockAdoptScopedExecution . mockReturnValue ( undefined )
426- mockLoadExecutionPointer . mockResolvedValue ( null )
427- mockReconnect . mockResolvedValue ( undefined )
428- mockResolveStartCandidates . mockReturnValue ( [ ] )
429- mockSelectBestTrigger . mockReturnValue ( [ ] )
430- vi . stubGlobal ( 'fetch' , mockFetch )
431- mockUploadInternalFileSession . mockRejectedValue (
432- new Error ( 'Workspace file storage limit exceeded' )
433- )
434- mockFetch . mockResolvedValue (
435- new Response ( JSON . stringify ( { error : 'Workspace file storage limit exceeded' } ) , {
436- status : 413 ,
437- headers : { 'Content-Type' : 'application/json' } ,
438- } )
439- )
440- mockExecute . mockResolvedValue ( undefined )
441- mockExecuteFromBlock . mockResolvedValue ( undefined )
442- workflowStoreState . edges . length = 0
443- } )
444-
445- afterEach ( ( ) => {
446- vi . unstubAllGlobals ( )
447- } )
448-
449- it ( 'does not execute and reports the exact server error when an explicit attachment fails' , async ( ) => {
450- const { result, unmount } = renderWorkflowExecutionHook ( )
451- const contextFile = new File ( [ 'context' ] , 'context.txt' , { type : 'text/plain' } )
452- const file = new File ( [ 'report' ] , 'report.pdf' , { type : 'application/pdf' } )
453- let uploadError : unknown
454-
455- mockUploadInternalFileSession . mockResolvedValueOnce ( {
456- id : 'attachment-context' ,
457- key : 'executions/context.txt' ,
458- url : '/uploads/context.txt' ,
459- name : contextFile . name ,
460- size : contextFile . size ,
461- type : contextFile . type ,
462- context : 'execution' ,
463- } )
464-
465- await act ( async ( ) => {
466- try {
467- await result ( ) . handleRunWorkflow ( {
468- input : 'Summarize this report' ,
469- conversationId : 'conversation-1' ,
470- files : [
471- {
472- name : contextFile . name ,
473- size : contextFile . size ,
474- type : contextFile . type ,
475- file : contextFile ,
476- } ,
477- {
478- name : file . name ,
479- size : file . size ,
480- type : file . type ,
481- file,
482- } ,
483- ] ,
484- } )
485- } catch ( error ) {
486- uploadError = error
487- }
488- } )
489-
490- expect ( uploadError ) . toBeInstanceOf ( WorkflowAttachmentUploadError )
491- expect ( ( uploadError as Error ) . message ) . toBe (
492- 'Failed to upload report.pdf: Workspace file storage limit exceeded'
493- )
494- expect ( mockExecute ) . not . toHaveBeenCalled ( )
495-
496- unmount ( )
497- } )
498-
499- it ( 'returns uploaded metadata without mutating or leaking local input into execution' , async ( ) => {
500- const { result, unmount } = renderWorkflowExecutionHook ( )
501- const file = new File ( [ 'diagram' ] , 'diagram.png' , { type : 'image/png' } )
502- const workflowInput = {
503- input : 'Describe this diagram' ,
504- conversationId : 'conversation-1' ,
505- files : [
506- {
507- name : file . name ,
508- size : file . size ,
509- type : file . type ,
510- file,
511- } ,
512- ] ,
513- }
514- let runResult : unknown
515-
516- mockUploadInternalFileSession . mockResolvedValueOnce ( {
517- id : 'attachment-diagram' ,
518- key : 'execution/diagram.png' ,
519- url : '/api/files/serve/execution%2Fdiagram.png' ,
520- name : file . name ,
521- size : file . size ,
522- type : file . type ,
523- context : 'execution' ,
524- } )
525-
526- await act ( async ( ) => {
527- runResult = await result ( ) . handleRunWorkflow ( workflowInput )
528- await drainStream ( runResult )
529- } )
530-
531- expect ( isChatWorkflowRunResult ( runResult ) ) . toBe ( true )
532- if ( ! isChatWorkflowRunResult ( runResult ) ) {
533- throw new Error ( 'Expected a chat workflow run result' )
534- }
535- expect ( runResult . uploadedAttachments ) . toEqual ( [
536- expect . objectContaining ( {
537- name : 'diagram.png' ,
538- url : '/api/files/serve/execution%2Fdiagram.png' ,
539- size : file . size ,
540- type : 'image/png' ,
541- key : 'execution/diagram.png' ,
542- } ) ,
543- ] )
544- expect ( workflowInput . files [ 0 ] . file ) . toBe ( file )
545- expect ( mockExecute ) . toHaveBeenCalledWith (
546- expect . objectContaining ( {
547- input : expect . objectContaining ( {
548- input : 'Describe this diagram' ,
549- conversationId : 'conversation-1' ,
550- files : [
551- expect . objectContaining ( {
552- name : 'diagram.png' ,
553- url : '/api/files/serve/execution%2Fdiagram.png' ,
554- } ) ,
555- ] ,
556- } ) ,
557- } )
558- )
417+ function resetWorkflowExecutionTestState ( ) {
418+ vi . clearAllMocks ( )
419+ mockBeginScopedExecution . mockReset ( ) . mockReturnValue ( { } )
420+ mockAdoptScopedExecution . mockReset ( ) . mockReturnValue ( undefined )
421+ mockEndScopedExecution . mockReset ( ) . mockReturnValue ( true )
422+ mockLoadExecutionPointer . mockReset ( ) . mockResolvedValue ( null )
423+ mockReconnect . mockReset ( ) . mockResolvedValue ( undefined )
424+ mockResolveStartCandidates . mockReset ( ) . mockReturnValue ( [ ] )
425+ mockSelectBestTrigger . mockReset ( ) . mockReturnValue ( [ ] )
426+ mockExecute . mockReset ( ) . mockResolvedValue ( undefined )
427+ mockExecuteFromBlock . mockReset ( ) . mockResolvedValue ( undefined )
428+ terminalStoreState . _hasHydrated = false
429+ executionStoreState . workflowExecutions . set ( 'workflow-1' , idleExecution )
430+ executionStoreState . getWorkflowExecution . mockReturnValue ( idleExecution )
431+ executionStoreState . getCurrentExecutionId . mockReturnValue ( null )
432+ workflowStoreState . edges . length = 0
433+ }
559434
560- unmount ( )
561- } )
435+ describe ( 'useWorkflowExecution lifecycle ownership' , ( ) => {
436+ beforeEach ( resetWorkflowExecutionTestState )
562437
563438 it ( 'does not let an overlapping run without lifecycle ownership end the active run' , async ( ) => {
564439 const persistenceExecution = { }
@@ -776,6 +651,140 @@ describe('useWorkflowExecution attachment uploads', () => {
776651
777652 unmount ( )
778653 } )
654+ } )
655+
656+ describe ( 'useWorkflowExecution attachment uploads' , ( ) => {
657+ beforeEach ( ( ) => {
658+ resetWorkflowExecutionTestState ( )
659+ vi . stubGlobal ( 'fetch' , mockFetch )
660+ mockUploadInternalFileSession . mockRejectedValue (
661+ new Error ( 'Workspace file storage limit exceeded' )
662+ )
663+ mockFetch . mockResolvedValue (
664+ new Response ( JSON . stringify ( { error : 'Workspace file storage limit exceeded' } ) , {
665+ status : 413 ,
666+ headers : { 'Content-Type' : 'application/json' } ,
667+ } )
668+ )
669+ } )
670+
671+ afterEach ( ( ) => {
672+ vi . unstubAllGlobals ( )
673+ } )
674+
675+ it ( 'does not execute and reports the exact server error when an explicit attachment fails' , async ( ) => {
676+ const { result, unmount } = renderWorkflowExecutionHook ( )
677+ const contextFile = new File ( [ 'context' ] , 'context.txt' , { type : 'text/plain' } )
678+ const file = new File ( [ 'report' ] , 'report.pdf' , { type : 'application/pdf' } )
679+ let uploadError : unknown
680+
681+ mockUploadInternalFileSession . mockResolvedValueOnce ( {
682+ id : 'attachment-context' ,
683+ key : 'executions/context.txt' ,
684+ url : '/uploads/context.txt' ,
685+ name : contextFile . name ,
686+ size : contextFile . size ,
687+ type : contextFile . type ,
688+ context : 'execution' ,
689+ } )
690+
691+ await act ( async ( ) => {
692+ try {
693+ await result ( ) . handleRunWorkflow ( {
694+ input : 'Summarize this report' ,
695+ conversationId : 'conversation-1' ,
696+ files : [
697+ {
698+ name : contextFile . name ,
699+ size : contextFile . size ,
700+ type : contextFile . type ,
701+ file : contextFile ,
702+ } ,
703+ {
704+ name : file . name ,
705+ size : file . size ,
706+ type : file . type ,
707+ file,
708+ } ,
709+ ] ,
710+ } )
711+ } catch ( error ) {
712+ uploadError = error
713+ }
714+ } )
715+
716+ expect ( uploadError ) . toBeInstanceOf ( WorkflowAttachmentUploadError )
717+ expect ( ( uploadError as Error ) . message ) . toBe (
718+ 'Failed to upload report.pdf: Workspace file storage limit exceeded'
719+ )
720+ expect ( mockExecute ) . not . toHaveBeenCalled ( )
721+
722+ unmount ( )
723+ } )
724+
725+ it ( 'returns uploaded metadata without mutating or leaking local input into execution' , async ( ) => {
726+ const { result, unmount } = renderWorkflowExecutionHook ( )
727+ const file = new File ( [ 'diagram' ] , 'diagram.png' , { type : 'image/png' } )
728+ const workflowInput = {
729+ input : 'Describe this diagram' ,
730+ conversationId : 'conversation-1' ,
731+ files : [
732+ {
733+ name : file . name ,
734+ size : file . size ,
735+ type : file . type ,
736+ file,
737+ } ,
738+ ] ,
739+ }
740+ let runResult : unknown
741+
742+ mockUploadInternalFileSession . mockResolvedValueOnce ( {
743+ id : 'attachment-diagram' ,
744+ key : 'execution/diagram.png' ,
745+ url : '/api/files/serve/execution%2Fdiagram.png' ,
746+ name : file . name ,
747+ size : file . size ,
748+ type : file . type ,
749+ context : 'execution' ,
750+ } )
751+
752+ await act ( async ( ) => {
753+ runResult = await result ( ) . handleRunWorkflow ( workflowInput )
754+ await drainStream ( runResult )
755+ } )
756+
757+ expect ( isChatWorkflowRunResult ( runResult ) ) . toBe ( true )
758+ if ( ! isChatWorkflowRunResult ( runResult ) ) {
759+ throw new Error ( 'Expected a chat workflow run result' )
760+ }
761+ expect ( runResult . uploadedAttachments ) . toEqual ( [
762+ expect . objectContaining ( {
763+ name : 'diagram.png' ,
764+ url : '/api/files/serve/execution%2Fdiagram.png' ,
765+ size : file . size ,
766+ type : 'image/png' ,
767+ key : 'execution/diagram.png' ,
768+ } ) ,
769+ ] )
770+ expect ( workflowInput . files [ 0 ] . file ) . toBe ( file )
771+ expect ( mockExecute ) . toHaveBeenCalledWith (
772+ expect . objectContaining ( {
773+ input : expect . objectContaining ( {
774+ input : 'Describe this diagram' ,
775+ conversationId : 'conversation-1' ,
776+ files : [
777+ expect . objectContaining ( {
778+ name : 'diagram.png' ,
779+ url : '/api/files/serve/execution%2Fdiagram.png' ,
780+ } ) ,
781+ ] ,
782+ } ) ,
783+ } )
784+ )
785+
786+ unmount ( )
787+ } )
779788
780789 it ( 'uses only projected live thinking without changing normal settle behavior' , async ( ) => {
781790 mockExecute . mockImplementationOnce ( async ( options ) => {
0 commit comments