@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.
Files changed (77) hide show
  1. package/bin/worker.sh +6 -0
  2. package/package.json +1 -1
  3. package/src/consts.js +0 -4
  4. package/src/errors.js +6 -2
  5. package/src/interface/evaluator.js +7 -20
  6. package/src/interface/evaluator.spec.js +117 -1
  7. package/src/interface/step.js +8 -9
  8. package/src/interface/step.spec.js +124 -0
  9. package/src/interface/validations/index.js +108 -0
  10. package/src/interface/validations/index.spec.js +182 -0
  11. package/src/interface/validations/schemas.js +113 -0
  12. package/src/interface/validations/schemas.spec.js +209 -0
  13. package/src/interface/webhook.js +1 -1
  14. package/src/interface/webhook.spec.js +1 -1
  15. package/src/interface/workflow.d.ts +10 -9
  16. package/src/interface/workflow.js +76 -164
  17. package/src/interface/workflow.spec.js +637 -521
  18. package/src/interface/workflow_activity_options.js +16 -0
  19. package/src/interface/workflow_utils.js +1 -1
  20. package/src/interface/zod_integration.spec.js +2 -2
  21. package/src/internal_utils/aggregations.js +0 -10
  22. package/src/internal_utils/aggregations.spec.js +1 -48
  23. package/src/internal_utils/errors.js +14 -8
  24. package/src/internal_utils/errors.spec.js +73 -27
  25. package/src/utils/index.d.ts +19 -0
  26. package/src/utils/utils.js +53 -0
  27. package/src/utils/utils.spec.js +105 -1
  28. package/src/worker/bundle.js +26 -0
  29. package/src/worker/bundle.spec.js +53 -0
  30. package/src/worker/bundler_options.js +1 -1
  31. package/src/worker/bundler_options.spec.js +1 -1
  32. package/src/worker/catalog_workflow/catalog_job.js +148 -0
  33. package/src/worker/catalog_workflow/catalog_job.spec.js +232 -0
  34. package/src/worker/check.js +24 -0
  35. package/src/worker/connection_monitor.js +112 -0
  36. package/src/worker/connection_monitor.spec.js +199 -0
  37. package/src/worker/index.js +146 -41
  38. package/src/worker/index.spec.js +281 -109
  39. package/src/worker/interceptors/activity.js +7 -24
  40. package/src/worker/interceptors/activity.spec.js +97 -66
  41. package/src/worker/interceptors/index.js +4 -7
  42. package/src/worker/interceptors/modules.js +15 -0
  43. package/src/worker/interceptors/workflow.js +6 -8
  44. package/src/worker/interceptors/workflow.spec.js +49 -42
  45. package/src/worker/interruption.js +33 -0
  46. package/src/worker/interruption.spec.js +86 -0
  47. package/src/worker/loader/activities.js +75 -0
  48. package/src/worker/loader/activities.spec.js +213 -0
  49. package/src/worker/loader/hooks.js +28 -0
  50. package/src/worker/loader/hooks.spec.js +64 -0
  51. package/src/worker/loader/matchers.js +46 -0
  52. package/src/worker/loader/matchers.spec.js +140 -0
  53. package/src/worker/{loader_tools.js → loader/tools.js} +19 -67
  54. package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +53 -85
  55. package/src/worker/loader/workflows.js +82 -0
  56. package/src/worker/loader/workflows.spec.js +256 -0
  57. package/src/worker/{setup_telemetry.js → telemetry.js} +9 -4
  58. package/src/worker/{setup_telemetry.spec.js → telemetry.spec.js} +3 -3
  59. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +5 -109
  60. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +31 -103
  61. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -6
  62. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +11 -83
  63. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +8 -11
  64. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +9 -9
  65. package/src/interface/validations/runtime.js +0 -20
  66. package/src/interface/validations/runtime.spec.js +0 -29
  67. package/src/interface/validations/schema_utils.js +0 -8
  68. package/src/interface/validations/schema_utils.spec.js +0 -67
  69. package/src/interface/validations/static.js +0 -137
  70. package/src/interface/validations/static.spec.js +0 -397
  71. package/src/interface/workflow.replay_compatibility.spec.js +0 -254
  72. package/src/worker/loader.js +0 -202
  73. package/src/worker/loader.spec.js +0 -498
  74. package/src/worker/shutdown.js +0 -26
  75. package/src/worker/shutdown.spec.js +0 -82
  76. package/src/worker/start_catalog.js +0 -96
  77. package/src/worker/start_catalog.spec.js +0 -179
