@outputai/core 0.10.1-next.09ed166.0 → 0.10.1-next.14a191e.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.10.1-next.09ed166.0",
3
+ "version": "0.10.1-next.14a191e.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "engines": {
@@ -41,17 +41,17 @@
41
41
  "@babel/parser": "7.29.2",
42
42
  "@babel/traverse": "7.29.0",
43
43
  "@babel/types": "7.29.0",
44
- "@temporalio/activity": "1.17.0",
45
- "@temporalio/client": "1.17.0",
46
- "@temporalio/common": "1.17.0",
47
- "@temporalio/worker": "1.17.0",
48
- "@temporalio/workflow": "1.17.0",
44
+ "@temporalio/activity": "1.20.3",
45
+ "@temporalio/client": "1.20.3",
46
+ "@temporalio/common": "1.20.3",
47
+ "@temporalio/worker": "1.20.3",
48
+ "@temporalio/workflow": "1.20.3",
49
49
  "decimal.js": "10.6.0",
50
50
  "folder-hash": "4.1.3",
51
51
  "json-stream-stringify": "3.1.6",
52
52
  "redis": "5.12.1",
53
53
  "stacktrace-parser": "0.1.11",
54
- "undici": "8.5.0",
54
+ "undici": "8.9.0",
55
55
  "winston": "3.19.0"
56
56
  },
