@outputai/core 0.10.1-next.f6a7c1a.0 → 0.11.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/package.json +13 -7
- package/src/consts.js +5 -2
- package/src/errors.d.ts +12 -2
- package/src/errors.js +17 -3
- package/src/errors.spec.js +19 -0
- package/src/helpers/activity_options.js +21 -0
- package/src/helpers/activity_options.spec.js +55 -0
- package/src/helpers/error_serializer.js +236 -0
- package/src/helpers/error_serializer.spec.js +413 -0
- package/src/helpers/errors.js +55 -13
- package/src/helpers/errors.spec.js +166 -66
- package/src/helpers/fetch.js +1 -38
- package/src/helpers/fetch.spec.js +1 -39
- package/src/helpers/function.js +13 -0
- package/src/helpers/function.spec.js +23 -1
- package/src/helpers/redact.js +51 -0
- package/src/helpers/redact.spec.js +66 -0
- package/src/helpers/string.js +10 -0
- package/src/helpers/string.spec.js +36 -1
- package/src/hooks/index.d.ts +35 -44
- package/src/hooks/index.js +10 -7
- package/src/hooks/index.spec.js +34 -7
- package/src/hooks/pending_hooks.js +24 -0
- package/src/hooks/pending_hooks.spec.js +62 -0
- package/src/interface/evaluator.d.ts +10 -9
- package/src/interface/evaluator.js +1 -6
- package/src/interface/evaluator.spec.js +10 -10
- package/src/interface/index.js +2 -1
- package/src/interface/step.d.ts +16 -15
- package/src/interface/step.js +1 -6
- package/src/interface/step.spec.js +10 -10
- package/src/interface/validations/index.js +33 -53
- package/src/interface/validations/index.spec.js +25 -17
- package/src/interface/webhook.js +1 -2
- package/src/interface/webhook.spec.js +25 -19
- package/src/interface/workflow.d.ts +25 -18
- package/src/interface/workflow.js +24 -52
- package/src/interface/workflow.spec.js +109 -108
- package/src/interface/workflow_activity_options.js +2 -5
- package/src/interface/workflow_utils.d.ts +15 -0
- package/src/interface/workflow_utils.js +19 -0
- package/src/interface/workflow_utils.spec.js +82 -1
- package/src/temporal/client.js +47 -0
- package/src/temporal/client.spec.js +124 -0
- package/src/temporal/index.d.ts +32 -0
- package/src/temporal/index.js +1 -0
- package/src/tracing/trace_engine.js +9 -11
- package/src/tracing/trace_engine.spec.js +67 -28
- package/src/worker/catalog_workflow/catalog_job.js +3 -3
- package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
- package/src/worker/configs.js +0 -3
- package/src/worker/index.js +21 -8
- package/src/worker/index.spec.js +30 -10
- package/src/worker/interceptors/activity.js +32 -19
- package/src/worker/interceptors/activity.spec.js +71 -166
- package/src/worker/interceptors/workflow.js +53 -11
- package/src/worker/interceptors/workflow.spec.js +176 -49
- package/src/worker/loader/workflows.js +16 -3
- package/src/worker/loader/workflows.spec.js +37 -5
- package/src/worker/log_hooks.js +13 -10
- package/src/worker/log_hooks.spec.js +12 -4
- package/src/worker/sinks.js +3 -1
- package/src/worker/sinks.spec.js +21 -4
- package/src/worker/telemetry.js +2 -1
- package/src/worker/temporal_logger.js +33 -0
- package/src/worker/temporal_logger.spec.js +90 -0
- package/src/helpers/aggregations.js +0 -44
- package/src/helpers/aggregations.spec.js +0 -92
- package/src/tracing/tools/utils.js +0 -42
- package/src/tracing/tools/utils.spec.js +0 -83
- package/src/worker/proxy.js +0 -20
- package/src/worker/proxy.spec.js +0 -51
package/src/worker/index.spec.js
CHANGED
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
configValues,
|
|
7
7
|
connectionMonitorInstance,
|
|
8
8
|
createCatalogMock,
|
|
9
|
+
flushPendingHooksMock,
|
|
9
10
|
initInterceptorsMock,
|
|
10
11
|
mainEventBusMock,
|
|
11
12
|
mockConnection,
|
|
@@ -13,7 +14,10 @@ const {
|
|
|
13
14
|
mockWorker,
|
|
14
15
|
promises,
|
|
15
16
|
resetPromises,
|
|
17
|
+
serializeErrorMock,
|
|
18
|
+
setupClientConfigMock,
|
|
16
19
|
setupInterruptionHandlerMock,
|
|
20
|
+
setupTemporalLoggerMock,
|
|
17
21
|
setupTelemetryMock
|
|
18
22
|
} = vi.hoisted( () => {
|
|
19
23
|
const createDeferred = () => {
|
|
@@ -46,7 +50,6 @@ const {
|
|
|
46
50
|
maxConcurrentActivityTaskPolls: 5,
|
|
47
51
|
maxConcurrentWorkflowTaskPolls: 5,
|
|
48
52
|
workerTuner: undefined,
|
|
49
|
-
processFailureShutdownDelay: 0,
|
|
50
53
|
shutdownForceTime: undefined,
|
|
51
54
|
shutdownGraceTime: undefined
|
|
52
55
|
};
|
|
@@ -105,6 +108,7 @@ const {
|
|
|
105
108
|
connectionMonitorInstance,
|
|
106
109
|
createCatalogMock: vi.fn().mockReturnValue( { workflows: [], activities: {} } ),
|
|
107
110
|
createWorkflowsEntryPointMock: vi.fn().mockReturnValue( '/fake/workflows/path.js' ),
|
|
111
|
+
flushPendingHooksMock: vi.fn().mockResolvedValue( undefined ),
|
|
108
112
|
hashSourceCodeMock: vi.fn().mockResolvedValue( 'catalog-hash' ),
|
|
109
113
|
initInterceptorsMock: vi.fn().mockReturnValue( [] ),
|
|
110
114
|
loadActivitiesMock: vi.fn().mockResolvedValue( {} ),
|
|
@@ -116,19 +120,24 @@ const {
|
|
|
116
120
|
mockWorker,
|
|
117
121
|
promises,
|
|
118
122
|
resetPromises,
|
|
123
|
+
serializeErrorMock: vi.fn( error => ( {
|
|
124
|
+
name: error.name,
|
|
125
|
+
message: error.message,
|
|
126
|
+
stack: error.stack
|
|
127
|
+
} ) ),
|
|
128
|
+
setupClientConfigMock: vi.fn(),
|
|
119
129
|
setupInterruptionHandlerMock: vi.fn(),
|
|
130
|
+
setupTemporalLoggerMock: vi.fn(),
|
|
120
131
|
setupTelemetryMock: vi.fn()
|
|
121
132
|
};
|
|
122
133
|
} );
|
|
123
134
|
|
|
124
135
|
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
125
|
-
vi.mock( '#
|
|
126
|
-
const actual = await importOriginal();
|
|
127
|
-
return { ...actual };
|
|
128
|
-
} );
|
|
136
|
+
vi.mock( '#helpers/error_serializer', () => ( { serializeError: serializeErrorMock } ) );
|
|
129
137
|
const initTracing = vi.fn().mockResolvedValue( undefined );
|
|
130
138
|
vi.mock( '#tracing', () => ( { init: initTracing } ) );
|
|
131
139
|
vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
|
|
140
|
+
vi.mock( '#hooks/pending_hooks', () => ( { flushPendingHooks: flushPendingHooksMock } ) );
|
|
132
141
|
|
|
133
142
|
const loadWorkflowsMock = vi.fn().mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/path.js' } );
|
|
134
143
|
const loadActivitiesMock = vi.fn().mockResolvedValue( { activities: {} } );
|
|
@@ -145,7 +154,6 @@ vi.mock( './sinks.js', () => ( { sinks: {} } ) );
|
|
|
145
154
|
vi.mock( './catalog_workflow/index.js', () => ( { createCatalog: createCatalogMock } ) );
|
|
146
155
|
vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
|
|
147
156
|
vi.mock( './interceptors/index.js', () => ( { initInterceptors: initInterceptorsMock } ) );
|
|
148
|
-
vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: vi.fn() } ) );
|
|
149
157
|
vi.mock( './telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
|
|
150
158
|
vi.mock( './interruption.js', () => ( { setupInterruptionHandler: setupInterruptionHandlerMock } ) );
|
|
151
159
|
vi.mock( './global_functions.js', () => ( { bindGlobalFunctions: bindGlobalFunctionsMock } ) );
|
|
@@ -160,6 +168,8 @@ vi.mock( './catalog_workflow/catalog_job.js', () => ( {
|
|
|
160
168
|
} )
|
|
161
169
|
} ) );
|
|
162
170
|
vi.mock( './log_hooks.js', () => ( {} ) );
|
|
171
|
+
vi.mock( './temporal_logger.js', () => ( { setupTemporalLogger: setupTemporalLoggerMock } ) );
|
|
172
|
+
vi.mock( '#temporal/client', () => ( { setupClientConfig: setupClientConfigMock } ) );
|
|
163
173
|
vi.mock( '@temporalio/worker', () => ( {
|
|
164
174
|
NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) },
|
|
165
175
|
Worker: { create: vi.fn().mockResolvedValue( mockWorker ) }
|
|
@@ -216,6 +226,7 @@ describe( 'worker/index', () => {
|
|
|
216
226
|
await vi.waitFor( () => expect( Worker.create ).toHaveBeenCalled() );
|
|
217
227
|
|
|
218
228
|
expect( loadHooksMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
229
|
+
expect( setupTemporalLoggerMock ).toHaveBeenCalledOnce();
|
|
219
230
|
expect( loadWorkflowsMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
220
231
|
expect( loadActivitiesMock ).toHaveBeenCalledWith( '/test/caller/dir', [] );
|
|
221
232
|
expect( initTracing ).toHaveBeenCalled();
|
|
@@ -227,6 +238,10 @@ describe( 'worker/index', () => {
|
|
|
227
238
|
apiKey: undefined,
|
|
228
239
|
proxy: undefined
|
|
229
240
|
} );
|
|
241
|
+
expect( setupClientConfigMock ).toHaveBeenCalledWith( {
|
|
242
|
+
connection: mockConnection,
|
|
243
|
+
namespace: configValues.namespace
|
|
244
|
+
} );
|
|
230
245
|
expect( bindGlobalFunctionsMock ).toHaveBeenCalledTimes( 1 );
|
|
231
246
|
expect( bindGlobalFunctionsMock.mock.invocationCallOrder[0] ).toBeLessThan(
|
|
232
247
|
NativeConnection.connect.mock.invocationCallOrder[0]
|
|
@@ -262,6 +277,7 @@ describe( 'worker/index', () => {
|
|
|
262
277
|
expect( catalogJobInstance.run ).toHaveBeenCalled();
|
|
263
278
|
|
|
264
279
|
await settleWorker();
|
|
280
|
+
await vi.waitFor( () => expect( flushPendingHooksMock ).toHaveBeenCalledOnce() );
|
|
265
281
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
|
|
266
282
|
} );
|
|
267
283
|
|
|
@@ -354,11 +370,15 @@ describe( 'worker/index', () => {
|
|
|
354
370
|
|
|
355
371
|
await vi.waitFor( () => {
|
|
356
372
|
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
357
|
-
error: 'Big Failure'
|
|
373
|
+
error: expect.objectContaining( { message: 'Big Failure' } )
|
|
358
374
|
} ) );
|
|
359
375
|
} );
|
|
376
|
+
expect( serializeErrorMock ).toHaveBeenCalledWith( error );
|
|
360
377
|
expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
|
|
361
378
|
await vi.waitFor( () => expect( exitMock ).toHaveBeenCalledWith( 1 ) );
|
|
379
|
+
expect( flushPendingHooksMock ).toHaveBeenCalledOnce();
|
|
380
|
+
expect( mainEventBusMock.emit.mock.invocationCallOrder[0] ).toBeLessThan( flushPendingHooksMock.mock.invocationCallOrder[0] );
|
|
381
|
+
expect( flushPendingHooksMock.mock.invocationCallOrder[0] ).toBeLessThan( exitMock.mock.invocationCallOrder[0] );
|
|
362
382
|
} );
|
|
363
383
|
|
|
364
384
|
it( 'throws connection monitor errors after graceful shutdown', async () => {
|
|
@@ -373,7 +393,7 @@ describe( 'worker/index', () => {
|
|
|
373
393
|
|
|
374
394
|
await vi.waitFor( () => {
|
|
375
395
|
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
376
|
-
error: 'connection lost'
|
|
396
|
+
error: expect.objectContaining( { message: 'connection lost' } )
|
|
377
397
|
} ) );
|
|
378
398
|
} );
|
|
379
399
|
expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
|
|
@@ -393,7 +413,7 @@ describe( 'worker/index', () => {
|
|
|
393
413
|
|
|
394
414
|
await vi.waitFor( () => {
|
|
395
415
|
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
396
|
-
error: 'catalog failed'
|
|
416
|
+
error: expect.objectContaining( { message: 'catalog failed' } )
|
|
397
417
|
} ) );
|
|
398
418
|
} );
|
|
399
419
|
expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
|
|
@@ -413,7 +433,7 @@ describe( 'worker/index', () => {
|
|
|
413
433
|
expect( catalogJobInstance.interrupt ).not.toHaveBeenCalled();
|
|
414
434
|
await vi.waitFor( () => {
|
|
415
435
|
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.objectContaining( {
|
|
416
|
-
error: 'worker create failed'
|
|
436
|
+
error: expect.objectContaining( { message: 'worker create failed' } )
|
|
417
437
|
} ) );
|
|
418
438
|
} );
|
|
419
439
|
} );
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { Context, activityInfo as activityInfoFn } from '@temporalio/activity';
|
|
1
|
+
import { Context, activityInfo as activityInfoFn, ApplicationFailure } from '@temporalio/activity';
|
|
2
|
+
import { CompleteAsyncError, TemporalFailure } from '@temporalio/common';
|
|
2
3
|
import { Storage } from '#async_storage';
|
|
3
4
|
import * as Tracing from '#tracing';
|
|
4
5
|
import { headersToObject } from './headers.js';
|
|
5
|
-
import {
|
|
6
|
+
import { ActivitySpecialOutput, BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
|
|
6
7
|
import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
|
|
7
8
|
import { mainEventBus } from '#bus';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { inheritsFromAnyNamedType } from '#helpers/errors';
|
|
10
|
+
import { serializeError } from '#helpers/error_serializer';
|
|
11
|
+
import { FatalError, TransparentFatalError } from '#errors';
|
|
10
12
|
|
|
11
13
|
/*
|
|
12
14
|
This interceptor wraps every activity execution with cross-cutting concerns:
|
|
@@ -51,12 +53,9 @@ export class ActivityExecutionInterceptor {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
const state = {
|
|
54
|
-
heartbeat: null
|
|
55
|
-
attributes: []
|
|
56
|
+
heartbeat: null
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
const addAttribute = attribute => state.attributes.push( attribute );
|
|
59
|
-
|
|
60
59
|
// Adds context accessible information
|
|
61
60
|
const storageContext = {
|
|
62
61
|
parentId: activityId,
|
|
@@ -64,8 +63,7 @@ export class ActivityExecutionInterceptor {
|
|
|
64
63
|
activityInfo,
|
|
65
64
|
workflowDetails,
|
|
66
65
|
workflowFilename,
|
|
67
|
-
traceInfo
|
|
68
|
-
addAttribute
|
|
66
|
+
traceInfo
|
|
69
67
|
};
|
|
70
68
|
|
|
71
69
|
mainEventBus.emit( BusEventType.ACTIVITY_START, { activityInfo, workflowDetails, outputActivityKind } );
|
|
@@ -77,19 +75,34 @@ export class ActivityExecutionInterceptor {
|
|
|
77
75
|
|
|
78
76
|
const output = await Storage.runWithContext( async _ => next( input ), storageContext );
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
mainEventBus.emit( BusEventType.ACTIVITY_END, { activityInfo, aggregations, workflowDetails, outputActivityKind } );
|
|
78
|
+
mainEventBus.emit( BusEventType.ACTIVITY_END, { activityInfo, workflowDetails, outputActivityKind } );
|
|
83
79
|
Tracing.addEventEnd( { id: activityId, details: output, traceInfo } );
|
|
84
80
|
|
|
85
|
-
return
|
|
81
|
+
return output;
|
|
86
82
|
|
|
87
83
|
} catch ( error ) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
// Record async completion handoff as a trace end without emitting a bus event.
|
|
85
|
+
if ( error instanceof CompleteAsyncError ) {
|
|
86
|
+
Tracing.addEventEnd( { id: activityId, details: ActivitySpecialOutput.ASYNC_HANDOFF, traceInfo } );
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const unwrappedError = error instanceof TransparentFatalError ? error.cause : error;
|
|
91
|
+
|
|
92
|
+
Tracing.addEventError( { id: activityId, details: unwrappedError, traceInfo } );
|
|
93
|
+
mainEventBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, workflowDetails, outputActivityKind, error: unwrappedError } );
|
|
94
|
+
|
|
95
|
+
// Native Temporal errors are just re-thrown
|
|
96
|
+
if ( error instanceof TemporalFailure ) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nonRetryable = error instanceof FatalError || inheritsFromAnyNamedType( error, activityInfo.retryPolicy?.nonRetryableErrorTypes ?? [] );
|
|
101
|
+
throw ApplicationFailure.fromError( unwrappedError, {
|
|
102
|
+
nonRetryable,
|
|
103
|
+
cause: unwrappedError,
|
|
104
|
+
details: [ { error: serializeError( unwrappedError, { dropKeys: [ 'stack' ] } ) } ]
|
|
105
|
+
} );
|
|
93
106
|
} finally {
|
|
94
107
|
clearInterval( state.heartbeat );
|
|
95
108
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { ApplicationFailure } from '@temporalio/common';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { ApplicationFailure, CompleteAsyncError } from '@temporalio/common';
|
|
3
|
+
import { ActivitySpecialOutput, BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
|
|
4
|
+
import { TransparentFatalError } from '#errors';
|
|
5
5
|
|
|
6
|
-
const METADATA_ACCESS_SYMBOL = vi.hoisted( () => Symbol( '__metadata' ) );
|
|
7
6
|
const heartbeatMock = vi.hoisted( () => vi.fn() );
|
|
8
7
|
const runWithContextMock = vi.hoisted( () => vi.fn().mockImplementation( async fn => fn() ) );
|
|
9
8
|
const activityInfoMock = vi.hoisted( () => ( {
|
|
@@ -64,15 +63,6 @@ vi.mock( './headers.js', () => ( {
|
|
|
64
63
|
const mainEventBusEmitMock = vi.fn();
|
|
65
64
|
vi.mock( '#bus', () => ( { mainEventBus: { emit: mainEventBusEmitMock } } ) );
|
|
66
65
|
|
|
67
|
-
vi.mock( '#consts', async importOriginal => {
|
|
68
|
-
const actual = await importOriginal();
|
|
69
|
-
return {
|
|
70
|
-
...actual, get METADATA_ACCESS_SYMBOL() {
|
|
71
|
-
return METADATA_ACCESS_SYMBOL;
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
} );
|
|
75
|
-
|
|
76
66
|
vi.mock( '../configs.js', () => ( {
|
|
77
67
|
get activityHeartbeatEnabled() {
|
|
78
68
|
return process.env.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED !== 'false';
|
|
@@ -93,24 +83,12 @@ const makeInput = () => ( {
|
|
|
93
83
|
headers: {}
|
|
94
84
|
} );
|
|
95
85
|
|
|
96
|
-
const httpRequestAttribute = {
|
|
97
|
-
type: Attribute.HTTPRequestCount.TYPE,
|
|
98
|
-
url: 'https://api.example.test/items',
|
|
99
|
-
requestId: 'req-1'
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const httpRequestAggregations = {
|
|
103
|
-
cost: { total: 0 },
|
|
104
|
-
tokens: { total: 0 },
|
|
105
|
-
httpRequests: { total: 1 }
|
|
106
|
-
};
|
|
107
|
-
|
|
108
86
|
describe( 'ActivityExecutionInterceptor', () => {
|
|
109
87
|
beforeEach( () => {
|
|
110
88
|
vi.clearAllMocks();
|
|
111
89
|
activityInfoMock.workflowType = 'myWorkflow';
|
|
90
|
+
delete activityInfoMock.retryPolicy;
|
|
112
91
|
vi.useFakeTimers();
|
|
113
|
-
vi.resetModules();
|
|
114
92
|
// Default: heartbeat enabled with 50ms interval for fast tests
|
|
115
93
|
vi.stubEnv( 'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED', 'true' );
|
|
116
94
|
vi.stubEnv( 'OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS', '50' );
|
|
@@ -126,22 +104,14 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
126
104
|
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
127
105
|
const next = vi.fn().mockResolvedValue( { result: 'ok' } );
|
|
128
106
|
|
|
129
|
-
|
|
130
|
-
vi.advanceTimersByTime( 0 );
|
|
131
|
-
const output = await promise;
|
|
132
|
-
|
|
133
|
-
expect( output ).toEqual( {
|
|
134
|
-
output: { result: 'ok' },
|
|
135
|
-
aggregations: null,
|
|
136
|
-
[ACTIVITY_WRAPPER_VERSION_FIELD]: 1
|
|
137
|
-
} );
|
|
107
|
+
await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( { result: 'ok' } );
|
|
138
108
|
expect( mainEventBusEmitMock ).toHaveBeenCalledWith(
|
|
139
109
|
BusEventType.ACTIVITY_START,
|
|
140
110
|
{ activityInfo: activityInfoMock, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
|
|
141
111
|
);
|
|
142
112
|
expect( mainEventBusEmitMock ).toHaveBeenCalledWith(
|
|
143
113
|
BusEventType.ACTIVITY_END,
|
|
144
|
-
{ activityInfo: activityInfoMock,
|
|
114
|
+
{ activityInfo: activityInfoMock, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
|
|
145
115
|
);
|
|
146
116
|
expect( addEventStartMock ).toHaveBeenCalledWith( {
|
|
147
117
|
id: 'act-1',
|
|
@@ -161,74 +131,36 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
161
131
|
workflowDetails: workflowDetailsMock,
|
|
162
132
|
outputActivityKind: 'step',
|
|
163
133
|
workflowFilename: '/workflows/myWorkflow.js',
|
|
164
|
-
traceInfo: traceInfoMock
|
|
165
|
-
addAttribute: expect.any( Function )
|
|
134
|
+
traceInfo: traceInfoMock
|
|
166
135
|
} )
|
|
167
136
|
);
|
|
168
137
|
} );
|
|
169
138
|
|
|
170
|
-
it( '
|
|
171
|
-
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
172
|
-
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
173
|
-
const next = vi.fn( () => ( { result: 'sync' } ) );
|
|
174
|
-
|
|
175
|
-
await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( {
|
|
176
|
-
output: { result: 'sync' },
|
|
177
|
-
aggregations: null,
|
|
178
|
-
[ACTIVITY_WRAPPER_VERSION_FIELD]: 1
|
|
179
|
-
} );
|
|
180
|
-
|
|
181
|
-
expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, expect.any( Object ) );
|
|
182
|
-
expect( addEventEndMock ).toHaveBeenCalledWith( { id: 'act-1', details: { result: 'sync' }, traceInfo: traceInfoMock } );
|
|
183
|
-
expect( addEventErrorMock ).not.toHaveBeenCalled();
|
|
184
|
-
} );
|
|
185
|
-
|
|
186
|
-
it( 'returns collected aggregations after successful execution', async () => {
|
|
187
|
-
runWithContextMock.mockImplementationOnce( async ( fn, ctx ) => {
|
|
188
|
-
ctx.addAttribute( httpRequestAttribute );
|
|
189
|
-
return fn();
|
|
190
|
-
} );
|
|
191
|
-
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
192
|
-
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
193
|
-
const next = vi.fn().mockResolvedValue( { result: 'ok' } );
|
|
194
|
-
|
|
195
|
-
await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( {
|
|
196
|
-
output: { result: 'ok' },
|
|
197
|
-
aggregations: httpRequestAggregations,
|
|
198
|
-
[ACTIVITY_WRAPPER_VERSION_FIELD]: 1
|
|
199
|
-
} );
|
|
200
|
-
|
|
201
|
-
expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, {
|
|
202
|
-
activityInfo: activityInfoMock,
|
|
203
|
-
aggregations: httpRequestAggregations,
|
|
204
|
-
workflowDetails: workflowDetailsMock,
|
|
205
|
-
outputActivityKind: 'step'
|
|
206
|
-
} );
|
|
207
|
-
} );
|
|
208
|
-
|
|
209
|
-
it( 'stores collected aggregations in ApplicationFailure details after failed execution', async () => {
|
|
210
|
-
runWithContextMock.mockImplementationOnce( async ( fn, ctx ) => {
|
|
211
|
-
ctx.addAttribute( httpRequestAttribute );
|
|
212
|
-
return fn();
|
|
213
|
-
} );
|
|
139
|
+
it( 'wraps native errors with serialized details', async () => {
|
|
214
140
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
215
141
|
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
216
142
|
const error = new Error( 'step failed' );
|
|
143
|
+
error.code = 'ESTEP';
|
|
217
144
|
const next = vi.fn().mockRejectedValue( error );
|
|
218
145
|
|
|
219
146
|
const thrown = await interceptor.execute( makeInput(), next ).catch( e => e );
|
|
147
|
+
|
|
220
148
|
expect( thrown ).toBeInstanceOf( ApplicationFailure );
|
|
221
149
|
expect( thrown ).toMatchObject( {
|
|
222
150
|
message: 'step failed',
|
|
223
151
|
type: 'Error',
|
|
224
152
|
details: [ {
|
|
225
|
-
|
|
153
|
+
error: {
|
|
154
|
+
name: 'Error',
|
|
155
|
+
message: 'step failed',
|
|
156
|
+
code: 'ESTEP'
|
|
157
|
+
}
|
|
226
158
|
} ],
|
|
227
|
-
cause: error
|
|
159
|
+
cause: error,
|
|
160
|
+
nonRetryable: false
|
|
228
161
|
} );
|
|
229
162
|
expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
|
|
230
163
|
activityInfo: activityInfoMock,
|
|
231
|
-
aggregations: httpRequestAggregations,
|
|
232
164
|
workflowDetails: workflowDetailsMock,
|
|
233
165
|
outputActivityKind: 'step',
|
|
234
166
|
error
|
|
@@ -237,105 +169,93 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
237
169
|
expect( addEventEndMock ).not.toHaveBeenCalled();
|
|
238
170
|
} );
|
|
239
171
|
|
|
240
|
-
it( '
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
} );
|
|
172
|
+
it( 'marks inherited configured error types as non-retryable', async () => {
|
|
173
|
+
class ValidationError extends Error {}
|
|
174
|
+
class SpecificValidationError extends ValidationError {}
|
|
175
|
+
activityInfoMock.retryPolicy = { nonRetryableErrorTypes: [ 'ValidationError' ] };
|
|
245
176
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
246
177
|
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
247
|
-
const error = new
|
|
248
|
-
error.details = [ { domain: { reason: 'bad-input' } } ];
|
|
178
|
+
const error = new SpecificValidationError( 'invalid input' );
|
|
249
179
|
const next = vi.fn().mockRejectedValue( error );
|
|
250
180
|
|
|
251
181
|
const thrown = await interceptor.execute( makeInput(), next ).catch( e => e );
|
|
252
182
|
|
|
253
|
-
expect( thrown
|
|
254
|
-
|
|
255
|
-
{ aggregations: httpRequestAggregations }
|
|
256
|
-
] );
|
|
257
|
-
} );
|
|
258
|
-
|
|
259
|
-
it( 'rethrows the original error when failed execution collected no attributes', async () => {
|
|
260
|
-
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
261
|
-
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
262
|
-
const error = new Error( 'step failed' );
|
|
263
|
-
const next = vi.fn().mockRejectedValue( error );
|
|
264
|
-
|
|
265
|
-
await expect( interceptor.execute( makeInput(), next ) ).rejects.toBe( error );
|
|
266
|
-
} );
|
|
267
|
-
|
|
268
|
-
it( 'rethrows the original error with existing details when failed execution collected no attributes', async () => {
|
|
269
|
-
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
270
|
-
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
271
|
-
const error = new Error( 'step failed' );
|
|
272
|
-
error.details = [ { domain: { reason: 'bad-input' } } ];
|
|
273
|
-
const next = vi.fn().mockRejectedValue( error );
|
|
274
|
-
|
|
275
|
-
await expect( interceptor.execute( makeInput(), next ) ).rejects.toBe( error );
|
|
276
|
-
expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
|
|
277
|
-
} );
|
|
278
|
-
|
|
279
|
-
it( 'wraps existing ApplicationFailure without creating a self-cause', async () => {
|
|
280
|
-
runWithContextMock.mockImplementationOnce( async ( fn, ctx ) => {
|
|
281
|
-
ctx.addAttribute( httpRequestAttribute );
|
|
282
|
-
return fn();
|
|
283
|
-
} );
|
|
284
|
-
const error = ApplicationFailure.create( {
|
|
285
|
-
message: 'application failed',
|
|
286
|
-
type: 'OriginalType',
|
|
183
|
+
expect( thrown ).toMatchObject( {
|
|
184
|
+
type: 'SpecificValidationError',
|
|
287
185
|
nonRetryable: true,
|
|
288
|
-
|
|
186
|
+
cause: error
|
|
289
187
|
} );
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
it( 'uses a transparent error cause while remaining non-retryable without retry policy metadata', async () => {
|
|
290
191
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
291
192
|
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
193
|
+
const cause = new TypeError( 'provider rejected request' );
|
|
194
|
+
cause.code = 'EAUTH';
|
|
195
|
+
const error = new TransparentFatalError( cause );
|
|
292
196
|
const next = vi.fn().mockRejectedValue( error );
|
|
293
197
|
|
|
294
198
|
const thrown = await interceptor.execute( makeInput(), next ).catch( e => e );
|
|
295
199
|
|
|
296
|
-
expect( thrown ).toBeInstanceOf( ApplicationFailure );
|
|
297
|
-
expect( thrown ).not.toBe( error );
|
|
298
|
-
expect( thrown.cause ).toBe( error );
|
|
299
|
-
expect( thrown.cause ).not.toBe( thrown );
|
|
300
200
|
expect( thrown ).toMatchObject( {
|
|
301
|
-
message: '
|
|
302
|
-
type: '
|
|
201
|
+
message: 'provider rejected request',
|
|
202
|
+
type: 'TypeError',
|
|
303
203
|
nonRetryable: true,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
204
|
+
cause,
|
|
205
|
+
details: [ {
|
|
206
|
+
error: {
|
|
207
|
+
name: 'TypeError',
|
|
208
|
+
message: 'provider rejected request',
|
|
209
|
+
code: 'EAUTH'
|
|
210
|
+
}
|
|
211
|
+
} ]
|
|
212
|
+
} );
|
|
213
|
+
expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'act-1', details: cause, traceInfo: traceInfoMock } );
|
|
214
|
+
expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
|
|
215
|
+
activityInfo: activityInfoMock,
|
|
216
|
+
workflowDetails: workflowDetailsMock,
|
|
217
|
+
outputActivityKind: 'step',
|
|
218
|
+
error: cause
|
|
308
219
|
} );
|
|
309
220
|
} );
|
|
310
221
|
|
|
311
|
-
it( '
|
|
222
|
+
it( 'rethrows existing Temporal failures unchanged', async () => {
|
|
312
223
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
313
224
|
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
314
|
-
const error =
|
|
225
|
+
const error = ApplicationFailure.nonRetryable( 'domain failed', 'DomainFailure', { reason: 'invalid' } );
|
|
315
226
|
const next = vi.fn().mockRejectedValue( error );
|
|
316
227
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
await expect( promise ).rejects.toThrow( 'step failed' );
|
|
321
|
-
expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.any( Object ) );
|
|
228
|
+
await expect( interceptor.execute( makeInput(), next ) ).rejects.toBe( error );
|
|
229
|
+
expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'act-1', details: error, traceInfo: traceInfoMock } );
|
|
322
230
|
expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
|
|
323
231
|
activityInfo: activityInfoMock,
|
|
324
|
-
aggregations: null,
|
|
325
232
|
workflowDetails: workflowDetailsMock,
|
|
326
233
|
outputActivityKind: 'step',
|
|
327
234
|
error
|
|
328
235
|
} );
|
|
329
|
-
expect( addEventStartMock ).toHaveBeenCalledOnce();
|
|
330
|
-
expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'act-1', details: error, traceInfo: traceInfoMock } );
|
|
331
|
-
expect( addEventEndMock ).not.toHaveBeenCalled();
|
|
332
236
|
} );
|
|
333
237
|
|
|
334
|
-
it( '
|
|
238
|
+
it( 'records async completion handoff as a trace end only', async () => {
|
|
239
|
+
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
240
|
+
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
241
|
+
const error = new CompleteAsyncError();
|
|
242
|
+
const next = vi.fn().mockRejectedValue( error );
|
|
243
|
+
|
|
244
|
+
await expect( interceptor.execute( makeInput(), next ) ).rejects.toBe( error );
|
|
245
|
+
expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.any( Object ) );
|
|
246
|
+
expect( mainEventBusEmitMock ).toHaveBeenCalledTimes( 1 );
|
|
247
|
+
expect( addEventEndMock ).toHaveBeenCalledWith( {
|
|
248
|
+
id: 'act-1',
|
|
249
|
+
details: ActivitySpecialOutput.ASYNC_HANDOFF,
|
|
250
|
+
traceInfo: traceInfoMock
|
|
251
|
+
} );
|
|
252
|
+
expect( addEventErrorMock ).not.toHaveBeenCalled();
|
|
253
|
+
} );
|
|
254
|
+
|
|
255
|
+
it( 'sends periodic heartbeats and stops after successful execution', async () => {
|
|
335
256
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
336
257
|
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
337
258
|
|
|
338
|
-
// next() resolves only after we manually resolve it, simulating a long-running activity
|
|
339
259
|
const deferred = { resolve: null };
|
|
340
260
|
const next = vi.fn().mockImplementation( () => new Promise( r => {
|
|
341
261
|
deferred.resolve = r;
|
|
@@ -356,16 +276,6 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
356
276
|
|
|
357
277
|
deferred.resolve( { result: 'done' } );
|
|
358
278
|
await promise;
|
|
359
|
-
} );
|
|
360
|
-
|
|
361
|
-
it( 'clears heartbeat interval after activity completes', async () => {
|
|
362
|
-
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
363
|
-
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
364
|
-
const next = vi.fn().mockResolvedValue( { result: 'ok' } );
|
|
365
|
-
|
|
366
|
-
const promise = interceptor.execute( makeInput(), next );
|
|
367
|
-
vi.advanceTimersByTime( 0 );
|
|
368
|
-
await promise;
|
|
369
279
|
|
|
370
280
|
heartbeatMock.mockClear();
|
|
371
281
|
vi.advanceTimersByTime( 500 );
|
|
@@ -379,7 +289,7 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
379
289
|
|
|
380
290
|
const promise = interceptor.execute( makeInput(), next );
|
|
381
291
|
vi.advanceTimersByTime( 0 );
|
|
382
|
-
await promise.
|
|
292
|
+
await expect( promise ).rejects.toBeInstanceOf( ApplicationFailure );
|
|
383
293
|
|
|
384
294
|
heartbeatMock.mockClear();
|
|
385
295
|
vi.advanceTimersByTime( 500 );
|
|
@@ -391,7 +301,6 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
391
301
|
const workflows = [ { name: 'myWorkflow', path: '/workflows/myWorkflow.js', aliases: [ 'myWorkflowOld' ] } ];
|
|
392
302
|
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows } );
|
|
393
303
|
|
|
394
|
-
// Override context to use alias as workflowType
|
|
395
304
|
activityInfoMock.workflowType = 'myWorkflowOld';
|
|
396
305
|
const next = vi.fn().mockResolvedValue( { result: 'ok' } );
|
|
397
306
|
|
|
@@ -399,14 +308,10 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
399
308
|
vi.advanceTimersByTime( 0 );
|
|
400
309
|
await promise;
|
|
401
310
|
|
|
402
|
-
// Should resolve to the correct path despite using the alias
|
|
403
311
|
expect( runWithContextMock ).toHaveBeenCalledWith(
|
|
404
312
|
expect.any( Function ),
|
|
405
313
|
expect.objectContaining( { workflowFilename: '/workflows/myWorkflow.js' } )
|
|
406
314
|
);
|
|
407
|
-
|
|
408
|
-
// Restore for other tests
|
|
409
|
-
activityInfoMock.workflowType = 'myWorkflow';
|
|
410
315
|
} );
|
|
411
316
|
|
|
412
317
|
it( 'does not heartbeat when OUTPUT_ACTIVITY_HEARTBEAT_ENABLED is false', async () => {
|