@output.ai/core 0.5.1 → 0.5.3
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/package.json +1 -1
- package/src/errors.d.ts +15 -0
- package/src/index.d.ts +17 -864
- package/src/index.js +3 -28
- package/src/interface/evaluation_result.d.ts +160 -0
- package/src/interface/evaluation_result.js +202 -0
- package/src/interface/evaluator.d.ts +70 -0
- package/src/interface/evaluator.js +1 -202
- package/src/interface/evaluator.spec.js +1 -1
- package/src/interface/index.d.ts +9 -0
- package/src/interface/index.js +19 -0
- package/src/interface/step.d.ts +138 -0
- package/src/interface/step.js +1 -0
- package/src/interface/types.d.ts +27 -0
- package/src/interface/webhook.d.ts +84 -0
- package/src/interface/workflow.d.ts +273 -0
- package/src/interface/workflow.js +2 -32
- package/src/interface/workflow.spec.js +462 -46
- package/src/interface/workflow_context.js +31 -0
- package/src/interface/workflow_utils.d.ts +53 -0
- package/src/interface/workflow_utils.js +1 -0
- package/src/tracing/tools/build_trace_tree.js +5 -1
- package/src/tracing/tools/build_trace_tree.spec.js +11 -0
- package/src/utils/index.d.ts +1 -1
- package/src/worker/catalog_workflow/workflow.js +9 -3
- package/src/worker/index.js +7 -39
- package/src/worker/index.spec.js +162 -0
- package/src/worker/interceptors/workflow.js +1 -1
- package/src/worker/shutdown.js +26 -0
- package/src/worker/shutdown.spec.js +82 -0
- package/src/worker/start_catalog.js +36 -0
- package/src/worker/start_catalog.spec.js +116 -0
- package/src/worker/webpack_loaders/tools.js +34 -4
- package/src/worker/webpack_loaders/tools.spec.js +4 -1
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +107 -68
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +251 -1
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -4
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +48 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +2 -1
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +3 -3
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +22 -0
|
@@ -1,79 +1,495 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
|
+
const inWorkflowContextMock = vi.hoisted( () => vi.fn( () => true ) );
|
|
4
5
|
const traceDestinationsStepMock = vi.fn().mockResolvedValue( { local: '/tmp/trace' } );
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const executeChildMock = vi.fn().mockResolvedValue( undefined );
|
|
7
|
+
const continueAsNewMock = vi.fn().mockResolvedValue( undefined );
|
|
8
|
+
|
|
9
|
+
const createStepsProxy = ( stepSpy = vi.fn() ) =>
|
|
10
|
+
new Proxy( {}, {
|
|
11
|
+
get: ( _, prop ) => {
|
|
12
|
+
if ( prop === '__internal#getTraceDestinations' ) {
|
|
13
|
+
return traceDestinationsStepMock;
|
|
14
|
+
}
|
|
15
|
+
if ( typeof prop === 'string' && ( prop.includes( '#' ) ) ) {
|
|
16
|
+
return stepSpy;
|
|
17
|
+
}
|
|
18
|
+
return vi.fn();
|
|
9
19
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} )
|
|
13
|
-
const
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
const stepSpyRef = { current: vi.fn().mockResolvedValue( {} ) };
|
|
23
|
+
const proxyActivitiesMock = vi.fn( () => {
|
|
24
|
+
stepSpyRef.current = vi.fn().mockResolvedValue( {} );
|
|
25
|
+
return createStepsProxy( stepSpyRef.current );
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
const workflowInfoReturn = {
|
|
14
29
|
workflowId: 'wf-test-123',
|
|
15
30
|
workflowType: 'test_wf',
|
|
16
31
|
memo: {},
|
|
17
32
|
startTime: new Date( '2025-01-01T00:00:00Z' ),
|
|
18
33
|
continueAsNewSuggested: false
|
|
19
|
-
}
|
|
34
|
+
};
|
|
35
|
+
const workflowInfoMock = vi.fn( () => ( { ...workflowInfoReturn } ) );
|
|
20
36
|
|
|
21
37
|
vi.mock( '@temporalio/workflow', () => ( {
|
|
22
|
-
proxyActivities: proxyActivitiesMock,
|
|
23
|
-
inWorkflowContext:
|
|
24
|
-
executeChild:
|
|
38
|
+
proxyActivities: ( ...args ) => proxyActivitiesMock( ...args ),
|
|
39
|
+
inWorkflowContext: inWorkflowContextMock,
|
|
40
|
+
executeChild: ( ...args ) => executeChildMock( ...args ),
|
|
25
41
|
workflowInfo: workflowInfoMock,
|
|
26
|
-
uuid4: () => '
|
|
42
|
+
uuid4: () => '550e8400e29b41d4a716446655440000',
|
|
27
43
|
ParentClosePolicy: { TERMINATE: 'TERMINATE', ABANDON: 'ABANDON' },
|
|
28
|
-
continueAsNew:
|
|
44
|
+
continueAsNew: continueAsNewMock
|
|
29
45
|
} ) );
|
|
30
46
|
|
|
31
|
-
vi.mock( '#consts',
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
vi.mock( '#consts', async importOriginal => {
|
|
48
|
+
const actual = await importOriginal();
|
|
49
|
+
return {
|
|
50
|
+
...actual,
|
|
51
|
+
SHARED_STEP_PREFIX: '__shared',
|
|
52
|
+
ACTIVITY_GET_TRACE_DESTINATIONS: '__internal#getTraceDestinations'
|
|
53
|
+
};
|
|
54
|
+
} );
|
|
36
55
|
|
|
37
56
|
describe( 'workflow()', () => {
|
|
38
57
|
beforeEach( () => {
|
|
39
58
|
vi.clearAllMocks();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
inWorkflowContextMock.mockReturnValue( true );
|
|
60
|
+
workflowInfoMock.mockReturnValue( { ...workflowInfoReturn } );
|
|
61
|
+
workflowInfoReturn.memo = {};
|
|
62
|
+
proxyActivitiesMock.mockImplementation( () => {
|
|
63
|
+
stepSpyRef.current = vi.fn().mockResolvedValue( {} );
|
|
64
|
+
return createStepsProxy( stepSpyRef.current );
|
|
65
|
+
} );
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
describe( 'options and defaults', () => {
|
|
69
|
+
it( 'does not throw when options is omitted (disableTrace defaults to false)', async () => {
|
|
70
|
+
const { workflow } = await import( './workflow.js' );
|
|
71
|
+
|
|
72
|
+
const wf = workflow( {
|
|
73
|
+
name: 'no_options_wf',
|
|
74
|
+
description: 'Workflow without options',
|
|
75
|
+
inputSchema: z.object( { value: z.string() } ),
|
|
76
|
+
outputSchema: z.object( { value: z.string() } ),
|
|
77
|
+
fn: async ( { value } ) => ( { value } )
|
|
78
|
+
} );
|
|
79
|
+
|
|
80
|
+
const result = await wf( { value: 'hello' } );
|
|
81
|
+
expect( result.output ).toEqual( { value: 'hello' } );
|
|
82
|
+
} );
|
|
83
|
+
|
|
84
|
+
it( 'respects disableTrace: true when options is provided', async () => {
|
|
85
|
+
const { workflow } = await import( './workflow.js' );
|
|
86
|
+
|
|
87
|
+
const wf = workflow( {
|
|
88
|
+
name: 'trace_disabled_wf',
|
|
89
|
+
description: 'Workflow with tracing disabled',
|
|
90
|
+
inputSchema: z.object( { value: z.string() } ),
|
|
91
|
+
outputSchema: z.object( { value: z.string() } ),
|
|
92
|
+
options: { disableTrace: true },
|
|
93
|
+
fn: async ( { value } ) => ( { value } )
|
|
94
|
+
} );
|
|
95
|
+
|
|
96
|
+
const result = await wf( { value: 'hello' } );
|
|
97
|
+
expect( result.output ).toEqual( { value: 'hello' } );
|
|
98
|
+
} );
|
|
99
|
+
|
|
100
|
+
it( 'merges custom activityOptions with defaults via deepMerge', async () => {
|
|
101
|
+
const { workflow } = await import( './workflow.js' );
|
|
102
|
+
|
|
103
|
+
workflow( {
|
|
104
|
+
name: 'custom_activity_wf',
|
|
105
|
+
description: 'Workflow with custom activity options',
|
|
106
|
+
inputSchema: z.object( {} ),
|
|
107
|
+
outputSchema: z.object( {} ),
|
|
108
|
+
options: {
|
|
109
|
+
activityOptions: {
|
|
110
|
+
startToCloseTimeout: '5m',
|
|
111
|
+
retry: { maximumAttempts: 5 }
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
fn: async () => ( {} )
|
|
115
|
+
} );
|
|
116
|
+
|
|
117
|
+
expect( proxyActivitiesMock ).toHaveBeenCalledWith(
|
|
118
|
+
expect.objectContaining( {
|
|
119
|
+
startToCloseTimeout: '5m',
|
|
120
|
+
retry: expect.objectContaining( { maximumAttempts: 5 } )
|
|
121
|
+
} )
|
|
122
|
+
);
|
|
46
123
|
} );
|
|
47
124
|
} );
|
|
48
125
|
|
|
49
|
-
|
|
50
|
-
|
|
126
|
+
describe( 'wrapper metadata', () => {
|
|
127
|
+
it( 'attaches name, description, inputSchema, outputSchema to wrapper via setMetadata', async () => {
|
|
128
|
+
const { workflow } = await import( './workflow.js' );
|
|
129
|
+
const inputSchema = z.object( { x: z.number() } );
|
|
130
|
+
const outputSchema = z.object( { y: z.number() } );
|
|
51
131
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
132
|
+
const wf = workflow( {
|
|
133
|
+
name: 'meta_wf',
|
|
134
|
+
description: 'Meta workflow',
|
|
135
|
+
inputSchema,
|
|
136
|
+
outputSchema,
|
|
137
|
+
fn: async input => ( { y: input.x } )
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
const symbols = Object.getOwnPropertySymbols( wf );
|
|
141
|
+
expect( symbols ).toHaveLength( 1 );
|
|
142
|
+
const meta = wf[symbols[0]];
|
|
143
|
+
expect( meta ).toEqual( { name: 'meta_wf', description: 'Meta workflow', inputSchema, outputSchema } );
|
|
144
|
+
} );
|
|
145
|
+
} );
|
|
146
|
+
|
|
147
|
+
describe( 'when not in workflow context (unit-test path)', () => {
|
|
148
|
+
it( 'validates input, runs fn with test context, validates output, returns plain output', async () => {
|
|
149
|
+
inWorkflowContextMock.mockReturnValue( false );
|
|
150
|
+
const { workflow } = await import( './workflow.js' );
|
|
151
|
+
|
|
152
|
+
const wf = workflow( {
|
|
153
|
+
name: 'unit_path_wf',
|
|
154
|
+
description: 'Unit path',
|
|
155
|
+
inputSchema: z.object( { a: z.string() } ),
|
|
156
|
+
outputSchema: z.object( { b: z.string() } ),
|
|
157
|
+
fn: async ( input, context ) => ( {
|
|
158
|
+
b: String( context.info.workflowId ) + input.a
|
|
159
|
+
} )
|
|
160
|
+
} );
|
|
161
|
+
|
|
162
|
+
const result = await wf( { a: '-ok' } );
|
|
163
|
+
expect( result ).toEqual( { b: 'test-workflow-ok' } );
|
|
164
|
+
expect( workflowInfoMock ).not.toHaveBeenCalled();
|
|
165
|
+
expect( traceDestinationsStepMock ).not.toHaveBeenCalled();
|
|
166
|
+
} );
|
|
167
|
+
|
|
168
|
+
it( 'merges extra.context into context when provided', async () => {
|
|
169
|
+
inWorkflowContextMock.mockReturnValue( false );
|
|
170
|
+
const { workflow } = await import( './workflow.js' );
|
|
171
|
+
|
|
172
|
+
const wf = workflow( {
|
|
173
|
+
name: 'extra_ctx_wf',
|
|
174
|
+
description: 'Extra context',
|
|
175
|
+
inputSchema: z.object( {} ),
|
|
176
|
+
outputSchema: z.object( { id: z.string() } ),
|
|
177
|
+
fn: async ( _, context ) => ( { id: context.extraId ?? 'default' } )
|
|
178
|
+
} );
|
|
179
|
+
|
|
180
|
+
const result = await wf( {}, { context: { extraId: 'injected' } } );
|
|
181
|
+
expect( result ).toEqual( { id: 'injected' } );
|
|
182
|
+
} );
|
|
183
|
+
} );
|
|
184
|
+
|
|
185
|
+
describe( 'input and output validation', () => {
|
|
186
|
+
it( 'throws ValidationError when input does not match inputSchema', async () => {
|
|
187
|
+
const { workflow } = await import( './workflow.js' );
|
|
188
|
+
const { ValidationError } = await import( '#errors' );
|
|
189
|
+
|
|
190
|
+
const wf = workflow( {
|
|
191
|
+
name: 'validate_in_wf',
|
|
192
|
+
description: 'Input validation',
|
|
193
|
+
inputSchema: z.object( { required: z.string() } ),
|
|
194
|
+
outputSchema: z.object( {} ),
|
|
195
|
+
fn: async () => ( {} )
|
|
196
|
+
} );
|
|
197
|
+
|
|
198
|
+
await expect( wf( { wrong: 1 } ) ).rejects.toThrow( ValidationError );
|
|
199
|
+
await expect( wf( { wrong: 1 } ) ).rejects.toThrow( /Workflow validate_in_wf input/ );
|
|
58
200
|
} );
|
|
59
201
|
|
|
60
|
-
|
|
61
|
-
|
|
202
|
+
it( 'throws ValidationError when output does not match outputSchema', async () => {
|
|
203
|
+
const { workflow } = await import( './workflow.js' );
|
|
204
|
+
const { ValidationError } = await import( '#errors' );
|
|
205
|
+
|
|
206
|
+
const wf = workflow( {
|
|
207
|
+
name: 'validate_out_wf',
|
|
208
|
+
description: 'Output validation',
|
|
209
|
+
inputSchema: z.object( {} ),
|
|
210
|
+
outputSchema: z.object( { required: z.string() } ),
|
|
211
|
+
fn: async () => ( { other: 1 } )
|
|
212
|
+
} );
|
|
213
|
+
|
|
214
|
+
await expect( wf( {} ) ).rejects.toThrow( ValidationError );
|
|
215
|
+
await expect( wf( {} ) ).rejects.toThrow( /Workflow validate_out_wf output/ );
|
|
216
|
+
} );
|
|
62
217
|
} );
|
|
63
218
|
|
|
64
|
-
|
|
65
|
-
|
|
219
|
+
describe( 'root workflow (in workflow context)', () => {
|
|
220
|
+
it( 'calls getTraceDestinations, returns { output, trace } and assigns executionContext to memo', async () => {
|
|
221
|
+
const { workflow } = await import( './workflow.js' );
|
|
222
|
+
|
|
223
|
+
const wf = workflow( {
|
|
224
|
+
name: 'root_wf',
|
|
225
|
+
description: 'Root',
|
|
226
|
+
inputSchema: z.object( {} ),
|
|
227
|
+
outputSchema: z.object( { v: z.number() } ),
|
|
228
|
+
fn: async () => ( { v: 42 } )
|
|
229
|
+
} );
|
|
66
230
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
231
|
+
const result = await wf( {} );
|
|
232
|
+
expect( traceDestinationsStepMock ).toHaveBeenCalledTimes( 1 );
|
|
233
|
+
expect( result ).toEqual( {
|
|
234
|
+
output: { v: 42 },
|
|
235
|
+
trace: { destinations: { local: '/tmp/trace' } }
|
|
236
|
+
} );
|
|
237
|
+
const memo = workflowInfoMock().memo;
|
|
238
|
+
expect( memo.executionContext ).toEqual( {
|
|
239
|
+
workflowId: 'wf-test-123',
|
|
240
|
+
workflowName: 'root_wf',
|
|
241
|
+
disableTrace: false,
|
|
242
|
+
startTime: new Date( '2025-01-01T00:00:00Z' ).getTime()
|
|
243
|
+
} );
|
|
74
244
|
} );
|
|
75
245
|
|
|
76
|
-
|
|
77
|
-
|
|
246
|
+
it( 'sets executionContext.disableTrace when options.disableTrace is true', async () => {
|
|
247
|
+
const { workflow } = await import( './workflow.js' );
|
|
248
|
+
|
|
249
|
+
const wf = workflow( {
|
|
250
|
+
name: 'root_no_trace_wf',
|
|
251
|
+
description: 'Root no trace',
|
|
252
|
+
inputSchema: z.object( {} ),
|
|
253
|
+
outputSchema: z.object( {} ),
|
|
254
|
+
options: { disableTrace: true },
|
|
255
|
+
fn: async () => ( {} )
|
|
256
|
+
} );
|
|
257
|
+
|
|
258
|
+
await wf( {} );
|
|
259
|
+
expect( workflowInfoMock().memo.executionContext.disableTrace ).toBe( true );
|
|
260
|
+
} );
|
|
261
|
+
} );
|
|
262
|
+
|
|
263
|
+
describe( 'child workflow (memo.executionContext already set)', () => {
|
|
264
|
+
it( 'does not call getTraceDestinations and returns plain output', async () => {
|
|
265
|
+
workflowInfoMock.mockReturnValue( {
|
|
266
|
+
...workflowInfoReturn,
|
|
267
|
+
memo: { executionContext: { workflowId: 'parent-1', workflowName: 'parent_wf' } }
|
|
268
|
+
} );
|
|
269
|
+
const { workflow } = await import( './workflow.js' );
|
|
270
|
+
|
|
271
|
+
const wf = workflow( {
|
|
272
|
+
name: 'child_wf',
|
|
273
|
+
description: 'Child',
|
|
274
|
+
inputSchema: z.object( {} ),
|
|
275
|
+
outputSchema: z.object( { x: z.string() } ),
|
|
276
|
+
fn: async () => ( { x: 'child' } )
|
|
277
|
+
} );
|
|
278
|
+
|
|
279
|
+
const result = await wf( {} );
|
|
280
|
+
expect( traceDestinationsStepMock ).not.toHaveBeenCalled();
|
|
281
|
+
expect( result ).toEqual( { x: 'child' } );
|
|
282
|
+
} );
|
|
283
|
+
} );
|
|
284
|
+
|
|
285
|
+
describe( 'bound this: invokeStep, invokeSharedStep, invokeEvaluator', () => {
|
|
286
|
+
it( 'invokeStep calls steps with workflowName#stepName', async () => {
|
|
287
|
+
const getCalls = [];
|
|
288
|
+
proxyActivitiesMock.mockImplementation( () => new Proxy( {}, {
|
|
289
|
+
get: ( _, prop ) => {
|
|
290
|
+
if ( prop === '__internal#getTraceDestinations' ) {
|
|
291
|
+
return traceDestinationsStepMock;
|
|
292
|
+
}
|
|
293
|
+
if ( typeof prop === 'string' && prop.includes( '#' ) ) {
|
|
294
|
+
getCalls.push( prop );
|
|
295
|
+
return vi.fn().mockResolvedValue( {} );
|
|
296
|
+
}
|
|
297
|
+
return vi.fn();
|
|
298
|
+
}
|
|
299
|
+
} ) );
|
|
300
|
+
|
|
301
|
+
const { workflow } = await import( './workflow.js' );
|
|
302
|
+
|
|
303
|
+
const wf = workflow( {
|
|
304
|
+
name: 'invoke_wf',
|
|
305
|
+
description: 'Invoke',
|
|
306
|
+
inputSchema: z.object( {} ),
|
|
307
|
+
outputSchema: z.object( {} ),
|
|
308
|
+
async fn() {
|
|
309
|
+
await this.invokeStep( 'myStep', { foo: 1 } );
|
|
310
|
+
return {};
|
|
311
|
+
}
|
|
312
|
+
} );
|
|
313
|
+
|
|
314
|
+
await wf( {} );
|
|
315
|
+
expect( getCalls ).toContain( 'invoke_wf#myStep' );
|
|
316
|
+
} );
|
|
317
|
+
|
|
318
|
+
it( 'invokeSharedStep calls steps with SHARED_STEP_PREFIX#stepName', async () => {
|
|
319
|
+
const { workflow } = await import( './workflow.js' );
|
|
320
|
+
const sharedSpy = vi.fn().mockResolvedValue( {} );
|
|
321
|
+
proxyActivitiesMock.mockImplementation( () => new Proxy( {}, {
|
|
322
|
+
get: ( _, prop ) => {
|
|
323
|
+
if ( prop === '__internal#getTraceDestinations' ) {
|
|
324
|
+
return traceDestinationsStepMock;
|
|
325
|
+
}
|
|
326
|
+
if ( prop === '__shared#sharedStep' ) {
|
|
327
|
+
return sharedSpy;
|
|
328
|
+
}
|
|
329
|
+
return vi.fn();
|
|
330
|
+
}
|
|
331
|
+
} ) );
|
|
332
|
+
|
|
333
|
+
const wf = workflow( {
|
|
334
|
+
name: 'shared_wf',
|
|
335
|
+
description: 'Shared',
|
|
336
|
+
inputSchema: z.object( {} ),
|
|
337
|
+
outputSchema: z.object( {} ),
|
|
338
|
+
async fn() {
|
|
339
|
+
await this.invokeSharedStep( 'sharedStep', { data: 2 } );
|
|
340
|
+
return {};
|
|
341
|
+
}
|
|
342
|
+
} );
|
|
343
|
+
|
|
344
|
+
await wf( {} );
|
|
345
|
+
expect( sharedSpy ).toHaveBeenCalledWith( { data: 2 }, undefined );
|
|
346
|
+
} );
|
|
347
|
+
|
|
348
|
+
it( 'invokeEvaluator calls steps with workflowName#evaluatorName', async () => {
|
|
349
|
+
const evalSpy = vi.fn().mockResolvedValue( true );
|
|
350
|
+
proxyActivitiesMock.mockImplementation( () => new Proxy( {}, {
|
|
351
|
+
get: ( _, prop ) => {
|
|
352
|
+
if ( prop === '__internal#getTraceDestinations' ) {
|
|
353
|
+
return traceDestinationsStepMock;
|
|
354
|
+
}
|
|
355
|
+
if ( prop === 'eval_wf#myEvaluator' ) {
|
|
356
|
+
return evalSpy;
|
|
357
|
+
}
|
|
358
|
+
return vi.fn();
|
|
359
|
+
}
|
|
360
|
+
} ) );
|
|
361
|
+
|
|
362
|
+
const { workflow } = await import( './workflow.js' );
|
|
363
|
+
|
|
364
|
+
const wf = workflow( {
|
|
365
|
+
name: 'eval_wf',
|
|
366
|
+
description: 'Eval',
|
|
367
|
+
inputSchema: z.object( {} ),
|
|
368
|
+
outputSchema: z.object( {} ),
|
|
369
|
+
async fn() {
|
|
370
|
+
await this.invokeEvaluator( 'myEvaluator', { x: 3 } );
|
|
371
|
+
return {};
|
|
372
|
+
}
|
|
373
|
+
} );
|
|
374
|
+
|
|
375
|
+
await wf( {} );
|
|
376
|
+
expect( evalSpy ).toHaveBeenCalledWith( { x: 3 }, undefined );
|
|
377
|
+
} );
|
|
378
|
+
} );
|
|
379
|
+
|
|
380
|
+
describe( 'startWorkflow', () => {
|
|
381
|
+
it( 'calls executeChild with correct args and TERMINATE when not detached', async () => {
|
|
382
|
+
const { workflow } = await import( './workflow.js' );
|
|
383
|
+
const { ParentClosePolicy } = await import( '@temporalio/workflow' );
|
|
384
|
+
|
|
385
|
+
const wf = workflow( {
|
|
386
|
+
name: 'parent_wf',
|
|
387
|
+
description: 'Parent',
|
|
388
|
+
inputSchema: z.object( {} ),
|
|
389
|
+
outputSchema: z.object( {} ),
|
|
390
|
+
async fn() {
|
|
391
|
+
await this.startWorkflow( 'child_wf', { id: 1 } );
|
|
392
|
+
return {};
|
|
393
|
+
}
|
|
394
|
+
} );
|
|
395
|
+
|
|
396
|
+
await wf( {} );
|
|
397
|
+
expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', {
|
|
398
|
+
args: [ { id: 1 } ],
|
|
399
|
+
workflowId: expect.stringMatching( /^wf-test-123-/ ),
|
|
400
|
+
parentClosePolicy: ParentClosePolicy.TERMINATE,
|
|
401
|
+
memo: expect.objectContaining( {
|
|
402
|
+
executionContext: expect.any( Object ),
|
|
403
|
+
parentId: 'wf-test-123'
|
|
404
|
+
} )
|
|
405
|
+
} );
|
|
406
|
+
} );
|
|
407
|
+
|
|
408
|
+
it( 'uses ABANDON when extra.detached is true', async () => {
|
|
409
|
+
const { workflow } = await import( './workflow.js' );
|
|
410
|
+
const { ParentClosePolicy } = await import( '@temporalio/workflow' );
|
|
411
|
+
|
|
412
|
+
const wf = workflow( {
|
|
413
|
+
name: 'detach_wf',
|
|
414
|
+
description: 'Detach',
|
|
415
|
+
inputSchema: z.object( {} ),
|
|
416
|
+
outputSchema: z.object( {} ),
|
|
417
|
+
async fn() {
|
|
418
|
+
await this.startWorkflow( 'child_wf', null, { detached: true } );
|
|
419
|
+
return {};
|
|
420
|
+
}
|
|
421
|
+
} );
|
|
422
|
+
|
|
423
|
+
await wf( {} );
|
|
424
|
+
expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
|
|
425
|
+
parentClosePolicy: ParentClosePolicy.ABANDON
|
|
426
|
+
} ) );
|
|
427
|
+
} );
|
|
428
|
+
|
|
429
|
+
it( 'passes empty args when input is null/omitted', async () => {
|
|
430
|
+
const { workflow } = await import( './workflow.js' );
|
|
431
|
+
|
|
432
|
+
const wf = workflow( {
|
|
433
|
+
name: 'no_input_wf',
|
|
434
|
+
description: 'No input',
|
|
435
|
+
inputSchema: z.object( {} ),
|
|
436
|
+
outputSchema: z.object( {} ),
|
|
437
|
+
async fn() {
|
|
438
|
+
await this.startWorkflow( 'child_wf' );
|
|
439
|
+
return {};
|
|
440
|
+
}
|
|
441
|
+
} );
|
|
442
|
+
|
|
443
|
+
await wf( {} );
|
|
444
|
+
expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
|
|
445
|
+
args: []
|
|
446
|
+
} ) );
|
|
447
|
+
} );
|
|
448
|
+
} );
|
|
449
|
+
|
|
450
|
+
describe( 'context.control', () => {
|
|
451
|
+
it( 'exposes info.workflowId and control/info namespaces when not in workflow context', async () => {
|
|
452
|
+
inWorkflowContextMock.mockReturnValue( false );
|
|
453
|
+
const { workflow } = await import( './workflow.js' );
|
|
454
|
+
|
|
455
|
+
const wf = workflow( {
|
|
456
|
+
name: 'control_wf',
|
|
457
|
+
description: 'Control',
|
|
458
|
+
inputSchema: z.object( {} ),
|
|
459
|
+
outputSchema: z.object( {
|
|
460
|
+
workflowId: z.string(),
|
|
461
|
+
hasControl: z.boolean(),
|
|
462
|
+
hasInfo: z.boolean()
|
|
463
|
+
} ),
|
|
464
|
+
fn: async ( _, context ) => ( {
|
|
465
|
+
workflowId: context.info?.workflowId,
|
|
466
|
+
hasControl: 'control' in context,
|
|
467
|
+
hasInfo: 'info' in context
|
|
468
|
+
} )
|
|
469
|
+
} );
|
|
470
|
+
|
|
471
|
+
const result = await wf( {} );
|
|
472
|
+
expect( result.workflowId ).toBe( 'test-workflow' );
|
|
473
|
+
expect( result.hasControl ).toBe( true );
|
|
474
|
+
expect( result.hasInfo ).toBe( true );
|
|
475
|
+
} );
|
|
476
|
+
} );
|
|
477
|
+
|
|
478
|
+
describe( 'error handling (root workflow)', () => {
|
|
479
|
+
it( 'rethrows error from fn and rejects with same message', async () => {
|
|
480
|
+
const { workflow } = await import( './workflow.js' );
|
|
481
|
+
|
|
482
|
+
const wf = workflow( {
|
|
483
|
+
name: 'err_wf',
|
|
484
|
+
description: 'Error',
|
|
485
|
+
inputSchema: z.object( {} ),
|
|
486
|
+
outputSchema: z.object( {} ),
|
|
487
|
+
fn: async () => {
|
|
488
|
+
throw new Error( 'workflow failed' );
|
|
489
|
+
}
|
|
490
|
+
} );
|
|
491
|
+
|
|
492
|
+
await expect( wf( {} ) ).rejects.toThrow( 'workflow failed' );
|
|
493
|
+
} );
|
|
78
494
|
} );
|
|
79
495
|
} );
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context instance builder
|
|
3
|
+
*/
|
|
4
|
+
export class Context {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds a new context instance
|
|
8
|
+
* @param {object} options - Arguments to build a new context instance
|
|
9
|
+
* @param {string} workflowId
|
|
10
|
+
* @param {function} continueAsNew
|
|
11
|
+
* @param {function} isContinueAsNewSuggested
|
|
12
|
+
* @returns {object} context
|
|
13
|
+
*/
|
|
14
|
+
static build( { workflowId, continueAsNew, isContinueAsNewSuggested } ) {
|
|
15
|
+
return {
|
|
16
|
+
/**
|
|
17
|
+
* Control namespace: This object adds functions to interact with Temporal flow mechanisms
|
|
18
|
+
*/
|
|
19
|
+
control: {
|
|
20
|
+
continueAsNew,
|
|
21
|
+
isContinueAsNewSuggested
|
|
22
|
+
},
|
|
23
|
+
/**
|
|
24
|
+
* Info namespace: abstracts workflowInfo()
|
|
25
|
+
*/
|
|
26
|
+
info: {
|
|
27
|
+
workflowId
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result of a single job executed by `executeInParallel`.
|
|
3
|
+
*
|
|
4
|
+
* @typeParam T - The return type of the job function
|
|
5
|
+
*/
|
|
6
|
+
export type ParallelJobResult<T> =
|
|
7
|
+
| { ok: true; result: T; index: number } |
|
|
8
|
+
{ ok: false; error: unknown; index: number };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute jobs in parallel with optional concurrency limit.
|
|
12
|
+
*
|
|
13
|
+
* Returns all job results (successes and failures) sorted by original job index.
|
|
14
|
+
* Each result contains `ok` (boolean), `index` (original position), and either
|
|
15
|
+
* `result` (on success) or `error` (on failure).
|
|
16
|
+
*
|
|
17
|
+
* Jobs must be wrapped in arrow functions—do not pass promises directly.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const results = await executeInParallel( {
|
|
22
|
+
* jobs: [
|
|
23
|
+
* () => myStep( data1 ),
|
|
24
|
+
* () => myStep( data2 ),
|
|
25
|
+
* () => myStep( data3 )
|
|
26
|
+
* ],
|
|
27
|
+
* concurrency: 2
|
|
28
|
+
* } );
|
|
29
|
+
*
|
|
30
|
+
* // Handle the discriminated union (result only exists when ok is true)
|
|
31
|
+
* const successfulResults = results.filter( r => r.ok ).map( r => r.result );
|
|
32
|
+
*
|
|
33
|
+
* // Or handle each result individually
|
|
34
|
+
* for ( const r of results ) {
|
|
35
|
+
* if ( r.ok ) {
|
|
36
|
+
* console.log( `Job ${r.index} succeeded:`, r.result );
|
|
37
|
+
* } else {
|
|
38
|
+
* console.log( `Job ${r.index} failed:`, r.error );
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @param params - Parameters object
|
|
44
|
+
* @param params.jobs - Array of arrow functions returning step/activity calls (not promises directly)
|
|
45
|
+
* @param params.concurrency - Max concurrent jobs (default: Infinity)
|
|
46
|
+
* @param params.onJobCompleted - Optional callback invoked as each job completes (in completion order)
|
|
47
|
+
* @returns Array of results sorted by original job index
|
|
48
|
+
*/
|
|
49
|
+
export declare function executeInParallel<T>( params: {
|
|
50
|
+
jobs: Array<() => Promise<T> | T>;
|
|
51
|
+
concurrency?: number;
|
|
52
|
+
onJobCompleted?: ( result: ParallelJobResult<T> ) => void;
|
|
53
|
+
} ): Promise<Array<ParallelJobResult<T>>>;
|
|
@@ -23,7 +23,7 @@ const createEntry = id => ( {
|
|
|
23
23
|
kind: '',
|
|
24
24
|
name: '',
|
|
25
25
|
startedAt: 0,
|
|
26
|
-
endedAt:
|
|
26
|
+
endedAt: null,
|
|
27
27
|
input: undefined,
|
|
28
28
|
output: undefined,
|
|
29
29
|
error: undefined,
|
|
@@ -75,5 +75,9 @@ export default entries => {
|
|
|
75
75
|
if ( !rootNode ) {
|
|
76
76
|
return null;
|
|
77
77
|
}
|
|
78
|
+
if ( !rootNode.endedAt ) {
|
|
79
|
+
rootNode.output = '<<Workflow did not finish yet. If this workflows is supposed to have been completed already, \
|
|
80
|
+
this can indicate it timed out or was interrupted.>>';
|
|
81
|
+
}
|
|
78
82
|
return rootNode;
|
|
79
83
|
};
|
|
@@ -6,6 +6,17 @@ describe( 'build_trace_tree', () => {
|
|
|
6
6
|
expect( buildTraceTree( [] ) ).toBeNull();
|
|
7
7
|
} );
|
|
8
8
|
|
|
9
|
+
it( 'sets root output with a fixed message when workflow has no end/error phase yet', () => {
|
|
10
|
+
const entries = [
|
|
11
|
+
{ kind: 'workflow', id: 'wf', parentId: undefined, phase: 'start', name: 'wf', details: {}, timestamp: 1000 }
|
|
12
|
+
];
|
|
13
|
+
const result = buildTraceTree( entries );
|
|
14
|
+
expect( result ).not.toBeNull();
|
|
15
|
+
expect( result.output ).toBe( '<<Workflow did not finish yet. If this workflows is supposed to have been completed already, \
|
|
16
|
+
this can indicate it timed out or was interrupted.>>' );
|
|
17
|
+
expect( result.endedAt ).toBeNull();
|
|
18
|
+
} );
|
|
19
|
+
|
|
9
20
|
it( 'returns null when there is no root (all entries have parentId)', () => {
|
|
10
21
|
const entries = [
|
|
11
22
|
{ id: 'a', parentId: 'x', phase: 'start', name: 'a', timestamp: 1 },
|
package/src/utils/index.d.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function isPlainObject( object: unknown ): boolean;
|
|
|
94
94
|
* @param arr - The array to shuffle
|
|
95
95
|
* @returns A shuffled array copy
|
|
96
96
|
*/
|
|
97
|
-
export function shuffleArray( arr:
|
|
97
|
+
export function shuffleArray( arr: unknown[] ): unknown[];
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Creates a new object by merging object `b` onto object `a`, biased toward `b`:
|