@outputai/core 0.14.0 → 0.14.1-next.2947341.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 (100) hide show
  1. package/dist/credentials/credentials.d.ts +6 -0
  2. package/dist/credentials/credentials.js +87 -0
  3. package/dist/credentials/encrypted_yaml_provider.d.ts +2 -0
  4. package/dist/credentials/encrypted_yaml_provider.js +77 -0
  5. package/dist/credentials/encryption.d.ts +3 -0
  6. package/dist/credentials/encryption.js +6 -0
  7. package/dist/credentials/errors.d.ts +13 -0
  8. package/dist/credentials/errors.js +39 -0
  9. package/dist/credentials/index.d.ts +6 -0
  10. package/dist/credentials/index.js +5 -0
  11. package/dist/credentials/init.d.ts +1 -0
  12. package/dist/credentials/init.js +7 -0
  13. package/dist/credentials/paths.d.ts +7 -0
  14. package/dist/credentials/paths.js +14 -0
  15. package/dist/credentials/provider_registry.d.ts +3 -0
  16. package/dist/credentials/provider_registry.js +6 -0
  17. package/dist/credentials/types.d.ts +12 -0
  18. package/dist/credentials/types.js +1 -0
  19. package/package.json +25 -14
  20. package/src/async_storage.d.ts +4 -0
  21. package/src/consts.js +0 -2
  22. package/src/helpers/object.d.ts +2 -0
  23. package/src/hooks/index.d.ts +0 -8
  24. package/src/hooks/index.js +0 -3
  25. package/src/logger/index.d.ts +4 -0
  26. package/src/worker/index.js +3 -1
  27. package/bin/healthcheck.spec.js +0 -90
  28. package/src/bus.spec.js +0 -196
  29. package/src/errors.spec.js +0 -19
  30. package/src/helpers/activity_options.spec.js +0 -55
  31. package/src/helpers/component.spec.js +0 -54
  32. package/src/helpers/error_serializer.spec.js +0 -479
  33. package/src/helpers/errors.spec.js +0 -189
  34. package/src/helpers/fetch.spec.js +0 -292
  35. package/src/helpers/function.spec.js +0 -24
  36. package/src/helpers/object.spec.js +0 -425
  37. package/src/helpers/promise.spec.js +0 -62
  38. package/src/helpers/redact.spec.js +0 -66
  39. package/src/helpers/string.spec.js +0 -99
  40. package/src/helpers/temporal_context.spec.ts +0 -83
  41. package/src/helpers/trace_info.spec.js +0 -45
  42. package/src/helpers/workflow_context.spec.js +0 -46
  43. package/src/hooks/index.spec.js +0 -265
  44. package/src/hooks/pending_hooks.spec.js +0 -62
  45. package/src/interface/evaluator.spec.js +0 -685
  46. package/src/interface/logger.spec.js +0 -172
  47. package/src/interface/step.spec.js +0 -128
  48. package/src/interface/validations/index.spec.js +0 -197
  49. package/src/interface/validations/schemas.spec.js +0 -229
  50. package/src/interface/webhook.spec.js +0 -170
  51. package/src/interface/workflow.spec.js +0 -710
  52. package/src/interface/workflow_utils.spec.js +0 -271
  53. package/src/interface/zod_integration.spec.js +0 -646
  54. package/src/internal_activities/index.spec.js +0 -186
  55. package/src/logger/development.spec.js +0 -87
  56. package/src/logger/index.spec.js +0 -27
  57. package/src/logger/production.spec.js +0 -71
  58. package/src/sdk/helpers/component_metadata.spec.js +0 -30
  59. package/src/sdk/helpers/objects.spec.js +0 -16
  60. package/src/sdk/helpers/path.spec.js +0 -102
  61. package/src/sdk/runtime/context.spec.js +0 -37
  62. package/src/sdk/runtime/events.spec.js +0 -40
  63. package/src/temporal/client.spec.js +0 -124
  64. package/src/tracing/processors/local/index.spec.js +0 -204
  65. package/src/tracing/processors/s3/configs.spec.js +0 -64
  66. package/src/tracing/processors/s3/index.spec.js +0 -185
  67. package/src/tracing/processors/s3/redis_client.spec.js +0 -179
  68. package/src/tracing/processors/s3/s3_client.spec.js +0 -68
  69. package/src/tracing/tools/build_trace_tree.spec.js +0 -216
  70. package/src/tracing/trace_engine.spec.js +0 -299
  71. package/src/worker/bundle.spec.js +0 -53
  72. package/src/worker/bundler_options.spec.js +0 -62
  73. package/src/worker/catalog_workflow/catalog_publisher.spec.js +0 -317
  74. package/src/worker/catalog_workflow/index.spec.js +0 -222
  75. package/src/worker/configs.spec.js +0 -213
  76. package/src/worker/configs_tuner_schema.spec.js +0 -127
  77. package/src/worker/connection_monitor.spec.js +0 -328
  78. package/src/worker/global_functions.spec.js +0 -55
  79. package/src/worker/index.spec.js +0 -526
  80. package/src/worker/interceptors/activity.spec.js +0 -369
  81. package/src/worker/interceptors/headers.spec.js +0 -73
  82. package/src/worker/interceptors/workflow.spec.js +0 -357
  83. package/src/worker/interruption.spec.js +0 -177
  84. package/src/worker/loader/activities.spec.js +0 -212
  85. package/src/worker/loader/hooks.spec.js +0 -64
  86. package/src/worker/loader/matchers.spec.js +0 -140
  87. package/src/worker/loader/tools.spec.js +0 -595
  88. package/src/worker/loader/workflows.spec.js +0 -287
  89. package/src/worker/log_hooks.spec.js +0 -303
  90. package/src/worker/shutdown.spec.js +0 -197
  91. package/src/worker/sinks.spec.js +0 -220
  92. package/src/worker/telemetry.spec.js +0 -80
  93. package/src/worker/temporal_logger.spec.js +0 -90
  94. package/src/worker/webpack_loaders/tools.spec.js +0 -314
  95. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +0 -134
  96. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +0 -154
  97. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +0 -165
  98. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +0 -202
  99. package/src/worker/webpack_loaders/workflow_validator/npm_workflow_export_resolve.spec.js +0 -374
  100. package/src/worker/worker_runner.spec.js +0 -220
