@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.
- package/dist/credentials/credentials.d.ts +6 -0
- package/dist/credentials/credentials.js +87 -0
- package/dist/credentials/encrypted_yaml_provider.d.ts +2 -0
- package/dist/credentials/encrypted_yaml_provider.js +77 -0
- package/dist/credentials/encryption.d.ts +3 -0
- package/dist/credentials/encryption.js +6 -0
- package/dist/credentials/errors.d.ts +13 -0
- package/dist/credentials/errors.js +39 -0
- package/dist/credentials/index.d.ts +6 -0
- package/dist/credentials/index.js +5 -0
- package/dist/credentials/init.d.ts +1 -0
- package/dist/credentials/init.js +7 -0
- package/dist/credentials/paths.d.ts +7 -0
- package/dist/credentials/paths.js +14 -0
- package/dist/credentials/provider_registry.d.ts +3 -0
- package/dist/credentials/provider_registry.js +6 -0
- package/dist/credentials/types.d.ts +12 -0
- package/dist/credentials/types.js +1 -0
- package/package.json +25 -14
- package/src/async_storage.d.ts +4 -0
- package/src/consts.js +0 -2
- package/src/helpers/object.d.ts +2 -0
- package/src/hooks/index.d.ts +0 -8
- package/src/hooks/index.js +0 -3
- package/src/logger/index.d.ts +4 -0
- package/src/worker/index.js +3 -1
- package/bin/healthcheck.spec.js +0 -90
- package/src/bus.spec.js +0 -196
- package/src/errors.spec.js +0 -19
- package/src/helpers/activity_options.spec.js +0 -55
- package/src/helpers/component.spec.js +0 -54
- package/src/helpers/error_serializer.spec.js +0 -479
- package/src/helpers/errors.spec.js +0 -189
- package/src/helpers/fetch.spec.js +0 -292
- package/src/helpers/function.spec.js +0 -24
- package/src/helpers/object.spec.js +0 -425
- package/src/helpers/promise.spec.js +0 -62
- package/src/helpers/redact.spec.js +0 -66
- package/src/helpers/string.spec.js +0 -99
- package/src/helpers/temporal_context.spec.ts +0 -83
- package/src/helpers/trace_info.spec.js +0 -45
- package/src/helpers/workflow_context.spec.js +0 -46
- package/src/hooks/index.spec.js +0 -265
- package/src/hooks/pending_hooks.spec.js +0 -62
- package/src/interface/evaluator.spec.js +0 -685
- package/src/interface/logger.spec.js +0 -172
- package/src/interface/step.spec.js +0 -128
- package/src/interface/validations/index.spec.js +0 -197
- package/src/interface/validations/schemas.spec.js +0 -229
- package/src/interface/webhook.spec.js +0 -170
- package/src/interface/workflow.spec.js +0 -710
- package/src/interface/workflow_utils.spec.js +0 -271
- package/src/interface/zod_integration.spec.js +0 -646
- package/src/internal_activities/index.spec.js +0 -186
- package/src/logger/development.spec.js +0 -87
- package/src/logger/index.spec.js +0 -27
- package/src/logger/production.spec.js +0 -71
- package/src/sdk/helpers/component_metadata.spec.js +0 -30
- package/src/sdk/helpers/objects.spec.js +0 -16
- package/src/sdk/helpers/path.spec.js +0 -102
- package/src/sdk/runtime/context.spec.js +0 -37
- package/src/sdk/runtime/events.spec.js +0 -40
- package/src/temporal/client.spec.js +0 -124
- package/src/tracing/processors/local/index.spec.js +0 -204
- package/src/tracing/processors/s3/configs.spec.js +0 -64
- package/src/tracing/processors/s3/index.spec.js +0 -185
- package/src/tracing/processors/s3/redis_client.spec.js +0 -179
- package/src/tracing/processors/s3/s3_client.spec.js +0 -68
- package/src/tracing/tools/build_trace_tree.spec.js +0 -216
- package/src/tracing/trace_engine.spec.js +0 -299
- package/src/worker/bundle.spec.js +0 -53
- package/src/worker/bundler_options.spec.js +0 -62
- package/src/worker/catalog_workflow/catalog_publisher.spec.js +0 -317
- package/src/worker/catalog_workflow/index.spec.js +0 -222
- package/src/worker/configs.spec.js +0 -213
- package/src/worker/configs_tuner_schema.spec.js +0 -127
- package/src/worker/connection_monitor.spec.js +0 -328
- package/src/worker/global_functions.spec.js +0 -55
- package/src/worker/index.spec.js +0 -526
- package/src/worker/interceptors/activity.spec.js +0 -369
- package/src/worker/interceptors/headers.spec.js +0 -73
- package/src/worker/interceptors/workflow.spec.js +0 -357
- package/src/worker/interruption.spec.js +0 -177
- package/src/worker/loader/activities.spec.js +0 -212
- package/src/worker/loader/hooks.spec.js +0 -64
- package/src/worker/loader/matchers.spec.js +0 -140
- package/src/worker/loader/tools.spec.js +0 -595
- package/src/worker/loader/workflows.spec.js +0 -287
- package/src/worker/log_hooks.spec.js +0 -303
- package/src/worker/shutdown.spec.js +0 -197
- package/src/worker/sinks.spec.js +0 -220
- package/src/worker/telemetry.spec.js +0 -80
- package/src/worker/temporal_logger.spec.js +0 -90
- package/src/worker/webpack_loaders/tools.spec.js +0 -314
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +0 -134
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +0 -154
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +0 -165
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +0 -202
- package/src/worker/webpack_loaders/workflow_validator/npm_workflow_export_resolve.spec.js +0 -374
- 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
|
-
} );
|