@outputai/core 0.10.1-next.b7b2fbe.0 → 0.10.1-next.bf95a6f.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 (99) hide show
  1. package/package.json +13 -7
  2. package/src/bus.js +58 -20
  3. package/src/bus.spec.js +135 -77
  4. package/src/consts.js +7 -3
  5. package/src/errors.d.ts +12 -2
  6. package/src/errors.js +17 -3
  7. package/src/errors.spec.js +19 -0
  8. package/src/helpers/activity_options.js +21 -0
  9. package/src/helpers/activity_options.spec.js +55 -0
  10. package/src/helpers/error_serializer.js +236 -0
  11. package/src/helpers/error_serializer.spec.js +413 -0
  12. package/src/helpers/errors.js +23 -14
  13. package/src/helpers/errors.spec.js +31 -75
  14. package/src/helpers/fetch.js +1 -38
  15. package/src/helpers/fetch.spec.js +1 -39
  16. package/src/helpers/function.js +13 -0
  17. package/src/helpers/function.spec.js +23 -1
  18. package/src/helpers/object.js +48 -35
  19. package/src/helpers/object.spec.js +123 -75
  20. package/src/helpers/redact.js +51 -0
  21. package/src/helpers/redact.spec.js +66 -0
  22. package/src/helpers/string.js +10 -0
  23. package/src/helpers/string.spec.js +36 -1
  24. package/src/hooks/index.d.ts +76 -78
  25. package/src/hooks/index.js +57 -49
  26. package/src/hooks/index.spec.js +104 -105
  27. package/src/hooks/pending_hooks.js +24 -0
  28. package/src/hooks/pending_hooks.spec.js +62 -0
  29. package/src/interface/evaluator.d.ts +10 -9
  30. package/src/interface/evaluator.js +1 -6
  31. package/src/interface/evaluator.spec.js +10 -10
  32. package/src/interface/index.js +2 -1
  33. package/src/interface/step.d.ts +16 -15
  34. package/src/interface/step.js +1 -6
  35. package/src/interface/step.spec.js +10 -10
  36. package/src/interface/validations/index.js +33 -53
  37. package/src/interface/validations/index.spec.js +25 -17
  38. package/src/interface/webhook.js +1 -2
  39. package/src/interface/webhook.spec.js +25 -19
  40. package/src/interface/workflow.d.ts +25 -18
  41. package/src/interface/workflow.js +64 -85
  42. package/src/interface/workflow.spec.js +220 -226
  43. package/src/interface/workflow_activity_options.js +2 -5
  44. package/src/interface/workflow_utils.d.ts +15 -0
  45. package/src/interface/workflow_utils.js +19 -0
  46. package/src/interface/workflow_utils.spec.js +82 -1
  47. package/src/sdk/helpers/objects.d.ts +24 -19
  48. package/src/sdk/runtime/events.d.ts +4 -6
  49. package/src/sdk/runtime/events.js +2 -15
  50. package/src/sdk/runtime/events.spec.js +14 -92
  51. package/src/temporal/client.js +47 -0
  52. package/src/temporal/client.spec.js +124 -0
  53. package/src/temporal/index.d.ts +32 -0
  54. package/src/temporal/index.js +1 -0
  55. package/src/tracing/trace_engine.js +9 -11
  56. package/src/tracing/trace_engine.spec.js +67 -28
  57. package/src/worker/catalog_workflow/catalog_job.js +3 -3
  58. package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
  59. package/src/worker/configs.js +0 -3
  60. package/src/worker/global_functions.js +2 -2
  61. package/src/worker/global_functions.spec.js +3 -3
  62. package/src/worker/index.js +24 -11
  63. package/src/worker/index.spec.js +36 -16
  64. package/src/worker/interceptors/activity.js +34 -21
  65. package/src/worker/interceptors/activity.spec.js +77 -172
  66. package/src/worker/interceptors/workflow.js +58 -16
  67. package/src/worker/interceptors/workflow.spec.js +183 -56
  68. package/src/worker/loader/activities.js +56 -40
  69. package/src/worker/loader/activities.spec.js +21 -22
  70. package/src/worker/loader/workflows.js +16 -3
  71. package/src/worker/loader/workflows.spec.js +36 -5
  72. package/src/worker/log_hooks.js +22 -19
  73. package/src/worker/log_hooks.spec.js +14 -6
  74. package/src/worker/sinks.js +8 -8
  75. package/src/worker/sinks.spec.js +9 -9
  76. package/src/worker/telemetry.js +2 -1
  77. package/src/worker/temporal_logger.js +33 -0
  78. package/src/worker/temporal_logger.spec.js +90 -0
  79. package/src/worker/webpack_loaders/consts.js +6 -0
  80. package/src/worker/webpack_loaders/tools.js +1 -146
  81. package/src/worker/webpack_loaders/tools.spec.js +0 -23
  82. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
  83. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
  84. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
  85. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
  86. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
  87. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
  88. package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
  89. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
  90. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
  91. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
  92. package/src/helpers/aggregations.js +0 -44
  93. package/src/helpers/aggregations.spec.js +0 -92
  94. package/src/tracing/tools/utils.js +0 -42
  95. package/src/tracing/tools/utils.spec.js +0 -83
  96. package/src/worker/proxy.js +0 -20
  97. package/src/worker/proxy.spec.js +0 -51
  98. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
  99. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