@@ -1,357 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { ApplicationFailure } from '@temporalio/common';
3
- import { FatalError, TransparentFatalError, ValidationError } from '#errors';
4
-
5
- const workflowInfoMock = vi.fn();
6
- const workflowStartMock = vi.fn();
7
- const workflowEndMock = vi.fn();
8
- const workflowErrorMock = vi.fn();
9
- const isCancellationMock = vi.fn();
10
- const upsertMemoMock = vi.fn();
11
- const traceInfoBuildMock = vi.fn();
12
- const startTime = new Date( '2026-06-02T09:00:00.000Z' );
13
- const runStartTime = new Date( '2026-06-02T09:05:00.000Z' );
14
- const workflowDetails = {
15
- attempt: 1,
16
- continuedFromExecutionRunId: undefined,
17
- firstExecutionRunId: 'first-run',
18
- parent: undefined,
19
- root: undefined,
20
- runId: 'run-1',
21
- runStartTime: runStartTime.getTime(),
22
- startTime: startTime.getTime(),
23
- workflowId: 'workflow-1',
24
- workflowType: 'MyWorkflow'
25
- };
26
-
27
- const workflowInfo = {
28
- attempt: 1,
29
- continuedFromExecutionRunId: undefined,
30
- firstExecutionRunId: 'first-run',
31
- parent: undefined,
32
- root: undefined,
33
- runId: 'run-1',
34
- runStartTime,
35
- startTime,
36
- workflowId: 'workflow-1',
37
- workflowType: 'MyWorkflow',
38
- memo: { traceInfo: { runId: 'root-run' } }
39
- };
40
-
41
- vi.mock( '@temporalio/workflow', async importOriginal => {
42
- const actual = await importOriginal();
43
- return {
44
- ...actual,
45
- workflowInfo: ( ...args ) => workflowInfoMock( ...args ),
46
- proxySinks: () => ( {
47
- workflow: { start: workflowStartMock, end: workflowEndMock, error: workflowErrorMock }
48
- } ),
49
- ContinueAsNew: class ContinueAsNew extends Error {
50
- constructor() {
51
- super( 'ContinueAsNew' );
52
- this.name = 'ContinueAsNew';
53
- }
54
- },
55
- isCancellation: ( ...args ) => isCancellationMock( ...args ),
56
- upsertMemo: ( ...args ) => upsertMemoMock( ...args )
57
- };
58
- } );
59
-
60
- vi.mock( '#helpers/trace_info', () => ( { TraceInfo: { build: ( ...args ) => traceInfoBuildMock( ...args ) } } ) );
61
-
62
- const memoToHeadersMock = vi.fn( memo => ( memo ? { ...memo, __asHeaders: true } : {} ) );
63
- vi.mock( './headers.js', () => ( { memoToHeaders: ( ...args ) => memoToHeadersMock( ...args ) } ) );
64
-
65
- const deepMergeMock = vi.fn( ( a, b ) => ( { ...( a || {} ), ...( b || {} ) } ) );
66
- vi.mock( '#helpers/object', () => ( { deepMerge: ( ...args ) => deepMergeMock( ...args ) } ) );
67
-
68
- const activityOptionsDefault = {};
69
- vi.mock( '../temp/__activity_options.js', () => ( { default: activityOptionsDefault } ) );
70
-
71
- const workflowOptionsDefault = {};
72
- vi.mock( '../temp/__workflow_options.js', () => ( { default: workflowOptionsDefault } ) );
73
-
74
- describe( 'workflow interceptors', () => {
75
- beforeEach( () => {
76
- vi.clearAllMocks();
77
- Object.keys( activityOptionsDefault ).forEach( key => delete activityOptionsDefault[key] );
78
- Object.keys( workflowOptionsDefault ).forEach( key => delete workflowOptionsDefault[key] );
79
- workflowOptionsDefault.MyWorkflow = { disableTrace: false };
80
- isCancellationMock.mockReturnValue( false );
81
- traceInfoBuildMock.mockReturnValue( { workflowId: 'workflow-1', runId: 'run-1' } );
82
- workflowInfoMock.mockReturnValue( workflowInfo );
83
- } );
84
-
85
- describe( 'HeadersInjectionInterceptor', () => {
86
- it( 'assigns memo as headers via memoToHeaders and calls next', async () => {
87
- const { interceptors } = await import( './workflow.js' );
88
- const { outbound } = interceptors();
89
- const interceptor = outbound[0];
90
- const input = { headers: { existing: 'header' }, activityType: 'MyWorkflow#step1', options: {} };
91
- const next = vi.fn().mockResolvedValue( 'result' );
92
-
93
- memoToHeadersMock.mockReturnValue( { traceInfo: workflowInfo.memo.traceInfo, workflowDetails } );
94
-
95
- const out = await interceptor.scheduleActivity( input, next );
96
-
97
- expect( memoToHeadersMock ).toHaveBeenCalledWith( {
98
- traceInfo: workflowInfo.memo.traceInfo,
99
- workflowDetails
100
- } );
101
- expect( input.headers ).toEqual( {
102
- existing: 'header',
103
- traceInfo: workflowInfo.memo.traceInfo,
104
- workflowDetails
105
- } );
106
- expect( next ).toHaveBeenCalledWith( input );
107
- expect( out ).toBe( 'result' );
108
- } );
109
-
110
- it( 'merges component activity options over the scheduled activity input options', async () => {
111
- activityOptionsDefault['MyWorkflow#step1'] = { scheduleToCloseTimeout: 60 };
112
- workflowInfoMock.mockReturnValue( {
113
- ...workflowInfo,
114
- memo: { traceInfo: workflowInfo.memo.traceInfo }
115
- } );
116
- memoToHeadersMock.mockReturnValue( {} );
117
- deepMergeMock.mockReturnValue( {
118
- heartbeatTimeout: 10,
119
- scheduleToCloseTimeout: 60,
120
- retry: { maximumAttempts: 2, nonRetryableErrorTypes: [ 'StepError' ] }
121
- } );
122
-
123
- const { interceptors } = await import( './workflow.js' );
124
- const { outbound } = interceptors();
125
- const interceptor = outbound[0];
126
- const input = { headers: {}, activityType: 'MyWorkflow#step1', options: { heartbeatTimeout: 10 } };
127
- const next = vi.fn().mockResolvedValue( undefined );
128
-
129
- await interceptor.scheduleActivity( input, next );
130
-
131
- expect( deepMergeMock ).toHaveBeenCalledWith( { heartbeatTimeout: 10 }, { scheduleToCloseTimeout: 60 } );
132
- expect( input.options ).toEqual( {
133
- heartbeatTimeout: 10,
134
- scheduleToCloseTimeout: 60,
135
- retry: {
136
- maximumAttempts: 2,
137
- nonRetryableErrorTypes: [ 'StepError', 'FatalError' ]
138
- }
139
- } );
140
- } );
141
-
142
- it( 'adds FatalError to activity retry options without duplicates', async () => {
143
- const { interceptors } = await import( './workflow.js' );
144
- const { outbound } = interceptors();
145
- const interceptor = outbound[0];
146
- const input = {
147
- headers: {},
148
- activityType: 'MyWorkflow#step1',
149
- options: {
150
- retry: {
151
- maximumAttempts: 4,
152
- nonRetryableErrorTypes: [ 'DomainError', 'FatalError' ]
153
- }
154
- }
155
- };
156
- const next = vi.fn().mockResolvedValue( undefined );
157
-
158
- await interceptor.scheduleActivity( input, next );
159
-
160
- expect( input.options.retry ).toEqual( {
161
- maximumAttempts: 4,
162
- nonRetryableErrorTypes: [ 'DomainError', 'FatalError' ]
163
- } );
164
- } );
165
- } );
166
-
167
- describe( 'WorkflowExecutionInterceptor', () => {
168
- it( 'upserts root trace info before starting the workflow trace', async () => {
169
- const { interceptors } = await import( './workflow.js' );
170
- const { inbound } = interceptors();
171
- const traceInfo = { workflowId: 'workflow-1', runId: 'run-1' };
172
- traceInfoBuildMock.mockReturnValue( traceInfo );
173
-
174
- await inbound[0].execute( { args: [ {} ] }, vi.fn().mockResolvedValue( undefined ) );
175
-
176
- expect( traceInfoBuildMock ).toHaveBeenCalledOnce();
177
- expect( upsertMemoMock ).toHaveBeenCalledWith( { payloadVersion: '2', traceInfo } );
178
- expect( upsertMemoMock.mock.invocationCallOrder[0] ).toBeLessThan( workflowStartMock.mock.invocationCallOrder[0] );
179
- } );
180
-
181
- it.each( [
182
- {
183
- name: 'tracing is disabled',
184
- info: workflowInfo,
185
- options: { disableTrace: true }
186
- },
187
- {
188
- name: 'the workflow has a root',
189
- info: { ...workflowInfo, root: { workflowId: 'root-workflow', runId: 'root-run' } },
190
- options: { disableTrace: false }
191
- }
192
- ] )( 'does not build trace info when $name', async ( { info, options } ) => {
193
- workflowInfoMock.mockReturnValue( info );
194
- workflowOptionsDefault.MyWorkflow = options;
195
- const { interceptors } = await import( './workflow.js' );
196
- const { inbound } = interceptors();
197
-
198
- await inbound[0].execute( { args: [ {} ] }, vi.fn().mockResolvedValue( undefined ) );
199
-
200
- expect( traceInfoBuildMock ).not.toHaveBeenCalled();
201
- expect( upsertMemoMock ).toHaveBeenCalledOnce();
202
- expect( upsertMemoMock ).toHaveBeenCalledWith( { payloadVersion: '2' } );
203
- } );
204
-
205
- it( 'calls sinks.workflow.start, next, then sinks.workflow.end on success', async () => {
206
- const { interceptors } = await import( './workflow.js' );
207
- const { inbound } = interceptors();
208
- const interceptor = inbound[0];
209
- const input = { args: [ { input: 'data' } ] };
210
- const next = vi.fn().mockResolvedValue( { output: 'ok' } );
211
-
212
- const result = await interceptor.execute( input, next );
213
-
214
- expect( workflowStartMock ).toHaveBeenCalledWith( { input: 'data' } );
215
- expect( next ).toHaveBeenCalledWith( input );
216
- expect( workflowEndMock ).toHaveBeenCalledWith( { output: 'ok' } );
217
- expect( result ).toEqual( { output: 'ok' } );
218
- expect( workflowErrorMock ).not.toHaveBeenCalled();
219
- } );
220
-
221
- it( 'rethrows native errors as Workflow Task failures without a terminal sink event', async () => {
222
- const { interceptors } = await import( './workflow.js' );
223
- const { inbound } = interceptors();
224
- const interceptor = inbound[0];
225
- const input = { args: [ {} ] };
226
- const err = new Error( 'workflow failed' );
227
- const next = vi.fn().mockRejectedValue( err );
228
-
229
- await expect( interceptor.execute( input, next ) ).rejects.toBe( err );
230
- expect( workflowStartMock ).toHaveBeenCalled();
231
- expect( workflowErrorMock ).not.toHaveBeenCalled();
232
- expect( workflowEndMock ).not.toHaveBeenCalled();
233
- } );
234
-
235
- it.each( [
236
- { ErrorType: FatalError, type: 'FatalError' },
237
- { ErrorType: ValidationError, type: 'ValidationError' }
238
- ] )( 'converts $type into a non-retryable ApplicationFailure with serialized details', async ( { ErrorType, type } ) => {
239
- const { interceptors } = await import( './workflow.js' );
240
- const { inbound } = interceptors();
241
- const interceptor = inbound[0];
242
- const err = new ErrorType( 'workflow failed' );
243
- err.code = 'EWORKFLOW';
244
- const next = vi.fn().mockRejectedValue( err );
245
-
246
- const thrown = await interceptor.execute( { args: [ {} ] }, next ).catch( e => e );
247
-
248
- expect( thrown ).toBeInstanceOf( ApplicationFailure );
249
- expect( thrown ).toMatchObject( {
250
- message: 'workflow failed',
251
- type,
252
- nonRetryable: true,
253
- details: [ {
254
- error: {
255
- name: type,
256
- message: 'workflow failed',
257
- code: 'EWORKFLOW'
258
- }
259
- } ],
260
- cause: err
261
- } );
262
- expect( workflowErrorMock ).toHaveBeenCalledWith( expect.objectContaining( {
263
- name: type,
264
- message: 'workflow failed',
265
- code: 'EWORKFLOW',
266
- stack: expect.any( String )
267
- } ) );
268
- expect( workflowErrorMock.mock.calls[0][0] ).not.toBeInstanceOf( Error );
269
- expect( workflowEndMock ).not.toHaveBeenCalled();
270
- } );
271
-
272
- it( 'uses a transparent error cause for the sink and ApplicationFailure', async () => {
273
- const { interceptors } = await import( './workflow.js' );
274
- const { inbound } = interceptors();
275
- const interceptor = inbound[0];
276
- const cause = new TypeError( 'provider rejected request' );
277
- cause.code = 'EAUTH';
278
- const error = new TransparentFatalError( cause );
279
- const next = vi.fn().mockRejectedValue( error );
280
-
281
- const thrown = await interceptor.execute( { args: [ {} ] }, next ).catch( e => e );
282
-
283
- expect( thrown ).toMatchObject( {
284
- message: 'provider rejected request',
285
- type: 'TypeError',
286
- nonRetryable: true,
287
- cause,
288
- details: [ {
289
- error: {
290
- name: 'TypeError',
291
- message: 'provider rejected request',
292
- code: 'EAUTH'
293
- }
294
- } ]
295
- } );
296
- expect( workflowErrorMock ).toHaveBeenCalledWith( expect.objectContaining( {
297
- name: 'TypeError',
298
- message: 'provider rejected request',
299
- code: 'EAUTH',
300
- stack: expect.any( String )
301
- } ) );
302
- expect( workflowErrorMock.mock.calls[0][0] ).not.toHaveProperty( 'cause' );
303
- expect( workflowEndMock ).not.toHaveBeenCalled();
304
- } );
305
-
306
- it( 'sinks and rethrows existing Temporal failures unchanged', async () => {
307
- const { interceptors } = await import( './workflow.js' );
308
- const { inbound } = interceptors();
309
- const interceptor = inbound[0];
310
- const err = ApplicationFailure.retryable( 'domain failed', 'DomainFailure', { reason: 'invalid' } );
311
- const next = vi.fn().mockRejectedValue( err );
312
-
313
- await expect( interceptor.execute( { args: [ {} ] }, next ) ).rejects.toBe( err );
314
- expect( workflowErrorMock ).toHaveBeenCalledWith( expect.objectContaining( {
315
- name: 'ApplicationFailure',
316
- message: 'domain failed',
317
- type: 'DomainFailure',
318
- details: [ { reason: 'invalid' } ],
319
- stack: expect.any( String )
320
- } ) );
321
- expect( workflowErrorMock.mock.calls[0][0] ).not.toBeInstanceOf( Error );
322
- expect( workflowEndMock ).not.toHaveBeenCalled();
323
- } );
324
-
325
- it( 'calls sinks.workflow.error and rethrows cancellation errors without wrapping', async () => {
326
- const { interceptors } = await import( './workflow.js' );
327
- const { inbound } = interceptors();
328
- const interceptor = inbound[0];
329
- const cancellation = new Error( 'Workflow cancelled' );
330
- const next = vi.fn().mockRejectedValue( cancellation );
331
- isCancellationMock.mockReturnValue( true );
332
-
333
- await expect( interceptor.execute( { args: [ {} ] }, next ) ).rejects.toBe( cancellation );
334
- expect( isCancellationMock ).toHaveBeenCalledWith( cancellation );
335
- expect( workflowErrorMock ).toHaveBeenCalledWith( {
336
- name: 'Error',
337
- message: 'Workflow cancelled'
338
- } );
339
- expect( workflowErrorMock.mock.calls[0][0] ).not.toBeInstanceOf( Error );
340
- expect( workflowEndMock ).not.toHaveBeenCalled();
341
- } );
342
-
343
- it( 'ends the workflow trace and rethrows ContinueAsNew', async () => {
344
- const { ContinueAsNew } = await import( '@temporalio/workflow' );
345
- const { WorkflowSpecialOutput } = await import( '#consts' );
346
- const { interceptors } = await import( './workflow.js' );
347
- const { inbound } = interceptors();
348
- const interceptor = inbound[0];
349
- const continueErr = new ContinueAsNew();
350
- const next = vi.fn().mockRejectedValue( continueErr );
351
-
352
- await expect( interceptor.execute( { args: [ {} ] }, next ) ).rejects.toThrow( ContinueAsNew );
353
- expect( workflowEndMock ).toHaveBeenCalledWith( WorkflowSpecialOutput.CONTINUED_AS_NEW );
354
- expect( workflowErrorMock ).not.toHaveBeenCalled();
355
- } );
356
- } );
357
- } );
@@ -1,177 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const DOUBLE_SIGNAL_IGNORE_TIME = 1000;
4
- const KILL_AFTER_UNCAUGHT_TIME = 60_000;
5
-
6
- const { mockLog, serializeErrorMock } = vi.hoisted( () => ( {
7
- mockLog: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
8
- serializeErrorMock: vi.fn( () => 'serialized' )
9
- } ) );
10
-
11
- vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
12
- vi.mock( '#helpers/error_serializer', () => ( { serializeError: serializeErrorMock } ) );
13
-
14
- // The module keeps its attachment state at module scope, so every test needs a fresh copy.
15
- const loadModule = async () => {
16
- vi.resetModules();
17
- return import( './interruption.js' );
18
- };
19
-
20
- describe( 'setupInterruptionHandler', () => {
21
- const handlers = {};
22
- const spies = {};
23
-
24
- beforeEach( () => {
25
- vi.clearAllMocks();
26
- Object.keys( handlers ).forEach( key => delete handlers[key] );
27
-
28
- spies.on = vi.spyOn( process, 'on' ).mockImplementation( ( event, handler ) => {
29
- handlers[event] = handler;
30
- return process;
31
- } );
32
- spies.exit = vi.spyOn( process, 'exit' ).mockImplementation( () => undefined );
33
- } );
34
-
35
- afterEach( () => {
36
- vi.useRealTimers();
37
- vi.restoreAllMocks();
38
- } );
39
-
40
- it( 'attaches a handler for every signal and uncaught error type', async () => {
41
- const { setupInterruptionHandler } = await loadModule();
42
-
43
- setupInterruptionHandler( new AbortController() );
44
-
45
- expect( Object.keys( handlers ) ).toEqual( [
46
- 'SIGTERM',
47
- 'SIGINT',
48
- 'SIGUSR2',
49
- 'uncaughtException',
50
- 'unhandledRejection'
51
- ] );
52
- } );
53
-
54
- it( 'attaches once and keeps the first abort controller', async () => {
55
- const { setupInterruptionHandler } = await loadModule();
56
- const first = new AbortController();
57
- const second = new AbortController();
58
-
59
- setupInterruptionHandler( first );
60
- const attachedCalls = spies.on.mock.calls.length;
61
- setupInterruptionHandler( second );
62
-
63
- expect( spies.on ).toHaveBeenCalledTimes( attachedCalls );
64
-
65
- handlers.SIGTERM();
66
-
67
- expect( first.signal.aborted ).toBe( true );
68
- expect( second.signal.aborted ).toBe( false );
69
- } );
70
-
71
- describe( 'signals', () => {
72
- it( 'aborts with a kill signal error naming the signal', async () => {
73
- const { setupInterruptionHandler, KillSignError } = await loadModule();
74
- const controller = new AbortController();
75
-
76
- setupInterruptionHandler( controller );
77
- handlers.SIGTERM();
78
-
79
- expect( mockLog.info ).toHaveBeenCalledWith( 'Signal Received', { signal: 'SIGTERM' } );
80
- expect( controller.signal.reason ).toBeInstanceOf( KillSignError );
81
- expect( controller.signal.reason.message ).toBe( 'SIGTERM' );
82
- expect( spies.exit ).not.toHaveBeenCalled();
83
- } );
84
-
85
- it( 'ignores a second signal received within the grace window', async () => {
86
- vi.useFakeTimers();
87
- const { setupInterruptionHandler } = await loadModule();
88
- const controller = new AbortController();
89
-
90
- setupInterruptionHandler( controller );
91
- handlers.SIGINT();
92
- vi.advanceTimersByTime( DOUBLE_SIGNAL_IGNORE_TIME - 1 );
93
- handlers.SIGINT();
94
-
95
- expect( mockLog.warn ).not.toHaveBeenCalledWith( 'Force quitting...' );
96
- expect( spies.exit ).not.toHaveBeenCalled();
97
- } );
98
-
99
- it( 'force quits on a second signal received after the grace window', async () => {
100
- vi.useFakeTimers();
101
- const { setupInterruptionHandler } = await loadModule();
102
- const controller = new AbortController();
103
-
104
- setupInterruptionHandler( controller );
105
- handlers.SIGINT();
106
- vi.advanceTimersByTime( DOUBLE_SIGNAL_IGNORE_TIME + 1 );
107
- handlers.SIGINT();
108
-
109
- expect( mockLog.warn ).toHaveBeenCalledWith( 'Force quitting...' );
110
- expect( spies.exit ).toHaveBeenCalledWith( 1 );
111
- } );
112
- } );
113
-
114
- describe( 'uncaught errors', () => {
115
- it( 'aborts with an uncaught error wrapping the original', async () => {
116
- const { setupInterruptionHandler, UncaughtError } = await loadModule();
117
- const controller = new AbortController();
118
- const error = new TypeError( 'boom' );
119
-
120
- setupInterruptionHandler( controller );
121
- handlers.uncaughtException( error );
122
-
123
- // Recorded here because a prior abort would leave the reason handler with nothing to report.
124
- expect( mockLog.warn ).toHaveBeenCalledWith( 'Uncaught exception', { type: 'uncaughtException', error: 'serialized' } );
125
- expect( serializeErrorMock ).toHaveBeenCalledWith( error );
126
- expect( mockLog.error ).not.toHaveBeenCalled();
127
- expect( controller.signal.reason ).toBeInstanceOf( UncaughtError );
128
- expect( controller.signal.reason.message ).toBe( 'uncaughtException' );
129
- expect( controller.signal.reason.cause ).toBe( error );
130
- } );
131
-
132
- it( 'wraps non error rejection values for unhandled rejections', async () => {
133
- const { setupInterruptionHandler, UncaughtError } = await loadModule();
134
- const controller = new AbortController();
135
-
136
- setupInterruptionHandler( controller );
137
- handlers.unhandledRejection( 'not an error' );
138
-
139
- expect( mockLog.warn ).toHaveBeenCalledWith( 'Uncaught exception', { type: 'unhandledRejection', error: 'serialized' } );
140
- expect( serializeErrorMock ).toHaveBeenCalledWith( 'not an error' );
141
- expect( mockLog.error ).not.toHaveBeenCalled();
142
- expect( controller.signal.reason ).toBeInstanceOf( UncaughtError );
143
- expect( controller.signal.reason.message ).toBe( 'unhandledRejection' );
144
- expect( controller.signal.reason.cause ).toBe( 'not an error' );
145
- } );
146
-
147
- it( 'forces an exit when the shutdown outlives the watchdog', async () => {
148
- vi.useFakeTimers();
149
- const { setupInterruptionHandler } = await loadModule();
150
-
151
- setupInterruptionHandler( new AbortController() );
152
- handlers.uncaughtException( new Error( 'boom' ) );
153
-
154
- vi.advanceTimersByTime( KILL_AFTER_UNCAUGHT_TIME - 1 );
155
-
156
- expect( spies.exit ).not.toHaveBeenCalled();
157
-
158
- vi.advanceTimersByTime( 1 );
159
-
160
- expect( mockLog.warn ).toHaveBeenCalledWith( 'Uncaught exception handling timed out, force quitting...' );
161
- expect( spies.exit ).toHaveBeenCalledWith( 1 );
162
- } );
163
-
164
- it( 'keeps the watchdog from holding the process open', async () => {
165
- const { setupInterruptionHandler } = await loadModule();
166
- setupInterruptionHandler( new AbortController() );
167
-
168
- const timer = { unref: vi.fn() };
169
- const setTimeoutSpy = vi.spyOn( globalThis, 'setTimeout' ).mockReturnValue( timer );
170
-
171
- handlers.uncaughtException( new Error( 'boom' ) );
172
-
173
- expect( setTimeoutSpy ).toHaveBeenCalledWith( expect.any( Function ), KILL_AFTER_UNCAUGHT_TIME );
174
- expect( timer.unref ).toHaveBeenCalledOnce();
175
- } );
176
- } );
177
- } );