@@ -0,0 +1,16 @@
1
+ import { FatalError, ValidationError } from '#errors';
2
+
3
+ export const defaultOptions = {
4
+ activityOptions: {
5
+ startToCloseTimeout: '20m',
6
+ heartbeatTimeout: '5m',
7
+ retry: {
8
+ initialInterval: '10s',
9
+ backoffCoefficient: 2.0,
10
+ maximumInterval: '2m',
11
+ maximumAttempts: 3,
12
+ nonRetryableErrorTypes: [ ValidationError.name, FatalError.name ]
13
+ }
14
+ },
15
+ disableTrace: false
16
+ };
@@ -1,5 +1,5 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
- import { validateExecuteInParallel } from './validations/static.js';
2
+ import { validateExecuteInParallel } from './validations/index.js';
3
3
 
4
4
  /**
5
5
  * Execute jobs in parallel with optional concurrency limit.
@@ -495,7 +495,7 @@ describe( 'Zod Schema Integration Tests', () => {
495
495
  await errorStep( { age: 16, email: 'invalid' } );
496
496
  expect.fail( 'Should have thrown an error' );
497
497
  } catch ( error ) {
498
- expect( error.message ).toContain( 'Step error_test input validation failed' );
498
+ expect( error.message ).toContain( 'Step "error_test" input validation failed' );
499
499
  }
500
500
  } );
501
501
 
@@ -527,7 +527,7 @@ describe( 'Zod Schema Integration Tests', () => {
527
527
  const metadata = testStep[METADATA_ACCESS_SYMBOL];
528
528
  expect( metadata.inputSchema ).toBe( zodSchema );
529
529
  expect( metadata.inputSchema ).not.toBe( null );
530
- expect( metadata.inputSchema._def ).toBeDefined(); // Zod-specific property
530
+ expect( metadata.inputSchema._zod?.def ).toBeDefined(); // Zod v4-specific property
531
531
  } );
532
532
 
533
533
  it( 'should handle deeply nested Zod schemas', async () => {
@@ -1,4 +1,3 @@
1
- import { deepMergeWithResolver } from '#utils';
2
1
  import { Attribute } from '#trace_attribute';
3
2
  import Decimal from 'decimal.js';
4
3
 
@@ -43,12 +42,3 @@ export const aggregateAttributes = attributes => ( {
43
42
  total: attributes.filter( a => Attribute.HTTPRequestCount.TYPE === a.type ).length
44
43
  }
45
44
  } );
46
-
47
- /**
48
- * Combine two or more aggregation objects into a single Aggregation, adding up the totals and merging keys.
49
- *
50
- * @param {Aggregation} aggregations
51
- * @returns {Aggregation}
52
- */
53
- export const mergeAggregations = ( ...aggregations ) =>
54
- aggregations.reduce( ( final, item ) => deepMergeWithResolver( final, item, ( a, b ) => Decimal( a ?? 0 ).add( b ?? 0 ).toNumber() ), {} );
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { Attribute } from '#trace_attribute';
3
- import { aggregateAttributes, mergeAggregations } from './aggregations.js';
3
+ import { aggregateAttributes } from './aggregations.js';
4
4
 