@@ -2,10 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { z } from 'zod';
3
3
  import {
4
4
  ACTIVITY_GET_TRACE_DESTINATIONS,
5
- ACTIVITY_WRAPPER_VERSION_FIELD,
6
- METADATA_ACCESS_SYMBOL,
7
- SHARED_STEP_PREFIX,
8
- WORKFLOW_WRAPPER_VERSION_FIELD
5
+ INVOKE_ACTIVITY_SYMBOL
9
6
  } from '#consts';
10
7
  import { ValidationError } from '#errors';
11
8
 
@@ -13,11 +10,12 @@ const inWorkflowContextMock = vi.hoisted( () => vi.fn() );
13
10
  const proxyActivitiesMock = vi.hoisted( () => vi.fn() );
14
11
  const executeChildMock = vi.hoisted( () => vi.fn() );
15
12
  const workflowInfoMock = vi.hoisted( () => vi.fn() );
13
+ const upsertMemoMock = vi.hoisted( () => vi.fn() );
16
14
  const continueAsNewMock = vi.hoisted( () => vi.fn() );
17
15
  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() );
16
+ const parseInputMock = vi.hoisted( () => vi.fn( input => input ) );
17
+ const parseOutputMock = vi.hoisted( () => vi.fn( output => output ) );
18
+ const parseInvocationOptionsMock = vi.hoisted( () => vi.fn( options => options ) );
21
19
  const validatorConstructorMock = vi.hoisted( () => vi.fn() );
22
20
  const createWorkflowMock = vi.hoisted( () => vi.fn( ( { handler } ) => handler ) );
23
21
 
