@outputai/core 0.5.1 → 0.5.2-next.17d8711.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 +1 -1
- package/src/activity_integration/event_id_integration.spec.js +52 -0
- package/src/activity_integration/events.spec.js +11 -8
- package/src/bus.js +19 -1
- package/src/bus.spec.js +108 -0
- package/src/hooks/index.d.ts +10 -0
- package/src/hooks/index.js +12 -12
- package/src/hooks/index.spec.js +32 -24
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { messageBus } from '#bus';
|
|
3
|
+
import { emitEvent } from './events.js';
|
|
4
|
+
|
|
5
|
+
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
|
+
|
|
7
|
+
describe( 'eventId integration', () => {
|
|
8
|
+
beforeEach( () => {
|
|
9
|
+
messageBus.removeAllListeners();
|
|
10
|
+
} );
|
|
11
|
+
|
|
12
|
+
it( 'stamps a UUID v4 eventId on every emit (end-to-end via messageBus)', () => {
|
|
13
|
+
const handler = vi.fn();
|
|
14
|
+
messageBus.on( 'external:cost:llm:request', handler );
|
|
15
|
+
|
|
16
|
+
emitEvent( 'cost:llm:request', { modelId: 'gpt-4o' } );
|
|
17
|
+
|
|
18
|
+
expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
19
|
+
eventId: expect.stringMatching( UUID_V4_REGEX ),
|
|
20
|
+
modelId: 'gpt-4o'
|
|
21
|
+
} ) );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
it( 'cost:http:request and http:request for the same fetch get distinct eventIds', () => {
|
|
25
|
+
const costHandler = vi.fn();
|
|
26
|
+
const reqHandler = vi.fn();
|
|
27
|
+
messageBus.on( 'external:cost:http:request', costHandler );
|
|
28
|
+
messageBus.on( 'external:http:request', reqHandler );
|
|
29
|
+
|
|
30
|
+
const sharedRequestId = 'req-xyz';
|
|
31
|
+
emitEvent( 'cost:http:request', { requestId: sharedRequestId, url: 'https://x.test', cost: 1 } );
|
|
32
|
+
emitEvent( 'http:request', { requestId: sharedRequestId, url: 'https://x.test', status: 200 } );
|
|
33
|
+
|
|
34
|
+
const costEventId = costHandler.mock.calls[0][0].eventId;
|
|
35
|
+
const reqEventId = reqHandler.mock.calls[0][0].eventId;
|
|
36
|
+
expect( costEventId ).toMatch( UUID_V4_REGEX );
|
|
37
|
+
expect( reqEventId ).toMatch( UUID_V4_REGEX );
|
|
38
|
+
expect( costEventId ).not.toBe( reqEventId );
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
it( 'honors a caller-supplied eventId end-to-end', () => {
|
|
42
|
+
const handler = vi.fn();
|
|
43
|
+
messageBus.on( 'external:custom:event', handler );
|
|
44
|
+
|
|
45
|
+
emitEvent( 'custom:event', { eventId: 'fixed-id-123', payload: 'hi' } );
|
|
46
|
+
|
|
47
|
+
expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
48
|
+
eventId: 'fixed-id-123',
|
|
49
|
+
payload: 'hi'
|
|
50
|
+
} ) );
|
|
51
|
+
} );
|
|
52
|
+
} );
|
|
@@ -13,6 +13,9 @@ vi.mock( '#bus', () => ( {
|
|
|
13
13
|
|
|
14
14
|
import { emitEvent } from './events.js';
|
|
15
15
|
|
|
16
|
+
// `eventId` stamping is the bus layer's responsibility (see bus.spec.js + the
|
|
17
|
+
// integration tests in event_id_integration.spec.js). Assertions here use
|
|
18
|
+
// `objectContaining` so they don't have to know about that enrichment.
|
|
16
19
|
describe( 'emitEvent', () => {
|
|
17
20
|
beforeEach( () => {
|
|
18
21
|
vi.clearAllMocks();
|
|
@@ -26,12 +29,12 @@ describe( 'emitEvent', () => {
|
|
|
26
29
|
|
|
27
30
|
emitEvent( 'cost:llm:request', { modelId: 'gpt-4o' } );
|
|
28
31
|
|
|
29
|
-
expect( emitMock ).toHaveBeenCalledWith( 'external:cost:llm:request', {
|
|
32
|
+
expect( emitMock ).toHaveBeenCalledWith( 'external:cost:llm:request', expect.objectContaining( {
|
|
30
33
|
workflowId: 'wf-1',
|
|
31
34
|
runId: 'run-1',
|
|
32
35
|
activityId: 'act-1',
|
|
33
36
|
modelId: 'gpt-4o'
|
|
34
|
-
} );
|
|
37
|
+
} ) );
|
|
35
38
|
} );
|
|
36
39
|
|
|
37
40
|
it( 'handles missing executionContext gracefully', () => {
|
|
@@ -39,12 +42,12 @@ describe( 'emitEvent', () => {
|
|
|
39
42
|
|
|
40
43
|
emitEvent( 'foo:bar', { x: 1 } );
|
|
41
44
|
|
|
42
|
-
expect( emitMock ).toHaveBeenCalledWith( 'external:foo:bar', {
|
|
45
|
+
expect( emitMock ).toHaveBeenCalledWith( 'external:foo:bar', expect.objectContaining( {
|
|
43
46
|
workflowId: undefined,
|
|
44
47
|
runId: undefined,
|
|
45
48
|
activityId: undefined,
|
|
46
49
|
x: 1
|
|
47
|
-
} );
|
|
50
|
+
} ) );
|
|
48
51
|
} );
|
|
49
52
|
|
|
50
53
|
it( 'handles missing payload', () => {
|
|
@@ -55,11 +58,11 @@ describe( 'emitEvent', () => {
|
|
|
55
58
|
|
|
56
59
|
emitEvent( 'lifecycle:start' );
|
|
57
60
|
|
|
58
|
-
expect( emitMock ).toHaveBeenCalledWith( 'external:lifecycle:start', {
|
|
61
|
+
expect( emitMock ).toHaveBeenCalledWith( 'external:lifecycle:start', expect.objectContaining( {
|
|
59
62
|
workflowId: 'wf-2',
|
|
60
63
|
runId: 'run-2',
|
|
61
64
|
activityId: 'act-2'
|
|
62
|
-
} );
|
|
65
|
+
} ) );
|
|
63
66
|
} );
|
|
64
67
|
|
|
65
68
|
it( 'does not let payload override workflowId / runId / activityId', () => {
|
|
@@ -77,11 +80,11 @@ describe( 'emitEvent', () => {
|
|
|
77
80
|
|
|
78
81
|
// Context fields are spread after the payload, so caller-supplied
|
|
79
82
|
// workflowId / runId / activityId cannot escape the executionContext.
|
|
80
|
-
expect( emitMock ).toHaveBeenCalledWith( 'external:cost:http:request', {
|
|
83
|
+
expect( emitMock ).toHaveBeenCalledWith( 'external:cost:http:request', expect.objectContaining( {
|
|
81
84
|
workflowId: 'wf-3',
|
|
82
85
|
runId: 'run-3',
|
|
83
86
|
activityId: 'act-3',
|
|
84
87
|
url: 'https://example.com'
|
|
85
|
-
} );
|
|
88
|
+
} ) );
|
|
86
89
|
} );
|
|
87
90
|
} );
|
package/src/bus.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { isPlainObject } from '#utils';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
const emitter = new EventEmitter();
|
|
6
|
+
const originalEmit = emitter.emit.bind( emitter );
|
|
7
|
+
|
|
8
|
+
const attachEventId = payload => ( { ...payload, eventId: payload.eventId ?? randomUUID() } );
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Every object payload emitted through `messageBus` is stamped with a UUID v4 `eventId`.
|
|
12
|
+
*/
|
|
13
|
+
emitter.emit = ( event, ...args ) => {
|
|
14
|
+
const [ payload, ...rest ] = args;
|
|
15
|
+
if ( !isPlainObject( payload ) ) {
|
|
16
|
+
return originalEmit( event, ...args );
|
|
17
|
+
}
|
|
18
|
+
return originalEmit( event, attachEventId( payload ), ...rest );
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const messageBus = emitter;
|
package/src/bus.spec.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { messageBus } from './bus.js';
|
|
3
|
+
|
|
4
|
+
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
5
|
+
|
|
6
|
+
describe( 'messageBus', () => {
|
|
7
|
+
beforeEach( () => {
|
|
8
|
+
messageBus.removeAllListeners();
|
|
9
|
+
} );
|
|
10
|
+
|
|
11
|
+
describe( 'eventId stamping', () => {
|
|
12
|
+
it( 'stamps a UUID v4 eventId on every object payload', () => {
|
|
13
|
+
const handler = vi.fn();
|
|
14
|
+
messageBus.on( 'test:event', handler );
|
|
15
|
+
|
|
16
|
+
messageBus.emit( 'test:event', { foo: 'bar' } );
|
|
17
|
+
|
|
18
|
+
expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
19
|
+
foo: 'bar',
|
|
20
|
+
eventId: expect.stringMatching( UUID_V4_REGEX )
|
|
21
|
+
} ) );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
it( 'gives distinct emits distinct eventIds', () => {
|
|
25
|
+
const handler = vi.fn();
|
|
26
|
+
messageBus.on( 'test:event', handler );
|
|
27
|
+
|
|
28
|
+
messageBus.emit( 'test:event', { i: 1 } );
|
|
29
|
+
messageBus.emit( 'test:event', { i: 2 } );
|
|
30
|
+
|
|
31
|
+
const first = handler.mock.calls[0][0].eventId;
|
|
32
|
+
const second = handler.mock.calls[1][0].eventId;
|
|
33
|
+
expect( first ).toMatch( UUID_V4_REGEX );
|
|
34
|
+
expect( second ).toMatch( UUID_V4_REGEX );
|
|
35
|
+
expect( first ).not.toBe( second );
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
it( 'preserves a caller-supplied eventId (deterministic retry case)', () => {
|
|
39
|
+
const handler = vi.fn();
|
|
40
|
+
messageBus.on( 'test:event', handler );
|
|
41
|
+
|
|
42
|
+
messageBus.emit( 'test:event', { eventId: 'fixed-id', foo: 'bar' } );
|
|
43
|
+
|
|
44
|
+
expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
45
|
+
eventId: 'fixed-id',
|
|
46
|
+
foo: 'bar'
|
|
47
|
+
} ) );
|
|
48
|
+
} );
|
|
49
|
+
|
|
50
|
+
it( 'does not mutate the caller-supplied payload object', () => {
|
|
51
|
+
const handler = vi.fn();
|
|
52
|
+
messageBus.on( 'test:event', handler );
|
|
53
|
+
|
|
54
|
+
const payload = { foo: 'bar' };
|
|
55
|
+
messageBus.emit( 'test:event', payload );
|
|
56
|
+
|
|
57
|
+
expect( payload ).not.toHaveProperty( 'eventId' );
|
|
58
|
+
} );
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
describe( 'pass-through behavior', () => {
|
|
62
|
+
it( 'passes primitive payloads through unchanged', () => {
|
|
63
|
+
const handler = vi.fn();
|
|
64
|
+
messageBus.on( 'test:event', handler );
|
|
65
|
+
|
|
66
|
+
messageBus.emit( 'test:event', 'a-string' );
|
|
67
|
+
messageBus.emit( 'test:event', 42 );
|
|
68
|
+
messageBus.emit( 'test:event', true );
|
|
69
|
+
|
|
70
|
+
expect( handler ).toHaveBeenNthCalledWith( 1, 'a-string' );
|
|
71
|
+
expect( handler ).toHaveBeenNthCalledWith( 2, 42 );
|
|
72
|
+
expect( handler ).toHaveBeenNthCalledWith( 3, true );
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
it( 'passes null and undefined payloads through unchanged', () => {
|
|
76
|
+
const handler = vi.fn();
|
|
77
|
+
messageBus.on( 'test:event', handler );
|
|
78
|
+
|
|
79
|
+
messageBus.emit( 'test:event', null );
|
|
80
|
+
messageBus.emit( 'test:event' );
|
|
81
|
+
|
|
82
|
+
expect( handler ).toHaveBeenNthCalledWith( 1, null );
|
|
83
|
+
expect( handler ).toHaveBeenNthCalledWith( 2 );
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
it( 'passes array payloads through unchanged (no key injection)', () => {
|
|
87
|
+
const handler = vi.fn();
|
|
88
|
+
messageBus.on( 'test:event', handler );
|
|
89
|
+
|
|
90
|
+
messageBus.emit( 'test:event', [ 1, 2, 3 ] );
|
|
91
|
+
|
|
92
|
+
expect( handler ).toHaveBeenCalledWith( [ 1, 2, 3 ] );
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'forwards additional positional args untouched', () => {
|
|
96
|
+
const handler = vi.fn();
|
|
97
|
+
messageBus.on( 'test:event', handler );
|
|
98
|
+
|
|
99
|
+
messageBus.emit( 'test:event', { foo: 'bar' }, 'extra', 99 );
|
|
100
|
+
|
|
101
|
+
expect( handler ).toHaveBeenCalledWith(
|
|
102
|
+
expect.objectContaining( { foo: 'bar', eventId: expect.stringMatching( UUID_V4_REGEX ) } ),
|
|
103
|
+
'extra',
|
|
104
|
+
99
|
|
105
|
+
);
|
|
106
|
+
} );
|
|
107
|
+
} );
|
|
108
|
+
} );
|
package/src/hooks/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Payload passed to the onError handler when a workflow, activity or runtime error occurs.
|
|
3
3
|
*/
|
|
4
4
|
export interface ErrorHookPayload {
|
|
5
|
+
/** UUID v4 stamped per emit. Stable per-emit idempotency key. */
|
|
6
|
+
eventId: string;
|
|
5
7
|
/** Origin of the error: workflow execution, activity execution, or runtime. */
|
|
6
8
|
source: 'workflow' | 'activity' | 'runtime';
|
|
7
9
|
/** Name of the workflow, when the error is scoped to a workflow or activity. */
|
|
@@ -16,6 +18,8 @@ export interface ErrorHookPayload {
|
|
|
16
18
|
* Payload passed to the onWorkflowStart handler when a workflow run begins.
|
|
17
19
|
*/
|
|
18
20
|
export interface WorkflowStartHookPayload {
|
|
21
|
+
/** UUID v4 stamped per emit. Stable per-emit idempotency key. */
|
|
22
|
+
eventId: string;
|
|
19
23
|
/** Workflow id (stable across retries / continue-as-new). */
|
|
20
24
|
id: string;
|
|
21
25
|
/** Temporal run id for the current execution attempt. */
|
|
@@ -28,6 +32,8 @@ export interface WorkflowStartHookPayload {
|
|
|
28
32
|
* Payload passed to the onWorkflowEnd handler when a workflow run completes successfully.
|
|
29
33
|
*/
|
|
30
34
|
export interface WorkflowEndHookPayload {
|
|
35
|
+
/** UUID v4 stamped per emit. Stable per-emit idempotency key. */
|
|
36
|
+
eventId: string;
|
|
31
37
|
/** Workflow id (stable across retries / continue-as-new). */
|
|
32
38
|
id: string;
|
|
33
39
|
/** Temporal run id for the current execution attempt. */
|
|
@@ -42,6 +48,8 @@ export interface WorkflowEndHookPayload {
|
|
|
42
48
|
* Payload passed to the onWorkflowError handler when a workflow run fails.
|
|
43
49
|
*/
|
|
44
50
|
export interface WorkflowErrorHookPayload {
|
|
51
|
+
/** UUID v4 stamped per emit. Stable per-emit idempotency key. */
|
|
52
|
+
eventId: string;
|
|
45
53
|
/** Workflow id (stable across retries / continue-as-new). */
|
|
46
54
|
id: string;
|
|
47
55
|
/** Temporal run id for the current execution attempt. */
|
|
@@ -107,6 +115,8 @@ export declare function onWorkflowError( handler: ( payload: WorkflowErrorHookPa
|
|
|
107
115
|
* to the fields listed here.
|
|
108
116
|
*/
|
|
109
117
|
export interface HttpRequestHookPayload {
|
|
118
|
+
/** UUID v4 stamped per emit. Stable per-emit idempotency key. */
|
|
119
|
+
eventId: string;
|
|
110
120
|
/** Workflow id (stable across retries / continue-as-new). */
|
|
111
121
|
workflowId: string;
|
|
112
122
|
/** Temporal run id for the current execution attempt. */
|
package/src/hooks/index.js
CHANGED
|
@@ -21,12 +21,12 @@ const safeInvoke = async ( fn, args, hookName ) => {
|
|
|
21
21
|
|
|
22
22
|
/** Triggers on any errors: workflow, activity and runtime */
|
|
23
23
|
export const onError = handler => {
|
|
24
|
-
messageBus.on( BusEventType.ACTIVITY_ERROR, async ( { id, name, workflowId, workflowName, error } ) =>
|
|
25
|
-
safeInvoke( handler, { source: 'activity', activityId: id, activityName: name, workflowId, workflowName, error }, 'onError' ) );
|
|
26
|
-
messageBus.on( BusEventType.WORKFLOW_ERROR, async ( { id, name, error } ) =>
|
|
27
|
-
safeInvoke( handler, { source: 'workflow', workflowId: id, workflowName: name, error }, 'onError' ) );
|
|
28
|
-
messageBus.on( BusEventType.RUNTIME_ERROR, async ( { error } ) =>
|
|
29
|
-
safeInvoke( handler, { source: 'runtime', error }, 'onError' ) );
|
|
24
|
+
messageBus.on( BusEventType.ACTIVITY_ERROR, async ( { eventId, id, name, workflowId, workflowName, error } ) =>
|
|
25
|
+
safeInvoke( handler, { eventId, source: 'activity', activityId: id, activityName: name, workflowId, workflowName, error }, 'onError' ) );
|
|
26
|
+
messageBus.on( BusEventType.WORKFLOW_ERROR, async ( { eventId, id, name, error } ) =>
|
|
27
|
+
safeInvoke( handler, { eventId, source: 'workflow', workflowId: id, workflowName: name, error }, 'onError' ) );
|
|
28
|
+
messageBus.on( BusEventType.RUNTIME_ERROR, async ( { eventId, error } ) =>
|
|
29
|
+
safeInvoke( handler, { eventId, source: 'runtime', error }, 'onError' ) );
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
/** Listen to worker before start events */
|
|
@@ -34,16 +34,16 @@ export const onBeforeWorkerStart = handler => messageBus.on( BusEventType.WORKER
|
|
|
34
34
|
safeInvoke( handler, undefined, 'onBeforeWorkerStart' ) );
|
|
35
35
|
|
|
36
36
|
/** Listen to workflow start events, excludes catalog workflow */
|
|
37
|
-
export const onWorkflowStart = handler => messageBus.on( BusEventType.WORKFLOW_START, ( { id, runId, name } ) =>
|
|
38
|
-
WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, runId, name }, 'onWorkflowStart' ) : null );
|
|
37
|
+
export const onWorkflowStart = handler => messageBus.on( BusEventType.WORKFLOW_START, ( { eventId, id, runId, name } ) =>
|
|
38
|
+
WORKFLOW_CATALOG !== name ? safeInvoke( handler, { eventId, id, runId, name }, 'onWorkflowStart' ) : null );
|
|
39
39
|
|
|
40
40
|
/** Listen to workflow end events, excludes catalog workflow */
|
|
41
|
-
export const onWorkflowEnd = handler => messageBus.on( BusEventType.WORKFLOW_END, ( { id, runId, name, duration } ) =>
|
|
42
|
-
WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, runId, name, duration }, 'onWorkflowEnd' ) : null );
|
|
41
|
+
export const onWorkflowEnd = handler => messageBus.on( BusEventType.WORKFLOW_END, ( { eventId, id, runId, name, duration } ) =>
|
|
42
|
+
WORKFLOW_CATALOG !== name ? safeInvoke( handler, { eventId, id, runId, name, duration }, 'onWorkflowEnd' ) : null );
|
|
43
43
|
|
|
44
44
|
/** Listen to workflow error events, excludes catalog workflow */
|
|
45
|
-
export const onWorkflowError = handler => messageBus.on( BusEventType.WORKFLOW_ERROR, ( { id, runId, name, duration, error } ) =>
|
|
46
|
-
WORKFLOW_CATALOG !== name ? safeInvoke( handler, { id, runId, name, duration, error }, 'onWorkflowError' ) : null );
|
|
45
|
+
export const onWorkflowError = handler => messageBus.on( BusEventType.WORKFLOW_ERROR, ( { eventId, id, runId, name, duration, error } ) =>
|
|
46
|
+
WORKFLOW_CATALOG !== name ? safeInvoke( handler, { eventId, id, runId, name, duration, error }, 'onWorkflowError' ) : null );
|
|
47
47
|
|
|
48
48
|
/** Generic listener for events emitted elsewhere (outside core) */
|
|
49
49
|
export const on = ( eventName, handler ) => messageBus.on( `external:${eventName}`, payload =>
|
package/src/hooks/index.spec.js
CHANGED
|
@@ -43,12 +43,13 @@ describe( 'hooks/index', () => {
|
|
|
43
43
|
expect( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, expect.any( Function ) );
|
|
44
44
|
} );
|
|
45
45
|
|
|
46
|
-
it( 'invokes handler with activity-shaped payload', async () => {
|
|
46
|
+
it( 'invokes handler with activity-shaped payload, forwarding eventId', async () => {
|
|
47
47
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
48
48
|
onError( handler );
|
|
49
49
|
|
|
50
50
|
const err = new Error( 'act-fail' );
|
|
51
51
|
await onHandlers[BusEventType.ACTIVITY_ERROR]( {
|
|
52
|
+
eventId: 'evt-act-1',
|
|
52
53
|
id: 'act-1',
|
|
53
54
|
name: 'wf#step',
|
|
54
55
|
workflowId: 'wf-run-1',
|
|
@@ -57,6 +58,7 @@ describe( 'hooks/index', () => {
|
|
|
57
58
|
} );
|
|
58
59
|
|
|
59
60
|
expect( handler ).toHaveBeenCalledWith( {
|
|
61
|
+
eventId: 'evt-act-1',
|
|
60
62
|
source: 'activity',
|
|
61
63
|
activityId: 'act-1',
|
|
62
64
|
activityName: 'wf#step',
|
|
@@ -66,18 +68,20 @@ describe( 'hooks/index', () => {
|
|
|
66
68
|
} );
|
|
67
69
|
} );
|
|
68
70
|
|
|
69
|
-
it( 'invokes handler with workflow-shaped payload', async () => {
|
|
71
|
+
it( 'invokes handler with workflow-shaped payload, forwarding eventId', async () => {
|
|
70
72
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
71
73
|
onError( handler );
|
|
72
74
|
|
|
73
75
|
const err = new Error( 'wf-fail' );
|
|
74
76
|
await onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
77
|
+
eventId: 'evt-wf-1',
|
|
75
78
|
id: 'wf-run-2',
|
|
76
79
|
name: 'myWorkflow',
|
|
77
80
|
error: err
|
|
78
81
|
} );
|
|
79
82
|
|
|
80
83
|
expect( handler ).toHaveBeenCalledWith( {
|
|
84
|
+
eventId: 'evt-wf-1',
|
|
81
85
|
source: 'workflow',
|
|
82
86
|
workflowId: 'wf-run-2',
|
|
83
87
|
workflowName: 'myWorkflow',
|
|
@@ -90,9 +94,9 @@ describe( 'hooks/index', () => {
|
|
|
90
94
|
onError( handler );
|
|
91
95
|
|
|
92
96
|
const error = new Error( 'rt' );
|
|
93
|
-
await onHandlers[BusEventType.RUNTIME_ERROR]( { error } );
|
|
97
|
+
await onHandlers[BusEventType.RUNTIME_ERROR]( { eventId: 'evt-rt-1', error } );
|
|
94
98
|
|
|
95
|
-
expect( handler ).toHaveBeenCalledWith( { source: 'runtime', error } );
|
|
99
|
+
expect( handler ).toHaveBeenCalledWith( { eventId: 'evt-rt-1', source: 'runtime', error } );
|
|
96
100
|
} );
|
|
97
101
|
} );
|
|
98
102
|
|
|
@@ -109,56 +113,60 @@ describe( 'hooks/index', () => {
|
|
|
109
113
|
} );
|
|
110
114
|
|
|
111
115
|
describe( 'onWorkflowStart', () => {
|
|
112
|
-
it( 'skips catalog workflow
|
|
116
|
+
it( 'skips catalog workflow and forwards eventId for real workflows', async () => {
|
|
113
117
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
114
118
|
onWorkflowStart( handler );
|
|
115
119
|
|
|
116
|
-
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
|
|
120
|
+
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
|
|
121
|
+
eventId: 'evt-ignored', id: '1', name: WORKFLOW_CATALOG
|
|
122
|
+
} ) );
|
|
117
123
|
expect( handler ).not.toHaveBeenCalled();
|
|
118
124
|
|
|
119
|
-
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
|
|
120
|
-
|
|
125
|
+
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
|
|
126
|
+
eventId: 'evt-start-1', id: '2', runId: 'run-2', name: 'myWorkflow'
|
|
127
|
+
} ) );
|
|
128
|
+
expect( handler ).toHaveBeenCalledWith( {
|
|
129
|
+
eventId: 'evt-start-1', id: '2', runId: 'run-2', name: 'myWorkflow'
|
|
130
|
+
} );
|
|
121
131
|
} );
|
|
122
132
|
} );
|
|
123
133
|
|
|
124
134
|
describe( 'onWorkflowEnd', () => {
|
|
125
|
-
it( 'skips catalog workflow
|
|
135
|
+
it( 'skips catalog workflow and forwards eventId for real workflows', async () => {
|
|
126
136
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
127
137
|
onWorkflowEnd( handler );
|
|
128
138
|
|
|
129
139
|
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( {
|
|
130
|
-
id: '1',
|
|
131
|
-
name: WORKFLOW_CATALOG,
|
|
132
|
-
duration: 10
|
|
140
|
+
eventId: 'evt-ignored', id: '1', name: WORKFLOW_CATALOG, duration: 10
|
|
133
141
|
} ) );
|
|
134
142
|
expect( handler ).not.toHaveBeenCalled();
|
|
135
143
|
|
|
136
|
-
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( {
|
|
137
|
-
|
|
144
|
+
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( {
|
|
145
|
+
eventId: 'evt-end-1', id: '2', runId: 'run-2', name: 'myWorkflow', duration: 5
|
|
146
|
+
} ) );
|
|
147
|
+
expect( handler ).toHaveBeenCalledWith( {
|
|
148
|
+
eventId: 'evt-end-1', id: '2', runId: 'run-2', name: 'myWorkflow', duration: 5
|
|
149
|
+
} );
|
|
138
150
|
} );
|
|
139
151
|
} );
|
|
140
152
|
|
|
141
153
|
describe( 'onWorkflowError', () => {
|
|
142
|
-
it( 'skips catalog workflow
|
|
154
|
+
it( 'skips catalog workflow and forwards eventId for real workflows', async () => {
|
|
143
155
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
144
156
|
const err = new Error( 'wf' );
|
|
145
157
|
onWorkflowError( handler );
|
|
146
158
|
|
|
147
159
|
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
148
|
-
id: '1',
|
|
149
|
-
name: WORKFLOW_CATALOG,
|
|
150
|
-
duration: 1,
|
|
151
|
-
error: err
|
|
160
|
+
eventId: 'evt-ignored', id: '1', name: WORKFLOW_CATALOG, duration: 1, error: err
|
|
152
161
|
} ) );
|
|
153
162
|
expect( handler ).not.toHaveBeenCalled();
|
|
154
163
|
|
|
155
164
|
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
156
|
-
id: '2',
|
|
157
|
-
name: 'myWorkflow',
|
|
158
|
-
duration: 2,
|
|
159
|
-
error: err
|
|
165
|
+
eventId: 'evt-err-1', id: '2', runId: 'run-2', name: 'myWorkflow', duration: 2, error: err
|
|
160
166
|
} ) );
|
|
161
|
-
expect( handler ).toHaveBeenCalledWith( {
|
|
167
|
+
expect( handler ).toHaveBeenCalledWith( {
|
|
168
|
+
eventId: 'evt-err-1', id: '2', runId: 'run-2', name: 'myWorkflow', duration: 2, error: err
|
|
169
|
+
} );
|
|
162
170
|
} );
|
|
163
171
|
} );
|
|
164
172
|
|