57
57
  "peerDependencies": {
@@ -69,6 +69,7 @@
69
69
  "#consts": "./src/consts.js",
70
70
  "#errors": "./src/errors.js",
71
71
  "#logger": "./src/logger/index.js",
72
+ "#hooks/*": "./src/hooks/*.js",
72
73
  "#helpers/*": "./src/helpers/*.js",
73
74
  "#tracing": "./src/tracing/internal_interface.js",
74
75
  "#trace_attribute": "./src/tracing/trace_attribute.js",
@@ -1,6 +1,6 @@
1
1
  import { FatalError } from '#errors';
2
2
 
3
- /** Matches red int "hot-red-pie", but not int "redact" */
3
+ /** Matches red in "hot-red-pie", but not in "redact" */
4
4
  const wordMatcher = term => new RegExp( `(?<![a-z\\d])${term}(?![a-z\\d])`, 'i' );
5
5
 
6
6
  /** Matches red in "acquired", but not in "redact" */
@@ -1,22 +1,24 @@
1
1
  import { mainEventBus, stepEventBus } from '#bus';
2
2
  import { BusEventType } from '#consts';
3
3
  import { createChildLogger } from '#logger';
4
+ import { pendingHooks } from './pending_hooks.js';
4
5
 
5
6
  const log = createChildLogger( 'Hooks' );
6
7
 
7
8
  /**
8
- * Invokes a function within a try catch and log the error
9
+ * Invokes a function within a try catch and log the error.
9
10
  *
10
11
  * @param {Function} fn
11
12
  * @param {any} args - Args to invoke the function with
12
13
  * @param {string} hookName - hookName to identify this hook function in the logs
13
14
  */
14
- const callHookCb = async ( fn, args, hookName ) => {
15
- try {
16
- await fn( args );
17
- } catch ( error ) {
18
- log.error( `${hookName} hook error`, { message: error.message, stack: error.stack } );
19
- }
15
+ const callHookCb = ( fn, args, hookName ) => {
16
+ const promise = Promise.resolve()
17
+ .then( () => fn( args ) )
18
+ .catch( e => log.error( `${hookName} hook error`, { message: e.message, stack: e.stack } ) )
19
+ .finally( () => pendingHooks.delete( promise ) );
20
+ pendingHooks.add( promise );
21
+ return promise;
20
22
  };
21
23
 
22
24
  // General Life-cycle
@@ -38,6 +38,7 @@ import {
38
38
  onWorkflowError,
39
39
  onWorkflowStart
40
40
  } from './index.js';
41
+ import { pendingHooks } from './pending_hooks.js';
41
42
 
42
43
  const workflowDetails = {
43
44
  workflowId: 'wf-1',
@@ -67,6 +68,7 @@ const aggregations = {
67
68
  describe( 'hooks/index', () => {
68
69
  beforeEach( () => {
69
70
  vi.clearAllMocks();
71
+ pendingHooks.clear();
70
72
  Object.keys( mainOnHandlers ).forEach( k => {
71
73
  delete mainOnHandlers[k];
72
74
  } );
@@ -158,6 +160,23 @@ describe( 'hooks/index', () => {
158
160
  } );
159
161
  } );
160
162
 
163
+ it( 'tracks a hook callback until it settles', async () => {
164
+ const deferred = { resolve: null };
165
+ const handlerPromise = new Promise( resolve => {
166
+ deferred.resolve = resolve;
167
+ } );
168
+ const handler = vi.fn( () => handlerPromise );
169
+ onActivityEnd( handler );
170
+
171
+ const callbackPromise = mainOnHandlers[BusEventType.ACTIVITY_END]( { activityInfo } );
172
+
173
+ expect( pendingHooks.size ).toBe( 1 );
174
+
175
+ deferred.resolve();
176
+ await callbackPromise;
177
+ expect( pendingHooks.size ).toBe( 0 );
178
+ } );
179
+
161
180
  describe( 'workflow lifecycle hooks', () => {
162
181
  const cases = [
163
182
  [ 'onWorkflowStart', onWorkflowStart, BusEventType.WORKFLOW_START, {} ],
@@ -0,0 +1,24 @@
1
+ import { createChildLogger } from '#logger';
2
+
3
+ export const pendingHooks = new Set();
4
+
5
+ const log = createChildLogger( 'Hooks' );
6
+ const flushTimeoutMs = 30_000;
7
+
8
+ /**
9
+ * Await all pending hooks to flush for a certain time
10
+ */
11
+ export const flushPendingHooks = async () => {
12
+ const state = { timeout: null };
13
+ try {
14
+ const flushed = await Promise.race( [
15
+ Promise.allSettled( [ ...pendingHooks ] ).then( _ => true ),
16
+ new Promise( r => state.timeout = setTimeout( () => r( false ), flushTimeoutMs ) )
17
+ ] );
18
+ if ( !flushed ) {
19
+ log.warn( 'Some hook callbacks exceeded the timeout and will not be awaited', { count: pendingHooks.size } );
20
+ }
21
+ } finally {
22
+ clearTimeout( state.timeout );
23
+ }
24
+ };
@@ -0,0 +1,62 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const logMock = vi.hoisted( () => ( { warn: vi.fn() } ) );
4
+
5
+ vi.mock( '#logger', () => ( { createChildLogger: () => logMock } ) );
6
+
7
+ import { flushPendingHooks, pendingHooks } from './pending_hooks.js';
8
+
9
+ describe( 'pending hooks', () => {
10
+ beforeEach( () => {
11
+ pendingHooks.clear();
12
+ vi.clearAllMocks();
13
+ } );
14
+
15
+ afterEach( () => {
16
+ pendingHooks.clear();
17
+ vi.useRealTimers();
18
+ } );
19
+
20
+ it( 'waits for pending hooks to settle', async () => {
21
+ const deferred = { resolve: null };
22
+ const hookPromise = new Promise( resolve => {
23
+ deferred.resolve = resolve;
24
+ } );
25
+ pendingHooks.add( hookPromise );
26
+
27
+ const flushPromise = flushPendingHooks();
28
+ const state = { flushed: false };
29
+ flushPromise.then( () => {
30
+ state.flushed = true;
31
+ } );
32
+
33
+ await Promise.resolve();
34
+ expect( state.flushed ).toBe( false );
35
+
36
+ deferred.resolve();
37
+ await flushPromise;
38
+ expect( state.flushed ).toBe( true );
39
+ } );
40
+
41
+ it( 'stops waiting after the timeout', async () => {
42
+ vi.useFakeTimers();
43
+ pendingHooks.add( new Promise( () => {} ) );
44
+
45
+ const flushPromise = flushPendingHooks();
46
+ await vi.advanceTimersByTimeAsync( 30_000 );
47
+
48
+ await expect( flushPromise ).resolves.toBeUndefined();
49
+ } );
50
+
51
+ it( 'logs the number of hooks that exceeded the timeout', async () => {
52
+ vi.useFakeTimers();
53
+ pendingHooks.add( new Promise( () => {} ) );
54
+ pendingHooks.add( new Promise( () => {} ) );
55
+
56
+ const flushPromise = flushPendingHooks();
57
+ await vi.advanceTimersByTimeAsync( 30_000 );
58
+ await flushPromise;
59
+
60
+ expect( logMock.warn ).toHaveBeenCalledWith( expect.any( String ), { count: 2 } );
61
+ } );
62
+ } );
@@ -8,7 +8,7 @@ import {
8
8
  import { evaluator } from './evaluator.js';
9
9
  import { step } from './step.js';
10
10
  import { workflow } from './workflow.js';
11
- import { executeInParallel } from './workflow_utils.js';
11
+ import { executeInParallel, hasErrorType } from './workflow_utils.js';
12
12
  import { sendHttpRequest, sendPostRequestAndAwaitWebhook } from './webhook.js';
13
13
  import { Logger } from './logger.js';
14
14
 
@@ -23,6 +23,7 @@ export {
23
23
  EvaluationVerdictResult,
24
24
  EvaluationFeedback,
25
25
  executeInParallel,
26
+ hasErrorType,
26
27
  sendHttpRequest,
27
28
  sendPostRequestAndAwaitWebhook
28
29
  };
@@ -74,3 +74,18 @@ export declare function executeInParallel<T extends readonly ( () => Promise<unk
74
74
  onJobCompleted?: ( result: ParallelResults<T>[number] ) => void;
75
75
  }
76
76
  ): Promise<ParallelResults<T>>;
77
+
78
+ /**
79
+ * Checks whether an error or any error in its cause chain matches an Error class.
80
+ *
81
+ * Temporal Application Failures are matched through their serialized `type`,
82
+ * in addition to standard `instanceof` and `name` checks.
83
+ *
84
+ * @param error - Error or failure chain to inspect
85
+ * @param ErrorType - Error class to match
86
+ * @returns Whether the Error class appears in the chain
87
+ */
88
+ export declare function hasErrorType<T extends Error>(
89
+ error: unknown,
90
+ ErrorType: abstract new ( ...args: never[] ) => T
91
+ ): boolean;
@@ -48,3 +48,22 @@ export const executeInParallel = async ( { jobs, concurrency = Infinity, onJobCo
48
48
 
49
49
  return results.sort( ( a, b ) => a.index - b.index );
50
50
  };
51
+
52
+ /**
53
+ * Check if an error, or any parts of its cause chain are instances of a given Error class.
54
+ * Also check .name and .type
55
+ *
56
+ * @param {Error} error
57
+ * @param {Error} ErrorType
58
+ * @returns {boolean}
59
+ */
60
+ export const hasErrorType = ( error, ErrorType, depth = 0 ) => {
61
+ if ( depth > 20 || typeof ErrorType !== 'function' ) {
62
+ return false;
63
+ }
64
+ const expectedType = ErrorType.name;
65
+ if ( error instanceof ErrorType || error?.type === expectedType || error?.name === expectedType || error === expectedType ) {
66
+ return true;
67
+ }
68
+ return error?.cause ? hasErrorType( error.cause, ErrorType, depth + 1 ) : false;
69
+ };
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import { executeInParallel } from './workflow_utils.js';
2
+ import { hasErrorType, executeInParallel } from './workflow_utils.js';
3
3
 
4
4
  describe( 'executeInParallel', () => {
5
5
  it( 'returns empty array for empty jobs', async () => {
@@ -188,3 +188,84 @@ describe( 'executeInParallel', () => {
188
188
  ] );
189
189
  } );
190
190
  } );
191
+
192
+ describe( 'hasErrorType', () => {
193
+ class FooError extends Error {}
194
+
195
+ it( 'matches a direct error instance', () => {
196
+ expect( hasErrorType( new FooError( 'foo' ), FooError ) ).toBe( true );
197
+ } );
198
+
199
+ it( 'matches an error instance in the cause chain', () => {
200
+ const error = new Error( 'outer', {
201
+ cause: new Error( 'middle', {
202
+ cause: new FooError( 'foo' )
203
+ } )
204
+ } );
205
+
206
+ expect( hasErrorType( error, FooError ) ).toBe( true );
207
+ } );
208
+
209
+ it( 'matches Temporal ApplicationFailure type in the cause chain', () => {
210
+ const applicationFailure = Object.assign( new Error( 'foo' ), {
211
+ name: 'ApplicationFailure',
212
+ type: 'FooError'
213
+ } );
214
+ const activityFailure = Object.assign( new Error( 'Activity execution failed', {
215
+ cause: applicationFailure
216
+ } ), {
217
+ name: 'ActivityFailure'
218
+ } );
219
+
220
+ expect( hasErrorType( activityFailure, FooError ) ).toBe( true );
221
+ } );
222
+
223
+ it( 'matches a serialized error name', () => {
224
+ const error = Object.assign( new Error( 'foo' ), { name: 'FooError' } );
225
+
226
+ expect( hasErrorType( error, FooError ) ).toBe( true );
227
+ } );
228
+
229
+ it( 'matches an error serialized as its type name', () => {
230
+ expect( hasErrorType( 'FooError', FooError ) ).toBe( true );
231
+ } );
232
+
233
+ it( 'matches an error type in a plain object cause chain', () => {
234
+ const error = {
235
+ name: 'ActivityFailure',
236
+ cause: {
237
+ name: 'ApplicationFailure',
238
+ type: 'FooError'
239
+ }
240
+ };
241
+
242
+ expect( hasErrorType( error, FooError ) ).toBe( true );
243
+ } );
244
+
245
+ it.each( [ null, undefined, '', 42, false ] )( 'returns false for unsupported error value %j', error => {
246
+ expect( hasErrorType( error, FooError ) ).toBe( false );
247
+ } );
248
+
249
+ it( 'returns false when ErrorType is not a class', () => {
250
+ expect( hasErrorType( new FooError( 'foo' ), null ) ).toBe( false );
251
+ } );
252
+
253
+ it( 'returns false when the requested error type is absent', () => {
254
+ const error = new Error( 'outer', { cause: new TypeError( 'inner' ) } );
255
+
256
+ expect( hasErrorType( error, FooError ) ).toBe( false );
257
+ } );
258
+
259
+ it( 'keeps plain Error matching backwards compatible', () => {
260
+ expect( hasErrorType( new Error( 'foo' ), Error ) ).toBe( true );
261
+ } );
262
+
263
+ it( 'stops traversing cause chains after the depth limit', () => {
264
+ const createCauseChain = depth => depth === 20 ?
265
+ new FooError( 'foo' ) :
266
+ new Error( `depth-${depth}`, { cause: createCauseChain( depth + 1 ) } );
267
+ const root = new Error( 'root', { cause: createCauseChain( 0 ) } );
268
+
269
+ expect( hasErrorType( root, FooError ) ).toBe( false );
270
+ } );
271
+ } );
@@ -36,8 +36,6 @@ const envVarSchema = z.object( {
36
36
  OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 2 * 60 * 1000 ) ), // 2min
37
37
  // Whether to send activity heartbeats (enabled by default)
38
38
  OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: z.transform( v => v === undefined ? true : isStringboolTrue( v ) ),
39
- // Time to allow for hooks to flush before shutdown
40
- OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 3000 ) ),
41
39
  // Set temporal worker shutdown force time
42
40
  TEMPORAL_SHUTDOWN_FORCE_TIME: durationSchema,
43
41
  // Set temporal worker shutdown grace time
@@ -70,7 +68,6 @@ export const catalogId = envVars.OUTPUT_CATALOG_ID;
70
68
  export const workerTelemetryIntervalMs = envVars.OUTPUT_WORKER_TELEMETRY_INTERVAL_MS;
71
69
  export const activityHeartbeatIntervalMs = envVars.OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS;
72
70
  export const activityHeartbeatEnabled = envVars.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED;
73
- export const processFailureShutdownDelay = envVars.OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY;
74
71
  export const shutdownForceTime = envVars.TEMPORAL_SHUTDOWN_FORCE_TIME;
75
72
  export const shutdownGraceTime = envVars.TEMPORAL_SHUTDOWN_GRACE_TIME;
76
73
  export const grpcProxy = envVars.TEMPORAL_GRPC_PROXY;
@@ -12,8 +12,8 @@ import { initInterceptors } from './interceptors/index.js';
12
12
  import { createChildLogger } from '#logger';
13
13
  import { setupInterruptionHandler } from './interruption.js';
14
14
  import { CatalogJob } from './catalog_workflow/catalog_job.js';
15
- import { bootstrapFetchProxy } from './proxy.js';
16
15
  import { mainEventBus } from '#bus';
16
+ import { flushPendingHooks } from '#hooks/pending_hooks';
17
17
  import { BusEventType } from '#consts';
18
18
  import { setupTelemetry } from './telemetry.js';
19
19
  import { TemporalConnectionMonitor } from './connection_monitor.js';
@@ -61,7 +61,6 @@ const execute = async () => {
61
61
  const { activities } = await loadActivities( callerDir, workflows );
62
62
 
63
63
  mainEventBus.emit( BusEventType.WORKER_BEFORE_START );
64
- bootstrapFetchProxy();
65
64
 
66
65
  log.info( 'Initializing tracing...' );
67
66
  await initTracing();
@@ -214,13 +213,18 @@ execute()
214
213
  .catch( e => log.warn( 'Connection close error', { error: e.message } ) );
215
214
  }
216
215
  } )
217
- .then( () => log.info( 'Bye' ) )
218
- .catch( error => {
216
+ .then( async () => {
217
+ log.info( 'Flushing hook callbacks...' );
218
+ await flushPendingHooks();
219
+ log.info( 'Bye' );
220
+ } )
221
+ .catch( async error => {
219
222
  log.error( 'Fatal error', { error: error.message, stack: error.stack } );
220
223
 
221
224
  mainEventBus.emit( BusEventType.RUNTIME_ERROR, { error } );
222
225
 
223
- const timeToFlushEvent = configs.processFailureShutdownDelay;
224
- log.info( `Exiting in ${timeToFlushEvent}ms` );
225
- setTimeout( () => process.exit( 1 ), timeToFlushEvent );
226
+ log.info( 'Flushing hook callbacks...' );
227
+ await flushPendingHooks();
228
+ log.info( 'Exiting...' );
229
+ setTimeout( () => process.exit( 1 ) );
226
230
  } );
@@ -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,
@@ -46,7 +47,6 @@ const {
46
47
  maxConcurrentActivityTaskPolls: 5,
47
48
  maxConcurrentWorkflowTaskPolls: 5,
48
49
  workerTuner: undefined,
49
- processFailureShutdownDelay: 0,
50
50
  shutdownForceTime: undefined,
51
51
  shutdownGraceTime: undefined
52
52
  };
@@ -105,6 +105,7 @@ const {
105
105
  connectionMonitorInstance,
106
106
  createCatalogMock: vi.fn().mockReturnValue( { workflows: [], activities: {} } ),
107
107
  createWorkflowsEntryPointMock: vi.fn().mockReturnValue( '/fake/workflows/path.js' ),
108
+ flushPendingHooksMock: vi.fn().mockResolvedValue( undefined ),
108
109
  hashSourceCodeMock: vi.fn().mockResolvedValue( 'catalog-hash' ),
109
110
  initInterceptorsMock: vi.fn().mockReturnValue( [] ),
110
111
  loadActivitiesMock: vi.fn().mockResolvedValue( {} ),
@@ -129,6 +130,7 @@ vi.mock( '#consts', async importOriginal => {
129
130
  const initTracing = vi.fn().mockResolvedValue( undefined );
130
131
  vi.mock( '#tracing', () => ( { init: initTracing } ) );
131
132
  vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
133
+ vi.mock( '#hooks/pending_hooks', () => ( { flushPendingHooks: flushPendingHooksMock } ) );
132
134
 
133
135
  const loadWorkflowsMock = vi.fn().mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/path.js' } );
134
136
  const loadActivitiesMock = vi.fn().mockResolvedValue( { activities: {} } );
@@ -145,7 +147,6 @@ vi.mock( './sinks.js', () => ( { sinks: {} } ) );
145
147
  vi.mock( './catalog_workflow/index.js', () => ( { createCatalog: createCatalogMock } ) );
146
148
  vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
147
149
  vi.mock( './interceptors/index.js', () => ( { initInterceptors: initInterceptorsMock } ) );
148
- vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: vi.fn() } ) );
149
150
  vi.mock( './telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
150
151
  vi.mock( './interruption.js', () => ( { setupInterruptionHandler: setupInterruptionHandlerMock } ) );
151
152
  vi.mock( './global_functions.js', () => ( { bindGlobalFunctions: bindGlobalFunctionsMock } ) );
@@ -262,6 +263,7 @@ describe( 'worker/index', () => {
262
263
  expect( catalogJobInstance.run ).toHaveBeenCalled();
263
264
 
264
265
  await settleWorker();
266
+ await vi.waitFor( () => expect( flushPendingHooksMock ).toHaveBeenCalledOnce() );
265
267
  expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
266
268
  } );
267
269
 
@@ -359,6 +361,9 @@ describe( 'worker/index', () => {
359
361
  } );
360
362
  expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
361
363
  await vi.waitFor( () => expect( exitMock ).toHaveBeenCalledWith( 1 ) );
364
+ expect( flushPendingHooksMock ).toHaveBeenCalledOnce();
365
+ expect( mainEventBusMock.emit.mock.invocationCallOrder[0] ).toBeLessThan( flushPendingHooksMock.mock.invocationCallOrder[0] );
366
+ expect( flushPendingHooksMock.mock.invocationCallOrder[0] ).toBeLessThan( exitMock.mock.invocationCallOrder[0] );
362
367
  } );
363
368
 
364
369
  it( 'throws connection monitor errors after graceful shutdown', async () => {
@@ -1,20 +0,0 @@
1
- import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
2
- import { createChildLogger } from '#logger';
3
-
4
- const log = createChildLogger( 'Proxy' );
5
-
6
- /**
7
- * Routes all `fetch()` calls (including those inside Temporal activities)
8
- * through an HTTP/HTTPS proxy when standard proxy env vars are set.
9
- * No-op when none are set.
10
- */
11
- export const bootstrapFetchProxy = () => {
12
- const httpProxyUrl = process.env.http_proxy ?? process.env.HTTP_PROXY;
13
- const httpsProxyUrl = process.env.https_proxy ?? process.env.HTTPS_PROXY;
14
-
15
- if ( httpProxyUrl?.length > 0 || httpsProxyUrl?.length > 0 ) {
16
- log.info( 'Proxy env vars detected, setting up global fetch dispatcher EnvHttpProxyAgent', { httpProxyUrl, httpsProxyUrl } );
17
- /* Ignore HTTP/2. Check: https://github.com/growthxai/output/issues/299 */
18
- setGlobalDispatcher( new EnvHttpProxyAgent( { allowH2: false } ) );
19
- }
20
- };
@@ -1,51 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- const mockSetGlobalDispatcher = vi.fn();
4
- const MockEnvHttpProxyAgent = vi.fn();
5
- const mockLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
6
-
7
- vi.mock( 'undici', () => ( {
8
- EnvHttpProxyAgent: MockEnvHttpProxyAgent,
9
- setGlobalDispatcher: mockSetGlobalDispatcher
10
- } ) );
11
-
12
- vi.mock( '#logger', () => ( {
13
- createChildLogger: () => mockLogger
14
- } ) );
15
-
16
- describe( 'worker/proxy', () => {
17
- const proxyEnv = [ 'http_proxy', 'HTTP_PROXY', 'https_proxy', 'HTTPS_PROXY' ];
18
-
19
- beforeEach( () => {
20
- vi.clearAllMocks();
21
- for ( const key of proxyEnv ) {
22
- delete process.env[key];
23
- }
24
- } );
25
-
26
- afterEach( () => {
27
- vi.resetModules();
28
- } );
29
-
30
- it( 'does nothing when no proxy env vars are set', async () => {
31
- const { bootstrapFetchProxy } = await import( './proxy.js' );
32
- bootstrapFetchProxy();
33
-
34
- expect( MockEnvHttpProxyAgent ).not.toHaveBeenCalled();
35
- expect( mockSetGlobalDispatcher ).not.toHaveBeenCalled();
36
- expect( mockLogger.info ).not.toHaveBeenCalled();
37
- } );
38
-
39
- it( 'sets global dispatcher when proxy URL is available', async () => {
40
- process.env.HTTP_PROXY = 'http://proxy:8080/';
41
- const { bootstrapFetchProxy } = await import( './proxy.js' );
42
- bootstrapFetchProxy();
43
-
44
- expect( MockEnvHttpProxyAgent ).toHaveBeenCalledWith( { allowH2: false } );
45
- expect( mockSetGlobalDispatcher ).toHaveBeenCalledTimes( 1 );
46
- expect( mockLogger.info ).toHaveBeenCalledWith(
47
- 'Proxy env vars detected, setting up global fetch dispatcher EnvHttpProxyAgent',
48
- { httpProxyUrl: 'http://proxy:8080/', httpsProxyUrl: undefined }
49
- );
50
- } );
51
- } );