@@ -29,9 +27,9 @@ vi.mock( './validations/index.js', () => {
29
27
 
30
28
  constructor( ...args ) {
31
29
  validatorConstructorMock( ...args );
32
- this.validateInput = validateInputMock;
33
- this.validateOutput = validateOutputMock;
34
- this.validateInvocationOptions = validateInvocationOptionsMock;
30
+ this.parseInput = parseInputMock;
31
+ this.parseOutput = parseOutputMock;
32
+ this.parseInvocationOptions = parseInvocationOptionsMock;
35
33
  }
36
34
  }
37
35
 
@@ -50,6 +48,7 @@ vi.mock( '@temporalio/workflow', async importOriginal => {
50
48
  proxyActivities: proxyActivitiesMock,
51
49
  executeChild: executeChildMock,
52
50
  workflowInfo: workflowInfoMock,
51
+ upsertMemo: upsertMemoMock,
53
52
  continueAsNew: continueAsNewMock,
54
53
  uuid4: () => '550e8400e29b41d4a716446655440000'
55
54
  };
@@ -60,28 +59,24 @@ const baseWorkflowInfo = () => ( {
60
59
  workflowType: 'test_wf',
61
60
  runId: 'run-123',
62
61
  startTime: new Date( '2025-01-01T00:00:00.000Z' ),
63
- memo: {},
62
+ memo: { traceInfo: { workflowId: 'workflow-123' } },
64
63
  continueAsNewSuggested: false
65
64
  } );
66
65
 
67
66
  const setWorkflowInfo = overrides => {
67
+ const base = baseWorkflowInfo();
68
68
  const info = {
69
- ...baseWorkflowInfo(),
69
+ ...base,
70
70
  ...overrides,
71
- memo: overrides?.memo ?? {}
71
+ memo: overrides?.memo ?? base.memo
72
72
  };
73
73
  workflowInfoMock.mockImplementation( () => info );
74
74
  return info;
75
75
  };
76
76
 
77
- const activityOutput = output => ( {
78
- output,
79
- [ACTIVITY_WRAPPER_VERSION_FIELD]: 1
80
- } );
81
-
82
77
  const createActivities = handlers => new Proxy( {}, {
83
78
  get: ( _, prop ) => typeof prop === 'string' ?
84
- handlers[prop] ?? vi.fn().mockResolvedValue( activityOutput( undefined ) ) :
79
+ handlers[prop] ?? vi.fn().mockResolvedValue( undefined ) :
85
80
  undefined
86
81
  } );
87
82
 
@@ -91,6 +86,13 @@ const mockActivities = handlers => {
91
86
  return activities;
92
87
  };
93
88
 
89
+ const installGlobalDispatcher = runId => {
90
+ const dispatcher = vi.fn();
91
+ dispatcher.runId = runId;
92
+ globalThis[INVOKE_ACTIVITY_SYMBOL] = dispatcher;
93
+ return dispatcher;
94
+ };
95
+
94
96
  const workflowDefinition = overrides => ( {
95
97
  name: 'test_wf',
96
98
  description: 'Test workflow',
@@ -100,16 +102,15 @@ const workflowDefinition = overrides => ( {
100
102
  ...overrides
101
103
  } );
102
104
 
103
- const invokeWorkflowFromHelper = ( wf, input, options ) => wf( input, options );
104
-
105
105
  describe( 'workflow()', () => {
106
106
  beforeEach( () => {
107
107
  vi.clearAllMocks();
108
+ delete globalThis[INVOKE_ACTIVITY_SYMBOL];
108
109
  inWorkflowContextMock.mockReturnValue( true );
109
- executeChildMock.mockResolvedValue( { output: {} } );
110
+ executeChildMock.mockResolvedValue( {} );
110
111
  setWorkflowInfo();
111
112
  mockActivities( {
112
- [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( { local: '/tmp/trace' } ) )
113
+ [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( { local: '/tmp/trace' } )
113
114
  } );
114
115
  } );
115
116
 
@@ -204,9 +205,9 @@ describe( 'workflow()', () => {
204
205
  value: 'test-workflow-ok',
205
206
  extra: 'custom'
206
207
  } );
207
- expect( validateInvocationOptionsMock ).toHaveBeenCalledWith( { context: { extra: 'custom' } } );
208
- expect( validateInputMock ).toHaveBeenCalledWith( { suffix: '-ok' } );
209
- expect( validateOutputMock ).toHaveBeenCalledWith( { value: 'test-workflow-ok', extra: 'custom' } );
208
+ expect( parseInvocationOptionsMock ).toHaveBeenCalledWith( { context: { extra: 'custom' } } );
209
+ expect( parseInputMock ).toHaveBeenCalledWith( { suffix: '-ok' } );
210
+ expect( parseOutputMock ).toHaveBeenCalledWith( { value: 'test-workflow-ok', extra: 'custom' } );
210
211
  expect( workflowInfoMock ).not.toHaveBeenCalled();
211
212
  expect( proxyActivitiesMock ).not.toHaveBeenCalled();
212
213
  } );
@@ -214,7 +215,7 @@ describe( 'workflow()', () => {
214
215
  it( 'does not run fn when plain function input validation fails', async () => {
215
216
  const { workflow } = await import( './workflow.js' );
216
217
  const error = new ValidationError( 'invalid input' );
217
- validateInputMock.mockImplementationOnce( () => {
218
+ parseInputMock.mockImplementationOnce( () => {
218
219
  throw error;
219
220
  } );
220
221
  const fn = vi.fn();
@@ -228,13 +229,13 @@ describe( 'workflow()', () => {
228
229
 
229
230
  await expect( wf( { value: 1 } ) ).rejects.toBe( error );
230
231
  expect( fn ).not.toHaveBeenCalled();
231
- expect( validateOutputMock ).not.toHaveBeenCalled();
232
+ expect( parseOutputMock ).not.toHaveBeenCalled();
232
233
  } );
233
234
 
234
235
  it( 'propagates plain function output validation errors after fn runs', async () => {
235
236
  const { workflow } = await import( './workflow.js' );
236
237
  const error = new ValidationError( 'invalid output' );
237
- validateOutputMock.mockImplementationOnce( () => {
238
+ parseOutputMock.mockImplementationOnce( () => {
238
239
  throw error;
239
240
  } );
240
241
  const output = { result: 1 };
@@ -251,24 +252,27 @@ describe( 'workflow()', () => {
251
252
  expect( fn ).toHaveBeenCalledWith( { value: 'ok' }, expect.objectContaining( {
252
253
  info: { workflowId: 'test-workflow', runId: 'test-run' }
253
254
  } ) );
254
- expect( validateOutputMock ).toHaveBeenCalledWith( output );
255
+ expect( parseOutputMock ).toHaveBeenCalledWith( output );
255
256
  } );
256
257
  } );
257
258
 
258
259
  describe( 'child workflow trigger path', () => {
259
- it( 'starts a child workflow when memo.stack already contains the current workflowId', async () => {
260
+ it( 'starts a child workflow and forwards child-safe invocation options', async () => {
260
261
  const { workflow } = await import( './workflow.js' );
261
262
  const { ParentClosePolicy } = await import( '@temporalio/workflow' );
263
+ installGlobalDispatcher( 'run-123' );
262
264
  const memo = {
263
- stack: [ 'root-workflow', 'workflow-123' ],
264
265
  traceInfo: { workflowId: 'root-workflow' },
265
266
  activityOptions: {
266
- startToCloseTimeout: '10m',
267
- retry: { maximumAttempts: 2 }
267
+ heartbeatTimeout: '30s',
268
+ retry: {
269
+ maximumInterval: '20s',
270
+ maximumAttempts: 4
271
+ }
268
272
  }
269
273
  };
270
274
  setWorkflowInfo( { memo } );
271
- executeChildMock.mockResolvedValueOnce( { output: { child: 'ok' } } );
275
+ executeChildMock.mockResolvedValueOnce( { child: 'ok' } );
272
276
 
273
277
  const wf = workflow( workflowDefinition( {
274
278
  name: 'child_target_wf',
@@ -279,13 +283,15 @@ describe( 'workflow()', () => {
279
283
 
280
284
  await expect( wf( { id: 1 }, {
281
285
  detached: true,
286
+ context: { testOnly: true },
282
287
  activityOptions: {
283
288
  startToCloseTimeout: '2m',
284
289
  retry: { maximumAttempts: 7 }
285
290
  }
286
291
  } ) ).resolves.toEqual( { child: 'ok' } );
287
- expect( validateInvocationOptionsMock ).toHaveBeenCalledWith( {
292
+ expect( parseInvocationOptionsMock ).toHaveBeenCalledWith( {
288
293
  detached: true,
294
+ context: { testOnly: true },
289
295
  activityOptions: {
290
296
  startToCloseTimeout: '2m',
291
297
  retry: { maximumAttempts: 7 }
@@ -293,26 +299,29 @@ describe( 'workflow()', () => {
293
299
  } );
294
300
 
295
301
  expect( executeChildMock ).toHaveBeenCalledWith( 'child_target_wf', {
296
- args: [ { id: 1 } ],
297
- workflowId: expect.stringMatching( /^workflow-123-/ ),
298
- parentClosePolicy: ParentClosePolicy.ABANDON,
299
- memo: {
300
- stack: [ 'root-workflow', 'workflow-123' ],
301
- traceInfo: { workflowId: 'root-workflow' },
302
+ args: [ {
303
+ id: 1
304
+ }, {
302
305
  activityOptions: {
303
306
  startToCloseTimeout: '2m',
304
307
  retry: { maximumAttempts: 7 }
305
308
  }
306
- }
309
+ } ],
310
+ workflowId: expect.stringMatching( /^workflow-123-/ ),
311
+ parentClosePolicy: ParentClosePolicy.ABANDON,
312
+ memo
307
313
  } );
314
+ expect( executeChildMock.mock.calls[0][1].args[1] ).not.toHaveProperty( 'detached' );
315
+ expect( executeChildMock.mock.calls[0][1].args[1] ).not.toHaveProperty( 'context' );
308
316
  expect( proxyActivitiesMock ).not.toHaveBeenCalled();
309
317
  } );
310
318
 
311
- it( 'uses empty args and terminate policy by default for child workflow execution', async () => {
319
+ it( 'uses undefined input and terminate policy by default for child workflow execution', async () => {
312
320
  const { workflow } = await import( './workflow.js' );
313
321
  const { ParentClosePolicy } = await import( '@temporalio/workflow' );
314
- setWorkflowInfo( { memo: { stack: [ 'workflow-123' ], activityOptions: { heartbeatTimeout: '1m' } } } );
315
- executeChildMock.mockResolvedValueOnce( { output: 'done' } );
322
+ installGlobalDispatcher( 'run-123' );
323
+ setWorkflowInfo( { memo: { traceInfo: { workflowId: 'root-workflow' } } } );
324
+ executeChildMock.mockResolvedValueOnce( 'done' );
316
325
 
317
326
  const wf = workflow( workflowDefinition( {
318
327
  name: 'no_input_child_wf',
@@ -323,92 +332,59 @@ describe( 'workflow()', () => {
323
332
 
324
333
  await expect( wf() ).resolves.toBe( 'done' );
325
334
  expect( executeChildMock ).toHaveBeenCalledWith( 'no_input_child_wf', expect.objectContaining( {
326
- args: [],
335
+ args: [ undefined, { activityOptions: undefined } ],
327
336
  parentClosePolicy: ParentClosePolicy.TERMINATE,
328
- memo: { stack: [ 'workflow-123' ], activityOptions: { heartbeatTimeout: '1m' } }
337
+ memo: { traceInfo: { workflowId: 'root-workflow' } }
329
338
  } ) );
330
339
  } );
331
340
 
332
- it( 'starts a child workflow when the call is made through a helper outside the handler', async () => {
341
+ it( 'propagates executeChild errors without root ApplicationFailure wrapping', async () => {
333
342
  const { workflow } = await import( './workflow.js' );
334
- const { ParentClosePolicy } = await import( '@temporalio/workflow' );
335
- const getTraceDestinations = vi.fn().mockResolvedValue( activityOutput( null ) );
336
- const info = setWorkflowInfo( { workflowType: 'indirect_parent_wf', memo: {} } );
337
- mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: getTraceDestinations } );
338
- executeChildMock.mockResolvedValueOnce( { output: { child: 'ok' } } );
339
- const childFn = vi.fn();
343
+ const error = new Error( 'child failed' );
344
+ installGlobalDispatcher( 'run-123' );
345
+ executeChildMock.mockRejectedValueOnce( error );
340
346
 
341
- const childWorkflow = workflow( workflowDefinition( {
342
- name: 'indirect_child_wf',
343
- inputSchema: z.object( { id: z.number() } ),
344
- outputSchema: z.object( { child: z.string() } ),
345
- fn: childFn
346
- } ) );
347
- const parentWorkflow = workflow( workflowDefinition( {
348
- name: 'indirect_parent_wf',
349
- outputSchema: z.object( { child: z.string() } ),
350
- fn: async () => invokeWorkflowFromHelper( childWorkflow, { id: 1 }, {
351
- activityOptions: {
352
- retry: { maximumAttempts: 1 }
353
- }
354
- } )
355
- } ) );
347
+ const wf = workflow( workflowDefinition( { name: 'failing_child_wf' } ) );
356
348
 
357
- await expect( parentWorkflow( {} ) ).resolves.toEqual( {
358
- [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
359
- output: { child: 'ok' },
360
- trace: { destinations: {} }
361
- } );
362
- expect( childFn ).not.toHaveBeenCalled();
363
- expect( executeChildMock ).toHaveBeenCalledWith( 'indirect_child_wf', expect.objectContaining( {
364
- args: [ { id: 1 } ],
365
- parentClosePolicy: ParentClosePolicy.TERMINATE,
366
- memo: expect.objectContaining( {
367
- stack: [ 'workflow-123' ],
368
- traceInfo: info.memo.traceInfo,
369
- activityOptions: expect.objectContaining( {
370
- retry: expect.objectContaining( { maximumAttempts: 1 } )
371
- } )
372
- } )
373
- } ) );
374
- expect( getTraceDestinations ).toHaveBeenCalledWith( info.memo.traceInfo );
349
+ await expect( wf( {} ) ).rejects.toBe( error );
375
350
  } );
351
+ } );
376
352
 
377
- it( 'does not fallback to child execution when the replayed workflow type matches an alias', async () => {
353
+ describe( 'workflow execution path', () => {
354
+ it( 'validates invocation options before reading workflow information', async () => {
378
355
  const { workflow } = await import( './workflow.js' );
379
- setWorkflowInfo( { workflowType: 'old_root_wf', memo: {} } );
356
+ const error = new ValidationError( 'invalid invocation options' );
357
+ parseInvocationOptionsMock.mockImplementationOnce( () => {
358
+ throw error;
359
+ } );
360
+ const wf = workflow( workflowDefinition() );
380
361
 
381
- const wf = workflow( workflowDefinition( {
382
- name: 'renamed_root_wf',
383
- aliases: [ 'old_root_wf' ],
384
- outputSchema: z.object( { ok: z.boolean() } ),
385
- fn: async () => ( { ok: true } )
386
- } ) );
362
+ await expect( wf( {}, { invalid: true } ) ).rejects.toBe( error );
387
363
 
388
- await expect( wf( {} ) ).resolves.toEqual( {
389
- [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
390
- output: { ok: true },
391
- trace: { destinations: { local: '/tmp/trace' } }
392
- } );
393
- expect( executeChildMock ).not.toHaveBeenCalled();
364
+ expect( upsertMemoMock ).not.toHaveBeenCalled();
365
+ expect( workflowInfoMock ).not.toHaveBeenCalled();
394
366
  } );
395
367
 
396
- it( 'propagates executeChild errors without root ApplicationFailure wrapping', async () => {
368
+ it( 'rejects a global activity dispatcher left by another workflow run', async () => {
397
369
  const { workflow } = await import( './workflow.js' );
398
- const error = new Error( 'child failed' );
399
- setWorkflowInfo( { memo: { stack: [ 'workflow-123' ] } } );
400
- executeChildMock.mockRejectedValueOnce( error );
370
+ const fn = vi.fn().mockResolvedValue( {} );
371
+ installGlobalDispatcher( 'stale-run' );
372
+ setWorkflowInfo( { runId: 'current-run' } );
401
373
 
402
- const wf = workflow( workflowDefinition( { name: 'failing_child_wf' } ) );
374
+ const wf = workflow( workflowDefinition( {
375
+ name: 'contamination_wf',
376
+ fn
377
+ } ) );
403
378
 
404
- await expect( wf( {} ) ).rejects.toBe( error );
379
+ await expect( wf( {} ) ).rejects.toThrow( /Contamination of the workflow Node global context/ );
380
+ expect( executeChildMock ).not.toHaveBeenCalled();
381
+ expect( proxyActivitiesMock ).not.toHaveBeenCalled();
382
+ expect( fn ).not.toHaveBeenCalled();
405
383
  } );
406
- } );
407
384
 
408
- describe( 'workflow execution path', () => {
409
- it( 'initializes root memo, skips trace destinations when trace is disabled, validates output, and returns an envelope', async () => {
385
+ it( 'skips trace destinations when trace is disabled and returns raw output', async () => {
410
386
  const { workflow } = await import( './workflow.js' );
411
- const getTraceDestinations = vi.fn().mockResolvedValue( activityOutput( { local: '/tmp/root-trace' } ) );
387
+ const getTraceDestinations = vi.fn().mockResolvedValue( { local: '/tmp/root-trace' } );
412
388
  const info = setWorkflowInfo( { workflowType: 'root_wf', memo: {} } );
413
389
  mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: getTraceDestinations } );
414
390
 
@@ -425,27 +401,82 @@ describe( 'workflow()', () => {
425
401
  fn: async ( _, context ) => ( { ok: context.info.workflowId === 'workflow-123' } )
426
402
  } ) );
427
403
 
428
- await expect( wf( {} ) ).resolves.toEqual( {
429
- [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
430
- output: { ok: true },
431
- trace: { destinations: {} }
432
- } );
433
- expect( info.memo.stack ).toEqual( [ 'workflow-123' ] );
404
+ await expect( wf( {} ) ).resolves.toEqual( { ok: true } );
434
405
  expect( info.memo.traceInfo ).toBeUndefined();
435
- expect( info.memo.activityOptions ).toEqual( expect.objectContaining( {
406
+ expect( info.memo.activityOptions ).toBeUndefined();
407
+ expect( proxyActivitiesMock ).toHaveBeenCalledWith( expect.objectContaining( {
436
408
  startToCloseTimeout: '5m',
437
409
  heartbeatTimeout: '5m',
438
410
  retry: expect.objectContaining( { maximumAttempts: 5 } )
439
411
  } ) );
440
- expect( proxyActivitiesMock ).toHaveBeenCalledWith( info.memo.activityOptions );
441
412
  expect( getTraceDestinations ).not.toHaveBeenCalled();
413
+ expect( upsertMemoMock ).toHaveBeenCalledOnce();
414
+ expect( upsertMemoMock ).toHaveBeenCalledWith( {
415
+ activityOptions: proxyActivitiesMock.mock.calls[0][0]
416
+ } );
417
+ } );
418
+
419
+ it( 'resolves activity options by invocation, definition, inherited memo, then default precedence', async () => {
420
+ const { workflow } = await import( './workflow.js' );
421
+ setWorkflowInfo( {
422
+ workflowType: 'activity_options_wf',
423
+ memo: {
424
+ traceInfo: { workflowId: 'workflow-123' },
425
+ activityOptions: {
426
+ heartbeatTimeout: '30s',
427
+ retry: {
428
+ maximumInterval: '30s',
429
+ maximumAttempts: 4
430
+ }
431
+ }
432
+ }
433
+ } );
434
+
435
+ const wf = workflow( workflowDefinition( {
436
+ name: 'activity_options_wf',
437
+ options: {
438
+ activityOptions: {
439
+ startToCloseTimeout: '5m',
440
+ retry: {
441
+ backoffCoefficient: 3,
442
+ maximumAttempts: 2
443
+ }
444
+ }
445
+ }
446
+ } ) );
447
+
448
+ await wf( {}, {
449
+ activityOptions: {
450
+ heartbeatTimeout: '1m',
451
+ retry: {
452
+ initialInterval: '1s',
453
+ maximumAttempts: 9
454
+ }
455
+ }
456
+ } );
457
+
458
+ expect( proxyActivitiesMock ).toHaveBeenCalledWith( {
459
+ startToCloseTimeout: '5m',
460
+ heartbeatTimeout: '1m',
461
+ retry: {
462
+ initialInterval: '1s',
463
+ backoffCoefficient: 3,
464
+ maximumInterval: '30s',
465
+ maximumAttempts: 9,
466
+ nonRetryableErrorTypes: [ 'FatalError' ]
467
+ }
468
+ } );
469
+ expect( upsertMemoMock ).toHaveBeenCalledOnce();
470
+ expect( upsertMemoMock ).toHaveBeenCalledWith( {
471
+ activityOptions: proxyActivitiesMock.mock.calls[0][0],
472
+ trace: { local: '/tmp/trace' }
473
+ } );
442
474
  } );
443
475
 
444
476
  it( 'runs non-root workflow execution without rebuilding trace info or fetching trace destinations', async () => {
445
477
  const { workflow } = await import( './workflow.js' );
446
- const getTraceDestinations = vi.fn().mockResolvedValue( activityOutput( { local: '/tmp/trace' } ) );
478
+ const getTraceDestinations = vi.fn().mockResolvedValue( { local: '/tmp/trace' } );
447
479
  const memo = {
448
- stack: [ 'root-workflow' ],
449
480
  traceInfo: { workflowId: 'root-workflow' },
450
481
  activityOptions: {
451
482
  startToCloseTimeout: '9m',
@@ -471,24 +502,27 @@ describe( 'workflow()', () => {
471
502
  fn: async () => ( { ok: true } )
472
503
  } ) );
473
504
 
474
- await expect( wf( {} ) ).resolves.toEqual( {
475
- [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
476
- output: { ok: true }
477
- } );
478
- expect( info.memo.stack ).toEqual( [ 'root-workflow', 'child-workflow' ] );
505
+ await expect( wf( {} ) ).resolves.toEqual( { ok: true } );
479
506
  expect( info.memo.traceInfo ).toBe( memo.traceInfo );
480
- expect( info.memo.activityOptions ).toEqual( expect.objectContaining( {
481
- startToCloseTimeout: '9m',
482
- retry: expect.objectContaining( { maximumAttempts: 8 } )
507
+ expect( proxyActivitiesMock ).toHaveBeenCalledWith( expect.objectContaining( {
508
+ startToCloseTimeout: '1m',
509
+ heartbeatTimeout: '5m',
510
+ retry: expect.objectContaining( { maximumAttempts: 2 } )
483
511
  } ) );
484
- expect( proxyActivitiesMock ).toHaveBeenCalledWith( info.memo.activityOptions );
485
512
  expect( getTraceDestinations ).not.toHaveBeenCalled();
513
+ expect( upsertMemoMock ).toHaveBeenCalledOnce();
514
+ expect( upsertMemoMock ).toHaveBeenCalledWith( {
515
+ activityOptions: proxyActivitiesMock.mock.calls[0][0]
516
+ } );
486
517
  } );
487
518
 
488
- it( 'returns empty trace destinations when getTraceDestinations returns no destinations', async () => {
519
+ it( 'stores empty trace destinations in memo and returns raw output', async () => {
489
520
  const { workflow } = await import( './workflow.js' );
490
- setWorkflowInfo( { workflowType: 'no_trace_dest_wf' } );
491
- mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( {} ) ) } );
521
+ setWorkflowInfo( {
522
+ workflowType: 'no_trace_dest_wf',
523
+ memo: { traceInfo: { workflowId: 'workflow-123' } }
524
+ } );
525
+ mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( {} ) } );
492
526
 
493
527
  const wf = workflow( workflowDefinition( {
494
528
  name: 'no_trace_dest_wf',
@@ -496,10 +530,11 @@ describe( 'workflow()', () => {
496
530
  fn: async () => ( { ok: true } )
497
531
  } ) );
498
532
 
499
- await expect( wf( {} ) ).resolves.toEqual( {
500
- [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
501
- output: { ok: true },
502
- trace: { destinations: {} }
533
+ await expect( wf( {} ) ).resolves.toEqual( { ok: true } );
534
+ expect( upsertMemoMock ).toHaveBeenCalledOnce();
535
+ expect( upsertMemoMock ).toHaveBeenCalledWith( {
536
+ activityOptions: proxyActivitiesMock.mock.calls[0][0],
537
+ trace: {}
503
538
  } );
504
539
  } );
505
540
 
@@ -516,29 +551,38 @@ describe( 'workflow()', () => {
516
551
  fn: async () => ( { result: 1 } )
517
552
  } ) );
518
553
 
519
- validateInputMock.mockImplementationOnce( () => {
554
+ parseInputMock.mockImplementationOnce( () => {
520
555
  throw inputError;
521
556
  } );
522
557
  await expect( wf( { value: 1 } ) ).rejects.toBe( inputError );
523
- expect( inputError[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
558
+ expect( upsertMemoMock ).toHaveBeenCalledWith( expect.objectContaining( {
559
+ trace: { local: '/tmp/trace' }
560
+ } ) );
524
561
 
525
- setWorkflowInfo( { workflowType: 'runtime_validation_wf', memo: {} } );
526
- validateOutputMock.mockImplementationOnce( () => {
562
+ delete globalThis[INVOKE_ACTIVITY_SYMBOL];
563
+ setWorkflowInfo( {
564
+ workflowType: 'runtime_validation_wf',
565
+ memo: { traceInfo: { workflowId: 'workflow-123' } }
566
+ } );
567
+ upsertMemoMock.mockClear();
568
+ parseOutputMock.mockImplementationOnce( () => {
527
569
  throw outputError;
528
570
  } );
529
571
  await expect( wf( { value: 'ok' } ) ).rejects.toBe( outputError );
530
- expect( outputError[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
572
+ expect( upsertMemoMock ).toHaveBeenCalledWith( expect.objectContaining( {
573
+ trace: { local: '/tmp/trace' }
574
+ } ) );
531
575
  } );
532
576
  } );
533
577
 
534
578
  describe( 'activity dispatchers', () => {
535
- it( 'invokes workflow-scoped steps and evaluators and unwraps activity output', async () => {
579
+ it( 'invokes workflow-scoped activities and returns their raw outputs', async () => {
536
580
  const { workflow } = await import( './workflow.js' );
537
581
  setWorkflowInfo( { workflowType: 'dispatch_wf' } );
538
- const step = vi.fn().mockResolvedValue( activityOutput( 'step-output' ) );
539
- const evaluator = vi.fn().mockResolvedValue( activityOutput( 'eval-output' ) );
582
+ const step = vi.fn().mockResolvedValue( 'step-output' );
583
+ const evaluator = vi.fn().mockResolvedValue( 'eval-output' );
540
584
  mockActivities( {
541
- [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( null ) ),
585
+ [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( {} ),
542
586
  'dispatch_wf#stepA': step,
543
587
  'dispatch_wf#evalA': evaluator
544
588
  } );
@@ -546,49 +590,44 @@ describe( 'workflow()', () => {
546
590
  const wf = workflow( workflowDefinition( {
547
591
  name: 'dispatch_wf',
548
592
  outputSchema: z.object( { stepResult: z.string(), evalResult: z.string() } ),
549
- async fn() {
593
+ fn: async () => {
550
594
  return {
551
- stepResult: await this.invokeStep( 'stepA', { a: 1 }, { b: 2 } ),
552
- evalResult: await this.invokeEvaluator( 'evalA', { c: 3 } )
595
+ stepResult: await globalThis[INVOKE_ACTIVITY_SYMBOL]( 'stepA', { a: 1 }, { b: 2 } ),
596
+ evalResult: await globalThis[INVOKE_ACTIVITY_SYMBOL]( 'evalA', { c: 3 } )
553
597
  };
554
598
  }
555
599
  } ) );
556
600
 
557
- await expect( wf( {} ) ).resolves.toEqual( {
558
- [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
559
- output: { stepResult: 'step-output', evalResult: 'eval-output' },
560
- trace: { destinations: {} }
561
- } );
601
+ await expect( wf( {} ) ).resolves.toEqual( { stepResult: 'step-output', evalResult: 'eval-output' } );
562
602
  expect( step ).toHaveBeenCalledWith( { a: 1 }, { b: 2 } );
563
603
  expect( evaluator ).toHaveBeenCalledWith( { c: 3 } );
564
604
  } );
565
605
 
566
- it( 'invokes shared steps and shared evaluators with the shared prefix', async () => {
606
+ it( 'invokes shared activities through the workflow namespace', async () => {
567
607
  const { workflow } = await import( './workflow.js' );
568
608
  setWorkflowInfo( { workflowType: 'shared_dispatch_wf' } );
569
- const sharedStep = vi.fn().mockResolvedValue( activityOutput( 'shared-step-output' ) );
570
- const sharedEvaluator = vi.fn().mockResolvedValue( activityOutput( 'shared-eval-output' ) );
609
+ const sharedStep = vi.fn().mockResolvedValue( 'shared-step-output' );
610
+ const sharedEvaluator = vi.fn().mockResolvedValue( 'shared-eval-output' );
571
611
  mockActivities( {
572
- [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( null ) ),
573
- [`${SHARED_STEP_PREFIX}#stepA`]: sharedStep,
574
- [`${SHARED_STEP_PREFIX}#evalA`]: sharedEvaluator
612
+ [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( {} ),
613
+ 'shared_dispatch_wf#stepA': sharedStep,
614
+ 'shared_dispatch_wf#evalA': sharedEvaluator
575
615
  } );
576
616
 
577
617
  const wf = workflow( workflowDefinition( {
578
618
  name: 'shared_dispatch_wf',
579
619
  outputSchema: z.object( { stepResult: z.string(), evalResult: z.string() } ),
580
- async fn() {
620
+ fn: async () => {
581
621
  return {
582
- stepResult: await this.invokeSharedStep( 'stepA' ),
583
- evalResult: await this.invokeSharedEvaluator( 'evalA', { x: 1 } )
622
+ stepResult: await globalThis[INVOKE_ACTIVITY_SYMBOL]( 'stepA' ),
623
+ evalResult: await globalThis[INVOKE_ACTIVITY_SYMBOL]( 'evalA', { x: 1 } )
584
624
  };
585
625
  }
586
626
  } ) );
587
627
 
588
628
  await expect( wf( {} ) ).resolves.toEqual( {
589
- [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
590
- output: { stepResult: 'shared-step-output', evalResult: 'shared-eval-output' },
591
- trace: { destinations: {} }
629
+ stepResult: 'shared-step-output',
630
+ evalResult: 'shared-eval-output'
592
631
  } );
593
632
  expect( sharedStep ).toHaveBeenCalledWith();
594
633
  expect( sharedEvaluator ).toHaveBeenCalledWith( { x: 1 } );
@@ -596,9 +635,10 @@ describe( 'workflow()', () => {
596
635
  } );
597
636
 
598
637
  describe( 'error handling', () => {
599
- it( 'attaches root trace destinations to root workflow errors before rethrowing', async () => {
638
+ it( 'stores root trace destinations in memo and preserves workflow errors', async () => {
600
639
  const { workflow } = await import( './workflow.js' );
601
- const error = new Error( 'root failed' );
640
+ const error = new Error( 'root failed with details' );
641
+ error.details = [ { domain: { reason: 'bad-input' } } ];
602
642
  setWorkflowInfo( { workflowType: 'root_error_wf' } );
603
643
 
604
644
  const wf = workflow( workflowDefinition( {
@@ -610,64 +650,13 @@ describe( 'workflow()', () => {
610
650
 
611
651
  const thrown = await wf( {} ).catch( e => e );
612
652
  expect( thrown ).toBe( error );
613
- expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
614
- } );
615
-
616
- it( 'preserves existing error details when attaching root trace metadata', async () => {
617
- const { workflow } = await import( './workflow.js' );
618
- const error = new Error( 'root failed with details' );
619
- error.details = [ { domain: { reason: 'bad-input' } } ];
620
- setWorkflowInfo( { workflowType: 'root_error_existing_details_wf' } );
621
-
622
- const wf = workflow( workflowDefinition( {
623
- name: 'root_error_existing_details_wf',
624
- fn: async () => {
625
- throw error;
626
- }
627
- } ) );
628
-
629
- await expect( wf( {} ) ).rejects.toBe( error );
630
653
  expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
631
- expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
632
- } );
633
-
634
- it( 'attaches empty trace destinations to root workflow errors when no destinations are available', async () => {
635
- const { workflow } = await import( './workflow.js' );
636
- setWorkflowInfo( { workflowType: 'root_error_no_trace_wf' } );
637
- mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( {} ) ) } );
638
- const error = new Error( 'root failed without trace' );
639
-
640
- const wf = workflow( workflowDefinition( {
641
- name: 'root_error_no_trace_wf',
642
- fn: async () => {
643
- throw error;
644
- }
654
+ expect( upsertMemoMock ).toHaveBeenCalledWith( expect.objectContaining( {
655
+ trace: { local: '/tmp/trace' }
645
656
  } ) );
646
-
647
- await expect( wf( {} ) ).rejects.toBe( error );
648
- expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: {} } } );
649
657
  } );
650
658
 
651
- it( 'preserves existing error details when no trace destinations are available', async () => {
652
- const { workflow } = await import( './workflow.js' );
653
- setWorkflowInfo( { workflowType: 'root_error_existing_details_no_trace_wf' } );
654
- mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( {} ) ) } );
655
- const error = new Error( 'root failed without trace' );
656
- error.details = [ { domain: { reason: 'bad-input' } } ];
657
-
658
- const wf = workflow( workflowDefinition( {
659
- name: 'root_error_existing_details_no_trace_wf',
660
- fn: async () => {
661
- throw error;
662
- }
663
- } ) );
664
-
665
- await expect( wf( {} ) ).rejects.toBe( error );
666
- expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
667
- expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: {} } } );
668
- } );
669
-
670
- it( 'attaches trace metadata to existing root ApplicationFailure without wrapping it', async () => {
659
+ it( 'preserves existing root ApplicationFailure metadata', async () => {
671
660
  const { workflow } = await import( './workflow.js' );
672
661
  const { ApplicationFailure } = await import( '@temporalio/workflow' );
673
662
  setWorkflowInfo( { workflowType: 'root_application_failure_wf' } );
@@ -690,7 +679,9 @@ describe( 'workflow()', () => {
690
679
  expect( thrown ).toBe( error );
691
680
  expect( error.type ).toBe( 'OriginalType' );
692
681
  expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
693
- expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
682
+ expect( upsertMemoMock ).toHaveBeenCalledWith( expect.objectContaining( {
683
+ trace: { local: '/tmp/trace' }
684
+ } ) );
694
685
  } );
695
686
 
696
687
  it( 'rethrows non-root workflow errors without ApplicationFailure wrapping', async () => {
@@ -699,7 +690,7 @@ describe( 'workflow()', () => {
699
690
  setWorkflowInfo( {
700
691
  workflowId: 'nested-workflow',
701
692
  root: { workflowId: 'root-workflow', runId: 'root-run' },
702
- memo: { stack: [ 'root-workflow' ], traceInfo: { workflowId: 'root-workflow' } }
693
+ memo: { traceInfo: { workflowId: 'root-workflow' } }
703
694
  } );
704
695
 
705
696
  const wf = workflow( workflowDefinition( {
@@ -710,7 +701,10 @@ describe( 'workflow()', () => {
710
701
  } ) );
711
702
 
712
703
  await expect( wf( {} ) ).rejects.toBe( error );
713
- expect( error[METADATA_ACCESS_SYMBOL] ).toBeUndefined();
704
+ expect( upsertMemoMock ).toHaveBeenCalledOnce();
705
+ expect( upsertMemoMock ).toHaveBeenCalledWith( {
706
+ activityOptions: proxyActivitiesMock.mock.calls[0][0]
707
+ } );
714
708
  } );
715
709
  } );
716
710
  } );