@outputai/core 0.7.1-next.bd6bd49.0 → 0.7.1-next.c005dac.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/worker/bundle.js +26 -0
- package/src/worker/bundle.spec.js +52 -0
- package/src/worker/check.js +24 -0
- package/src/worker/index.js +1 -1
- package/src/worker/index.spec.js +1 -1
- 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 +4 -7
- package/src/worker/interceptors/workflow.spec.js +49 -42
- package/src/worker/loader_tools.js +1 -1
- package/src/worker/loader_tools.spec.js +36 -0
- 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
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import {
|
|
4
|
-
validateStep,
|
|
5
|
-
validateWorkflow,
|
|
6
|
-
validateRequestPayload,
|
|
7
|
-
validateEvaluator,
|
|
8
|
-
validateExecuteInParallel,
|
|
9
|
-
StaticValidationError
|
|
10
|
-
} from './static.js';
|
|
11
|
-
|
|
12
|
-
const validArgs = Object.freeze( {
|
|
13
|
-
name: 'valid_name',
|
|
14
|
-
description: 'desc',
|
|
15
|
-
inputSchema: z.object( {} ),
|
|
16
|
-
outputSchema: z.object( {} ),
|
|
17
|
-
fn: () => {}
|
|
18
|
-
} );
|
|
19
|
-
|
|
20
|
-
describe( 'interface/validator', () => {
|
|
21
|
-
describe( 'validateStep', () => {
|
|
22
|
-
it( 'passes for valid args', () => {
|
|
23
|
-
expect( () => validateStep( { ...validArgs } ) ).not.toThrow();
|
|
24
|
-
} );
|
|
25
|
-
|
|
26
|
-
it( 'rejects missing name', () => {
|
|
27
|
-
const error = new StaticValidationError( '✖ Invalid input: expected string, received undefined\n → at name' );
|
|
28
|
-
expect( () => validateStep( { ...validArgs, name: undefined } ) ).toThrow( error );
|
|
29
|
-
} );
|
|
30
|
-
|
|
31
|
-
it( 'rejects non-string name', () => {
|
|
32
|
-
const error = new StaticValidationError( '✖ Invalid input: expected string, received number\n → at name' );
|
|
33
|
-
expect( () => validateStep( { ...validArgs, name: 123 } ) ).toThrow( error );
|
|
34
|
-
} );
|
|
35
|
-
|
|
36
|
-
it( 'rejects invalid name pattern', () => {
|
|
37
|
-
const error = new StaticValidationError( '✖ Invalid string: must match pattern /^[a-z_][a-z0-9_]*$/i\n → at name' );
|
|
38
|
-
expect( () => validateStep( { ...validArgs, name: '-bad' } ) ).toThrow( error );
|
|
39
|
-
} );
|
|
40
|
-
|
|
41
|
-
it( 'rejects non-string description', () => {
|
|
42
|
-
const error = new StaticValidationError( '✖ Invalid input: expected string, received number\n → at description' );
|
|
43
|
-
expect( () => validateStep( { ...validArgs, description: 10 } ) ).toThrow( error );
|
|
44
|
-
} );
|
|
45
|
-
|
|
46
|
-
it( 'rejects non-Zod inputSchema', () => {
|
|
47
|
-
const error = new StaticValidationError( '✖ Schema must be a Zod schema\n → at inputSchema' );
|
|
48
|
-
expect( () => validateStep( { ...validArgs, inputSchema: 'not-a-zod-schema' } ) ).toThrow( error );
|
|
49
|
-
} );
|
|
50
|
-
|
|
51
|
-
it( 'rejects JSON Schema inputSchema', () => {
|
|
52
|
-
const error = new StaticValidationError( '✖ Schema must be a Zod schema\n → at inputSchema' );
|
|
53
|
-
expect( () => validateStep( { ...validArgs, inputSchema: { type: 'object' } } ) ).toThrow( error );
|
|
54
|
-
} );
|
|
55
|
-
|
|
56
|
-
it( 'rejects non-Zod outputSchema', () => {
|
|
57
|
-
const error = new StaticValidationError( '✖ Schema must be a Zod schema\n → at outputSchema' );
|
|
58
|
-
expect( () => validateStep( { ...validArgs, outputSchema: 10 } ) ).toThrow( error );
|
|
59
|
-
} );
|
|
60
|
-
|
|
61
|
-
it( 'rejects JSON Schema outputSchema', () => {
|
|
62
|
-
const error = new StaticValidationError( '✖ Schema must be a Zod schema\n → at outputSchema' );
|
|
63
|
-
expect( () => validateStep( { ...validArgs, outputSchema: { type: 'string' } } ) ).toThrow( error );
|
|
64
|
-
} );
|
|
65
|
-
|
|
66
|
-
it( 'rejects missing fn', () => {
|
|
67
|
-
const error = new StaticValidationError( '✖ Invalid input: expected function, received undefined\n → at fn' );
|
|
68
|
-
expect( () => validateStep( { ...validArgs, fn: undefined } ) ).toThrow( error );
|
|
69
|
-
} );
|
|
70
|
-
|
|
71
|
-
it( 'rejects non-function fn', () => {
|
|
72
|
-
const error = new StaticValidationError( '✖ Invalid input: expected function, received string\n → at fn' );
|
|
73
|
-
expect( () => validateStep( { ...validArgs, fn: 'not-fn' } ) ).toThrow( error );
|
|
74
|
-
} );
|
|
75
|
-
|
|
76
|
-
it( 'passes with options.activityOptions.retry (second-level options)', () => {
|
|
77
|
-
const args = {
|
|
78
|
-
...validArgs,
|
|
79
|
-
options: {
|
|
80
|
-
activityOptions: {
|
|
81
|
-
retry: {
|
|
82
|
-
initialInterval: '1s',
|
|
83
|
-
backoffCoefficient: 2,
|
|
84
|
-
maximumInterval: '10s',
|
|
85
|
-
maximumAttempts: 3,
|
|
86
|
-
nonRetryableErrorTypes: [ 'SomeError' ]
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
expect( () => validateStep( args ) ).not.toThrow();
|
|
92
|
-
} );
|
|
93
|
-
|
|
94
|
-
it( 'passes with options.activityOptions.activityId as string', () => {
|
|
95
|
-
expect( () => validateStep( { ...validArgs, options: { activityOptions: { activityId: 'act-123' } } } ) ).not.toThrow();
|
|
96
|
-
} );
|
|
97
|
-
|
|
98
|
-
it( 'rejects non-string options.activityOptions.activityId', () => {
|
|
99
|
-
expect( () => validateStep( { ...validArgs, options: { activityOptions: { activityId: 123 } } } ) ).toThrow( StaticValidationError );
|
|
100
|
-
} );
|
|
101
|
-
|
|
102
|
-
it( 'passes with valid options.activityOptions.cancellationType values', () => {
|
|
103
|
-
for ( const v of [ 'TRY_CANCEL', 'WAIT_CANCELLATION_COMPLETED', 'ABANDON' ] ) {
|
|
104
|
-
expect( () => validateStep( { ...validArgs, options: { activityOptions: { cancellationType: v } } } ) ).not.toThrow();
|
|
105
|
-
}
|
|
106
|
-
} );
|
|
107
|
-
|
|
108
|
-
it( 'rejects invalid options.activityOptions.cancellationType', () => {
|
|
109
|
-
const args = { ...validArgs, options: { activityOptions: { cancellationType: 'INVALID' } } };
|
|
110
|
-
expect( () => validateStep( args ) ).toThrow( StaticValidationError );
|
|
111
|
-
} );
|
|
112
|
-
|
|
113
|
-
it( 'accepts duration fields in options.activityOptions', () => {
|
|
114
|
-
const options = {
|
|
115
|
-
activityOptions: {
|
|
116
|
-
heartbeatTimeout: '1s',
|
|
117
|
-
scheduleToCloseTimeout: '2m',
|
|
118
|
-
scheduleToStartTimeout: '3m',
|
|
119
|
-
startToCloseTimeout: '4m'
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
expect( () => validateStep( { ...validArgs, options } ) ).not.toThrow();
|
|
123
|
-
} );
|
|
124
|
-
|
|
125
|
-
it( 'rejects invalid duration string in options.activityOptions.heartbeatTimeout', () => {
|
|
126
|
-
expect( () => validateStep( { ...validArgs, options: { activityOptions: { heartbeatTimeout: '5x' } } } ) ).toThrow( StaticValidationError );
|
|
127
|
-
} );
|
|
128
|
-
|
|
129
|
-
it( 'passes with options.activityOptions.summary string', () => {
|
|
130
|
-
expect( () => validateStep( { ...validArgs, options: { activityOptions: { summary: 'brief' } } } ) ).not.toThrow();
|
|
131
|
-
} );
|
|
132
|
-
|
|
133
|
-
it( 'rejects non-string options.activityOptions.summary', () => {
|
|
134
|
-
expect( () => validateStep( { ...validArgs, options: { activityOptions: { summary: 42 } } } ) ).toThrow( StaticValidationError );
|
|
135
|
-
} );
|
|
136
|
-
|
|
137
|
-
it( 'passes with options.activityOptions.priority valid payload', () => {
|
|
138
|
-
const options = {
|
|
139
|
-
activityOptions: {
|
|
140
|
-
priority: {
|
|
141
|
-
fairnessKey: 'user-1',
|
|
142
|
-
fairnessWeight: 1.5,
|
|
143
|
-
priorityKey: 10
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
expect( () => validateStep( { ...validArgs, options } ) ).not.toThrow();
|
|
148
|
-
} );
|
|
149
|
-
|
|
150
|
-
it( 'rejects invalid options.activityOptions.priority values', () => {
|
|
151
|
-
const options = { activityOptions: { priority: { fairnessWeight: 0, priorityKey: 0 } } };
|
|
152
|
-
expect( () => validateStep( { ...validArgs, options } ) ).toThrow( StaticValidationError );
|
|
153
|
-
} );
|
|
154
|
-
|
|
155
|
-
it( 'rejects invalid options.activityOptions.retry values', () => {
|
|
156
|
-
const options = { activityOptions: { retry: { backoffCoefficient: 0.5, maximumAttempts: 0, nonRetryableErrorTypes: [ 1 ] } } };
|
|
157
|
-
expect( () => validateStep( { ...validArgs, options } ) ).toThrow( StaticValidationError );
|
|
158
|
-
} );
|
|
159
|
-
|
|
160
|
-
it( 'rejects unknown keys inside options.activityOptions due to strictObject', () => {
|
|
161
|
-
expect( () => validateStep( { ...validArgs, options: { activityOptions: { unknownKey: true } } } ) ).toThrow( StaticValidationError );
|
|
162
|
-
} );
|
|
163
|
-
|
|
164
|
-
it( 'rejects unknown top-level keys due to strictObject', () => {
|
|
165
|
-
expect( () => validateStep( { ...validArgs, extra: 123 } ) ).toThrow( StaticValidationError );
|
|
166
|
-
} );
|
|
167
|
-
} );
|
|
168
|
-
|
|
169
|
-
describe( 'validateWorkflow', () => {
|
|
170
|
-
it( 'passes for valid args', () => {
|
|
171
|
-
expect( () => validateWorkflow( { ...validArgs } ) ).not.toThrow();
|
|
172
|
-
} );
|
|
173
|
-
|
|
174
|
-
it( 'passes with options.disableTrace true', () => {
|
|
175
|
-
expect( () => validateWorkflow( { ...validArgs, options: { disableTrace: true } } ) ).not.toThrow();
|
|
176
|
-
} );
|
|
177
|
-
|
|
178
|
-
it( 'passes with options.disableTrace false', () => {
|
|
179
|
-
expect( () => validateWorkflow( { ...validArgs, options: { disableTrace: false } } ) ).not.toThrow();
|
|
180
|
-
} );
|
|
181
|
-
|
|
182
|
-
it( 'passes with options.activityOptions and options.disableTrace', () => {
|
|
183
|
-
expect( () => validateWorkflow( {
|
|
184
|
-
...validArgs,
|
|
185
|
-
options: { activityOptions: { activityId: 'wf-1' }, disableTrace: true }
|
|
186
|
-
} ) ).not.toThrow();
|
|
187
|
-
} );
|
|
188
|
-
|
|
189
|
-
it( 'rejects non-boolean options.disableTrace', () => {
|
|
190
|
-
expect( () => validateWorkflow( { ...validArgs, options: { disableTrace: 'yes' } } ) ).toThrow( StaticValidationError );
|
|
191
|
-
} );
|
|
192
|
-
|
|
193
|
-
it( 'passes with valid aliases array', () => {
|
|
194
|
-
expect( () => validateWorkflow( { ...validArgs, aliases: [ 'old_name', 'legacy_v1' ] } ) ).not.toThrow();
|
|
195
|
-
} );
|
|
196
|
-
|
|
197
|
-
it( 'passes with empty aliases array', () => {
|
|
198
|
-
expect( () => validateWorkflow( { ...validArgs, aliases: [] } ) ).not.toThrow();
|
|
199
|
-
} );
|
|
200
|
-
|
|
201
|
-
it( 'passes without aliases (optional)', () => {
|
|
202
|
-
expect( () => validateWorkflow( { ...validArgs } ) ).not.toThrow();
|
|
203
|
-
} );
|
|
204
|
-
|
|
205
|
-
it( 'rejects aliases with invalid name pattern', () => {
|
|
206
|
-
expect( () => validateWorkflow( { ...validArgs, aliases: [ '-bad' ] } ) ).toThrow( StaticValidationError );
|
|
207
|
-
} );
|
|
208
|
-
|
|
209
|
-
it( 'rejects non-array aliases', () => {
|
|
210
|
-
expect( () => validateWorkflow( { ...validArgs, aliases: 'not_array' } ) ).toThrow( StaticValidationError );
|
|
211
|
-
} );
|
|
212
|
-
} );
|
|
213
|
-
|
|
214
|
-
describe( 'aliases rejected on steps and evaluators', () => {
|
|
215
|
-
it( 'validateStep rejects aliases due to strictObject', () => {
|
|
216
|
-
expect( () => validateStep( { ...validArgs, aliases: [ 'alias' ] } ) ).toThrow( StaticValidationError );
|
|
217
|
-
} );
|
|
218
|
-
|
|
219
|
-
it( 'validateEvaluator rejects aliases due to strictObject', () => {
|
|
220
|
-
const { outputSchema, ...evalArgs } = validArgs;
|
|
221
|
-
expect( () => validateEvaluator( { ...evalArgs, aliases: [ 'alias' ] } ) ).toThrow( StaticValidationError );
|
|
222
|
-
} );
|
|
223
|
-
} );
|
|
224
|
-
|
|
225
|
-
describe( 'validateEvaluator', () => {
|
|
226
|
-
const base = Object.freeze( {
|
|
227
|
-
name: 'valid_name',
|
|
228
|
-
description: 'desc',
|
|
229
|
-
inputSchema: z.object( {} ),
|
|
230
|
-
fn: () => {}
|
|
231
|
-
} );
|
|
232
|
-
|
|
233
|
-
it( 'passes for valid args (no outputSchema)', () => {
|
|
234
|
-
expect( () => validateEvaluator( { ...base } ) ).not.toThrow();
|
|
235
|
-
} );
|
|
236
|
-
|
|
237
|
-
it( 'rejects invalid name pattern', () => {
|
|
238
|
-
const error = new StaticValidationError( '✖ Invalid string: must match pattern /^[a-z_][a-z0-9_]*$/i\n → at name' );
|
|
239
|
-
expect( () => validateEvaluator( { ...base, name: '-bad' } ) ).toThrow( error );
|
|
240
|
-
} );
|
|
241
|
-
|
|
242
|
-
it( 'rejects non-Zod inputSchema', () => {
|
|
243
|
-
const error = new StaticValidationError( '✖ Schema must be a Zod schema\n → at inputSchema' );
|
|
244
|
-
expect( () => validateEvaluator( { ...base, inputSchema: 'not-a-zod-schema' } ) ).toThrow( error );
|
|
245
|
-
} );
|
|
246
|
-
|
|
247
|
-
it( 'rejects missing fn', () => {
|
|
248
|
-
const error = new StaticValidationError( '✖ Invalid input: expected function, received undefined\n → at fn' );
|
|
249
|
-
expect( () => validateEvaluator( { ...base, fn: undefined } ) ).toThrow( error );
|
|
250
|
-
} );
|
|
251
|
-
} );
|
|
252
|
-
|
|
253
|
-
describe( 'validate request', () => {
|
|
254
|
-
it( 'passes with valid http url', () => {
|
|
255
|
-
expect( () => validateRequestPayload( { url: 'http://example.com', method: 'GET' } ) ).not.toThrow();
|
|
256
|
-
} );
|
|
257
|
-
|
|
258
|
-
it( 'passes with valid https url', () => {
|
|
259
|
-
expect( () => validateRequestPayload( { url: 'https://example.com/path?q=1', method: 'GET' } ) ).not.toThrow();
|
|
260
|
-
} );
|
|
261
|
-
|
|
262
|
-
it( 'rejects missing url', () => {
|
|
263
|
-
const error = new StaticValidationError( '✖ Invalid input: expected string, received undefined\n → at url' );
|
|
264
|
-
expect( () => validateRequestPayload( { method: 'GET' } ) ).toThrow( error );
|
|
265
|
-
} );
|
|
266
|
-
|
|
267
|
-
it( 'rejects invalid scheme', () => {
|
|
268
|
-
const error = new StaticValidationError( '✖ Invalid URL\n → at url' );
|
|
269
|
-
expect( () => validateRequestPayload( { url: 'ftp://example.com', method: 'GET' } ) ).toThrow( error );
|
|
270
|
-
} );
|
|
271
|
-
|
|
272
|
-
it( 'rejects malformed url', () => {
|
|
273
|
-
const error = new StaticValidationError( '✖ Invalid URL\n → at url' );
|
|
274
|
-
expect( () => validateRequestPayload( { url: 'http:////', method: 'GET' } ) ).toThrow( error );
|
|
275
|
-
} );
|
|
276
|
-
|
|
277
|
-
it( 'rejects missing method', () => {
|
|
278
|
-
expect( () => validateRequestPayload( { url: 'https://example.com' } ) ).toThrow( StaticValidationError );
|
|
279
|
-
} );
|
|
280
|
-
|
|
281
|
-
it( 'passes with headers as string map', () => {
|
|
282
|
-
const request = {
|
|
283
|
-
url: 'https://example.com',
|
|
284
|
-
method: 'GET',
|
|
285
|
-
headers: { 'x-api-key': 'abc', accept: 'application/json' }
|
|
286
|
-
};
|
|
287
|
-
expect( () => validateRequestPayload( request ) ).not.toThrow();
|
|
288
|
-
} );
|
|
289
|
-
|
|
290
|
-
it( 'rejects non-object headers', () => {
|
|
291
|
-
const request = {
|
|
292
|
-
url: 'https://example.com',
|
|
293
|
-
method: 'GET',
|
|
294
|
-
headers: 5
|
|
295
|
-
};
|
|
296
|
-
expect( () => validateRequestPayload( request ) ).toThrow( StaticValidationError );
|
|
297
|
-
} );
|
|
298
|
-
|
|
299
|
-
it( 'rejects headers with non-string values', () => {
|
|
300
|
-
const request = {
|
|
301
|
-
url: 'https://example.com',
|
|
302
|
-
method: 'GET',
|
|
303
|
-
headers: { 'x-num': 123 }
|
|
304
|
-
};
|
|
305
|
-
expect( () => validateRequestPayload( request ) ).toThrow( StaticValidationError );
|
|
306
|
-
} );
|
|
307
|
-
|
|
308
|
-
it( 'passes with payload object', () => {
|
|
309
|
-
const request = {
|
|
310
|
-
url: 'https://example.com/api',
|
|
311
|
-
method: 'POST',
|
|
312
|
-
payload: { a: 1, b: 'two' }
|
|
313
|
-
};
|
|
314
|
-
expect( () => validateRequestPayload( request ) ).not.toThrow();
|
|
315
|
-
} );
|
|
316
|
-
|
|
317
|
-
it( 'passes with payload string', () => {
|
|
318
|
-
const request = {
|
|
319
|
-
url: 'https://example.com/upload',
|
|
320
|
-
method: 'POST',
|
|
321
|
-
payload: 'raw-body'
|
|
322
|
-
};
|
|
323
|
-
expect( () => validateRequestPayload( request ) ).not.toThrow();
|
|
324
|
-
} );
|
|
325
|
-
} );
|
|
326
|
-
|
|
327
|
-
describe( 'validateExecuteInParallel', () => {
|
|
328
|
-
const validArgs = Object.freeze( {
|
|
329
|
-
jobs: [ () => {}, () => {} ],
|
|
330
|
-
concurrency: 5
|
|
331
|
-
} );
|
|
332
|
-
|
|
333
|
-
it( 'passes for valid args', () => {
|
|
334
|
-
expect( () => validateExecuteInParallel( { ...validArgs } ) ).not.toThrow();
|
|
335
|
-
} );
|
|
336
|
-
|
|
337
|
-
it( 'rejects missing concurrency', () => {
|
|
338
|
-
const error = new StaticValidationError( '✖ Invalid input\n → at concurrency' );
|
|
339
|
-
expect( () => validateExecuteInParallel( { jobs: validArgs.jobs } ) ).toThrow( error );
|
|
340
|
-
} );
|
|
341
|
-
|
|
342
|
-
it( 'passes with onJobCompleted callback', () => {
|
|
343
|
-
const args = {
|
|
344
|
-
...validArgs,
|
|
345
|
-
onJobCompleted: () => {}
|
|
346
|
-
};
|
|
347
|
-
expect( () => validateExecuteInParallel( args ) ).not.toThrow();
|
|
348
|
-
} );
|
|
349
|
-
|
|
350
|
-
it( 'passes with concurrency 1', () => {
|
|
351
|
-
expect( () => validateExecuteInParallel( { ...validArgs, concurrency: 1 } ) ).not.toThrow();
|
|
352
|
-
} );
|
|
353
|
-
|
|
354
|
-
it( 'passes with concurrency Infinity', () => {
|
|
355
|
-
expect( () => validateExecuteInParallel( { ...validArgs, concurrency: Infinity } ) ).not.toThrow();
|
|
356
|
-
} );
|
|
357
|
-
|
|
358
|
-
it( 'rejects missing jobs', () => {
|
|
359
|
-
const error = new StaticValidationError( '✖ Invalid input: expected array, received undefined\n → at jobs' );
|
|
360
|
-
expect( () => validateExecuteInParallel( { concurrency: 5 } ) ).toThrow( error );
|
|
361
|
-
} );
|
|
362
|
-
|
|
363
|
-
it( 'rejects non-array jobs', () => {
|
|
364
|
-
const error = new StaticValidationError( '✖ Invalid input: expected array, received string\n → at jobs' );
|
|
365
|
-
expect( () => validateExecuteInParallel( { jobs: 'not-array', concurrency: 5 } ) ).toThrow( error );
|
|
366
|
-
} );
|
|
367
|
-
|
|
368
|
-
it( 'passes with empty jobs array', () => {
|
|
369
|
-
expect( () => validateExecuteInParallel( { jobs: [], concurrency: 5 } ) ).not.toThrow();
|
|
370
|
-
} );
|
|
371
|
-
|
|
372
|
-
it( 'rejects jobs array with non-function', () => {
|
|
373
|
-
const error = new StaticValidationError( '✖ Invalid input: expected function, received string\n → at jobs[1]' );
|
|
374
|
-
expect( () => validateExecuteInParallel( { jobs: [ () => {}, 'not-function' ], concurrency: 5 } ) ).toThrow( error );
|
|
375
|
-
} );
|
|
376
|
-
|
|
377
|
-
it( 'rejects non-number concurrency', () => {
|
|
378
|
-
const error = new StaticValidationError( '✖ Invalid input\n → at concurrency' );
|
|
379
|
-
expect( () => validateExecuteInParallel( { jobs: validArgs.jobs, concurrency: '5' } ) ).toThrow( error );
|
|
380
|
-
} );
|
|
381
|
-
|
|
382
|
-
it( 'rejects zero concurrency', () => {
|
|
383
|
-
const error = new StaticValidationError( '✖ Too small: expected number to be >=1\n → at concurrency' );
|
|
384
|
-
expect( () => validateExecuteInParallel( { jobs: validArgs.jobs, concurrency: 0 } ) ).toThrow( error );
|
|
385
|
-
} );
|
|
386
|
-
|
|
387
|
-
it( 'rejects negative concurrency', () => {
|
|
388
|
-
const error = new StaticValidationError( '✖ Too small: expected number to be >=1\n → at concurrency' );
|
|
389
|
-
expect( () => validateExecuteInParallel( { ...validArgs, concurrency: -1 } ) ).toThrow( error );
|
|
390
|
-
} );
|
|
391
|
-
|
|
392
|
-
it( 'rejects non-function onJobCompleted', () => {
|
|
393
|
-
const error = new StaticValidationError( '✖ Invalid input: expected function, received string\n → at onJobCompleted' );
|
|
394
|
-
expect( () => validateExecuteInParallel( { ...validArgs, onJobCompleted: 'not-function' } ) ).toThrow( error );
|
|
395
|
-
} );
|
|
396
|
-
} );
|
|
397
|
-
} );
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { ACTIVITY_GET_TRACE_DESTINATIONS, METADATA_ACCESS_SYMBOL, WORKFLOW_WRAPPER_VERSION_FIELD } from '#consts';
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
const inWorkflowContextMock = vi.hoisted( () => vi.fn( () => true ) );
|
|
6
|
-
const defineSignalMock = vi.hoisted( () => vi.fn( name => name ) );
|
|
7
|
-
const setHandlerMock = vi.hoisted( () => vi.fn() );
|
|
8
|
-
const traceDestinationsStepMock = vi.fn().mockResolvedValue( { local: '/tmp/trace' } );
|
|
9
|
-
const executeChildMock = vi.fn().mockResolvedValue( undefined );
|
|
10
|
-
const continueAsNewMock = vi.fn().mockResolvedValue( undefined );
|
|
11
|
-
|
|
12
|
-
const createStepsProxy = ( stepSpy = vi.fn() ) =>
|
|
13
|
-
new Proxy( {}, {
|
|
14
|
-
get: ( _, prop ) => {
|
|
15
|
-
if ( prop === ACTIVITY_GET_TRACE_DESTINATIONS ) {
|
|
16
|
-
return traceDestinationsStepMock;
|
|
17
|
-
}
|
|
18
|
-
if ( typeof prop === 'string' && prop.includes( '#' ) ) {
|
|
19
|
-
return stepSpy;
|
|
20
|
-
}
|
|
21
|
-
return vi.fn();
|
|
22
|
-
}
|
|
23
|
-
} );
|
|
24
|
-
|
|
25
|
-
const proxyActivitiesMock = vi.fn( () => createStepsProxy() );
|
|
26
|
-
|
|
27
|
-
const workflowInfoReturn = {
|
|
28
|
-
workflowId: 'wf-test-123',
|
|
29
|
-
workflowType: 'test_wf',
|
|
30
|
-
memo: {},
|
|
31
|
-
startTime: new Date( '2025-01-01T00:00:00Z' ),
|
|
32
|
-
continueAsNewSuggested: false
|
|
33
|
-
};
|
|
34
|
-
const workflowInfoMock = vi.fn( () => ( { ...workflowInfoReturn } ) );
|
|
35
|
-
|
|
36
|
-
vi.mock( '@temporalio/workflow', () => ( {
|
|
37
|
-
proxyActivities: ( ...args ) => proxyActivitiesMock( ...args ),
|
|
38
|
-
inWorkflowContext: inWorkflowContextMock,
|
|
39
|
-
executeChild: ( ...args ) => executeChildMock( ...args ),
|
|
40
|
-
workflowInfo: workflowInfoMock,
|
|
41
|
-
uuid4: () => '550e8400e29b41d4a716446655440000',
|
|
42
|
-
ParentClosePolicy: { TERMINATE: 'TERMINATE', ABANDON: 'ABANDON' },
|
|
43
|
-
ChildWorkflowFailure: class ChildWorkflowFailure extends Error {
|
|
44
|
-
constructor( message, cause ) {
|
|
45
|
-
super( message );
|
|
46
|
-
this.name = 'ChildWorkflowFailure';
|
|
47
|
-
this.cause = cause;
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
continueAsNew: continueAsNewMock,
|
|
51
|
-
defineSignal: ( ...args ) => defineSignalMock( ...args ),
|
|
52
|
-
setHandler: ( ...args ) => setHandlerMock( ...args )
|
|
53
|
-
} ) );
|
|
54
|
-
|
|
55
|
-
vi.mock( '#consts', async importOriginal => {
|
|
56
|
-
const actual = await importOriginal();
|
|
57
|
-
return {
|
|
58
|
-
...actual,
|
|
59
|
-
SHARED_STEP_PREFIX: '__shared',
|
|
60
|
-
ACTIVITY_GET_TRACE_DESTINATIONS: '__internal#getTraceDestinations'
|
|
61
|
-
};
|
|
62
|
-
} );
|
|
63
|
-
|
|
64
|
-
describe( 'workflow() replay compatibility', () => {
|
|
65
|
-
beforeEach( () => {
|
|
66
|
-
vi.clearAllMocks();
|
|
67
|
-
inWorkflowContextMock.mockReturnValue( true );
|
|
68
|
-
defineSignalMock.mockImplementation( name => name );
|
|
69
|
-
workflowInfoReturn.memo = {};
|
|
70
|
-
workflowInfoMock.mockReturnValue( { ...workflowInfoReturn } );
|
|
71
|
-
proxyActivitiesMock.mockImplementation( () => createStepsProxy() );
|
|
72
|
-
traceDestinationsStepMock.mockResolvedValue( { local: '/tmp/trace' } );
|
|
73
|
-
} );
|
|
74
|
-
|
|
75
|
-
it( 'preserves old plain trace destination activity results', async () => {
|
|
76
|
-
const { workflow } = await import( './workflow.js' );
|
|
77
|
-
|
|
78
|
-
const wf = workflow( {
|
|
79
|
-
name: 'root_wf',
|
|
80
|
-
description: 'Root',
|
|
81
|
-
inputSchema: z.object( {} ),
|
|
82
|
-
outputSchema: z.object( { v: z.number() } ),
|
|
83
|
-
fn: async () => ( { v: 42 } )
|
|
84
|
-
} );
|
|
85
|
-
|
|
86
|
-
const result = await wf( {} );
|
|
87
|
-
expect( result ).toEqual( {
|
|
88
|
-
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
89
|
-
output: { v: 42 },
|
|
90
|
-
trace: { destinations: { local: '/tmp/trace' } },
|
|
91
|
-
aggregations: null
|
|
92
|
-
} );
|
|
93
|
-
} );
|
|
94
|
-
|
|
95
|
-
it( 'converts old add_attribute signals into aggregations', async () => {
|
|
96
|
-
const { workflow } = await import( './workflow.js' );
|
|
97
|
-
const { Attribute } = await import( '#trace_attribute' );
|
|
98
|
-
const handlers = { addAttribute: () => {} };
|
|
99
|
-
setHandlerMock.mockImplementation( ( signalName, handler ) => {
|
|
100
|
-
if ( signalName === 'add_attribute' ) {
|
|
101
|
-
handlers.addAttribute = handler;
|
|
102
|
-
}
|
|
103
|
-
} );
|
|
104
|
-
|
|
105
|
-
const httpRequest = {
|
|
106
|
-
type: Attribute.HTTPRequestCount.TYPE,
|
|
107
|
-
url: 'https://api.example.test/items',
|
|
108
|
-
requestId: 'req-1'
|
|
109
|
-
};
|
|
110
|
-
const httpCost = {
|
|
111
|
-
type: Attribute.HTTPRequestCost.TYPE,
|
|
112
|
-
url: 'https://api.example.test/items',
|
|
113
|
-
requestId: 'req-1',
|
|
114
|
-
total: 2.5
|
|
115
|
-
};
|
|
116
|
-
const llmUsage = {
|
|
117
|
-
type: Attribute.LLMUsage.TYPE,
|
|
118
|
-
modelId: 'gpt-4o',
|
|
119
|
-
total: 0.25,
|
|
120
|
-
usage: [
|
|
121
|
-
{ type: 'input', ppm: 5, amount: 20_000, total: 0.1 },
|
|
122
|
-
{ type: 'output', ppm: 30, amount: 5_000, total: 0.15 }
|
|
123
|
-
],
|
|
124
|
-
tokensUsed: 25_000
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const wf = workflow( {
|
|
128
|
-
name: 'attr_wf',
|
|
129
|
-
description: 'Attributes',
|
|
130
|
-
inputSchema: z.object( {} ),
|
|
131
|
-
outputSchema: z.object( { ok: z.boolean() } ),
|
|
132
|
-
fn: async () => {
|
|
133
|
-
handlers.addAttribute( httpRequest );
|
|
134
|
-
handlers.addAttribute( httpCost );
|
|
135
|
-
handlers.addAttribute( llmUsage );
|
|
136
|
-
return { ok: true };
|
|
137
|
-
}
|
|
138
|
-
} );
|
|
139
|
-
|
|
140
|
-
const result = await wf( {} );
|
|
141
|
-
expect( result ).not.toHaveProperty( 'attributes' );
|
|
142
|
-
expect( result.aggregations ).toEqual( {
|
|
143
|
-
cost: { total: 2.75 },
|
|
144
|
-
tokens: {
|
|
145
|
-
total: 25_000,
|
|
146
|
-
input: 20_000,
|
|
147
|
-
output: 5_000
|
|
148
|
-
},
|
|
149
|
-
httpRequests: { total: 1 }
|
|
150
|
-
} );
|
|
151
|
-
} );
|
|
152
|
-
|
|
153
|
-
it( 'preserves old plain activity results from steps', async () => {
|
|
154
|
-
const stepSpy = vi.fn().mockResolvedValue( { legacy: true } );
|
|
155
|
-
proxyActivitiesMock.mockImplementation( () => createStepsProxy( stepSpy ) );
|
|
156
|
-
const { workflow } = await import( './workflow.js' );
|
|
157
|
-
|
|
158
|
-
const wf = workflow( {
|
|
159
|
-
name: 'legacy_step_wf',
|
|
160
|
-
description: 'Legacy step result',
|
|
161
|
-
inputSchema: z.object( {} ),
|
|
162
|
-
outputSchema: z.object( { legacy: z.boolean() } ),
|
|
163
|
-
async fn() {
|
|
164
|
-
return this.invokeStep( 'myStep', { foo: 1 } );
|
|
165
|
-
}
|
|
166
|
-
} );
|
|
167
|
-
|
|
168
|
-
const result = await wf( {} );
|
|
169
|
-
expect( result.output ).toEqual( { legacy: true } );
|
|
170
|
-
expect( result.aggregations ).toEqual( null );
|
|
171
|
-
} );
|
|
172
|
-
|
|
173
|
-
it( 'converts old child workflow attributes into parent aggregations', async () => {
|
|
174
|
-
const { workflow } = await import( './workflow.js' );
|
|
175
|
-
const { Attribute } = await import( '#trace_attribute' );
|
|
176
|
-
const childAttribute = {
|
|
177
|
-
type: Attribute.LLMUsage.TYPE,
|
|
178
|
-
modelId: 'gpt-4o',
|
|
179
|
-
total: 0.4,
|
|
180
|
-
tokensUsed: 20,
|
|
181
|
-
usage: [
|
|
182
|
-
{ type: 'input', ppm: 10, amount: 20, total: 0.4 }
|
|
183
|
-
]
|
|
184
|
-
};
|
|
185
|
-
executeChildMock.mockResolvedValueOnce( {
|
|
186
|
-
output: { child: 'ok' },
|
|
187
|
-
attributes: [ childAttribute ]
|
|
188
|
-
} );
|
|
189
|
-
|
|
190
|
-
const wf = workflow( {
|
|
191
|
-
name: 'merge_child_wf',
|
|
192
|
-
description: 'Merge child attributes',
|
|
193
|
-
inputSchema: z.object( {} ),
|
|
194
|
-
outputSchema: z.object( { child: z.string() } ),
|
|
195
|
-
async fn() {
|
|
196
|
-
return this.startWorkflow( 'child_wf', { id: 1 } );
|
|
197
|
-
}
|
|
198
|
-
} );
|
|
199
|
-
|
|
200
|
-
const result = await wf( {} );
|
|
201
|
-
expect( result ).toEqual( {
|
|
202
|
-
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
203
|
-
output: { child: 'ok' },
|
|
204
|
-
trace: { destinations: { local: '/tmp/trace' } },
|
|
205
|
-
aggregations: {
|
|
206
|
-
cost: { total: 0.4 },
|
|
207
|
-
tokens: {
|
|
208
|
-
total: 20,
|
|
209
|
-
input: 20
|
|
210
|
-
},
|
|
211
|
-
httpRequests: { total: 0 }
|
|
212
|
-
}
|
|
213
|
-
} );
|
|
214
|
-
} );
|
|
215
|
-
|
|
216
|
-
it( 'converts old child workflow error attributes into parent error metadata aggregations', async () => {
|
|
217
|
-
const { workflow } = await import( './workflow.js' );
|
|
218
|
-
const { ChildWorkflowFailure } = await import( '@temporalio/workflow' );
|
|
219
|
-
const { Attribute } = await import( '#trace_attribute' );
|
|
220
|
-
const childAttribute = {
|
|
221
|
-
type: Attribute.HTTPRequestCost.TYPE,
|
|
222
|
-
url: 'https://api.example.test',
|
|
223
|
-
requestId: 'req-child',
|
|
224
|
-
total: 2
|
|
225
|
-
};
|
|
226
|
-
const childError = new ChildWorkflowFailure( 'child failed', {
|
|
227
|
-
message: 'Child workflow execution failed',
|
|
228
|
-
details: [ { attributes: [ childAttribute ] } ]
|
|
229
|
-
} );
|
|
230
|
-
executeChildMock.mockRejectedValueOnce( childError );
|
|
231
|
-
|
|
232
|
-
const wf = workflow( {
|
|
233
|
-
name: 'child_error_wf',
|
|
234
|
-
description: 'Child error attributes',
|
|
235
|
-
inputSchema: z.object( {} ),
|
|
236
|
-
outputSchema: z.object( {} ),
|
|
237
|
-
async fn() {
|
|
238
|
-
await this.startWorkflow( 'child_wf', { id: 1 } );
|
|
239
|
-
return {};
|
|
240
|
-
}
|
|
241
|
-
} );
|
|
242
|
-
|
|
243
|
-
await expect( wf( {} ) ).rejects.toThrow( 'child failed' );
|
|
244
|
-
expect( childError[METADATA_ACCESS_SYMBOL] ).toEqual( {
|
|
245
|
-
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
246
|
-
trace: { destinations: { local: '/tmp/trace' } },
|
|
247
|
-
aggregations: {
|
|
248
|
-
cost: { total: 2 },
|
|
249
|
-
tokens: { total: 0 },
|
|
250
|
-
httpRequests: { total: 0 }
|
|
251
|
-
}
|
|
252
|
-
} );
|
|
253
|
-
} );
|
|
254
|
-
} );
|