@outputai/core 0.7.1-next.de30052.0 → 0.7.1-next.ed233ce.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/worker.sh +6 -0
- package/package.json +1 -1
- package/src/consts.js +0 -4
- package/src/errors.js +6 -2
- package/src/interface/evaluator.js +7 -20
- package/src/interface/evaluator.spec.js +117 -1
- package/src/interface/step.js +8 -9
- package/src/interface/step.spec.js +124 -0
- package/src/interface/validations/index.js +108 -0
- package/src/interface/validations/index.spec.js +182 -0
- package/src/interface/validations/schemas.js +113 -0
- package/src/interface/validations/schemas.spec.js +209 -0
- package/src/interface/webhook.js +1 -1
- package/src/interface/webhook.spec.js +1 -1
- package/src/interface/workflow.d.ts +10 -9
- package/src/interface/workflow.js +76 -164
- package/src/interface/workflow.spec.js +637 -521
- package/src/interface/workflow_activity_options.js +16 -0
- package/src/interface/workflow_utils.js +1 -1
- package/src/interface/zod_integration.spec.js +2 -2
- package/src/internal_utils/aggregations.js +0 -10
- package/src/internal_utils/aggregations.spec.js +1 -48
- package/src/internal_utils/errors.js +14 -8
- package/src/internal_utils/errors.spec.js +73 -27
- package/src/utils/index.d.ts +19 -0
- package/src/utils/utils.js +53 -0
- package/src/utils/utils.spec.js +105 -1
- package/src/worker/bundle.js +26 -0
- package/src/worker/bundle.spec.js +53 -0
- package/src/worker/bundler_options.js +1 -1
- package/src/worker/bundler_options.spec.js +1 -1
- package/src/worker/catalog_workflow/catalog_job.js +148 -0
- package/src/worker/catalog_workflow/catalog_job.spec.js +232 -0
- package/src/worker/check.js +24 -0
- package/src/worker/connection_monitor.js +112 -0
- package/src/worker/connection_monitor.spec.js +199 -0
- package/src/worker/index.js +146 -41
- package/src/worker/index.spec.js +281 -109
- package/src/worker/interceptors/activity.js +7 -24
- package/src/worker/interceptors/activity.spec.js +97 -66
- package/src/worker/interceptors/index.js +4 -7
- package/src/worker/interceptors/modules.js +15 -0
- package/src/worker/interceptors/workflow.js +6 -8
- package/src/worker/interceptors/workflow.spec.js +49 -42
- package/src/worker/interruption.js +33 -0
- package/src/worker/interruption.spec.js +86 -0
- package/src/worker/loader/activities.js +75 -0
- package/src/worker/loader/activities.spec.js +213 -0
- package/src/worker/loader/hooks.js +28 -0
- package/src/worker/loader/hooks.spec.js +64 -0
- package/src/worker/loader/matchers.js +46 -0
- package/src/worker/loader/matchers.spec.js +140 -0
- package/src/worker/{loader_tools.js → loader/tools.js} +19 -67
- package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +53 -85
- package/src/worker/loader/workflows.js +82 -0
- package/src/worker/loader/workflows.spec.js +256 -0
- package/src/worker/{setup_telemetry.js → telemetry.js} +9 -4
- package/src/worker/{setup_telemetry.spec.js → telemetry.spec.js} +3 -3
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +5 -109
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +31 -103
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -6
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +11 -83
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +8 -11
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +9 -9
- package/src/interface/validations/runtime.js +0 -20
- package/src/interface/validations/runtime.spec.js +0 -29
- package/src/interface/validations/schema_utils.js +0 -8
- package/src/interface/validations/schema_utils.spec.js +0 -67
- package/src/interface/validations/static.js +0 -137
- package/src/interface/validations/static.spec.js +0 -397
- package/src/interface/workflow.replay_compatibility.spec.js +0 -254
- package/src/worker/loader.js +0 -202
- package/src/worker/loader.spec.js +0 -498
- package/src/worker/shutdown.js +0 -26
- package/src/worker/shutdown.spec.js +0 -82
- package/src/worker/start_catalog.js +0 -96
- package/src/worker/start_catalog.spec.js +0 -179
|
@@ -1,684 +1,800 @@
|
|
|
1
|
-
import { ACTIVITY_WRAPPER_VERSION_FIELD, Signal, WORKFLOW_WRAPPER_VERSION_FIELD } from '#consts';
|
|
2
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
2
|
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
ACTIVITY_GET_TRACE_DESTINATIONS,
|
|
5
|
+
ACTIVITY_WRAPPER_VERSION_FIELD,
|
|
6
|
+
METADATA_ACCESS_SYMBOL,
|
|
7
|
+
SHARED_STEP_PREFIX,
|
|
8
|
+
WORKFLOW_WRAPPER_VERSION_FIELD
|
|
9
|
+
} from '#consts';
|
|
10
|
+
import { ValidationError } from '#errors';
|
|
11
|
+
|
|
12
|
+
const inWorkflowContextMock = vi.hoisted( () => vi.fn() );
|
|
13
|
+
const proxyActivitiesMock = vi.hoisted( () => vi.fn() );
|
|
14
|
+
const executeChildMock = vi.hoisted( () => vi.fn() );
|
|
15
|
+
const workflowInfoMock = vi.hoisted( () => vi.fn() );
|
|
16
|
+
const continueAsNewMock = vi.hoisted( () => vi.fn() );
|
|
17
|
+
const validateDefinitionMock = vi.hoisted( () => vi.fn() );
|
|
18
|
+
const validateInputMock = vi.hoisted( () => vi.fn() );
|
|
19
|
+
const validateOutputMock = vi.hoisted( () => vi.fn() );
|
|
20
|
+
const validateInvocationOptionsMock = vi.hoisted( () => vi.fn() );
|
|
21
|
+
const validatorConstructorMock = vi.hoisted( () => vi.fn() );
|
|
22
|
+
|
|
23
|
+
vi.mock( './validations/index.js', () => {
|
|
24
|
+
class WorkflowValidator {
|
|
25
|
+
static validateDefinition( ...args ) {
|
|
26
|
+
return validateDefinitionMock( ...args );
|
|
27
|
+
}
|
|
4
28
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const traceDestinationsStepMock = vi.fn().mockResolvedValue( { local: '/tmp/trace' } );
|
|
11
|
-
const executeChildMock = vi.fn().mockResolvedValue( undefined );
|
|
12
|
-
const continueAsNewMock = vi.fn().mockResolvedValue( undefined );
|
|
13
|
-
|
|
14
|
-
const createStepsProxy = ( stepSpy = vi.fn() ) =>
|
|
15
|
-
new Proxy( {}, {
|
|
16
|
-
get: ( _, prop ) => {
|
|
17
|
-
if ( prop === '__internal#getTraceDestinations' ) {
|
|
18
|
-
return traceDestinationsStepMock;
|
|
19
|
-
}
|
|
20
|
-
if ( typeof prop === 'string' && ( prop.includes( '#' ) ) ) {
|
|
21
|
-
return stepSpy;
|
|
22
|
-
}
|
|
23
|
-
return vi.fn();
|
|
29
|
+
constructor( ...args ) {
|
|
30
|
+
validatorConstructorMock( ...args );
|
|
31
|
+
this.validateInput = validateInputMock;
|
|
32
|
+
this.validateOutput = validateOutputMock;
|
|
33
|
+
this.validateInvocationOptions = validateInvocationOptionsMock;
|
|
24
34
|
}
|
|
25
|
-
}
|
|
35
|
+
}
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
const proxyActivitiesMock = vi.fn( () => {
|
|
29
|
-
stepSpyRef.current = vi.fn().mockResolvedValue( {} );
|
|
30
|
-
return createStepsProxy( stepSpyRef.current );
|
|
37
|
+
return { WorkflowValidator };
|
|
31
38
|
} );
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
vi.mock( '@temporalio/workflow', async importOriginal => {
|
|
41
|
+
const actual = await importOriginal();
|
|
42
|
+
return {
|
|
43
|
+
...actual,
|
|
44
|
+
inWorkflowContext: inWorkflowContextMock,
|
|
45
|
+
proxyActivities: proxyActivitiesMock,
|
|
46
|
+
executeChild: executeChildMock,
|
|
47
|
+
workflowInfo: workflowInfoMock,
|
|
48
|
+
continueAsNew: continueAsNewMock,
|
|
49
|
+
uuid4: () => '550e8400e29b41d4a716446655440000'
|
|
50
|
+
};
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
const baseWorkflowInfo = () => ( {
|
|
54
|
+
workflowId: 'workflow-123',
|
|
35
55
|
workflowType: 'test_wf',
|
|
36
|
-
runId: 'run-
|
|
56
|
+
runId: 'run-123',
|
|
57
|
+
startTime: new Date( '2025-01-01T00:00:00.000Z' ),
|
|
37
58
|
memo: {},
|
|
38
|
-
startTime: new Date( '2025-01-01T00:00:00Z' ),
|
|
39
59
|
continueAsNewSuggested: false
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
const setWorkflowInfo = overrides => {
|
|
63
|
+
const info = {
|
|
64
|
+
...baseWorkflowInfo(),
|
|
65
|
+
...overrides,
|
|
66
|
+
memo: overrides?.memo ?? {}
|
|
67
|
+
};
|
|
68
|
+
workflowInfoMock.mockImplementation( () => info );
|
|
69
|
+
return info;
|
|
40
70
|
};
|
|
41
|
-
const workflowInfoMock = vi.fn( () => ( { ...workflowInfoReturn } ) );
|
|
42
|
-
|
|
43
|
-
vi.mock( '@temporalio/workflow', () => ( {
|
|
44
|
-
proxyActivities: ( ...args ) => proxyActivitiesMock( ...args ),
|
|
45
|
-
inWorkflowContext: inWorkflowContextMock,
|
|
46
|
-
executeChild: ( ...args ) => executeChildMock( ...args ),
|
|
47
|
-
workflowInfo: workflowInfoMock,
|
|
48
|
-
uuid4: () => '550e8400e29b41d4a716446655440000',
|
|
49
|
-
ParentClosePolicy: { TERMINATE: 'TERMINATE', ABANDON: 'ABANDON' },
|
|
50
|
-
ChildWorkflowFailure: class ChildWorkflowFailure extends Error {
|
|
51
|
-
constructor( message, cause ) {
|
|
52
|
-
super( message );
|
|
53
|
-
this.name = 'ChildWorkflowFailure';
|
|
54
|
-
this.cause = cause;
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
continueAsNew: continueAsNewMock,
|
|
58
|
-
defineSignal: ( ...args ) => defineSignalMock( ...args ),
|
|
59
|
-
setHandler: ( ...args ) => setHandlerMock( ...args )
|
|
60
|
-
} ) );
|
|
61
71
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
const activityOutput = output => ( {
|
|
73
|
+
output,
|
|
74
|
+
[ACTIVITY_WRAPPER_VERSION_FIELD]: 1
|
|
75
|
+
} );
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
const createActivities = handlers => new Proxy( {}, {
|
|
78
|
+
get: ( _, prop ) => typeof prop === 'string' ?
|
|
79
|
+
handlers[prop] ?? vi.fn().mockResolvedValue( activityOutput( undefined ) ) :
|
|
80
|
+
undefined
|
|
81
|
+
} );
|
|
69
82
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
const mockActivities = handlers => {
|
|
84
|
+
const activities = createActivities( handlers );
|
|
85
|
+
proxyActivitiesMock.mockReturnValue( activities );
|
|
86
|
+
return activities;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const workflowDefinition = overrides => ( {
|
|
90
|
+
name: 'test_wf',
|
|
91
|
+
description: 'Test workflow',
|
|
92
|
+
inputSchema: z.object( {} ),
|
|
93
|
+
outputSchema: z.object( {} ),
|
|
94
|
+
fn: async () => ( {} ),
|
|
95
|
+
...overrides
|
|
77
96
|
} );
|
|
78
97
|
|
|
98
|
+
const invokeWorkflowFromHelper = ( wf, input, options ) => wf( input, options );
|
|
99
|
+
|
|
79
100
|
describe( 'workflow()', () => {
|
|
80
101
|
beforeEach( () => {
|
|
81
102
|
vi.clearAllMocks();
|
|
82
103
|
inWorkflowContextMock.mockReturnValue( true );
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
executeChildMock.mockResolvedValue( { output: {} } );
|
|
105
|
+
setWorkflowInfo();
|
|
106
|
+
mockActivities( {
|
|
107
|
+
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( { local: '/tmp/trace' } ) )
|
|
108
|
+
} );
|
|
109
|
+
} );
|
|
110
|
+
|
|
111
|
+
it( 'validates workflow definition at creation time', async () => {
|
|
112
|
+
const { workflow } = await import( './workflow.js' );
|
|
113
|
+
const inputSchema = z.object( {} );
|
|
114
|
+
const outputSchema = z.object( {} );
|
|
115
|
+
const fn = async () => ( {} );
|
|
116
|
+
const options = { activityOptions: { retry: { maximumAttempts: 1 } } };
|
|
117
|
+
|
|
118
|
+
workflow( {
|
|
119
|
+
name: 'definition_wf',
|
|
120
|
+
description: 'Definition workflow',
|
|
121
|
+
inputSchema,
|
|
122
|
+
outputSchema,
|
|
123
|
+
fn,
|
|
124
|
+
options,
|
|
125
|
+
aliases: [ 'old_definition_wf' ]
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
expect( validateDefinitionMock ).toHaveBeenCalledWith( {
|
|
129
|
+
name: 'definition_wf',
|
|
130
|
+
description: 'Definition workflow',
|
|
131
|
+
inputSchema,
|
|
132
|
+
outputSchema,
|
|
133
|
+
fn,
|
|
134
|
+
options,
|
|
135
|
+
aliases: [ 'old_definition_wf' ]
|
|
136
|
+
} );
|
|
137
|
+
expect( validatorConstructorMock ).toHaveBeenCalledWith( {
|
|
138
|
+
name: 'definition_wf',
|
|
139
|
+
inputSchema,
|
|
140
|
+
outputSchema
|
|
141
|
+
} );
|
|
142
|
+
} );
|
|
143
|
+
|
|
144
|
+
it( 'propagates definition validation errors', async () => {
|
|
145
|
+
const { workflow } = await import( './workflow.js' );
|
|
146
|
+
const error = new ValidationError( 'invalid definition' );
|
|
147
|
+
validateDefinitionMock.mockImplementationOnce( () => {
|
|
148
|
+
throw error;
|
|
90
149
|
} );
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
150
|
+
|
|
151
|
+
expect( () => workflow( workflowDefinition( { name: 'invalid_name' } ) ) ).toThrow( error );
|
|
152
|
+
} );
|
|
153
|
+
|
|
154
|
+
it( 'attaches workflow metadata to the wrapper', async () => {
|
|
155
|
+
const { workflow } = await import( './workflow.js' );
|
|
156
|
+
const inputSchema = z.object( { value: z.string() } );
|
|
157
|
+
const outputSchema = z.object( { ok: z.boolean() } );
|
|
158
|
+
|
|
159
|
+
const wf = workflow( workflowDefinition( {
|
|
160
|
+
name: 'metadata_wf',
|
|
161
|
+
description: 'Metadata workflow',
|
|
162
|
+
inputSchema,
|
|
163
|
+
outputSchema,
|
|
164
|
+
aliases: [ 'metadata_alias' ],
|
|
165
|
+
fn: async () => ( { ok: true } )
|
|
97
166
|
} ) );
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
167
|
+
|
|
168
|
+
const [ metadataSymbol ] = Object.getOwnPropertySymbols( wf );
|
|
169
|
+
expect( wf[metadataSymbol] ).toEqual( {
|
|
170
|
+
name: 'metadata_wf',
|
|
171
|
+
description: 'Metadata workflow',
|
|
172
|
+
inputSchema,
|
|
173
|
+
outputSchema,
|
|
174
|
+
aliases: [ 'metadata_alias' ]
|
|
101
175
|
} );
|
|
102
176
|
} );
|
|
103
177
|
|
|
104
|
-
describe( '
|
|
105
|
-
|
|
178
|
+
describe( 'outside Temporal workflow context', () => {
|
|
179
|
+
beforeEach( () => {
|
|
180
|
+
inWorkflowContextMock.mockReturnValue( false );
|
|
181
|
+
} );
|
|
182
|
+
|
|
183
|
+
it( 'runs as a plain function with real test WorkflowContext and merged extra context', async () => {
|
|
106
184
|
const { workflow } = await import( './workflow.js' );
|
|
107
185
|
|
|
108
|
-
const wf = workflow( {
|
|
109
|
-
name: '
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
186
|
+
const wf = workflow( workflowDefinition( {
|
|
187
|
+
name: 'plain_wf',
|
|
188
|
+
inputSchema: z.object( { suffix: z.string() } ),
|
|
189
|
+
outputSchema: z.object( { value: z.string(), extra: z.string() } ),
|
|
190
|
+
fn: async ( input, context ) => ( {
|
|
191
|
+
value: `${context.info.workflowId}${input.suffix}`,
|
|
192
|
+
extra: context.extra
|
|
193
|
+
} )
|
|
194
|
+
} ) );
|
|
115
195
|
|
|
116
|
-
|
|
117
|
-
|
|
196
|
+
await expect( wf( { suffix: '-ok' }, { context: { extra: 'custom' } } ) ).resolves.toEqual( {
|
|
197
|
+
value: 'test-workflow-ok',
|
|
198
|
+
extra: 'custom'
|
|
199
|
+
} );
|
|
200
|
+
expect( validateInvocationOptionsMock ).toHaveBeenCalledWith( { context: { extra: 'custom' } } );
|
|
201
|
+
expect( validateInputMock ).toHaveBeenCalledWith( { suffix: '-ok' } );
|
|
202
|
+
expect( validateOutputMock ).toHaveBeenCalledWith( { value: 'test-workflow-ok', extra: 'custom' } );
|
|
203
|
+
expect( workflowInfoMock ).not.toHaveBeenCalled();
|
|
204
|
+
expect( proxyActivitiesMock ).not.toHaveBeenCalled();
|
|
118
205
|
} );
|
|
119
206
|
|
|
120
|
-
it( '
|
|
207
|
+
it( 'does not run fn when plain function input validation fails', async () => {
|
|
121
208
|
const { workflow } = await import( './workflow.js' );
|
|
209
|
+
const error = new ValidationError( 'invalid input' );
|
|
210
|
+
validateInputMock.mockImplementationOnce( () => {
|
|
211
|
+
throw error;
|
|
212
|
+
} );
|
|
213
|
+
const fn = vi.fn();
|
|
122
214
|
|
|
123
|
-
const wf = workflow( {
|
|
124
|
-
name: '
|
|
125
|
-
description: 'Workflow with tracing disabled',
|
|
215
|
+
const wf = workflow( workflowDefinition( {
|
|
216
|
+
name: 'plain_validation_wf',
|
|
126
217
|
inputSchema: z.object( { value: z.string() } ),
|
|
127
|
-
outputSchema: z.object( {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
} );
|
|
218
|
+
outputSchema: z.object( { result: z.string() } ),
|
|
219
|
+
fn
|
|
220
|
+
} ) );
|
|
131
221
|
|
|
132
|
-
|
|
133
|
-
expect(
|
|
222
|
+
await expect( wf( { value: 1 } ) ).rejects.toBe( error );
|
|
223
|
+
expect( fn ).not.toHaveBeenCalled();
|
|
224
|
+
expect( validateOutputMock ).not.toHaveBeenCalled();
|
|
134
225
|
} );
|
|
135
226
|
|
|
136
|
-
it( '
|
|
227
|
+
it( 'propagates plain function output validation errors after fn runs', async () => {
|
|
137
228
|
const { workflow } = await import( './workflow.js' );
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
description: 'Workflow with custom activity options',
|
|
142
|
-
inputSchema: z.object( {} ),
|
|
143
|
-
outputSchema: z.object( {} ),
|
|
144
|
-
options: {
|
|
145
|
-
activityOptions: {
|
|
146
|
-
startToCloseTimeout: '5m',
|
|
147
|
-
retry: { maximumAttempts: 5 }
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
fn: async () => ( {} )
|
|
229
|
+
const error = new ValidationError( 'invalid output' );
|
|
230
|
+
validateOutputMock.mockImplementationOnce( () => {
|
|
231
|
+
throw error;
|
|
151
232
|
} );
|
|
233
|
+
const output = { result: 1 };
|
|
234
|
+
const fn = vi.fn().mockResolvedValue( output );
|
|
152
235
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
236
|
+
const wf = workflow( workflowDefinition( {
|
|
237
|
+
name: 'plain_output_validation_wf',
|
|
238
|
+
inputSchema: z.object( { value: z.string() } ),
|
|
239
|
+
outputSchema: z.object( { result: z.string() } ),
|
|
240
|
+
fn
|
|
241
|
+
} ) );
|
|
242
|
+
|
|
243
|
+
await expect( wf( { value: 'ok' } ) ).rejects.toBe( error );
|
|
244
|
+
expect( fn ).toHaveBeenCalledWith( { value: 'ok' }, expect.objectContaining( {
|
|
245
|
+
info: { workflowId: 'test-workflow', runId: 'test-run' }
|
|
246
|
+
} ) );
|
|
247
|
+
expect( validateOutputMock ).toHaveBeenCalledWith( output );
|
|
159
248
|
} );
|
|
160
249
|
} );
|
|
161
250
|
|
|
162
|
-
describe( '
|
|
163
|
-
it( '
|
|
251
|
+
describe( 'child workflow trigger path', () => {
|
|
252
|
+
it( 'starts a child workflow when memo.stack already contains the current workflowId', async () => {
|
|
164
253
|
const { workflow } = await import( './workflow.js' );
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
254
|
+
const { ParentClosePolicy } = await import( '@temporalio/workflow' );
|
|
255
|
+
const memo = {
|
|
256
|
+
stack: [ 'root-workflow', 'workflow-123' ],
|
|
257
|
+
traceInfo: { workflowId: 'root-workflow' },
|
|
258
|
+
activityOptions: {
|
|
259
|
+
startToCloseTimeout: '10m',
|
|
260
|
+
retry: { maximumAttempts: 2 }
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
setWorkflowInfo( { memo } );
|
|
264
|
+
executeChildMock.mockResolvedValueOnce( { output: { child: 'ok' } } );
|
|
265
|
+
|
|
266
|
+
const wf = workflow( workflowDefinition( {
|
|
267
|
+
name: 'child_target_wf',
|
|
268
|
+
inputSchema: z.object( { id: z.number() } ),
|
|
269
|
+
outputSchema: z.object( { child: z.string() } ),
|
|
270
|
+
fn: vi.fn()
|
|
271
|
+
} ) );
|
|
272
|
+
|
|
273
|
+
await expect( wf( { id: 1 }, {
|
|
274
|
+
detached: true,
|
|
275
|
+
activityOptions: {
|
|
276
|
+
startToCloseTimeout: '2m',
|
|
277
|
+
retry: { maximumAttempts: 7 }
|
|
278
|
+
}
|
|
279
|
+
} ) ).resolves.toEqual( { child: 'ok' } );
|
|
280
|
+
expect( validateInvocationOptionsMock ).toHaveBeenCalledWith( {
|
|
281
|
+
detached: true,
|
|
282
|
+
activityOptions: {
|
|
283
|
+
startToCloseTimeout: '2m',
|
|
284
|
+
retry: { maximumAttempts: 7 }
|
|
285
|
+
}
|
|
174
286
|
} );
|
|
175
287
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
288
|
+
expect( executeChildMock ).toHaveBeenCalledWith( 'child_target_wf', {
|
|
289
|
+
args: [ { id: 1 } ],
|
|
290
|
+
workflowId: expect.stringMatching( /^workflow-123-/ ),
|
|
291
|
+
parentClosePolicy: ParentClosePolicy.ABANDON,
|
|
292
|
+
memo: {
|
|
293
|
+
stack: [ 'root-workflow', 'workflow-123' ],
|
|
294
|
+
traceInfo: { workflowId: 'root-workflow' },
|
|
295
|
+
activityOptions: {
|
|
296
|
+
startToCloseTimeout: '2m',
|
|
297
|
+
retry: { maximumAttempts: 7 }
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} );
|
|
301
|
+
expect( proxyActivitiesMock ).not.toHaveBeenCalled();
|
|
180
302
|
} );
|
|
181
|
-
} );
|
|
182
303
|
|
|
183
|
-
|
|
184
|
-
it( 'validates input, runs fn with test context, validates output, returns plain output', async () => {
|
|
185
|
-
inWorkflowContextMock.mockReturnValue( false );
|
|
304
|
+
it( 'uses empty args and terminate policy by default for child workflow execution', async () => {
|
|
186
305
|
const { workflow } = await import( './workflow.js' );
|
|
306
|
+
const { ParentClosePolicy } = await import( '@temporalio/workflow' );
|
|
307
|
+
setWorkflowInfo( { memo: { stack: [ 'workflow-123' ], activityOptions: { heartbeatTimeout: '1m' } } } );
|
|
308
|
+
executeChildMock.mockResolvedValueOnce( { output: 'done' } );
|
|
309
|
+
|
|
310
|
+
const wf = workflow( workflowDefinition( {
|
|
311
|
+
name: 'no_input_child_wf',
|
|
312
|
+
inputSchema: undefined,
|
|
313
|
+
outputSchema: z.string(),
|
|
314
|
+
fn: vi.fn()
|
|
315
|
+
} ) );
|
|
187
316
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
317
|
+
await expect( wf() ).resolves.toBe( 'done' );
|
|
318
|
+
expect( executeChildMock ).toHaveBeenCalledWith( 'no_input_child_wf', expect.objectContaining( {
|
|
319
|
+
args: [],
|
|
320
|
+
parentClosePolicy: ParentClosePolicy.TERMINATE,
|
|
321
|
+
memo: { stack: [ 'workflow-123' ], activityOptions: { heartbeatTimeout: '1m' } }
|
|
322
|
+
} ) );
|
|
323
|
+
} );
|
|
324
|
+
|
|
325
|
+
it( 'starts a child workflow when the call is made through a helper outside the handler', async () => {
|
|
326
|
+
const { workflow } = await import( './workflow.js' );
|
|
327
|
+
const { ParentClosePolicy } = await import( '@temporalio/workflow' );
|
|
328
|
+
const getTraceDestinations = vi.fn().mockResolvedValue( activityOutput( null ) );
|
|
329
|
+
const info = setWorkflowInfo( { workflowType: 'indirect_parent_wf', memo: {} } );
|
|
330
|
+
mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: getTraceDestinations } );
|
|
331
|
+
executeChildMock.mockResolvedValueOnce( { output: { child: 'ok' } } );
|
|
332
|
+
const childFn = vi.fn();
|
|
333
|
+
|
|
334
|
+
const childWorkflow = workflow( workflowDefinition( {
|
|
335
|
+
name: 'indirect_child_wf',
|
|
336
|
+
inputSchema: z.object( { id: z.number() } ),
|
|
337
|
+
outputSchema: z.object( { child: z.string() } ),
|
|
338
|
+
fn: childFn
|
|
339
|
+
} ) );
|
|
340
|
+
const parentWorkflow = workflow( workflowDefinition( {
|
|
341
|
+
name: 'indirect_parent_wf',
|
|
342
|
+
outputSchema: z.object( { child: z.string() } ),
|
|
343
|
+
fn: async () => invokeWorkflowFromHelper( childWorkflow, { id: 1 }, {
|
|
344
|
+
activityOptions: {
|
|
345
|
+
retry: { maximumAttempts: 1 }
|
|
346
|
+
}
|
|
195
347
|
} )
|
|
196
|
-
} );
|
|
348
|
+
} ) );
|
|
197
349
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
350
|
+
await expect( parentWorkflow( {} ) ).resolves.toEqual( {
|
|
351
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
352
|
+
output: { child: 'ok' }
|
|
353
|
+
} );
|
|
354
|
+
expect( childFn ).not.toHaveBeenCalled();
|
|
355
|
+
expect( executeChildMock ).toHaveBeenCalledWith( 'indirect_child_wf', expect.objectContaining( {
|
|
356
|
+
args: [ { id: 1 } ],
|
|
357
|
+
parentClosePolicy: ParentClosePolicy.TERMINATE,
|
|
358
|
+
memo: expect.objectContaining( {
|
|
359
|
+
stack: [ 'workflow-123' ],
|
|
360
|
+
traceInfo: info.memo.traceInfo,
|
|
361
|
+
activityOptions: expect.objectContaining( {
|
|
362
|
+
retry: expect.objectContaining( { maximumAttempts: 1 } )
|
|
363
|
+
} )
|
|
364
|
+
} )
|
|
365
|
+
} ) );
|
|
366
|
+
expect( getTraceDestinations ).toHaveBeenCalledWith( info.memo.traceInfo );
|
|
202
367
|
} );
|
|
203
368
|
|
|
204
|
-
it( '
|
|
205
|
-
inWorkflowContextMock.mockReturnValue( false );
|
|
369
|
+
it( 'falls back to workflow type matching when replaying an old child call without memo.stack', async () => {
|
|
206
370
|
const { workflow } = await import( './workflow.js' );
|
|
371
|
+
const { ParentClosePolicy } = await import( '@temporalio/workflow' );
|
|
372
|
+
setWorkflowInfo( { workflowType: 'old_parent_wf', memo: { traceInfo: { workflowId: 'root-workflow' } } } );
|
|
373
|
+
executeChildMock.mockResolvedValueOnce( { output: { child: 'replayed' } } );
|
|
374
|
+
const fn = vi.fn();
|
|
207
375
|
|
|
208
|
-
const wf = workflow( {
|
|
209
|
-
name: '
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
} );
|
|
376
|
+
const wf = workflow( workflowDefinition( {
|
|
377
|
+
name: 'old_child_wf',
|
|
378
|
+
inputSchema: z.object( { id: z.number() } ),
|
|
379
|
+
outputSchema: z.object( { child: z.string() } ),
|
|
380
|
+
fn
|
|
381
|
+
} ) );
|
|
215
382
|
|
|
216
|
-
|
|
217
|
-
expect(
|
|
383
|
+
await expect( wf( { id: 7 } ) ).resolves.toEqual( { child: 'replayed' } );
|
|
384
|
+
expect( fn ).not.toHaveBeenCalled();
|
|
385
|
+
expect( executeChildMock ).toHaveBeenCalledWith( 'old_child_wf', expect.objectContaining( {
|
|
386
|
+
args: [ { id: 7 } ],
|
|
387
|
+
parentClosePolicy: ParentClosePolicy.TERMINATE,
|
|
388
|
+
memo: { traceInfo: { workflowId: 'root-workflow' } }
|
|
389
|
+
} ) );
|
|
390
|
+
expect( proxyActivitiesMock ).not.toHaveBeenCalled();
|
|
218
391
|
} );
|
|
219
|
-
} );
|
|
220
392
|
|
|
221
|
-
|
|
222
|
-
it( 'throws ValidationError when input does not match inputSchema', async () => {
|
|
393
|
+
it( 'does not fallback to child execution when the replayed workflow type matches an alias', async () => {
|
|
223
394
|
const { workflow } = await import( './workflow.js' );
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const wf = workflow( {
|
|
227
|
-
name: '
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
} );
|
|
395
|
+
setWorkflowInfo( { workflowType: 'old_root_wf', memo: {} } );
|
|
396
|
+
|
|
397
|
+
const wf = workflow( workflowDefinition( {
|
|
398
|
+
name: 'renamed_root_wf',
|
|
399
|
+
aliases: [ 'old_root_wf' ],
|
|
400
|
+
outputSchema: z.object( { ok: z.boolean() } ),
|
|
401
|
+
fn: async () => ( { ok: true } )
|
|
402
|
+
} ) );
|
|
233
403
|
|
|
234
|
-
await expect( wf( {
|
|
235
|
-
|
|
404
|
+
await expect( wf( {} ) ).resolves.toEqual( {
|
|
405
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
406
|
+
output: { ok: true },
|
|
407
|
+
trace: { destinations: { local: '/tmp/trace' } }
|
|
408
|
+
} );
|
|
409
|
+
expect( executeChildMock ).not.toHaveBeenCalled();
|
|
236
410
|
} );
|
|
237
411
|
|
|
238
|
-
it( '
|
|
412
|
+
it( 'propagates executeChild errors without root ApplicationFailure wrapping', async () => {
|
|
239
413
|
const { workflow } = await import( './workflow.js' );
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
inputSchema: z.object( {} ),
|
|
246
|
-
outputSchema: z.object( { required: z.string() } ),
|
|
247
|
-
fn: async () => ( { other: 1 } )
|
|
248
|
-
} );
|
|
414
|
+
const error = new Error( 'child failed' );
|
|
415
|
+
setWorkflowInfo( { memo: { stack: [ 'workflow-123' ] } } );
|
|
416
|
+
executeChildMock.mockRejectedValueOnce( error );
|
|
417
|
+
|
|
418
|
+
const wf = workflow( workflowDefinition( { name: 'failing_child_wf' } ) );
|
|
249
419
|
|
|
250
|
-
await expect( wf( {} ) ).rejects.
|
|
251
|
-
await expect( wf( {} ) ).rejects.toThrow( /Workflow validate_out_wf output/ );
|
|
420
|
+
await expect( wf( {} ) ).rejects.toBe( error );
|
|
252
421
|
} );
|
|
253
422
|
} );
|
|
254
423
|
|
|
255
|
-
describe( '
|
|
256
|
-
it( '
|
|
257
|
-
traceDestinationsStepMock.mockResolvedValueOnce( {
|
|
258
|
-
output: { local: '/tmp/wrapped-trace' },
|
|
259
|
-
aggregations: null,
|
|
260
|
-
[ACTIVITY_WRAPPER_VERSION_FIELD]: 1
|
|
261
|
-
} );
|
|
424
|
+
describe( 'workflow execution path', () => {
|
|
425
|
+
it( 'initializes root memo, gets trace destinations, validates output, and returns an envelope', async () => {
|
|
262
426
|
const { workflow } = await import( './workflow.js' );
|
|
427
|
+
const getTraceDestinations = vi.fn().mockResolvedValue( activityOutput( { local: '/tmp/root-trace' } ) );
|
|
428
|
+
const info = setWorkflowInfo( { workflowType: 'root_wf', memo: {} } );
|
|
429
|
+
mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: getTraceDestinations } );
|
|
263
430
|
|
|
264
|
-
const wf = workflow( {
|
|
265
|
-
name: '
|
|
266
|
-
description: 'Wrapped trace',
|
|
267
|
-
inputSchema: z.object( {} ),
|
|
431
|
+
const wf = workflow( workflowDefinition( {
|
|
432
|
+
name: 'root_wf',
|
|
268
433
|
outputSchema: z.object( { ok: z.boolean() } ),
|
|
269
|
-
|
|
270
|
-
|
|
434
|
+
options: {
|
|
435
|
+
disableTrace: true,
|
|
436
|
+
activityOptions: {
|
|
437
|
+
startToCloseTimeout: '5m',
|
|
438
|
+
retry: { maximumAttempts: 5 }
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
fn: async ( _, context ) => ( { ok: context.info.workflowId === 'workflow-123' } )
|
|
442
|
+
} ) );
|
|
271
443
|
|
|
272
|
-
|
|
273
|
-
expect( traceDestinationsStepMock ).toHaveBeenCalledTimes( 1 );
|
|
274
|
-
expect( result ).toEqual( {
|
|
444
|
+
await expect( wf( {} ) ).resolves.toEqual( {
|
|
275
445
|
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
276
446
|
output: { ok: true },
|
|
277
|
-
trace: { destinations: { local: '/tmp/
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
disableTrace: false
|
|
447
|
+
trace: { destinations: { local: '/tmp/root-trace' } }
|
|
448
|
+
} );
|
|
449
|
+
expect( info.memo.stack ).toEqual( [ 'workflow-123' ] );
|
|
450
|
+
expect( info.memo.traceInfo ).toEqual( {
|
|
451
|
+
workflowId: 'workflow-123',
|
|
452
|
+
workflowType: 'root_wf',
|
|
453
|
+
runId: 'run-123',
|
|
454
|
+
startTime: new Date( '2025-01-01T00:00:00.000Z' ).getTime(),
|
|
455
|
+
disableTrace: true
|
|
287
456
|
} );
|
|
288
|
-
expect(
|
|
289
|
-
|
|
457
|
+
expect( info.memo.activityOptions ).toEqual( expect.objectContaining( {
|
|
458
|
+
startToCloseTimeout: '5m',
|
|
459
|
+
heartbeatTimeout: '5m',
|
|
460
|
+
retry: expect.objectContaining( { maximumAttempts: 5 } )
|
|
461
|
+
} ) );
|
|
462
|
+
expect( proxyActivitiesMock ).toHaveBeenCalledWith( info.memo.activityOptions );
|
|
463
|
+
expect( getTraceDestinations ).toHaveBeenCalledWith( info.memo.traceInfo );
|
|
290
464
|
} );
|
|
291
465
|
|
|
292
|
-
it( '
|
|
466
|
+
it( 'runs non-root workflow execution without rebuilding trace info or fetching trace destinations', async () => {
|
|
293
467
|
const { workflow } = await import( './workflow.js' );
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
468
|
+
const getTraceDestinations = vi.fn().mockResolvedValue( activityOutput( { local: '/tmp/trace' } ) );
|
|
469
|
+
const memo = {
|
|
470
|
+
stack: [ 'root-workflow' ],
|
|
471
|
+
traceInfo: { workflowId: 'root-workflow' },
|
|
472
|
+
activityOptions: {
|
|
473
|
+
startToCloseTimeout: '9m',
|
|
474
|
+
retry: { maximumAttempts: 8 }
|
|
298
475
|
}
|
|
476
|
+
};
|
|
477
|
+
mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: getTraceDestinations } );
|
|
478
|
+
const info = setWorkflowInfo( {
|
|
479
|
+
workflowId: 'child-workflow',
|
|
480
|
+
root: { workflowId: 'root-workflow', runId: 'root-run' },
|
|
481
|
+
memo
|
|
299
482
|
} );
|
|
300
483
|
|
|
301
|
-
const wf = workflow( {
|
|
302
|
-
name: '
|
|
303
|
-
|
|
304
|
-
|
|
484
|
+
const wf = workflow( workflowDefinition( {
|
|
485
|
+
name: 'nested_wf',
|
|
486
|
+
options: {
|
|
487
|
+
activityOptions: {
|
|
488
|
+
startToCloseTimeout: '1m',
|
|
489
|
+
retry: { maximumAttempts: 2 }
|
|
490
|
+
}
|
|
491
|
+
},
|
|
305
492
|
outputSchema: z.object( { ok: z.boolean() } ),
|
|
306
|
-
fn: async () => {
|
|
307
|
-
|
|
308
|
-
return { ok: true };
|
|
309
|
-
}
|
|
310
|
-
} );
|
|
493
|
+
fn: async () => ( { ok: true } )
|
|
494
|
+
} ) );
|
|
311
495
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
expect(
|
|
496
|
+
await expect( wf( {} ) ).resolves.toEqual( {
|
|
497
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
498
|
+
output: { ok: true }
|
|
499
|
+
} );
|
|
500
|
+
expect( info.memo.stack ).toEqual( [ 'root-workflow', 'child-workflow' ] );
|
|
501
|
+
expect( info.memo.traceInfo ).toBe( memo.traceInfo );
|
|
502
|
+
expect( info.memo.activityOptions ).toEqual( expect.objectContaining( {
|
|
503
|
+
startToCloseTimeout: '9m',
|
|
504
|
+
retry: expect.objectContaining( { maximumAttempts: 8 } )
|
|
505
|
+
} ) );
|
|
506
|
+
expect( proxyActivitiesMock ).toHaveBeenCalledWith( info.memo.activityOptions );
|
|
507
|
+
expect( getTraceDestinations ).not.toHaveBeenCalled();
|
|
317
508
|
} );
|
|
318
509
|
|
|
319
|
-
it( '
|
|
510
|
+
it( 'omits trace from the root result when getTraceDestinations returns no destinations', async () => {
|
|
320
511
|
const { workflow } = await import( './workflow.js' );
|
|
512
|
+
setWorkflowInfo( { workflowType: 'no_trace_dest_wf' } );
|
|
513
|
+
mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( null ) ) } );
|
|
321
514
|
|
|
322
|
-
const wf = workflow( {
|
|
323
|
-
name: '
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
outputSchema: z.object( {} ),
|
|
327
|
-
options: { disableTrace: true },
|
|
328
|
-
fn: async () => ( {} )
|
|
329
|
-
} );
|
|
330
|
-
|
|
331
|
-
await wf( {} );
|
|
332
|
-
expect( workflowInfoMock().memo.traceInfo ).toEqual( expect.objectContaining( {
|
|
333
|
-
workflowId: 'trace-workflow-id',
|
|
334
|
-
workflowType: 'trace-workflow-type',
|
|
335
|
-
runId: 'trace-run-id',
|
|
336
|
-
disableTrace: true
|
|
515
|
+
const wf = workflow( workflowDefinition( {
|
|
516
|
+
name: 'no_trace_dest_wf',
|
|
517
|
+
outputSchema: z.object( { ok: z.boolean() } ),
|
|
518
|
+
fn: async () => ( { ok: true } )
|
|
337
519
|
} ) );
|
|
338
|
-
expect( traceInfoBuildMock ).toHaveBeenCalledWith( { disableTrace: true } );
|
|
339
|
-
} );
|
|
340
|
-
} );
|
|
341
520
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
workflowInfoMock.mockReturnValue( {
|
|
346
|
-
...workflowInfoReturn,
|
|
347
|
-
root: { workflowId: 'root-wf', runId: 'root-run' },
|
|
348
|
-
memo: { traceInfo }
|
|
521
|
+
await expect( wf( {} ) ).resolves.toEqual( {
|
|
522
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
523
|
+
output: { ok: true }
|
|
349
524
|
} );
|
|
350
|
-
|
|
525
|
+
} );
|
|
351
526
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
fn: async () => ( { x: 'child' } )
|
|
527
|
+
it( 'supports old unwrapped trace destination activity results during replay', async () => {
|
|
528
|
+
const { workflow } = await import( './workflow.js' );
|
|
529
|
+
setWorkflowInfo( { workflowType: 'old_trace_payload_wf' } );
|
|
530
|
+
mockActivities( {
|
|
531
|
+
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( { local: '/tmp/old-trace' } )
|
|
358
532
|
} );
|
|
359
533
|
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
534
|
+
const wf = workflow( workflowDefinition( {
|
|
535
|
+
name: 'old_trace_payload_wf',
|
|
536
|
+
outputSchema: z.object( { ok: z.boolean() } ),
|
|
537
|
+
fn: async () => ( { ok: true } )
|
|
538
|
+
} ) );
|
|
539
|
+
|
|
540
|
+
await expect( wf( {} ) ).resolves.toEqual( {
|
|
364
541
|
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
365
|
-
output: {
|
|
366
|
-
|
|
542
|
+
output: { ok: true },
|
|
543
|
+
trace: { destinations: { local: '/tmp/old-trace' } }
|
|
367
544
|
} );
|
|
368
545
|
} );
|
|
369
|
-
} );
|
|
370
|
-
|
|
371
|
-
describe( 'bound this: invokeStep, invokeSharedStep, invokeEvaluator', () => {
|
|
372
|
-
it( 'invokeStep unwraps step output and merges step aggregations', async () => {
|
|
373
|
-
const stepSpy = vi.fn().mockResolvedValue( {
|
|
374
|
-
output: { value: 'wrapped' },
|
|
375
|
-
aggregations: { cost: { total: 0 }, tokens: { total: 0 }, httpRequests: { total: 1 } },
|
|
376
|
-
[ACTIVITY_WRAPPER_VERSION_FIELD]: 1
|
|
377
|
-
} );
|
|
378
|
-
proxyActivitiesMock.mockImplementation( () => createStepsProxy( stepSpy ) );
|
|
379
546
|
|
|
547
|
+
it( 'supports old null trace destination activity results during replay', async () => {
|
|
380
548
|
const { workflow } = await import( './workflow.js' );
|
|
549
|
+
setWorkflowInfo( { workflowType: 'old_null_trace_payload_wf' } );
|
|
550
|
+
mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( null ) } );
|
|
381
551
|
|
|
382
|
-
const wf = workflow( {
|
|
383
|
-
name: '
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
async fn() {
|
|
388
|
-
return this.invokeStep( 'myStep', { foo: 1 } );
|
|
389
|
-
}
|
|
390
|
-
} );
|
|
552
|
+
const wf = workflow( workflowDefinition( {
|
|
553
|
+
name: 'old_null_trace_payload_wf',
|
|
554
|
+
outputSchema: z.object( { ok: z.boolean() } ),
|
|
555
|
+
fn: async () => ( { ok: true } )
|
|
556
|
+
} ) );
|
|
391
557
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
558
|
+
await expect( wf( {} ) ).resolves.toEqual( {
|
|
559
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
560
|
+
output: { ok: true }
|
|
561
|
+
} );
|
|
396
562
|
} );
|
|
397
563
|
|
|
398
|
-
it( '
|
|
564
|
+
it( 'validates input and output inside workflow context', async () => {
|
|
399
565
|
const { workflow } = await import( './workflow.js' );
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return vi.fn();
|
|
410
|
-
}
|
|
566
|
+
const inputError = new ValidationError( 'invalid workflow input' );
|
|
567
|
+
const outputError = new ValidationError( 'invalid workflow output' );
|
|
568
|
+
setWorkflowInfo( { workflowType: 'runtime_validation_wf' } );
|
|
569
|
+
|
|
570
|
+
const wf = workflow( workflowDefinition( {
|
|
571
|
+
name: 'runtime_validation_wf',
|
|
572
|
+
inputSchema: z.object( { value: z.string() } ),
|
|
573
|
+
outputSchema: z.object( { result: z.string() } ),
|
|
574
|
+
fn: async () => ( { result: 1 } )
|
|
411
575
|
} ) );
|
|
412
576
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
description: 'Shared',
|
|
416
|
-
inputSchema: z.object( {} ),
|
|
417
|
-
outputSchema: z.object( {} ),
|
|
418
|
-
async fn() {
|
|
419
|
-
await this.invokeSharedStep( 'sharedStep', { data: 2 } );
|
|
420
|
-
return {};
|
|
421
|
-
}
|
|
577
|
+
validateInputMock.mockImplementationOnce( () => {
|
|
578
|
+
throw inputError;
|
|
422
579
|
} );
|
|
580
|
+
await expect( wf( { value: 1 } ) ).rejects.toBe( inputError );
|
|
581
|
+
expect( inputError[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
|
|
423
582
|
|
|
424
|
-
|
|
425
|
-
|
|
583
|
+
setWorkflowInfo( { workflowType: 'runtime_validation_wf', memo: {} } );
|
|
584
|
+
validateOutputMock.mockImplementationOnce( () => {
|
|
585
|
+
throw outputError;
|
|
586
|
+
} );
|
|
587
|
+
await expect( wf( { value: 'ok' } ) ).rejects.toBe( outputError );
|
|
588
|
+
expect( outputError[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
|
|
426
589
|
} );
|
|
590
|
+
} );
|
|
427
591
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
592
|
+
describe( 'activity dispatchers', () => {
|
|
593
|
+
it( 'invokes workflow-scoped steps and evaluators and unwraps activity output', async () => {
|
|
594
|
+
const { workflow } = await import( './workflow.js' );
|
|
595
|
+
setWorkflowInfo( { workflowType: 'dispatch_wf' } );
|
|
596
|
+
const step = vi.fn().mockResolvedValue( activityOutput( 'step-output' ) );
|
|
597
|
+
const evaluator = vi.fn().mockResolvedValue( activityOutput( 'eval-output' ) );
|
|
598
|
+
mockActivities( {
|
|
599
|
+
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( null ) ),
|
|
600
|
+
'dispatch_wf#stepA': step,
|
|
601
|
+
'dispatch_wf#evalA': evaluator
|
|
602
|
+
} );
|
|
603
|
+
|
|
604
|
+
const wf = workflow( workflowDefinition( {
|
|
605
|
+
name: 'dispatch_wf',
|
|
606
|
+
outputSchema: z.object( { stepResult: z.string(), evalResult: z.string() } ),
|
|
607
|
+
async fn() {
|
|
608
|
+
return {
|
|
609
|
+
stepResult: await this.invokeStep( 'stepA', { a: 1 }, { b: 2 } ),
|
|
610
|
+
evalResult: await this.invokeEvaluator( 'evalA', { c: 3 } )
|
|
611
|
+
};
|
|
439
612
|
}
|
|
440
613
|
} ) );
|
|
441
614
|
|
|
442
|
-
|
|
615
|
+
await expect( wf( {} ) ).resolves.toEqual( {
|
|
616
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
617
|
+
output: { stepResult: 'step-output', evalResult: 'eval-output' }
|
|
618
|
+
} );
|
|
619
|
+
expect( step ).toHaveBeenCalledWith( { a: 1 }, { b: 2 } );
|
|
620
|
+
expect( evaluator ).toHaveBeenCalledWith( { c: 3 } );
|
|
621
|
+
} );
|
|
443
622
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
623
|
+
it( 'invokes shared steps and shared evaluators with the shared prefix', async () => {
|
|
624
|
+
const { workflow } = await import( './workflow.js' );
|
|
625
|
+
setWorkflowInfo( { workflowType: 'shared_dispatch_wf' } );
|
|
626
|
+
const sharedStep = vi.fn().mockResolvedValue( activityOutput( 'shared-step-output' ) );
|
|
627
|
+
const sharedEvaluator = vi.fn().mockResolvedValue( activityOutput( 'shared-eval-output' ) );
|
|
628
|
+
mockActivities( {
|
|
629
|
+
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( null ) ),
|
|
630
|
+
[`${SHARED_STEP_PREFIX}#stepA`]: sharedStep,
|
|
631
|
+
[`${SHARED_STEP_PREFIX}#evalA`]: sharedEvaluator
|
|
632
|
+
} );
|
|
633
|
+
|
|
634
|
+
const wf = workflow( workflowDefinition( {
|
|
635
|
+
name: 'shared_dispatch_wf',
|
|
636
|
+
outputSchema: z.object( { stepResult: z.string(), evalResult: z.string() } ),
|
|
449
637
|
async fn() {
|
|
450
|
-
|
|
451
|
-
|
|
638
|
+
return {
|
|
639
|
+
stepResult: await this.invokeSharedStep( 'stepA' ),
|
|
640
|
+
evalResult: await this.invokeSharedEvaluator( 'evalA', { x: 1 } )
|
|
641
|
+
};
|
|
452
642
|
}
|
|
453
|
-
} );
|
|
643
|
+
} ) );
|
|
454
644
|
|
|
455
|
-
await wf( {} )
|
|
456
|
-
|
|
645
|
+
await expect( wf( {} ) ).resolves.toEqual( {
|
|
646
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
647
|
+
output: { stepResult: 'shared-step-output', evalResult: 'shared-eval-output' }
|
|
648
|
+
} );
|
|
649
|
+
expect( sharedStep ).toHaveBeenCalledWith();
|
|
650
|
+
expect( sharedEvaluator ).toHaveBeenCalledWith( { x: 1 } );
|
|
457
651
|
} );
|
|
458
|
-
} );
|
|
459
652
|
|
|
460
|
-
|
|
461
|
-
it( 'calls executeChild with correct args and TERMINATE when not detached', async () => {
|
|
653
|
+
it( 'supports old unwrapped step and evaluator activity results during replay', async () => {
|
|
462
654
|
const { workflow } = await import( './workflow.js' );
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
655
|
+
setWorkflowInfo( { workflowType: 'old_activity_payload_wf' } );
|
|
656
|
+
const step = vi.fn().mockResolvedValue( 'legacy-step-output' );
|
|
657
|
+
const evaluator = vi.fn().mockResolvedValue( 'legacy-eval-output' );
|
|
658
|
+
mockActivities( {
|
|
659
|
+
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( null ) ),
|
|
660
|
+
'old_activity_payload_wf#stepA': step,
|
|
661
|
+
'old_activity_payload_wf#evalA': evaluator
|
|
662
|
+
} );
|
|
663
|
+
|
|
664
|
+
const wf = workflow( workflowDefinition( {
|
|
665
|
+
name: 'old_activity_payload_wf',
|
|
666
|
+
outputSchema: z.object( { stepResult: z.string(), evalResult: z.string() } ),
|
|
471
667
|
async fn() {
|
|
472
|
-
|
|
473
|
-
|
|
668
|
+
return {
|
|
669
|
+
stepResult: await this.invokeStep( 'stepA' ),
|
|
670
|
+
evalResult: await this.invokeEvaluator( 'evalA' )
|
|
671
|
+
};
|
|
474
672
|
}
|
|
475
|
-
} );
|
|
673
|
+
} ) );
|
|
476
674
|
|
|
477
|
-
await wf( {} )
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
workflowId: expect.stringMatching( /^wf-test-123-/ ),
|
|
481
|
-
parentClosePolicy: ParentClosePolicy.TERMINATE,
|
|
482
|
-
memo: expect.objectContaining( {
|
|
483
|
-
traceInfo: {
|
|
484
|
-
workflowId: 'trace-workflow-id',
|
|
485
|
-
workflowType: 'trace-workflow-type',
|
|
486
|
-
runId: 'trace-run-id',
|
|
487
|
-
startTime: 12345,
|
|
488
|
-
disableTrace: false
|
|
489
|
-
},
|
|
490
|
-
activityOptions: expect.objectContaining( {
|
|
491
|
-
startToCloseTimeout: '20m',
|
|
492
|
-
heartbeatTimeout: '5m'
|
|
493
|
-
} )
|
|
494
|
-
} )
|
|
675
|
+
await expect( wf( {} ) ).resolves.toEqual( {
|
|
676
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
677
|
+
output: { stepResult: 'legacy-step-output', evalResult: 'legacy-eval-output' }
|
|
495
678
|
} );
|
|
496
|
-
const [ , childOptions ] = executeChildMock.mock.calls[0];
|
|
497
|
-
expect( childOptions.memo ).not.toHaveProperty( 'executionContext' );
|
|
498
|
-
expect( childOptions.memo ).not.toHaveProperty( 'parentId' );
|
|
499
679
|
} );
|
|
680
|
+
} );
|
|
500
681
|
|
|
501
|
-
|
|
682
|
+
describe( 'error handling', () => {
|
|
683
|
+
it( 'attaches root trace destinations to root workflow errors before rethrowing', async () => {
|
|
502
684
|
const { workflow } = await import( './workflow.js' );
|
|
503
|
-
const
|
|
504
|
-
|
|
685
|
+
const error = new Error( 'root failed' );
|
|
686
|
+
setWorkflowInfo( { workflowType: 'root_error_wf' } );
|
|
505
687
|
|
|
506
|
-
const wf = workflow( {
|
|
507
|
-
name: '
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
outputSchema: z.object( {} ),
|
|
511
|
-
async fn() {
|
|
512
|
-
await this.startWorkflow( 'child_wf', null, { detached: true } );
|
|
513
|
-
return {};
|
|
688
|
+
const wf = workflow( workflowDefinition( {
|
|
689
|
+
name: 'root_error_wf',
|
|
690
|
+
fn: async () => {
|
|
691
|
+
throw error;
|
|
514
692
|
}
|
|
515
|
-
} );
|
|
516
|
-
|
|
517
|
-
await wf( {} );
|
|
518
|
-
expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
|
|
519
|
-
args: [ null ],
|
|
520
|
-
parentClosePolicy: ParentClosePolicy.ABANDON
|
|
521
693
|
} ) );
|
|
694
|
+
|
|
695
|
+
const thrown = await wf( {} ).catch( e => e );
|
|
696
|
+
expect( thrown ).toBe( error );
|
|
697
|
+
expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
|
|
522
698
|
} );
|
|
523
699
|
|
|
524
|
-
it( '
|
|
700
|
+
it( 'preserves existing error details when attaching root trace metadata', async () => {
|
|
525
701
|
const { workflow } = await import( './workflow.js' );
|
|
526
|
-
|
|
702
|
+
const error = new Error( 'root failed with details' );
|
|
703
|
+
error.details = [ { domain: { reason: 'bad-input' } } ];
|
|
704
|
+
setWorkflowInfo( { workflowType: 'root_error_existing_details_wf' } );
|
|
527
705
|
|
|
528
|
-
const wf = workflow( {
|
|
529
|
-
name: '
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
outputSchema: z.object( {} ),
|
|
533
|
-
async fn() {
|
|
534
|
-
await this.startWorkflow( 'child_wf' );
|
|
535
|
-
return {};
|
|
706
|
+
const wf = workflow( workflowDefinition( {
|
|
707
|
+
name: 'root_error_existing_details_wf',
|
|
708
|
+
fn: async () => {
|
|
709
|
+
throw error;
|
|
536
710
|
}
|
|
537
|
-
} );
|
|
538
|
-
|
|
539
|
-
await wf( {} );
|
|
540
|
-
expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
|
|
541
|
-
args: []
|
|
542
711
|
} ) );
|
|
712
|
+
|
|
713
|
+
await expect( wf( {} ) ).rejects.toBe( error );
|
|
714
|
+
expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
|
|
715
|
+
expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
|
|
543
716
|
} );
|
|
544
717
|
|
|
545
|
-
it( '
|
|
718
|
+
it( 'rethrows root workflow errors without metadata when trace destinations are unavailable', async () => {
|
|
546
719
|
const { workflow } = await import( './workflow.js' );
|
|
547
|
-
|
|
720
|
+
setWorkflowInfo( { workflowType: 'root_error_no_trace_wf' } );
|
|
721
|
+
mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( null ) ) } );
|
|
722
|
+
const error = new Error( 'root failed without trace' );
|
|
548
723
|
|
|
549
|
-
const wf = workflow( {
|
|
550
|
-
name: '
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
outputSchema: z.object( {} ),
|
|
554
|
-
async fn() {
|
|
555
|
-
await this.startWorkflow( 'child_wf', { id: 1 }, {
|
|
556
|
-
options: {
|
|
557
|
-
activityOptions: {
|
|
558
|
-
startToCloseTimeout: '2m',
|
|
559
|
-
retry: { maximumAttempts: 7 }
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
} );
|
|
563
|
-
return {};
|
|
724
|
+
const wf = workflow( workflowDefinition( {
|
|
725
|
+
name: 'root_error_no_trace_wf',
|
|
726
|
+
fn: async () => {
|
|
727
|
+
throw error;
|
|
564
728
|
}
|
|
565
|
-
} );
|
|
566
|
-
|
|
567
|
-
await wf( {} );
|
|
568
|
-
expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
|
|
569
|
-
memo: expect.objectContaining( {
|
|
570
|
-
traceInfo: expect.objectContaining( {
|
|
571
|
-
workflowId: 'trace-workflow-id',
|
|
572
|
-
runId: 'trace-run-id'
|
|
573
|
-
} ),
|
|
574
|
-
activityOptions: expect.objectContaining( {
|
|
575
|
-
startToCloseTimeout: '2m',
|
|
576
|
-
heartbeatTimeout: '5m',
|
|
577
|
-
retry: expect.objectContaining( {
|
|
578
|
-
initialInterval: '10s',
|
|
579
|
-
maximumAttempts: 7
|
|
580
|
-
} )
|
|
581
|
-
} )
|
|
582
|
-
} )
|
|
583
729
|
} ) );
|
|
730
|
+
|
|
731
|
+
await expect( wf( {} ) ).rejects.toBe( error );
|
|
732
|
+
expect( error[METADATA_ACCESS_SYMBOL] ).toBeUndefined();
|
|
584
733
|
} );
|
|
585
734
|
|
|
586
|
-
it( '
|
|
735
|
+
it( 'preserves existing error details when trace destinations are unavailable', async () => {
|
|
587
736
|
const { workflow } = await import( './workflow.js' );
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
tokens: { total: 4, input: 4 },
|
|
593
|
-
httpRequests: { total: 2 }
|
|
594
|
-
}
|
|
595
|
-
} );
|
|
737
|
+
setWorkflowInfo( { workflowType: 'root_error_existing_details_no_trace_wf' } );
|
|
738
|
+
mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( null ) ) } );
|
|
739
|
+
const error = new Error( 'root failed without trace' );
|
|
740
|
+
error.details = [ { domain: { reason: 'bad-input' } } ];
|
|
596
741
|
|
|
597
|
-
const wf = workflow( {
|
|
598
|
-
name: '
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
outputSchema: z.object( { child: z.string() } ),
|
|
602
|
-
async fn() {
|
|
603
|
-
return this.startWorkflow( 'child_wf', { id: 1 } );
|
|
742
|
+
const wf = workflow( workflowDefinition( {
|
|
743
|
+
name: 'root_error_existing_details_no_trace_wf',
|
|
744
|
+
fn: async () => {
|
|
745
|
+
throw error;
|
|
604
746
|
}
|
|
605
|
-
} );
|
|
747
|
+
} ) );
|
|
606
748
|
|
|
607
|
-
|
|
608
|
-
expect(
|
|
609
|
-
|
|
610
|
-
output: { child: 'ok' },
|
|
611
|
-
trace: { destinations: { local: '/tmp/trace' } },
|
|
612
|
-
aggregations: {
|
|
613
|
-
cost: { total: 1.5 },
|
|
614
|
-
tokens: { total: 4, input: 4 },
|
|
615
|
-
httpRequests: { total: 2 }
|
|
616
|
-
}
|
|
617
|
-
} );
|
|
749
|
+
await expect( wf( {} ) ).rejects.toBe( error );
|
|
750
|
+
expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
|
|
751
|
+
expect( error[METADATA_ACCESS_SYMBOL] ).toBeUndefined();
|
|
618
752
|
} );
|
|
619
753
|
|
|
620
|
-
it( '
|
|
754
|
+
it( 'attaches trace metadata to existing root ApplicationFailure without wrapping it', async () => {
|
|
621
755
|
const { workflow } = await import( './workflow.js' );
|
|
622
|
-
const {
|
|
623
|
-
|
|
624
|
-
const
|
|
625
|
-
message: '
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
tokens: { total: 8, output: 8 },
|
|
630
|
-
httpRequests: { total: 0 }
|
|
631
|
-
}
|
|
632
|
-
} ]
|
|
756
|
+
const { ApplicationFailure } = await import( '@temporalio/workflow' );
|
|
757
|
+
setWorkflowInfo( { workflowType: 'root_application_failure_wf' } );
|
|
758
|
+
const error = ApplicationFailure.create( {
|
|
759
|
+
message: 'root application failed',
|
|
760
|
+
type: 'OriginalType',
|
|
761
|
+
nonRetryable: true,
|
|
762
|
+
details: [ { domain: { reason: 'bad-input' } } ]
|
|
633
763
|
} );
|
|
634
|
-
executeChildMock.mockRejectedValueOnce( childError );
|
|
635
764
|
|
|
636
|
-
const wf = workflow( {
|
|
637
|
-
name: '
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
outputSchema: z.object( {} ),
|
|
641
|
-
async fn() {
|
|
642
|
-
await this.startWorkflow( 'child_wf', { id: 1 } );
|
|
643
|
-
return {};
|
|
765
|
+
const wf = workflow( workflowDefinition( {
|
|
766
|
+
name: 'root_application_failure_wf',
|
|
767
|
+
fn: async () => {
|
|
768
|
+
throw error;
|
|
644
769
|
}
|
|
645
|
-
} );
|
|
770
|
+
} ) );
|
|
646
771
|
|
|
647
|
-
await
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
tokens: { total: 8, output: 8 },
|
|
654
|
-
httpRequests: { total: 0 }
|
|
655
|
-
}
|
|
656
|
-
} );
|
|
772
|
+
const thrown = await wf( {} ).catch( e => e );
|
|
773
|
+
|
|
774
|
+
expect( thrown ).toBe( error );
|
|
775
|
+
expect( error.type ).toBe( 'OriginalType' );
|
|
776
|
+
expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
|
|
777
|
+
expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
|
|
657
778
|
} );
|
|
658
|
-
} );
|
|
659
779
|
|
|
660
|
-
|
|
661
|
-
it( 'rethrows error from fn with trace and aggregation metadata', async () => {
|
|
780
|
+
it( 'rethrows non-root workflow errors without ApplicationFailure wrapping', async () => {
|
|
662
781
|
const { workflow } = await import( './workflow.js' );
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
782
|
+
const error = new Error( 'nested failed' );
|
|
783
|
+
setWorkflowInfo( {
|
|
784
|
+
workflowId: 'nested-workflow',
|
|
785
|
+
root: { workflowId: 'root-workflow', runId: 'root-run' },
|
|
786
|
+
memo: { stack: [ 'root-workflow' ], traceInfo: { workflowId: 'root-workflow' } }
|
|
787
|
+
} );
|
|
788
|
+
|
|
789
|
+
const wf = workflow( workflowDefinition( {
|
|
790
|
+
name: 'nested_error_wf',
|
|
671
791
|
fn: async () => {
|
|
672
792
|
throw error;
|
|
673
793
|
}
|
|
674
|
-
} );
|
|
794
|
+
} ) );
|
|
675
795
|
|
|
676
|
-
await expect( wf( {} ) ).rejects.
|
|
677
|
-
expect( error[METADATA_ACCESS_SYMBOL] ).
|
|
678
|
-
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
679
|
-
trace: { destinations: { local: '/tmp/trace' } },
|
|
680
|
-
aggregations: null
|
|
681
|
-
} );
|
|
796
|
+
await expect( wf( {} ) ).rejects.toBe( error );
|
|
797
|
+
expect( error[METADATA_ACCESS_SYMBOL] ).toBeUndefined();
|
|
682
798
|
} );
|
|
683
799
|
} );
|
|
684
800
|
} );
|