@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,124 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const loadMock = vi.fn();
4
- vi.mock( '#async_storage', () => ( {
5
- Storage: { load: loadMock }
6
- } ) );
7
-
8
- const getHandleMock = vi.fn();
9
- const clientConstructorMock = vi.fn( function () {
10
- this.workflow = { getHandle: getHandleMock };
11
- } );
12
- vi.mock( '@temporalio/client', () => ( {
13
- Client: clientConstructorMock
14
- } ) );
15
-
16
- const connection = {
17
- fake: 'native-connection',
18
- close: vi.fn().mockResolvedValue( undefined )
19
- };
20
- const activityContext = {
21
- activityInfo: {
22
- workflowExecution: {
23
- workflowId: 'workflow-1',
24
- runId: 'run-1'
25
- }
26
- }
27
- };
28
-
29
- describe( 'Temporal client', () => {
30
- beforeEach( () => {
31
- vi.clearAllMocks();
32
- vi.resetModules();
33
- } );
34
-
35
- describe( 'createTemporalClient', () => {
36
- it( 'throws a FatalError outside the worker runtime', async () => {
37
- const { createTemporalClient } = await import( './client.js' );
38
- const { FatalError } = await import( '#errors' );
39
-
40
- expect( () => createTemporalClient() ).toThrow( FatalError );
41
- } );
42
-
43
- it( 'creates a client with the worker connection and namespace', async () => {
44
- const { createTemporalClient, setupClientConfig } = await import( './client.js' );
45
- setupClientConfig( { connection, namespace: 'namespace-1' } );
46
-
47
- const client = createTemporalClient();
48
-
49
- expect( clientConstructorMock ).toHaveBeenCalledWith( {
50
- connection: expect.anything(),
51
- namespace: 'namespace-1'
52
- } );
53
- expect( clientConstructorMock.mock.calls[0][0].connection ).not.toBe( connection );
54
- expect( client ).toBeInstanceOf( clientConstructorMock );
55
- } );
56
-
57
- it( 'delegates connection access but prevents closing the worker connection', async () => {
58
- const { createTemporalClient, setupClientConfig } = await import( './client.js' );
59
- const { FatalError } = await import( '#errors' );
60
- setupClientConfig( { connection, namespace: 'namespace-1' } );
61
-
62
- createTemporalClient();
63
- const clientConnection = clientConstructorMock.mock.calls[0][0].connection;
64
-
65
- expect( clientConnection.fake ).toBe( 'native-connection' );
66
- await expect( clientConnection.close() ).rejects.toThrow( FatalError );
67
- expect( connection.close ).not.toHaveBeenCalled();
68
- } );
69
-
70
- it( 'returns a new client on each call', async () => {
71
- const { createTemporalClient, setupClientConfig } = await import( './client.js' );
72
- setupClientConfig( { connection, namespace: 'namespace-1' } );
73
-
74
- expect( createTemporalClient() ).not.toBe( createTemporalClient() );
75
- expect( clientConstructorMock ).toHaveBeenCalledTimes( 2 );
76
- } );
77
- } );
78
-
79
- describe( 'getCurrentWorkflowHandle', () => {
80
- it( 'throws a FatalError outside a Temporal activity context', async () => {
81
- loadMock.mockReturnValue( undefined );
82
- const { getCurrentWorkflowHandle, setupClientConfig } = await import( './client.js' );
83
- const { FatalError } = await import( '#errors' );
84
- setupClientConfig( { connection, namespace: 'namespace-1' } );
85
-
86
- expect( () => getCurrentWorkflowHandle() ).toThrow( FatalError );
87
- } );
88
-
89
- it( 'throws a FatalError outside the worker runtime', async () => {
90
- loadMock.mockReturnValue( activityContext );
91
- const { getCurrentWorkflowHandle } = await import( './client.js' );
92
- const { FatalError } = await import( '#errors' );
93
-
94
- expect( () => getCurrentWorkflowHandle() ).toThrow( FatalError );
95
- } );
96
-
97
- it( 'returns the handle for the current workflow execution', async () => {
98
- const handle = { signal: vi.fn() };
99
- loadMock.mockReturnValue( activityContext );
100
- getHandleMock.mockReturnValue( handle );
101
- const { getCurrentWorkflowHandle, setupClientConfig } = await import( './client.js' );
102
- setupClientConfig( { connection, namespace: 'namespace-1' } );
103
-
104
- expect( getCurrentWorkflowHandle() ).toBe( handle );
105
- expect( clientConstructorMock ).toHaveBeenCalledWith( {
106
- connection: expect.anything(),
107
- namespace: 'namespace-1'
108
- } );
109
- expect( getHandleMock ).toHaveBeenCalledWith( 'workflow-1', 'run-1' );
110
- } );
111
-
112
- it( 'reuses its client across calls', async () => {
113
- loadMock.mockReturnValue( activityContext );
114
- const { getCurrentWorkflowHandle, setupClientConfig } = await import( './client.js' );
115
- setupClientConfig( { connection, namespace: 'namespace-1' } );
116
-
117
- getCurrentWorkflowHandle();
118
- getCurrentWorkflowHandle();
119
-
120
- expect( clientConstructorMock ).toHaveBeenCalledTimes( 1 );
121
- expect( getHandleMock ).toHaveBeenCalledTimes( 2 );
122
- } );
123
- } );
124
- } );
@@ -1,204 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
-
3
- // In-memory fs mock store
4
- const store = { files: new Map() };
5
- const mkdirSyncMock = vi.fn();
6
- const writeFileSyncMock = vi.fn();
7
- const appendFileSyncMock = vi.fn( ( path, data ) => {
8
- const prev = store.files.get( path ) ?? '';
9
- store.files.set( path, prev + data );
10
- } );
11
- const readFileSyncMock = vi.fn( path => store.files.get( path ) ?? '' );
12
- const readdirSyncMock = vi.fn( () => [] );
13
- const rmSyncMock = vi.fn();
14
- const createWriteStreamMock = vi.fn( path => ( { path } ) );
15
-
16
- vi.mock( 'node:fs', () => ( {
17
- mkdirSync: mkdirSyncMock,
18
- writeFileSync: writeFileSyncMock,
19
- appendFileSync: appendFileSyncMock,
20
- readFileSync: readFileSyncMock,
21
- readdirSync: readdirSyncMock,
22
- rmSync: rmSyncMock,
23
- createWriteStream: createWriteStreamMock
24
- } ) );
25
-
26
- const pipelineMock = vi.fn( async ( source, destination ) => {
27
- const chunks = [];
28
- for await ( const chunk of source ) {
29
- chunks.push( Buffer.isBuffer( chunk ) ? chunk : Buffer.from( chunk ) );
30
- }
31
- store.files.set( destination.path, Buffer.concat( chunks ).toString( 'utf8' ) );
32
- } );
33
- vi.mock( 'node:stream/promises', () => ( { pipeline: pipelineMock } ) );
34
-
35
- vi.mock( 'json-stream-stringify', async () => {
36
- const { Readable } = await import( 'node:stream' );
37
- return {
38
- JsonStreamStringify: class extends Readable {
39
- constructor( body ) {
40
- super();
41
- this.body = body;
42
- }
43
-
44
- _read() {
45
- this.push( JSON.stringify( this.body ) );
46
- this.push( null );
47
- }
48
- }
49
- };
50
- } );
51
-
52
- const buildTraceTreeMock = vi.fn( entries => ( { count: entries.length } ) );
53
- vi.mock( '../../tools/build_trace_tree.js', () => ( { default: buildTraceTreeMock } ) );
54
-
55
- /** Flush happens when the root id matches runId and action is not 'start', or when action is 'error'. */
56
- const rootStart = ( runId, ts ) => ( { id: runId, action: 'start', timestamp: ts } );
57
- const rootEnd = ( runId, ts ) => ( { id: runId, action: 'end', timestamp: ts } );
58
- const childTick = ( id, ts ) => ( { id, action: 'tick', timestamp: ts } );
59
-
60
- describe( 'tracing/processors/local', () => {
61
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
62
- const traceInfo = {
63
- workflowId: 'id1',
64
- runId: 'run-1',
65
- workflowType: 'WF',
66
- startTime
67
- };
68
-
69
- beforeEach( () => {
70
- vi.clearAllMocks();
71
- store.files.clear();
72
- process.argv[2] = '/tmp/project';
73
- delete process.env.OUTPUT_TRACE_HOST_PATH;
74
- } );
75
-
76
- it( 'init(): creates temp dir and cleans up old files', async () => {
77
- const { init } = await import( './index.js' );
78
-
79
- const now = Date.now();
80
- readdirSyncMock.mockReturnValue( [ `${now - ( 8 * 24 * 60 * 60 * 1000 )}_old.trace`, `${now}_new.trace` ] );
81
-
82
- init();
83
-
84
- expect( mkdirSyncMock ).toHaveBeenCalledWith( expect.stringMatching( /temp\/traces$/ ), { recursive: true } );
85
- expect( rmSyncMock ).toHaveBeenCalledTimes( 1 );
86
- } );
87
-
88
- it( 'exec(): appends each entry and writes aggregated tree once on root workflow end', async () => {
89
- const { exec, init } = await import( './index.js' );
90
- init();
91
-
92
- await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
93
- await exec( { traceInfo, entry: childTick( 'child-1', startTime + 1 ) } );
94
- await exec( { traceInfo, entry: rootEnd( traceInfo.runId, startTime + 2 ) } );
95
-
96
- expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
97
- expect( buildTraceTreeMock.mock.calls[0][0] ).toHaveLength( 3 );
98
-
99
- expect( createWriteStreamMock ).toHaveBeenCalledTimes( 1 );
100
- expect( pipelineMock ).toHaveBeenCalledTimes( 1 );
101
- const [ writtenPath ] = createWriteStreamMock.mock.calls[0];
102
- expect( writtenPath ).toMatch( /\/tmp\/project\/logs\/runs\/WF\// );
103
- expect( JSON.parse( store.files.get( writtenPath ) ).count ).toBe( 3 );
104
- } );
105
-
106
- it( 'exec(): does not build or write on non-flush entries', async () => {
107
- const { exec, init } = await import( './index.js' );
108
- init();
109
-
110
- await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
111
- await exec( { traceInfo, entry: childTick( 'child-1', startTime + 1 ) } );
112
-
113
- expect( buildTraceTreeMock ).not.toHaveBeenCalled();
114
- expect( writeFileSyncMock ).not.toHaveBeenCalled();
115
- expect( createWriteStreamMock ).not.toHaveBeenCalled();
116
- expect( pipelineMock ).not.toHaveBeenCalled();
117
- } );
118
-
119
- it( 'exec(): flushes on error action before root end', async () => {
120
- const { exec, init } = await import( './index.js' );
121
- init();
122
-
123
- await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
124
- await exec( { traceInfo, entry: { id: 'step-1', action: 'error', timestamp: startTime + 1 } } );
125
-
126
- expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
127
- expect( buildTraceTreeMock.mock.calls[0][0] ).toHaveLength( 2 );
128
- expect( createWriteStreamMock ).toHaveBeenCalledTimes( 1 );
129
- expect( pipelineMock ).toHaveBeenCalledTimes( 1 );
130
- } );
131
-
132
- it( 'getDestination(): returns absolute path under callerDir logs', async () => {
133
- const { getDestination } = await import( './index.js' );
134
-
135
- const destination = getDestination( {
136
- ...traceInfo,
137
- workflowId: 'workflow-id-123',
138
- workflowType: 'test-workflow'
139
- } );
140
-
141
- expect( destination ).toMatch( /^\/|^[A-Z]:\\/i );
142
- expect( destination ).toBe(
143
- '/tmp/project/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json'
144
- );
145
- } );
146
-
147
- it( 'exec(): writes under process.argv[2] logs even when OUTPUT_TRACE_HOST_PATH is set', async () => {
148
- const { exec, init } = await import( './index.js' );
149
-
150
- process.env.OUTPUT_TRACE_HOST_PATH = '/host/path/logs';
151
-
152
- init();
153
-
154
- await exec( { traceInfo, entry: rootStart( traceInfo.runId, startTime ) } );
155
- await exec( { traceInfo, entry: rootEnd( traceInfo.runId, startTime + 1 ) } );
156
-
157
- expect( createWriteStreamMock ).toHaveBeenCalledTimes( 1 );
158
- const [ writtenPath ] = createWriteStreamMock.mock.calls[0];
159
-
160
- expect( writtenPath ).not.toContain( '/host/path/logs' );
161
- expect( writtenPath ).toMatch( /\/tmp\/project\/logs\/runs\/WF\// );
162
- } );
163
-
164
- it( 'getDestination(): returns OUTPUT_TRACE_HOST_PATH when set', async () => {
165
- const { getDestination } = await import( './index.js' );
166
-
167
- process.env.OUTPUT_TRACE_HOST_PATH = '/host/path/logs';
168
-
169
- const destination = getDestination( {
170
- ...traceInfo,
171
- workflowId: 'workflow-id-123',
172
- workflowType: 'test-workflow'
173
- } );
174
-
175
- expect( destination ).toBe( '/host/path/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
176
- } );
177
-
178
- it( 'separation of write and report paths works correctly', async () => {
179
- const { exec, getDestination, init } = await import( './index.js' );
180
-
181
- process.env.OUTPUT_TRACE_HOST_PATH = '/Users/ben/project/logs';
182
-
183
- init();
184
-
185
- const testTraceInfo = {
186
- ...traceInfo,
187
- workflowId: 'workflow-id-123',
188
- runId: 'run-123',
189
- workflowType: 'test-workflow'
190
- };
191
-
192
- await exec( { traceInfo: testTraceInfo, entry: rootStart( testTraceInfo.runId, startTime ) } );
193
- await exec( { traceInfo: testTraceInfo, entry: rootEnd( testTraceInfo.runId, startTime + 1 ) } );
194
-
195
- const destination = getDestination( testTraceInfo );
196
-
197
- const [ writtenPath ] = createWriteStreamMock.mock.calls[0];
198
- expect( writtenPath ).not.toContain( '/Users/ben/project' );
199
- expect( writtenPath ).toMatch( /\/tmp\/project\/logs\/runs\/test-workflow\// );
200
- expect( JSON.parse( store.files.get( writtenPath ) ).count ).toBe( 2 );
201
-
202
- expect( destination ).toBe( '/Users/ben/project/logs/runs/test-workflow/2020-01-02-03-04-05-678Z_workflow-id-123.json' );
203
- } );
204
- } );
@@ -1,64 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- async function loadModule() {
4
- vi.resetModules();
5
- return import( './configs.js' );
6
- }
7
-
8
- describe( 'tracing/processors/s3/configs', () => {
9
- const required = {
10
- OUTPUT_AWS_REGION: 'us-east-1',
11
- OUTPUT_AWS_ACCESS_KEY_ID: 'id',
12
- OUTPUT_AWS_SECRET_ACCESS_KEY: 'sek',
13
- OUTPUT_TRACE_REMOTE_S3_BUCKET: 'bkt',
14
- OUTPUT_REDIS_URL: 'redis://localhost:6379'
15
- };
16
-
17
- beforeEach( () => {
18
- vi.stubEnv( 'OUTPUT_AWS_REGION', required.OUTPUT_AWS_REGION );
19
- vi.stubEnv( 'OUTPUT_AWS_ACCESS_KEY_ID', required.OUTPUT_AWS_ACCESS_KEY_ID );
20
- vi.stubEnv( 'OUTPUT_AWS_SECRET_ACCESS_KEY', required.OUTPUT_AWS_SECRET_ACCESS_KEY );
21
- vi.stubEnv( 'OUTPUT_TRACE_REMOTE_S3_BUCKET', required.OUTPUT_TRACE_REMOTE_S3_BUCKET );
22
- vi.stubEnv( 'OUTPUT_REDIS_URL', required.OUTPUT_REDIS_URL );
23
- } );
24
-
25
- afterEach( () => {
26
- vi.unstubAllEnvs();
27
- } );
28
-
29
- it( 'loadEnv() throws when required env vars are missing', async () => {
30
- vi.stubEnv( 'OUTPUT_REDIS_URL', undefined );
31
- const { loadEnv } = await loadModule();
32
- expect( () => loadEnv() ).toThrow( /OUTPUT_REDIS_URL/ );
33
- } );
34
-
35
- it( 'loadEnv() populates getVars() with parsed env', async () => {
36
- const { loadEnv, getVars } = await loadModule();
37
- loadEnv();
38
- const vars = getVars();
39
- expect( vars.awsRegion ).toBe( required.OUTPUT_AWS_REGION );
40
- expect( vars.awsAccessKeyId ).toBe( required.OUTPUT_AWS_ACCESS_KEY_ID );
41
- expect( vars.awsSecretAccessKey ).toBe( required.OUTPUT_AWS_SECRET_ACCESS_KEY );
42
- expect( vars.remoteS3Bucket ).toBe( required.OUTPUT_TRACE_REMOTE_S3_BUCKET );
43
- expect( vars.redisUrl ).toBe( required.OUTPUT_REDIS_URL );
44
- expect( vars.redisIncompleteWorkflowsTTL ).toBe( 60 * 60 * 24 * 7 );
45
- } );
46
-
47
- it( 'loadEnv() uses OUTPUT_REDIS_TRACE_TTL when set', async () => {
48
- vi.stubEnv( 'OUTPUT_REDIS_TRACE_TTL', '3600' );
49
- const { loadEnv, getVars } = await loadModule();
50
- loadEnv();
51
- expect( getVars().redisIncompleteWorkflowsTTL ).toBe( 3600 );
52
- } );
53
-
54
- it( 'getVars() throws when loadEnv() was not called', async () => {
55
- const { getVars } = await loadModule();
56
- expect( () => getVars() ).toThrow( 'Env vars not loaded. Use loadEnv() first.' );
57
- } );
58
-
59
- it( 'loadEnv() throws when OUTPUT_REDIS_TRACE_TTL is invalid', async () => {
60
- vi.stubEnv( 'OUTPUT_REDIS_TRACE_TTL', 'not-a-number' );
61
- const { loadEnv } = await loadModule();
62
- expect( () => loadEnv() ).toThrow();
63
- } );
64
- } );
@@ -1,185 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- const loadEnvMock = vi.fn();
4
- const getVarsMock = vi.fn( () => ( {
5
- remoteS3Bucket: 'bkt',
6
- redisIncompleteWorkflowsTTL: 3600
7
- } ) );
8
- vi.mock( './configs.js', () => ( { loadEnv: loadEnvMock, getVars: getVarsMock } ) );
9
-
10
- const redisMulti = {
11
- zAdd: vi.fn().mockReturnThis(),
12
- expire: vi.fn().mockReturnThis(),
13
- exec: vi.fn()
14
- };
15
- const zRangeMock = vi.fn();
16
- const delMock = vi.fn().mockResolvedValue( undefined );
17
- const getRedisClientMock = vi.fn( async () => ( {
18
- multi: () => redisMulti,
19
- zRange: zRangeMock,
20
- del: delMock
21
- } ) );
22
- vi.mock( './redis_client.js', () => ( { getRedisClient: getRedisClientMock } ) );
23
-
24
- const uploadMock = vi.fn();
25
- vi.mock( './s3_client.js', () => ( { upload: uploadMock } ) );
26
-
27
- const buildTraceTreeMock = vi.fn( entries => ( { count: entries.length } ) );
28
- vi.mock( '../../tools/build_trace_tree.js', () => ( { default: buildTraceTreeMock } ) );
29
-
30
- vi.mock( 'json-stream-stringify', async () => {
31
- const { Readable } = await import( 'node:stream' );
32
- return {
33
- JsonStreamStringify: class extends Readable {
34
- constructor( body ) {
35
- super();
36
- this.body = body;
37
- }
38
-
39
- _read() {
40
- this.push( JSON.stringify( this.body ) );
41
- this.push( null );
42
- }
43
- }
44
- };
45
- } );
46
-
47
- const streamToString = async stream => {
48
- const chunks = [];
49
- for await ( const chunk of stream ) {
50
- chunks.push( Buffer.isBuffer( chunk ) ? chunk : Buffer.from( chunk ) );
51
- }
52
- return Buffer.concat( chunks ).toString( 'utf8' );
53
- };
54
-
55
- describe( 'tracing/processors/s3', () => {
56
- const startTime = Date.parse( '2020-01-02T03:04:05.678Z' );
57
- const traceInfo = {
58
- workflowId: 'id1',
59
- runId: 'run-1',
60
- workflowType: 'WF',
61
- startTime
62
- };
63
-
64
- beforeEach( () => {
65
- vi.useFakeTimers();
66
- vi.clearAllMocks();
67
- getVarsMock.mockReturnValue( { remoteS3Bucket: 'bkt', redisIncompleteWorkflowsTTL: 3600, traceUploadDelayMs: 10_000 } );
68
- } );
69
-
70
- afterEach( () => {
71
- vi.useRealTimers();
72
- } );
73
-
74
- it( 'init(): loads config and ensures redis client is created', async () => {
75
- const { init } = await import( './index.js' );
76
- await init();
77
- expect( loadEnvMock ).toHaveBeenCalledTimes( 1 );
78
- expect( getRedisClientMock ).toHaveBeenCalledTimes( 1 );
79
- } );
80
-
81
- it( 'exec(): accumulates via redis, uploads only on root workflow end', async () => {
82
- const { exec } = await import( './index.js' );
83
-
84
- redisMulti.exec.mockResolvedValue( [] );
85
-
86
- const workflowStart = { id: 'run-1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
87
- const activityStart = {
88
- id: 'act-1',
89
- name: 'DoSomething',
90
- kind: 'step',
91
- parentId: 'run-1',
92
- action: 'start',
93
- details: {},
94
- timestamp: startTime + 1
95
- };
96
- const workflowEnd = { id: 'run-1', action: 'end', details: { ok: true }, timestamp: startTime + 2 };
97
- zRangeMock.mockResolvedValue( [
98
- JSON.stringify( workflowStart ),
99
- JSON.stringify( activityStart ),
100
- JSON.stringify( workflowEnd )
101
- ] );
102
-
103
- await exec( { traceInfo, entry: workflowStart } );
104
- await exec( { traceInfo, entry: activityStart } );
105
- // Root end: id matches runId and not start — triggers the 10s delay before upload
106
- const endPromise = exec( { traceInfo, entry: workflowEnd } );
107
- await vi.advanceTimersByTimeAsync( 10_000 );
108
- await endPromise;
109
-
110
- expect( redisMulti.zAdd ).toHaveBeenCalledTimes( 3 );
111
- expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
112
- expect( zRangeMock ).toHaveBeenCalledTimes( 1 );
113
- expect( uploadMock ).toHaveBeenCalledTimes( 1 );
114
- const { key, content } = uploadMock.mock.calls[0][0];
115
- expect( key ).toMatch( /^WF\/2020\/01\/02\// );
116
- expect( JSON.parse( await streamToString( content ) ).count ).toBe( 3 );
117
- expect( delMock ).toHaveBeenCalledTimes( 1 );
118
- expect( delMock ).toHaveBeenCalledWith( 'traces/run-1' );
119
- } );
120
-
121
- it( 'getDestination(): returns S3 URL using bucket and key from getVars', async () => {
122
- getVarsMock.mockReturnValue( { remoteS3Bucket: 'my-bucket', redisIncompleteWorkflowsTTL: 3600, traceUploadDelayMs: 10_000 } );
123
- const { getDestination } = await import( './index.js' );
124
- const url = getDestination( traceInfo );
125
- expect( getVarsMock ).toHaveBeenCalled();
126
- expect( url ).toBe(
127
- 'https://my-bucket.s3.amazonaws.com/WF/2020/01/02/2020-01-02-03-04-05-678Z_id1.json'
128
- );
129
- } );
130
-
131
- it( 'exec(): sets expiry on the redis key for each entry', async () => {
132
- const { exec } = await import( './index.js' );
133
-
134
- redisMulti.exec.mockResolvedValue( [] );
135
- const workflowStart = {
136
- kind: 'workflow', id: 'run-1', name: 'WF', parentId: undefined, action: 'start', details: {}, timestamp: startTime
137
- };
138
- zRangeMock.mockResolvedValue( [ JSON.stringify( workflowStart ) ] );
139
-
140
- await exec( { traceInfo, entry: workflowStart } );
141
-
142
- expect( redisMulti.expire ).toHaveBeenCalledTimes( 1 );
143
- expect( redisMulti.expire ).toHaveBeenCalledWith( 'traces/run-1', 3600 );
144
- } );
145
-
146
- it( 'exec(): does not treat a non-root end (e.g. step without parentId) as root workflow end — regression for wrong root detection', async () => {
147
- const { exec } = await import( './index.js' );
148
-
149
- redisMulti.exec.mockResolvedValue( [] );
150
- const workflowStart = { id: 'run-1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
151
- const stepEndNoParent = { id: 'step-1', action: 'end', details: { done: true }, timestamp: startTime + 1 };
152
- zRangeMock.mockResolvedValue( [
153
- JSON.stringify( workflowStart ),
154
- JSON.stringify( stepEndNoParent )
155
- ] );
156
-
157
- await exec( { traceInfo, entry: workflowStart } );
158
- await exec( { traceInfo, entry: stepEndNoParent } );
159
-
160
- expect( redisMulti.zAdd ).toHaveBeenCalledTimes( 2 );
161
- expect( buildTraceTreeMock ).not.toHaveBeenCalled();
162
- expect( uploadMock ).not.toHaveBeenCalled();
163
- expect( delMock ).not.toHaveBeenCalled();
164
- } );
165
-
166
- it( 'exec(): when buildTraceTree returns null (incomplete tree), does not upload or bust cache', async () => {
167
- const { exec } = await import( './index.js' );
168
-
169
- redisMulti.exec.mockResolvedValue( [] );
170
- const workflowEnd = {
171
- kind: 'workflow', id: 'run-1', name: 'WF', parentId: undefined, action: 'end', details: {}, timestamp: startTime
172
- };
173
- zRangeMock.mockResolvedValue( [ JSON.stringify( workflowEnd ) ] );
174
- buildTraceTreeMock.mockReturnValueOnce( null );
175
-
176
- const endPromise = exec( { traceInfo, entry: workflowEnd } );
177
- await vi.advanceTimersByTimeAsync( 10_000 );
178
- await endPromise;
179
-
180
- expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
181
- expect( uploadMock ).not.toHaveBeenCalled();
182
- expect( delMock ).not.toHaveBeenCalled();
183
- } );
184
- } );
185
-