@@ -585,202 +585,66 @@ describe('e2e', () => {
585585 expect ( returnValue ) . toEqual ( [ 0 , 1 , 2 , 3 , 4 ] ) ;
586586 } ) ;
587587
588- // ==================== ERROR HANDLING TESTS ====================
589- describe ( 'error handling' , ( ) => {
590- describe ( 'error propagation' , ( ) => {
591- describe ( 'workflow errors' , ( ) => {
592- test (
593- 'nested function calls preserve message and stack trace' ,
594- { timeout : 60_000 } ,
595- async ( ) => {
596- const run = await triggerWorkflow ( 'errorWorkflowNested' , [ ] ) ;
597- const result = await getWorkflowReturnValue ( run . runId ) ;
598-
599- expect ( result . name ) . toBe ( 'WorkflowRunFailedError' ) ;
600- expect ( result . cause . message ) . toContain ( 'Nested workflow error' ) ;
601- // Stack shows call chain: errorNested1 -> errorNested2 -> errorNested3
602- expect ( result . cause . stack ) . toContain ( 'errorNested1' ) ;
603- expect ( result . cause . stack ) . toContain ( 'errorNested2' ) ;
604- expect ( result . cause . stack ) . toContain ( 'errorNested3' ) ;
605- expect ( result . cause . stack ) . toContain ( 'errorWorkflowNested' ) ;
606- expect ( result . cause . stack ) . toContain ( '99_e2e.ts' ) ;
607- expect ( result . cause . stack ) . not . toContain ( 'evalmachine' ) ;
608-
609- const { json : runData } = await cliInspectJson ( `runs ${ run . runId } ` ) ;
610- expect ( runData . status ) . toBe ( 'failed' ) ;
611- }
612- ) ;
613-
614- test (
615- 'cross-file imports preserve message and stack trace' ,
616- { timeout : 60_000 } ,
617- async ( ) => {
618- const run = await triggerWorkflow ( 'errorWorkflowCrossFile' , [ ] ) ;
619- const result = await getWorkflowReturnValue ( run . runId ) ;
620-
621- expect ( result . name ) . toBe ( 'WorkflowRunFailedError' ) ;
622- expect ( result . cause . message ) . toContain (
623- 'Error from imported helper module'
624- ) ;
625- expect ( result . cause . stack ) . toContain ( 'throwError' ) ;
626- expect ( result . cause . stack ) . toContain ( 'callThrower' ) ;
627- expect ( result . cause . stack ) . toContain ( 'errorWorkflowCrossFile' ) ;
628- expect ( result . cause . stack ) . not . toContain ( 'evalmachine' ) ;
629-
630- // helpers.ts reference (known issue: vite-based frameworks dev mode)
631- const isViteBasedFrameworkDevMode =
632- ( process . env . APP_NAME === 'sveltekit' ||
633- process . env . APP_NAME === 'vite' ||
634- process . env . APP_NAME === 'astro' ) &&
635- isLocalDeployment ( ) ;
636- if ( ! isViteBasedFrameworkDevMode ) {
637- expect ( result . cause . stack ) . toContain ( 'helpers.ts' ) ;
638- }
639-
640- const { json : runData } = await cliInspectJson ( `runs ${ run . runId } ` ) ;
641- expect ( runData . status ) . toBe ( 'failed' ) ;
642- }
643- ) ;
644- } ) ;
588+ test ( 'retryAttemptCounterWorkflow' , { timeout : 60_000 } , async ( ) => {
589+ const run = await triggerWorkflow ( 'retryAttemptCounterWorkflow' , [ ] ) ;
590+ const returnValue = await getWorkflowReturnValue ( run . runId ) ;
645591
646- describe ( 'step errors' , ( ) => {
647- test (
648- 'basic step error preserves message' ,
649- { timeout : 60_000 } ,
650- async ( ) => {
651- const run = await triggerWorkflow ( 'errorStepBasic' , [ ] ) ;
652- const result = await getWorkflowReturnValue ( run . runId ) ;
653-
654- expect ( result . name ) . toBe ( 'WorkflowRunFailedError' ) ;
655- expect ( result . cause . message ) . toContain ( 'Step error message' ) ;
656-
657- const { json : steps } = await cliInspectJson (
658- `steps --runId ${ run . runId } `
659- ) ;
660- const failedStep = steps . find ( ( s : any ) =>
661- s . stepName . includes ( 'errorStepFn' )
662- ) ;
663- expect ( failedStep . status ) . toBe ( 'failed' ) ;
664-
665- const { json : runData } = await cliInspectJson ( `runs ${ run . runId } ` ) ;
666- expect ( runData . status ) . toBe ( 'failed' ) ;
667- }
668- ) ;
592+ // The step should have succeeded on attempt 3
593+ expect ( returnValue ) . toEqual ( { finalAttempt : 3 } ) ;
669594
670- test (
671- 'cross-file step error preserves message and function names in stack' ,
672- { timeout : 60_000 } ,
673- async ( ) => {
674- const run = await triggerWorkflow ( 'errorStepCrossFile' , [ ] ) ;
675- const result = await getWorkflowReturnValue ( run . runId ) ;
676-
677- expect ( result . name ) . toBe ( 'WorkflowRunFailedError' ) ;
678- expect ( result . cause . message ) . toContain (
679- 'Step error from imported helper module'
680- ) ;
681-
682- const { json : steps } = await cliInspectJson (
683- `steps --runId ${ run . runId } `
684- ) ;
685- const failedStep = steps . find ( ( s : any ) =>
686- s . stepName . includes ( 'stepThatThrowsFromHelper' )
687- ) ;
688- expect ( failedStep . status ) . toBe ( 'failed' ) ;
689- // Note: Step errors don't have source-mapped stack traces (known limitation)
690- expect ( failedStep . error . stack ) . toContain ( 'throwErrorFromStep' ) ;
691- expect ( failedStep . error . stack ) . toContain (
692- 'stepThatThrowsFromHelper'
693- ) ;
694-
695- const { json : runData } = await cliInspectJson ( `runs ${ run . runId } ` ) ;
696- expect ( runData . status ) . toBe ( 'failed' ) ;
697- }
698- ) ;
699- } ) ;
595+ // Also verify the run data shows the correct output
596+ const { json : runData } = await cliInspectJson (
597+ `runs ${ run . runId } --withData`
598+ ) ;
599+ expect ( runData ) . toMatchObject ( {
600+ runId : run . runId ,
601+ status : 'completed' ,
602+ output : { finalAttempt : 3 } ,
700603 } ) ;
701604
702- describe ( 'retry behavior' , ( ) => {
703- test (
704- 'regular Error retries until success' ,
705- { timeout : 60_000 } ,
706- async ( ) => {
707- const run = await triggerWorkflow ( 'errorRetrySuccess' , [ ] ) ;
708- const result = await getWorkflowReturnValue ( run . runId ) ;
709-
710- expect ( result . finalAttempt ) . toBe ( 3 ) ;
711-
712- const { json : steps } = await cliInspectJson (
713- `steps --runId ${ run . runId } `
714- ) ;
715- const step = steps . find ( ( s : any ) =>
716- s . stepName . includes ( 'retryUntilAttempt3' )
717- ) ;
718- expect ( step . status ) . toBe ( 'completed' ) ;
719- expect ( step . attempt ) . toBe ( 3 ) ;
720- }
721- ) ;
605+ // Query steps separately to verify the step data
606+ const { json : stepsData } = await cliInspectJson (
607+ `steps --runId ${ run . runId } --withData`
608+ ) ;
609+ expect ( stepsData ) . toBeDefined ( ) ;
610+ expect ( Array . isArray ( stepsData ) ) . toBe ( true ) ;
611+ expect ( stepsData . length ) . toBeGreaterThan ( 0 ) ;
722612
723- test (
724- 'FatalError fails immediately without retries' ,
725- { timeout : 60_000 } ,
726- async ( ) => {
727- const run = await triggerWorkflow ( 'errorRetryFatal' , [ ] ) ;
728- const result = await getWorkflowReturnValue ( run . runId ) ;
729-
730- expect ( result . name ) . toBe ( 'WorkflowRunFailedError' ) ;
731- expect ( result . cause . message ) . toContain ( 'Fatal step error' ) ;
732-
733- const { json : steps } = await cliInspectJson (
734- `steps --runId ${ run . runId } `
735- ) ;
736- const step = steps . find ( ( s : any ) =>
737- s . stepName . includes ( 'throwFatalError' )
738- ) ;
739- expect ( step . status ) . toBe ( 'failed' ) ;
740- expect ( step . attempt ) . toBe ( 1 ) ;
741- }
742- ) ;
613+ // Find the stepThatRetriesAndSucceeds step
614+ const retryStep = stepsData . find ( ( s : any ) =>
615+ s . stepName . includes ( 'stepThatRetriesAndSucceeds' )
616+ ) ;
617+ expect ( retryStep ) . toBeDefined ( ) ;
618+ expect ( retryStep . status ) . toBe ( 'completed' ) ;
619+ expect ( retryStep . attempt ) . toBe ( 3 ) ;
620+ expect ( retryStep . output ) . toEqual ( [ 3 ] ) ;
621+ } ) ;
743622
744- test (
745- 'RetryableError respects custom retryAfter delay' ,
746- { timeout : 60_000 } ,
747- async ( ) => {
748- const run = await triggerWorkflow ( 'errorRetryCustomDelay' , [ ] ) ;
749- const result = await getWorkflowReturnValue ( run . runId ) ;
623+ test ( 'retryableAndFatalErrorWorkflow' , { timeout : 60_000 } , async ( ) => {
624+ const run = await triggerWorkflow ( 'retryableAndFatalErrorWorkflow' , [ ] ) ;
625+ const returnValue = await getWorkflowReturnValue ( run . runId ) ;
626+ expect ( returnValue . retryableResult . attempt ) . toEqual ( 2 ) ;
627+ expect ( returnValue . retryableResult . duration ) . toBeGreaterThan ( 10_000 ) ;
628+ expect ( returnValue . gotFatalError ) . toBe ( true ) ;
629+ } ) ;
750630
751- expect ( result . attempt ) . toBe ( 2 ) ;
752- expect ( result . duration ) . toBeGreaterThan ( 10_000 ) ;
753- }
754- ) ;
631+ test (
632+ 'maxRetriesZeroWorkflow - maxRetries=0 runs once without retrying' ,
633+ { timeout : 60_000 } ,
634+ async ( ) => {
635+ const run = await triggerWorkflow ( 'maxRetriesZeroWorkflow' , [ ] ) ;
636+ const returnValue = await getWorkflowReturnValue ( run . runId ) ;
755637
756- test ( 'maxRetries=0 disables retries' , { timeout : 60_000 } , async ( ) => {
757- const run = await triggerWorkflow ( 'errorRetryDisabled' , [ ] ) ;
758- const result = await getWorkflowReturnValue ( run . runId ) ;
638+ // The step with maxRetries=0 that succeeds should have run on attempt 1
639+ expect ( returnValue . successResult ) . toEqual ( { attempt : 1 } ) ;
759640
760- expect ( result . failed ) . toBe ( true ) ;
761- expect ( result . attempt ) . toBe ( 1 ) ;
762- } ) ;
763- } ) ;
641+ // The step with maxRetries=0 that fails should have thrown an error
642+ expect ( returnValue . gotError ) . toBe ( true ) ;
764643
765- describe ( 'catchability' , ( ) => {
766- test (
767- 'FatalError can be caught and detected with FatalError.is()' ,
768- { timeout : 60_000 } ,
769- async ( ) => {
770- const run = await triggerWorkflow ( 'errorFatalCatchable' , [ ] ) ;
771- const result = await getWorkflowReturnValue ( run . runId ) ;
772-
773- expect ( result . caught ) . toBe ( true ) ;
774- expect ( result . isFatal ) . toBe ( true ) ;
775-
776- // Verify workflow completed successfully (error was caught)
777- const { json : runData } = await cliInspectJson ( `runs ${ run . runId } ` ) ;
778- expect ( runData . status ) . toBe ( 'completed' ) ;
779- }
780- ) ;
781- } ) ;
782- } ) ;
783- // ==================== END ERROR HANDLING TESTS ====================
644+ // The failing step should have only run once (attempt 1), not retried
645+ expect ( returnValue . failedAttempt ) . toBe ( 1 ) ;
646+ }
647+ ) ;
784648
785649 test (
786650 'stepDirectCallWorkflow - calling step functions directly outside workflow context' ,
@@ -812,6 +676,68 @@ describe('e2e', () => {
812676 }
813677 ) ;
814678
679+ test (
680+ 'crossFileErrorWorkflow - stack traces work across imported modules' ,
681+ { timeout : 60_000 } ,
682+ async ( ) => {
683+ // This workflow intentionally throws an error from an imported helper module
684+ // to verify that stack traces correctly show cross-file call chains
685+ const run = await triggerWorkflow ( 'crossFileErrorWorkflow' , [ ] ) ;
686+ const returnValue = await getWorkflowReturnValue ( run . runId ) ;
687+
688+ // The workflow should fail with error response containing both top-level and cause
689+ expect ( returnValue ) . toHaveProperty ( 'name' ) ;
690+ expect ( returnValue . name ) . toBe ( 'WorkflowRunFailedError' ) ;
691+ expect ( returnValue ) . toHaveProperty ( 'message' ) ;
692+
693+ // Verify the cause property contains the structured error
694+ expect ( returnValue ) . toHaveProperty ( 'cause' ) ;
695+ expect ( returnValue . cause ) . toBeTypeOf ( 'object' ) ;
696+ expect ( returnValue . cause ) . toHaveProperty ( 'message' ) ;
697+ expect ( returnValue . cause . message ) . toContain (
698+ 'Error from imported helper module'
699+ ) ;
700+
701+ // Verify the stack trace is present in the cause
702+ expect ( returnValue . cause ) . toHaveProperty ( 'stack' ) ;
703+ expect ( typeof returnValue . cause . stack ) . toBe ( 'string' ) ;
704+
705+ // Known issue: vite-based frameworks dev mode has incorrect source map mappings for bundled imports.
706+ // esbuild with bundle:true inlines helpers.ts but source maps incorrectly map to 99_e2e.ts
707+ // This works correctly in production and other frameworks.
708+ // TODO: Investigate esbuild source map generation for bundled modules
709+ const isViteBasedFrameworkDevMode =
710+ ( process . env . APP_NAME === 'sveltekit' ||
711+ process . env . APP_NAME === 'vite' ||
712+ process . env . APP_NAME === 'astro' ) &&
713+ isLocalDeployment ( ) ;
714+
715+ if ( ! isViteBasedFrameworkDevMode ) {
716+ // Stack trace should include frames from the helper module (helpers.ts)
717+ expect ( returnValue . cause . stack ) . toContain ( 'helpers.ts' ) ;
718+ }
719+
720+ // These checks should work in all modes
721+ expect ( returnValue . cause . stack ) . toContain ( 'throwError' ) ;
722+ expect ( returnValue . cause . stack ) . toContain ( 'callThrower' ) ;
723+
724+ // Stack trace should include frames from the workflow file (99_e2e.ts)
725+ expect ( returnValue . cause . stack ) . toContain ( '99_e2e.ts' ) ;
726+ expect ( returnValue . cause . stack ) . toContain ( 'crossFileErrorWorkflow' ) ;
727+
728+ // Stack trace should NOT contain 'evalmachine' anywhere
729+ expect ( returnValue . cause . stack ) . not . toContain ( 'evalmachine' ) ;
730+
731+ // Verify the run failed with structured error
732+ const { json : runData } = await cliInspectJson ( `runs ${ run . runId } ` ) ;
733+ expect ( runData . status ) . toBe ( 'failed' ) ;
734+ expect ( runData . error ) . toBeTypeOf ( 'object' ) ;
735+ expect ( runData . error . message ) . toContain (
736+ 'Error from imported helper module'
737+ ) ;
738+ }
739+ ) ;
740+
815741 test (
816742 'hookCleanupTestWorkflow - hook token reuse after workflow completion' ,
817743 { timeout : 60_000 } ,
0 commit comments