5
5
  describe( 'aggregateAttributes', () => {
6
6
  it( 'returns zeroed aggregations when there are no attributes', () => {
@@ -90,50 +90,3 @@ describe( 'aggregateAttributes', () => {
90
90
  } );
91
91
  } );
92
92
 
93
- describe( 'mergeAggregations', () => {
94
- it( 'returns an empty object when no aggregations are provided', () => {
95
- expect( mergeAggregations() ).toEqual( {} );
96
- } );
97
-
98
- it( 'sums nested aggregation values', () => {
99
- expect( mergeAggregations(
100
- {
101
- cost: { total: 1.2 },
102
- tokens: { total: 10, input: 6 },
103
- httpRequests: { total: 2 }
104
- },
105
- {
106
- cost: { total: 0.3 },
107
- tokens: { total: 5, input: 2, output: 3 },
108
- httpRequests: { total: 1 }
109
- }
110
- ) ).toEqual( {
111
- cost: { total: 1.5 },
112
- tokens: { total: 15, input: 8, output: 3 },
113
- httpRequests: { total: 3 }
114
- } );
115
- } );
116
-
117
- it( 'handles undefined or partial aggregation objects', () => {
118
- expect( mergeAggregations(
119
- undefined,
120
- { cost: { total: 2 } },
121
- { tokens: { total: 4, reasoning: 1 } },
122
- { httpRequests: { total: 3 } }
123
- ) ).toEqual( {
124
- cost: { total: 2 },
125
- tokens: { total: 4, reasoning: 1 },
126
- httpRequests: { total: 3 }
127
- } );
128
- } );
129
-
130
- it( 'does not mutate source aggregation objects', () => {
131
- const first = { cost: { total: 1 }, tokens: { total: 2 }, httpRequests: { total: 3 } };
132
- const second = { cost: { total: 4 }, tokens: { total: 5 }, httpRequests: { total: 6 } };
133
-
134
- mergeAggregations( first, second );
135
-
136
- expect( first ).toEqual( { cost: { total: 1 }, tokens: { total: 2 }, httpRequests: { total: 3 } } );
137
- expect( second ).toEqual( { cost: { total: 4 }, tokens: { total: 5 }, httpRequests: { total: 6 } } );
138
- } );
139
- } );
@@ -1,10 +1,16 @@
1
+ import { ApplicationFailure } from '@temporalio/common';
2
+
1
3
  /**
2
- * Extract a property from the error .details.
3
- * If error does not have details, navigate up the .cause chain.
4
- *
5
- * @param {Error} e
6
- * @param {string} key
7
- * @returns {any} The value of the property
4
+ * Builds a Temporal ApplicationFailure based on an error attaching info to its details
5
+ * @param {Error} error
6
+ * @param {unknown} info
7
+ * @returns {ApplicationFailure}
8
8
  */
9
- export const extractErrorDetail = ( e, key ) =>
10
- e ? ( e.details?.find?.( d => d[key] )?.[key] ?? extractErrorDetail( e.cause, key ) ) : null;
9
+ export const buildApplicationFailureWithDetails = ( error, info ) =>
10
+ ApplicationFailure.create( {
11
+ message: error.message,
12
+ type: error.type ?? error.constructor?.name ?? error.name,
13
+ nonRetryable: error.nonRetryable,
14
+ details: ( Array.isArray( error.details ) ? error.details : [] ).concat( info ),
15
+ cause: error
16
+ } );
@@ -1,43 +1,89 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { extractErrorDetail } from './errors.js';
2
+ import { ApplicationFailure } from '@temporalio/common';
3
+ import { buildApplicationFailureWithDetails } from './errors.js';
3
4
 
4
- describe( 'extractErrorDetail', () => {
5
- it( 'returns a matching value from error details', () => {
6
- const error = new Error( 'failed' );
7
- error.details = [
8
- { requestId: 'req-1' },
9
- { workflowId: 'workflow-1' }
10
- ];
5
+ class CustomFailure extends Error {}
11
6
 
12
- expect( extractErrorDetail( error, 'workflowId' ) ).toBe( 'workflow-1' );
7
+ describe( 'buildApplicationFailureWithDetails', () => {
8
+ it( 'wraps a regular error in an ApplicationFailure with appended details', () => {
9
+ const error = new Error( 'step failed' );
10
+ const info = { aggregations: { cost: { total: 1 } } };
11
+
12
+ const failure = buildApplicationFailureWithDetails( error, info );
13
+
14
+ expect( failure ).toBeInstanceOf( ApplicationFailure );
15
+ expect( failure ).toMatchObject( {
16
+ message: 'step failed',
17
+ type: 'Error',
18
+ nonRetryable: false,
19
+ details: [ info ],
20
+ cause: error
21
+ } );
13
22
  } );
14
23
 
15
- it( 'walks the cause chain until it finds matching details', () => {
16
- const root = new Error( 'root' );
17
- root.details = [ { traceId: 'trace-1' } ];
18
- const wrapped = new Error( 'wrapped', { cause: root } );
24
+ it( 'uses the original constructor name for custom errors', () => {
25
+ const error = new CustomFailure( 'custom failed' );
26
+ const info = { trace: { destinations: { local: '/tmp/trace' } } };
19
27
 
20
- expect( extractErrorDetail( wrapped, 'traceId' ) ).toBe( 'trace-1' );
28
+ const failure = buildApplicationFailureWithDetails( error, info );
29
+
30
+ expect( failure ).toMatchObject( {
31
+ message: 'custom failed',
32
+ type: 'CustomFailure',
33
+ details: [ info ],
34
+ cause: error
35
+ } );
21
36
  } );
22
37
 
23
- it( 'prefers details from the current error over causes', () => {
24
- const root = new Error( 'root' );
25
- root.details = [ { traceId: 'root-trace' } ];
26
- const wrapped = new Error( 'wrapped', { cause: root } );
27
- wrapped.details = [ { traceId: 'wrapped-trace' } ];
38
+ it( 'preserves existing details and appends new info without mutating the original error', () => {
39
+ const existingDetails = [ { domain: { reason: 'bad-input' } } ];
40
+ const error = new Error( 'step failed' );
41
+ error.details = existingDetails;
42
+ const info = { aggregations: { httpRequests: { total: 1 } } };
43
+
44
+ const failure = buildApplicationFailureWithDetails( error, info );
28
45
 
29
- expect( extractErrorDetail( wrapped, 'traceId' ) ).toBe( 'wrapped-trace' );
46
+ expect( failure.details ).toEqual( [
47
+ { domain: { reason: 'bad-input' } },
48
+ info
49
+ ] );
50
+ expect( error.details ).toBe( existingDetails );
51
+ expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
30
52
  } );
31
53
 
32
- it( 'returns null when the key is not found', () => {
33
- const root = new Error( 'root' );
34
- root.details = [ { traceId: 'trace-1' } ];
35
- const wrapped = new Error( 'wrapped', { cause: root } );
54
+ it( 'ignores non-array details on the original error', () => {
55
+ const error = new Error( 'step failed' );
56
+ error.details = { domain: { reason: 'bad-input' } };
57
+ const info = { aggregations: { tokens: { total: 3 } } };
36
58
 
37
- expect( extractErrorDetail( wrapped, 'missing' ) ).toBeNull();
59
+ const failure = buildApplicationFailureWithDetails( error, info );
60
+
61
+ expect( failure.details ).toEqual( [ info ] );
38
62
  } );
39
63
 
40
- it( 'returns null for empty errors', () => {
41
- expect( extractErrorDetail( null, 'traceId' ) ).toBeNull();
64
+ it( 'preserves ApplicationFailure type, nonRetryable flag, and details while avoiding self-cause', () => {
65
+ const original = ApplicationFailure.create( {
66
+ message: 'application failed',
67
+ type: 'DomainFailure',
68
+ nonRetryable: true,
69
+ details: [ { domain: { reason: 'bad-input' } } ]
70
+ } );
71
+ const info = { aggregations: { cost: { total: 2 } } };
72
+
73
+ const failure = buildApplicationFailureWithDetails( original, info );
74
+
75
+ expect( failure ).toBeInstanceOf( ApplicationFailure );
76
+ expect( failure ).not.toBe( original );
77
+ expect( failure.cause ).toBe( original );
78
+ expect( failure.cause ).not.toBe( failure );
79
+ expect( failure ).toMatchObject( {
80
+ message: 'application failed',
81
+ type: 'DomainFailure',
82
+ nonRetryable: true,
83
+ details: [
84
+ { domain: { reason: 'bad-input' } },
85
+ info
86
+ ]
87
+ } );
42
88
  } );
43
89
  } );
@@ -159,3 +159,22 @@ export function allSettledWithTimeout<T>(
159
159
  promises: Array<T | PromiseLike<T>>,
160
160
  timeoutMs: number
161
161
  ): Promise<PromiseSettledResult<Awaited<T>>[]>;
162
+
163
+ /**
164
+ * Promise wrapper that can be resolved externally.
165
+ */
166
+ export class CancellablePromise {
167
+ /** The internal promise */
168
+ readonly promise: Promise<void>;
169
+ /** Whether the promise is already resolved or not */
170
+ readonly completed: boolean;
171
+ /** Resolves the promise */
172
+ complete(): void;
173
+ }
174
+
175
+ /**
176
+ * Returns a function that invokes the wrapped function once.
177
+ */
178
+ export function runOnce<Args extends unknown[], Return>(
179
+ fn: ( ...args: Args ) => Return
180
+ ): ( ...args: Args ) => Return;
@@ -279,3 +279,56 @@ export const allSettledWithTimeout = ( () => {
279
279
  }
280
280
  };
281
281
  } )();
282
+
283
+ /**
284
+ * Builds a promise that can be resolved from the outside.
285
+ */
286
+ export class CancellablePromise {
287
+ #promise = null;
288
+ #complete = null;
289
+ #completed = false;
290
+
291
+ constructor() {
292
+ this.#promise = new Promise( resolve => {
293
+ this.#complete = () => {
294
+ resolve();
295
+ this.#completed = true;
296
+ };
297
+ } );
298
+ }
299
+ /** Retrieves the promise */
300
+ get promise() {
301
+ return this.#promise;
302
+ }
303
+ /** Returns whether the promise is resolved or not */
304
+ get completed() {
305
+ return this.#completed;
306
+ }
307
+ /** Resolves the promise */
308
+ complete() {
309
+ this.#complete();
310
+ }
311
+ };
312
+
313
+ /**
314
+ * Returns a function that invokes the fn argument when called once, further calls do nothing.
315
+ * @param {Function} fn
316
+ * @returns {Function}
317
+ */
318
+ export const runOnce = fn => {
319
+ const state = { executed: false, result: undefined };
320
+ return ( ...args ) => {
321
+ if ( !state.executed ) {
322
+ state.executed = true;
323
+ return state.result = fn( ...args );
324
+ }
325
+ return state.result;
326
+ };
327
+ };
328
+
329
+ /**
330
+ * Escape regexp characters in a string
331
+ * @param {*} value
332
+ * @returns
333
+ */
334
+ export const rxEscape = v => v.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
@@ -8,7 +8,10 @@ import {
8
8
  deepMergeWithResolver,
9
9
  isPlainObject,
10
10
  toUrlSafeBase64,
11
- allSettledWithTimeout
11
+ allSettledWithTimeout,
12
+ CancellablePromise,
13
+ runOnce,
14
+ rxEscape
12
15
  } from './utils.js';
13
16
 
14
17
  describe( 'clone', () => {
@@ -142,6 +145,107 @@ describe( 'allSettledWithTimeout', () => {
142
145
  } );
143
146
  } );
144
147
 
148
+ describe( 'CancellablePromise', () => {
149
+ it( 'exposes a pending promise until it is completed', async () => {
150
+ const cancellable = new CancellablePromise();
151
+ const onComplete = vi.fn();
152
+
153
+ cancellable.promise.then( onComplete );
154
+ await Promise.resolve();
155
+
156
+ expect( cancellable.completed ).toBe( false );
157
+ expect( onComplete ).not.toHaveBeenCalled();
158
+
159
+ cancellable.complete();
160
+ await cancellable.promise;
161
+
162
+ expect( cancellable.completed ).toBe( true );
163
+ expect( onComplete ).toHaveBeenCalledOnce();
164
+ } );
165
+
166
+ it( 'can be completed multiple times without resolving again', async () => {
167
+ const cancellable = new CancellablePromise();
168
+ const onComplete = vi.fn();
169
+
170
+ cancellable.promise.then( onComplete );
171
+ cancellable.complete();
172
+ cancellable.complete();
173
+ await cancellable.promise;
174
+ await Promise.resolve();
175
+
176
+ expect( cancellable.completed ).toBe( true );
177
+ expect( onComplete ).toHaveBeenCalledOnce();
178
+ } );
179
+ } );
180
+
181
+ describe( 'runOnce', () => {
182
+ it( 'calls the wrapped function only once', () => {
183
+ const fn = vi.fn();
184
+ const once = runOnce( fn );
185
+
186
+ once();
187
+ once();
188
+ once();
189
+
190
+ expect( fn ).toHaveBeenCalledOnce();
191
+ } );
192
+
193
+ it( 'passes arguments and replays the first call result', () => {
194
+ const fn = vi.fn( ( a, b ) => a + b );
195
+ const once = runOnce( fn );
196
+
197
+ expect( once( 2, 3 ) ).toBe( 5 );
198
+ expect( once( 4, 5 ) ).toBe( 5 );
199
+ expect( once( 6, 7 ) ).toBe( 5 );
200
+ expect( fn ).toHaveBeenCalledWith( 2, 3 );
201
+ expect( fn ).toHaveBeenCalledOnce();
202
+ } );
203
+
204
+ it( 'replays the first returned promise', () => {
205
+ const result = Promise.resolve( 'done' );
206
+ const fn = vi.fn( () => result );
207
+ const once = runOnce( fn );
208
+
209
+ expect( once() ).toBe( result );
210
+ expect( once() ).toBe( result );
211
+ expect( fn ).toHaveBeenCalledOnce();
212
+ } );
213
+
214
+ it( 'does not retry when the first call throws', () => {
215
+ const error = new Error( 'boom' );
216
+ const fn = vi.fn( () => {
217
+ throw error;
218
+ } );
219
+ const once = runOnce( fn );
220
+
221
+ expect( () => once() ).toThrow( error );
222
+ expect( once() ).toBeUndefined();
223
+ expect( fn ).toHaveBeenCalledOnce();
224
+ } );
225
+ } );
226
+
227
+ describe( 'rxEscape', () => {
228
+ it( 'escapes all regexp metacharacters', () => {
229
+ expect( rxEscape( '.*+?^${}()|[]\\' ) ).toBe( '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\' );
230
+ } );
231
+
232
+ it( 'keeps file URL paths matchable as literal regexp input', () => {
233
+ const path = 'file://foo/bar';
234
+ const rx = new RegExp( `^${rxEscape( path )}$` );
235
+
236
+ expect( rx.test( path ) ).toBe( true );
237
+ expect( rx.test( 'file://foo/bar/baz' ) ).toBe( false );
238
+ } );
239
+
240
+ it( 'keeps Windows paths matchable as literal regexp input', () => {
241
+ const path = String.raw`C:\foo\bar`;
242
+ const rx = new RegExp( `^${rxEscape( path )}$` );
243
+
244
+ expect( rx.test( path ) ).toBe( true );
245
+ expect( rx.test( String.raw`C:\foo\bar\baz` ) ).toBe( false );
246
+ } );
247
+ } );
248
+
145
249
  describe( 'serializeFetchResponse', () => {
146
250
  it( 'serializes JSON response body and flattens headers', async () => {
147
251
  const payload = { a: 1, b: 'two' };
@@ -0,0 +1,26 @@
1
+ import { bundleWorkflowCode } from '@temporalio/worker';
2
+ import { loadWorkflows } from './loader/workflows.js';
3
+ import { loadActivities } from './loader/activities.js';
4
+ import { webpackConfigHook } from './bundler_options.js';
5
+ import { workflowInterceptorModules } from './interceptors/modules.js';
6
+
7
+ /**
8
+ * Bundle a project's workflows exactly as the worker does, without a Temporal server.
9
+ *
10
+ * Mirrors the worker's startup prep (`loadWorkflows` -> `loadActivities` ->
11
+ * `createWorkflowsEntryPoint`) and then runs the same bundler (`bundleWorkflowCode`)
12
+ * with the same inputs `Worker.create` derives — `webpackConfigHook` and
13
+ * `workflowInterceptorModules` — so it stays in parity with worker startup. Rejects if
14
+ * the Temporal webpack bundler fails, e.g. a `node:` built-in in the workflow's
15
+ * transitive import graph.
16
+ *
17
+ * @param {string} rootDir directory to discover workflows in
18
+ * @returns {Promise<import('@temporalio/worker').WorkflowBundleWithSourceMap>}
19
+ */
20
+ export async function bundleWorkflows( rootDir ) {
21
+ const { workflows, entrypoint: workflowsPath } = await loadWorkflows( rootDir );
22
+ // Writes worker/temp/__activity_options.js, which the workflow interceptor module
23
+ // imports — the worker generates it via loadActivities() before Worker.create bundles.
24
+ await loadActivities( rootDir, workflows );
25
+ return bundleWorkflowCode( { workflowsPath, workflowInterceptorModules, webpackConfigHook } );
26
+ }
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ // Stub the activity interceptor so the real interceptors/index.js imports cleanly.
4
+ vi.mock( './interceptors/activity.js', () => ( { ActivityExecutionInterceptor: class {} } ) );
5
+
6
+ vi.mock( '@temporalio/worker', () => ( {
7
+ bundleWorkflowCode: vi.fn().mockResolvedValue( { code: '', sourceMap: '' } )
8
+ } ) );
9
+
10
+ vi.mock( './loader/workflows.js', () => ( {
11
+ loadWorkflows: vi.fn().mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/entrypoint.js' } )
12
+ } ) );
13
+
14
+ vi.mock( './loader/activities.js', () => ( {
15
+ loadActivities: vi.fn().mockResolvedValue( { activities: {} } )
16
+ } ) );
17
+
18
+ vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
19
+
20
+ import { bundleWorkflowCode } from '@temporalio/worker';
21
+ import { loadWorkflows } from './loader/workflows.js';
22
+ import { loadActivities } from './loader/activities.js';
23
+ import { webpackConfigHook } from './bundler_options.js';
24
+ import { initInterceptors } from './interceptors/index.js';
25
+ import { workflowInterceptorModules } from './interceptors/modules.js';
26
+ import { bundleWorkflows } from './bundle.js';
27
+
28
+ describe( 'output-worker --check parity', () => {
29
+ beforeEach( () => {
30
+ vi.clearAllMocks();
31
+ loadWorkflows.mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/entrypoint.js' } );
32
+ loadActivities.mockResolvedValue( { activities: {} } );
33
+ bundleWorkflowCode.mockResolvedValue( { code: '', sourceMap: '' } );
34
+ } );
35
+
36
+ it( 'worker registers the shared workflow interceptor modules', () => {
37
+ const { workflowModules } = initInterceptors( { activities: {}, workflows: [], connection: {} } );
38
+ // The check (bundleWorkflows) and the worker must register the very same modules.
39
+ expect( workflowModules ).toBe( workflowInterceptorModules );
40
+ } );
41
+
42
+ it( 'check bundles with the same inputs Worker.create derives', async () => {
43
+ await bundleWorkflows( '/project' );
44
+
45
+ expect( loadWorkflows ).toHaveBeenCalledWith( '/project' );
46
+ expect( loadActivities ).toHaveBeenCalledWith( '/project', [] );
47
+ expect( bundleWorkflowCode ).toHaveBeenCalledWith( {
48
+ workflowsPath: '/fake/workflows/entrypoint.js',
49
+ workflowInterceptorModules,
50
+ webpackConfigHook
51
+ } );
52
+ } );
53
+ } );
@@ -4,7 +4,7 @@ import {
4
4
  findPackageRoot,
5
5
  isPathDescendentFromNodeModules,
6
6
  packageExposesWorkflows
7
- } from './loader_tools.js';
7
+ } from './loader/tools.js';
8
8
 
9
9
  const __dirname = dirname( fileURLToPath( import.meta.url ) );
10
10
  const workerDir = __dirname; // sdk/core/src/worker
@@ -33,7 +33,7 @@ describe( 'webpackConfigHook loader excludes', () => {
33
33
 
34
34
  it( 'excludes worker and interface internals', () => {
35
35
  for ( const exclude of buildExcludes() ) {
36
- expect( exclude( join( __dirname, 'loader.js' ) ) ).toBe( true );
36
+ expect( exclude( join( __dirname, 'loader', 'tools.js' ) ) ).toBe( true );
37
37
  expect( exclude( join( __dirname, '..', 'interface', 'index.js' ) ) ).toBe( true );
38
38
  }
39
39
  } );