@output.ai/core 0.5.0 → 0.5.2

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.
@@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest';
2
2
  import { Readable } from 'node:stream';
3
3
  import {
4
4
  clone,
5
- mergeActivityOptions,
6
5
  serializeBodyAndInferContentType,
7
6
  serializeFetchResponse,
8
7
  deepMerge,
@@ -223,50 +222,6 @@ describe( 'serializeBodyAndInferContentType', () => {
223
222
  } );
224
223
  } );
225
224
 
226
- describe( 'mergeActivityOptions', () => {
227
- it( 'recursively merges nested objects', () => {
228
- const base = {
229
- taskQueue: 'q1',
230
- retry: { maximumAttempts: 3, backoffCoefficient: 2 }
231
- };
232
- const ext = {
233
- retry: { maximumAttempts: 5, initialInterval: '1s' }
234
- };
235
-
236
- const result = mergeActivityOptions( base, ext );
237
-
238
- expect( result ).toEqual( {
239
- taskQueue: 'q1',
240
- retry: { maximumAttempts: 5, backoffCoefficient: 2, initialInterval: '1s' }
241
- } );
242
- } );
243
-
244
- it( 'omitted properties in second do not overwrite first', () => {
245
- const base = {
246
- taskQueue: 'q2',
247
- retry: { initialInterval: '2s', backoffCoefficient: 2 }
248
- };
249
- const ext = {
250
- retry: { backoffCoefficient: 3 }
251
- };
252
-
253
- const result = mergeActivityOptions( base, ext );
254
-
255
- expect( result.retry.initialInterval ).toBe( '2s' );
256
- expect( result.retry.backoffCoefficient ).toBe( 3 );
257
- expect( result.taskQueue ).toBe( 'q2' );
258
- } );
259
-
260
- it( 'handles omitted second argument by returning a clone', () => {
261
- const base = { taskQueue: 'q3', retry: { maximumAttempts: 2 } };
262
-
263
- const result = mergeActivityOptions( base );
264
-
265
- expect( result ).toEqual( base );
266
- expect( result ).not.toBe( base );
267
- } );
268
- } );
269
-
270
225
  describe( 'deepMerge', () => {
271
226
  it( 'Overwrites properties in object "a"', () => {
272
227
  const a = {
@@ -316,13 +271,43 @@ describe( 'deepMerge', () => {
316
271
  } );
317
272
  } );
318
273
 
319
- it( 'Throw error on non iterable object types', () => {
320
- expect( () => deepMerge( Function, Function ) ).toThrow( Error );
321
- expect( () => deepMerge( () => {}, () => {} ) ).toThrow( Error );
322
- expect( () => deepMerge( 'a', 'a' ) ).toThrow( Error );
323
- expect( () => deepMerge( true, true ) ).toThrow( Error );
324
- expect( () => deepMerge( /a/, /a/ ) ).toThrow( Error );
325
- expect( () => deepMerge( [], [] ) ).toThrow( Error );
274
+ it( 'Merge object is a clone', () => {
275
+ const a = {
276
+ a: 1
277
+ };
278
+ const b = {
279
+ b: 1
280
+ };
281
+ const result = deepMerge( a, b );
282
+ a.a = 2;
283
+ b.b = 2;
284
+ expect( result.a ).toEqual( 1 );
285
+ } );
286
+
287
+ it( 'Returns copy of "a" if "b" is not an object', () => {
288
+ const a = {
289
+ a: 1
290
+ };
291
+ expect( deepMerge( a, null ) ).toEqual( { a: 1 } );
292
+ expect( deepMerge( a, undefined ) ).toEqual( { a: 1 } );
293
+ } );
294
+
295
+ it( 'Copy of object "a" is a clone', () => {
296
+ const a = {
297
+ a: 1
298
+ };
299
+ const result = deepMerge( a, null );
300
+ a.a = 2;
301
+ expect( result.a ).toEqual( 1 );
302
+ } );
303
+
304
+ it( 'Throws when first argument is not a plain object', () => {
305
+ expect( () => deepMerge( Function ) ).toThrow( Error );
306
+ expect( () => deepMerge( () => {} ) ).toThrow( Error );
307
+ expect( () => deepMerge( 'a' ) ).toThrow( Error );
308
+ expect( () => deepMerge( true ) ).toThrow( Error );
309
+ expect( () => deepMerge( /a/ ) ).toThrow( Error );
310
+ expect( () => deepMerge( [] ) ).toThrow( Error );
326
311
  expect( () => deepMerge( class Foo {}, class Foo {} ) ).toThrow( Error );
327
312
  expect( () => deepMerge( Number.constructor, Number.constructor ) ).toThrow( Error );
328
313
  expect( () => deepMerge( Number.constructor.prototype, Number.constructor.prototype ) ).toThrow( Error );
@@ -377,6 +362,8 @@ describe( 'isPlainObject', () => {
377
362
  } );
378
363
 
379
364
  it( 'Returns false for primitives', () => {
365
+ expect( isPlainObject( null ) ).toBe( false );
366
+ expect( isPlainObject( undefined ) ).toBe( false );
380
367
  expect( isPlainObject( false ) ).toBe( false );
381
368
  expect( isPlainObject( true ) ).toBe( false );
382
369
  expect( isPlainObject( 1 ) ).toBe( false );
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ const mockLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
4
+ vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
5
+
6
+ vi.mock( '#consts', () => ( { WORKFLOW_CATALOG: 'catalog' } ) );
7
+
8
+ vi.mock( '#tracing', () => ( { init: vi.fn().mockResolvedValue( undefined ) } ) );
9
+
10
+ const configValues = {
11
+ address: 'localhost:7233',
12
+ apiKey: undefined,
13
+ namespace: 'default',
14
+ taskQueue: 'test-queue',
15
+ catalogId: 'test-catalog',
16
+ maxConcurrentWorkflowTaskExecutions: 200,
17
+ maxConcurrentActivityTaskExecutions: 40,
18
+ maxCachedWorkflows: 1000,
19
+ maxConcurrentActivityTaskPolls: 5,
20
+ maxConcurrentWorkflowTaskPolls: 5
21
+ };
22
+ vi.mock( './configs.js', () => configValues );
23
+
24
+ const loadWorkflowsMock = vi.fn().mockResolvedValue( [] );
25
+ const loadActivitiesMock = vi.fn().mockResolvedValue( {} );
26
+ const createWorkflowsEntryPointMock = vi.fn().mockReturnValue( '/fake/workflows/path.js' );
27
+ vi.mock( './loader.js', () => ( {
28
+ loadWorkflows: loadWorkflowsMock,
29
+ loadActivities: loadActivitiesMock,
30
+ createWorkflowsEntryPoint: createWorkflowsEntryPointMock
31
+ } ) );
32
+
33
+ vi.mock( './sinks.js', () => ( { sinks: {} } ) );
34
+
35
+ const createCatalogMock = vi.fn().mockReturnValue( { workflows: [], activities: {} } );
36
+ vi.mock( './catalog_workflow/index.js', () => ( { createCatalog: createCatalogMock } ) );
37
+
38
+ vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
39
+
40
+ const initInterceptorsMock = vi.fn().mockReturnValue( [] );
41
+ vi.mock( './interceptors.js', () => ( { initInterceptors: initInterceptorsMock } ) );
42
+
43
+ const runState = { resolve: null };
44
+ const runPromise = new Promise( r => {
45
+ runState.resolve = r;
46
+ } );
47
+ const shutdownMock = vi.fn();
48
+ const mockConnection = { close: vi.fn().mockResolvedValue( undefined ) };
49
+ const mockWorker = { run: () => runPromise, shutdown: shutdownMock };
50
+
51
+ vi.mock( '@temporalio/worker', () => ( {
52
+ Worker: { create: vi.fn().mockResolvedValue( mockWorker ) },
53
+ NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) }
54
+ } ) );
55
+
56
+ const workflowStartMock = vi.fn().mockResolvedValue( undefined );
57
+ vi.mock( '@temporalio/client', () => ( {
58
+ Client: vi.fn().mockImplementation( () => ( {
59
+ workflow: { start: workflowStartMock }
60
+ } ) )
61
+ } ) );
62
+
63
+ vi.mock( '@temporalio/common', () => ( {
64
+ WorkflowIdConflictPolicy: { TERMINATE_EXISTING: 'TERMINATE_EXISTING' }
65
+ } ) );
66
+
67
+ describe( 'worker/index', () => {
68
+ const exitMock = vi.fn();
69
+ const originalArgv = process.argv;
70
+ const originalExit = process.exit;
71
+ const originalOn = process.on;
72
+
73
+ beforeEach( () => {
74
+ vi.clearAllMocks();
75
+ process.argv = [ ...originalArgv.slice( 0, 2 ), '/test/caller/dir' ];
76
+ process.exit = exitMock;
77
+ } );
78
+
79
+ afterEach( () => {
80
+ process.argv = originalArgv;
81
+ process.exit = originalExit;
82
+ process.on = originalOn;
83
+ configValues.apiKey = undefined;
84
+ } );
85
+
86
+ it( 'loads configs, workflows, activities and creates worker with correct options', async () => {
87
+ const { Worker, NativeConnection } = await import( '@temporalio/worker' );
88
+ const { Client } = await import( '@temporalio/client' );
89
+ const { init: initTracing } = await import( '#tracing' );
90
+
91
+ import( './index.js' );
92
+
93
+ await vi.waitFor( () => {
94
+ expect( loadWorkflowsMock ).toHaveBeenCalledWith( '/test/caller/dir' );
95
+ } );
96
+ expect( loadActivitiesMock ).toHaveBeenCalledWith( '/test/caller/dir', [] );
97
+ expect( createWorkflowsEntryPointMock ).toHaveBeenCalledWith( [] );
98
+ expect( initTracing ).toHaveBeenCalled();
99
+ expect( createCatalogMock ).toHaveBeenCalledWith( { workflows: [], activities: {} } );
100
+ expect( NativeConnection.connect ).toHaveBeenCalledWith( {
101
+ address: configValues.address,
102
+ tls: false,
103
+ apiKey: undefined
104
+ } );
105
+ expect( Worker.create ).toHaveBeenCalledWith( expect.objectContaining( {
106
+ namespace: configValues.namespace,
107
+ taskQueue: configValues.taskQueue,
108
+ workflowsPath: '/fake/workflows/path.js',
109
+ activities: {},
110
+ maxConcurrentWorkflowTaskExecutions: configValues.maxConcurrentWorkflowTaskExecutions,
111
+ maxConcurrentActivityTaskExecutions: configValues.maxConcurrentActivityTaskExecutions,
112
+ maxCachedWorkflows: configValues.maxCachedWorkflows,
113
+ maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
114
+ maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
115
+ } ) );
116
+ expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {} } );
117
+ expect( Client ).toHaveBeenCalledWith( { connection: mockConnection, namespace: configValues.namespace } );
118
+ expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
119
+ taskQueue: configValues.taskQueue,
120
+ workflowId: configValues.catalogId,
121
+ workflowIdConflictPolicy: 'TERMINATE_EXISTING',
122
+ args: [ { workflows: [], activities: {} } ]
123
+ } );
124
+
125
+ runState.resolve();
126
+ await vi.waitFor( () => {
127
+ expect( mockConnection.close ).toHaveBeenCalled();
128
+ } );
129
+ expect( exitMock ).toHaveBeenCalledWith( 0 );
130
+ } );
131
+
132
+ it( 'enables TLS when apiKey is set', async () => {
133
+ configValues.apiKey = 'secret';
134
+ vi.resetModules();
135
+
136
+ const { NativeConnection } = await import( '@temporalio/worker' );
137
+ import( './index.js' );
138
+
139
+ await vi.waitFor( () => {
140
+ expect( NativeConnection.connect ).toHaveBeenCalledWith( expect.objectContaining( {
141
+ tls: true,
142
+ apiKey: 'secret'
143
+ } ) );
144
+ } );
145
+ await vi.waitFor( () => expect( exitMock ).toHaveBeenCalled() );
146
+ } );
147
+
148
+ it( 'registers SIGTERM and SIGINT handlers that shut down worker', async () => {
149
+ const onMock = vi.fn();
150
+ process.on = onMock;
151
+ vi.resetModules();
152
+
153
+ import( './index.js' );
154
+
155
+ await vi.waitFor( () => {
156
+ expect( onMock ).toHaveBeenCalledWith( 'SIGTERM', expect.any( Function ) );
157
+ expect( onMock ).toHaveBeenCalledWith( 'SIGINT', expect.any( Function ) );
158
+ } );
159
+
160
+ const sigtermHandler = onMock.mock.calls.find( c => c[0] === 'SIGTERM' )?.[1];
161
+ expect( sigtermHandler ).toBeDefined();
162
+ sigtermHandler();
163
+ expect( shutdownMock ).toHaveBeenCalled();
164
+
165
+ runState.resolve();
166
+ await vi.waitFor( () => expect( exitMock ).toHaveBeenCalled() );
167
+ } );
168
+
169
+ it( 'calls process.exit(1) on fatal error', async () => {
170
+ loadWorkflowsMock.mockRejectedValueOnce( new Error( 'load failed' ) );
171
+ vi.resetModules();
172
+
173
+ import( './index.js' );
174
+
175
+ await vi.waitFor( () => {
176
+ expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.any( Object ) );
177
+ } );
178
+ expect( exitMock ).toHaveBeenCalledWith( 1 );
179
+ } );
180
+ } );
@@ -1,7 +1,7 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
2
  import { workflowInfo, proxySinks, ApplicationFailure, ContinueAsNew } from '@temporalio/workflow';
3
3
  import { memoToHeaders } from '../sandboxed_utils.js';
4
- import { mergeActivityOptions } from '#utils';
4
+ import { deepMerge } from '#utils';
5
5
  import { METADATA_ACCESS_SYMBOL } from '#consts';
6
6
  // this is a dynamic generated file with activity configs overwrites
7
7
  import stepOptions from '../temp/__activity_options.js';
@@ -22,7 +22,7 @@ class HeadersInjectionInterceptor {
22
22
  // apply per-invocation options passed as second argument by rewritten calls
23
23
  const options = stepOptions[input.activityType];
24
24
  if ( options ) {
25
- input.options = mergeActivityOptions( memo.activityOptions, options );
25
+ input.options = deepMerge( memo.activityOptions, options );
26
26
  }
27
27
  return next( input );
28
28
  }