@outputai/core 0.14.0 → 0.14.1-next.1ec4416.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,213 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- const CONFIG_KEYS = [
4
- 'OUTPUT_CATALOG_ID',
5
- 'TEMPORAL_ADDRESS',
6
- 'TEMPORAL_API_KEY',
7
- 'TEMPORAL_NAMESPACE',
8
- 'TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS',
9
- 'TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_EXECUTIONS',
10
- 'TEMPORAL_MAX_CACHED_WORKFLOWS',
11
- 'TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_POLLS',
12
- 'TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_POLLS',
13
- 'TEMPORAL_WORKER_TUNER',
14
- 'OUTPUT_WORKER_TELEMETRY_INTERVAL_MS',
15
- 'OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS',
16
- 'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED',
17
- 'TEMPORAL_SHUTDOWN_FORCE_TIME',
18
- 'TEMPORAL_SHUTDOWN_GRACE_TIME',
19
- 'OUTPUT_HOOK_FLUSH_TIMEOUT_MS'
20
- ];
21
-
22
- const setEnv = ( overrides = {} ) => {
23
- process.env.OUTPUT_CATALOG_ID = overrides.OUTPUT_CATALOG_ID ?? 'test-catalog';
24
- CONFIG_KEYS.forEach( key => {
25
- if ( overrides[key] !== undefined ) {
26
- process.env[key] = String( overrides[key] );
27
- }
28
- } );
29
- };
30
-
31
- const clearEnv = () => {
32
- CONFIG_KEYS.forEach( key => delete process.env[key] );
33
- };
34
-
35
- async function loadConfigs() {
36
- vi.resetModules();
37
- return import( './configs.js' );
38
- }
39
-
40
- describe( 'worker/configs', () => {
41
- beforeEach( () => clearEnv() );
42
- afterEach( () => clearEnv() );
43
-
44
- it( 'throws when OUTPUT_CATALOG_ID is missing', async () => {
45
- clearEnv();
46
- vi.resetModules();
47
-
48
- await expect( import( './configs.js' ) ).rejects.toThrow();
49
- } );
50
-
51
- it( 'throws when OUTPUT_CATALOG_ID does not match regex', async () => {
52
- setEnv( { OUTPUT_CATALOG_ID: 'invalid space' } );
53
- vi.resetModules();
54
-
55
- await expect( import( './configs.js' ) ).rejects.toThrow();
56
- } );
57
-
58
- it( 'uses defaults when only OUTPUT_CATALOG_ID is set', async () => {
59
- setEnv();
60
- const configs = await loadConfigs();
61
-
62
- expect( configs.address ).toBe( 'localhost:7233' );
63
- expect( configs.namespace ).toBe( 'default' );
64
- expect( configs.maxConcurrentActivityTaskExecutions ).toBe( 40 );
65
- expect( configs.maxConcurrentWorkflowTaskExecutions ).toBe( 200 );
66
- expect( configs.maxCachedWorkflows ).toBe( 1000 );
67
- expect( configs.maxConcurrentActivityTaskPolls ).toBe( 5 );
68
- expect( configs.maxConcurrentWorkflowTaskPolls ).toBe( 5 );
69
- expect( configs.workerTuner ).toBeUndefined();
70
- expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
71
- expect( configs.activityHeartbeatIntervalMs ).toBe( 2 * 60 * 1000 );
72
- expect( configs.activityHeartbeatEnabled ).toBe( true );
73
- expect( configs.shutdownForceTime ).toBe( '20s' );
74
- expect( configs.shutdownGraceTime ).toBe( '15s' );
75
- expect( configs.hookFlushTimeoutMs ).toBe( 5000 );
76
- expect( configs.taskQueue ).toBe( 'test-catalog' );
77
- expect( configs.catalogId ).toBe( 'test-catalog' );
78
- } );
79
-
80
- it( 'treats empty string for optional number as default (preprocess)', async () => {
81
- setEnv( { TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS: '' } );
82
- const configs = await loadConfigs();
83
-
84
- expect( configs.maxConcurrentActivityTaskExecutions ).toBe( 40 );
85
- } );
86
-
87
- it( 'treats empty string for worker telemetry interval as default', async () => {
88
- setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '' } );
89
- const configs = await loadConfigs();
90
-
91
- expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
92
- } );
93
-
94
- it( 'parses Temporal worker tuner JSON', async () => {
95
- const workerTuner = {
96
- tunerOptions: {
97
- targetMemoryUsage: 0.8,
98
- targetCpuUsage: 0.9
99
- },
100
- activityTaskSlotOptions: {
101
- minimumSlots: 1,
102
- maximumSlots: 100,
103
- rampThrottle: '50ms'
104
- }
105
- };
106
-
107
- setEnv( { TEMPORAL_WORKER_TUNER: JSON.stringify( workerTuner ) } );
108
- const configs = await loadConfigs();
109
-
110
- expect( configs.workerTuner ).toEqual( workerTuner );
111
- } );
112
-
113
- it( 'parses custom numeric env vars', async () => {
114
- setEnv( {
115
- TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS: '10',
116
- TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASK_EXECUTIONS: '50',
117
- TEMPORAL_MAX_CACHED_WORKFLOWS: '500',
118
- OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '30000',
119
- OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS: '60000'
120
- } );
121
- const configs = await loadConfigs();
122
-
123
- expect( configs.maxConcurrentActivityTaskExecutions ).toBe( 10 );
124
- expect( configs.maxConcurrentWorkflowTaskExecutions ).toBe( 50 );
125
- expect( configs.maxCachedWorkflows ).toBe( 500 );
126
- expect( configs.workerTelemetryIntervalMs ).toBe( 30000 );
127
- expect( configs.activityHeartbeatIntervalMs ).toBe( 60000 );
128
- } );
129
-
130
- it( 'parses Temporal shutdown durations', async () => {
131
- setEnv( {
132
- TEMPORAL_SHUTDOWN_FORCE_TIME: '15000',
133
- TEMPORAL_SHUTDOWN_GRACE_TIME: '15s'
134
- } );
135
- const configs = await loadConfigs();
136
-
137
- expect( configs.shutdownForceTime ).toBe( '15000' );
138
- expect( configs.shutdownGraceTime ).toBe( '15s' );
139
- } );
140
-
141
- it( 'falls back to the defaults when Temporal shutdown durations are empty', async () => {
142
- setEnv( {
143
- TEMPORAL_SHUTDOWN_FORCE_TIME: '',
144
- TEMPORAL_SHUTDOWN_GRACE_TIME: '',
145
- OUTPUT_HOOK_FLUSH_TIMEOUT_MS: ''
146
- } );
147
- const configs = await loadConfigs();
148
-
149
- expect( configs.shutdownForceTime ).toBe( '20s' );
150
- expect( configs.shutdownGraceTime ).toBe( '15s' );
151
- expect( configs.hookFlushTimeoutMs ).toBe( 5000 );
152
- } );
153
-
154
- it( 'throws when Temporal shutdown durations are invalid', async () => {
155
- setEnv( { TEMPORAL_SHUTDOWN_FORCE_TIME: 'soon' } );
156
- vi.resetModules();
157
-
158
- await expect( import( './configs.js' ) ).rejects.toThrow();
159
- } );
160
-
161
- it( 'allows zero for worker telemetry interval', async () => {
162
- setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '0' } );
163
- const configs = await loadConfigs();
164
-
165
- expect( configs.workerTelemetryIntervalMs ).toBe( 0 );
166
- } );
167
-
168
- it( 'throws when worker telemetry interval is negative', async () => {
169
- setEnv( { OUTPUT_WORKER_TELEMETRY_INTERVAL_MS: '-1' } );
170
- vi.resetModules();
171
-
172
- await expect( import( './configs.js' ) ).rejects.toThrow();
173
- } );
174
-
175
- it( 'throws when optional number is zero or negative', async () => {
176
- setEnv( { TEMPORAL_MAX_CONCURRENT_ACTIVITY_TASK_EXECUTIONS: '0' } );
177
- vi.resetModules();
178
-
179
- await expect( import( './configs.js' ) ).rejects.toThrow();
180
- } );
181
-
182
- it( 'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: "true"|"1"|"on" → true', async () => {
183
- for ( const val of [ 'true', '1', 'on' ] ) {
184
- setEnv( { OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: val } );
185
- const configs = await loadConfigs();
186
- expect( configs.activityHeartbeatEnabled ).toBe( true );
187
- clearEnv();
188
- }
189
- } );
190
-
191
- it( 'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: "false"|other → false, undefined → true', async () => {
192
- setEnv( { OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: 'false' } );
193
- const configsFalse = await loadConfigs();
194
- expect( configsFalse.activityHeartbeatEnabled ).toBe( false );
195
-
196
- setEnv( { OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: '0' } );
197
- const configsZero = await loadConfigs();
198
- expect( configsZero.activityHeartbeatEnabled ).toBe( false );
199
-
200
- clearEnv();
201
- setEnv(); // only OUTPUT_CATALOG_ID; OUTPUT_ACTIVITY_HEARTBEAT_ENABLED absent → default true
202
- const configsDefault = await loadConfigs();
203
- expect( configsDefault.activityHeartbeatEnabled ).toBe( true );
204
- } );
205
-
206
- it( 'parses TEMPORAL_ADDRESS and TEMPORAL_NAMESPACE', async () => {
207
- setEnv( { TEMPORAL_ADDRESS: 'temporal:7233', TEMPORAL_NAMESPACE: 'my-ns' } );
208
- const configs = await loadConfigs();
209
-
210
- expect( configs.address ).toBe( 'temporal:7233' );
211
- expect( configs.namespace ).toBe( 'my-ns' );
212
- } );
213
- } );
@@ -1,127 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { workerTunerEnvSchema } from './configs_tuner_schema.js';
3
-
4
- const parseWorkerTuner = value => workerTunerEnvSchema.parse( value );
5
-
6
- describe( 'worker/configs_tuner_schema', () => {
7
- it( 'treats empty string as unset', () => {
8
- expect( parseWorkerTuner( '' ) ).toBeUndefined();
9
- } );
10
-
11
- it( 'parses resource-based Temporal worker tuner JSON', () => {
12
- const workerTuner = {
13
- tunerOptions: {
14
- targetMemoryUsage: 0.8,
15
- targetCpuUsage: 0.9
16
- },
17
- activityTaskSlotOptions: {
18
- minimumSlots: 1,
19
- maximumSlots: 100,
20
- rampThrottle: '50ms'
21
- }
22
- };
23
-
24
- expect( parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toEqual( workerTuner );
25
- } );
26
-
27
- it( 'parses per-task Temporal worker tuner JSON', () => {
28
- const resourceBasedSupplier = {
29
- type: 'resource-based',
30
- tunerOptions: {
31
- targetMemoryUsage: 0.8,
32
- targetCpuUsage: 0.9
33
- },
34
- minimumSlots: 1,
35
- maximumSlots: 100,
36
- rampThrottle: '50ms'
37
- };
38
- const workerTuner = {
39
- workflowTaskSlotSupplier: {
40
- type: 'fixed-size',
41
- numSlots: 10
42
- },
43
- activityTaskSlotSupplier: resourceBasedSupplier,
44
- localActivityTaskSlotSupplier: {
45
- type: 'fixed-size',
46
- numSlots: 10
47
- },
48
- nexusTaskSlotSupplier: {
49
- type: 'fixed-size',
50
- numSlots: 10
51
- }
52
- };
53
-
54
- expect( parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toEqual( workerTuner );
55
- } );
56
-
57
- it( 'throws when tuner is not valid JSON', () => {
58
- expect( () => parseWorkerTuner( '{invalid' ) ).toThrow();
59
- } );
60
-
61
- it( 'throws when tuner is not a JSON object', () => {
62
- expect( () => parseWorkerTuner( '[]' ) ).toThrow();
63
- } );
64
-
65
- it( 'throws when tuner is missing required per-task suppliers', () => {
66
- const workerTuner = {
67
- workflowTaskSlotSupplier: {
68
- type: 'fixed-size',
69
- numSlots: 10
70
- },
71
- activityTaskSlotSupplier: {
72
- type: 'fixed-size',
73
- numSlots: 10
74
- }
75
- };
76
-
77
- expect( () => parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toThrow();
78
- } );
79
-
80
- it( 'throws when tuner uses custom suppliers', () => {
81
- const workerTuner = {
82
- workflowTaskSlotSupplier: {
83
- type: 'custom'
84
- },
85
- activityTaskSlotSupplier: {
86
- type: 'fixed-size',
87
- numSlots: 10
88
- },
89
- localActivityTaskSlotSupplier: {
90
- type: 'fixed-size',
91
- numSlots: 10
92
- },
93
- nexusTaskSlotSupplier: {
94
- type: 'fixed-size',
95
- numSlots: 10
96
- }
97
- };
98
-
99
- expect( () => parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toThrow();
100
- } );
101
-
102
- it( 'throws when target usage is outside range', () => {
103
- const workerTuner = {
104
- tunerOptions: {
105
- targetMemoryUsage: 1.5,
106
- targetCpuUsage: 0.9
107
- }
108
- };
109
-
110
- expect( () => parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toThrow();
111
- } );
112
-
113
- it( 'throws when minimum slots exceeds maximum slots', () => {
114
- const workerTuner = {
115
- tunerOptions: {
116
- targetMemoryUsage: 0.8,
117
- targetCpuUsage: 0.9
118
- },
119
- activityTaskSlotOptions: {
120
- minimumSlots: 10,
121
- maximumSlots: 5
122
- }
123
- };
124
-
125
- expect( () => parseWorkerTuner( JSON.stringify( workerTuner ) ) ).toThrow();
126
- } );
127
- } );
@@ -1,328 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { ConnectionLostError, TemporalConnectionMonitor } from './connection_monitor.js';
3
-
4
- const CHECK_TIMEOUT_MS = 50;
5
- const CHECK_INTERVAL_MS = 100;
6
-
7
- const { scheduledDelays, delayMock, mockLog } = vi.hoisted( () => {
8
- const scheduledDelays = [];
9
- const abortError = () => Object.assign( new Error( 'The operation was aborted' ), { name: 'AbortError' } );
10
-
11
- // Mirrors node:timers/promises: rejects with an AbortError when the signal aborts, or if already aborted.
12
- const delayMock = vi.fn( ( ms, value, options ) => new Promise( ( resolve, reject ) => {
13
- const signal = options?.signal;
14
-
15
- if ( signal?.aborted ) {
16
- reject( abortError() );
17
- return;
18
- }
19
-
20
- scheduledDelays.push( { ms, value, options, resolve } );
21
- signal?.addEventListener( 'abort', () => reject( abortError() ), { once: true } );
22
- } ) );
23
-
24
- return {
25
- scheduledDelays,
26
- delayMock,
27
- mockLog: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }
28
- };
29
- } );
30
-
31
- vi.mock( 'node:timers/promises', () => ( { setTimeout: delayMock } ) );
32
- vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
33
-
34
- const delayOptions = { ref: false, signal: expect.any( AbortSignal ) };
35
-
36
- const createMonitor = ( check, overrides = {}, signal = new AbortController().signal ) =>
37
- new TemporalConnectionMonitor( {
38
- connection: { workflowService: { getSystemInfo: check } },
39
- signal,
40
- overrides: { checkIntervalMs: CHECK_INTERVAL_MS, checkTimeoutMs: CHECK_TIMEOUT_MS, ...overrides }
41
- } );
42
-
43
- const flushPromises = async () => Array
44
- .from( { length: 10 } )
45
- .reduce( promise => promise.then( () => Promise.resolve() ), Promise.resolve() );
46
-
47
- const resolveNextDelay = ms => {
48
- const index = scheduledDelays.findIndex( scheduled => scheduled.ms === ms );
49
- expect( index ).not.toBe( -1 );
50
- const [ scheduled ] = scheduledDelays.splice( index, 1 );
51
- scheduled.resolve( scheduled.value );
52
- };
53
-
54
- describe( 'TemporalConnectionMonitor', () => {
55
- beforeEach( () => {
56
- vi.clearAllMocks();
57
- scheduledDelays.length = 0;
58
- } );
59
-
60
- describe( 'health checks', () => {
61
- it( 'logs healthy and schedules the next check when the workflow service is reachable', async () => {
62
- const check = vi.fn().mockResolvedValue( {} );
63
- const monitor = createMonitor( check );
64
-
65
- const run = monitor.start();
66
- await flushPromises();
67
-
68
- expect( check ).toHaveBeenCalledWith( {} );
69
- expect( mockLog.info ).toHaveBeenCalledWith( 'Healthy' );
70
- expect( delayMock ).toHaveBeenCalledWith( CHECK_TIMEOUT_MS, 0, delayOptions );
71
- expect( delayMock ).toHaveBeenCalledWith( CHECK_INTERVAL_MS, true, delayOptions );
72
- expect( monitor.running ).toBe( true );
73
-
74
- await monitor.stop();
75
- await run;
76
-
77
- expect( monitor.running ).toBe( false );
78
- } );
79
-
80
- it( 'counts a timed out check as a transient failure', async () => {
81
- const check = vi.fn().mockReturnValue( new Promise( () => {} ) );
82
- const monitor = createMonitor( check );
83
-
84
- const run = monitor.start();
85
- resolveNextDelay( CHECK_TIMEOUT_MS );
86
- await flushPromises();
87
-
88
- expect( mockLog.warn ).toHaveBeenCalledWith( 'Connection unhealthy', {
89
- error: 'Connection health check timed out',
90
- failures: 1
91
- } );
92
-
93
- await monitor.stop();
94
- await run;
95
- } );
96
-
97
- it( 'logs recovered without a healthy line when the first check fails', async () => {
98
- const check = vi.fn()
99
- .mockRejectedValueOnce( new Error( 'temporary outage' ) )
100
- .mockResolvedValue( {} );
101
- const monitor = createMonitor( check );
102
-
103
- const run = monitor.start();
104
- await flushPromises();
105
-
106
- expect( mockLog.warn ).toHaveBeenCalledWith( 'Connection unhealthy', { error: 'temporary outage', failures: 1 } );
107
-
108
- resolveNextDelay( CHECK_INTERVAL_MS );
109
- await flushPromises();
110
-
111
- expect( mockLog.info ).toHaveBeenCalledOnce();
112
- expect( mockLog.info ).toHaveBeenCalledWith( 'Recovered' );
113
-
114
- await monitor.stop();
115
- await run;
116
- } );
117
-
118
- it( 'logs healthy once while the connection stays reachable', async () => {
119
- const check = vi.fn().mockResolvedValue( {} );
120
- const monitor = createMonitor( check );
121
-
122
- const run = monitor.start();
123
- await flushPromises();
124
- resolveNextDelay( CHECK_INTERVAL_MS );
125
- await flushPromises();
126
- resolveNextDelay( CHECK_INTERVAL_MS );
127
- await flushPromises();
128
-
129
- expect( check ).toHaveBeenCalledTimes( 3 );
130
- expect( mockLog.info ).toHaveBeenCalledOnce();
131
- expect( mockLog.info ).toHaveBeenCalledWith( 'Healthy' );
132
-
133
- await monitor.stop();
134
- await run;
135
- } );
136
-
137
- it( 'logs recovered on a later failure without repeating healthy', async () => {
138
- const check = vi.fn()
139
- .mockResolvedValueOnce( {} )
140
- .mockRejectedValueOnce( new Error( 'temporary outage' ) )
141
- .mockResolvedValue( {} );
142
- const monitor = createMonitor( check );
143
-
144
- const run = monitor.start();
145
- await flushPromises();
146
- resolveNextDelay( CHECK_INTERVAL_MS );
147
- await flushPromises();
148
- resolveNextDelay( CHECK_INTERVAL_MS );
149
- await flushPromises();
150
-
151
- expect( mockLog.info ).toHaveBeenCalledTimes( 2 );
152
- expect( mockLog.info ).toHaveBeenNthCalledWith( 1, 'Healthy' );
153
- expect( mockLog.info ).toHaveBeenNthCalledWith( 2, 'Recovered' );
154
-
155
- await monitor.stop();
156
- await run;
157
- } );
158
-
159
- it( 'describes non error rejections with their string value', async () => {
160
- const check = vi.fn().mockRejectedValue( 'socket hang up' );
161
- const monitor = createMonitor( check, { maxFailures: 2 } );
162
-
163
- const run = monitor.start();
164
- const rejection = expect( run ).rejects.toThrow( ConnectionLostError );
165
-
166
- await flushPromises();
167
- resolveNextDelay( CHECK_INTERVAL_MS );
168
- await flushPromises();
169
- await rejection;
170
-
171
- expect( mockLog.warn ).toHaveBeenCalledWith( 'Connection unhealthy', { error: 'socket hang up', failures: 1 } );
172
- expect( mockLog.warn ).toHaveBeenCalledWith( 'Connection lost', { error: 'socket hang up', failures: 2 } );
173
- } );
174
- } );
175
-
176
- describe( 'connection loss', () => {
177
- it( 'rejects after the maximum consecutive failures', async () => {
178
- const check = vi.fn().mockRejectedValue( new Error( 'connection refused' ) );
179
- const monitor = createMonitor( check, { maxFailures: 3 } );
180
-
181
- const run = monitor.start();
182
- const rejection = expect( run ).rejects.toThrow( ConnectionLostError );
183
-
184
- await flushPromises();
185
- resolveNextDelay( CHECK_INTERVAL_MS );
186
- await flushPromises();
187
- resolveNextDelay( CHECK_INTERVAL_MS );
188
- await flushPromises();
189
- await rejection;
190
-
191
- expect( check ).toHaveBeenCalledTimes( 3 );
192
- expect( mockLog.warn ).toHaveBeenCalledTimes( 3 );
193
- expect( mockLog.warn ).toHaveBeenCalledWith( 'Connection lost', { error: 'connection refused', failures: 3 } );
194
- expect( monitor.running ).toBe( false );
195
- } );
196
-
197
- it( 'preserves the original failure as the error cause', async () => {
198
- const error = new Error( 'connection refused' );
199
- const check = vi.fn().mockRejectedValue( error );
200
- const monitor = createMonitor( check, { maxFailures: 1 } );
201
-
202
- await expect( monitor.start() ).rejects.toMatchObject( {
203
- name: 'ConnectionLostError',
204
- message: 'Connection lost',
205
- cause: error
206
- } );
207
- } );
208
-
209
- it( 'stops watching once the connection is lost', async () => {
210
- const check = vi.fn().mockRejectedValue( new Error( 'connection refused' ) );
211
- const monitor = createMonitor( check, { maxFailures: 1 } );
212
-
213
- await expect( monitor.start() ).rejects.toThrow( ConnectionLostError );
214
- await flushPromises();
215
-
216
- expect( check ).toHaveBeenCalledOnce();
217
- expect( monitor.running ).toBe( false );
218
- } );
219
-
220
- it( 'reports the loss through stop so it is never silent', async () => {
221
- const check = vi.fn().mockRejectedValue( new Error( 'connection refused' ) );
222
- const monitor = createMonitor( check, { maxFailures: 1 } );
223
-
224
- await expect( monitor.start() ).rejects.toThrow( ConnectionLostError );
225
- await expect( monitor.stop() ).rejects.toThrow( ConnectionLostError );
226
- } );
227
- } );
228
-
229
- describe( 'abort', () => {
230
- it( 'never checks when the signal is already aborted', async () => {
231
- const controller = new AbortController();
232
- controller.abort();
233
- const check = vi.fn();
234
- const monitor = createMonitor( check, {}, controller.signal );
235
-
236
- await expect( monitor.start() ).resolves.toBeUndefined();
237
-
238
- expect( check ).not.toHaveBeenCalled();
239
- expect( monitor.running ).toBe( false );
240
- } );
241
-
242
- it( 'stops without reporting when aborted during a check', async () => {
243
- const controller = new AbortController();
244
- const check = vi.fn().mockReturnValue( new Promise( () => {} ) );
245
- const monitor = createMonitor( check, {}, controller.signal );
246
-
247
- const run = monitor.start();
248
- await flushPromises();
249
-
250
- expect( monitor.running ).toBe( true );
251
-
252
- controller.abort();
253
-
254
- await expect( run ).resolves.toBeUndefined();
255
- expect( mockLog.warn ).not.toHaveBeenCalled();
256
- expect( monitor.running ).toBe( false );
257
- } );
258
-
259
- it( 'stops without reporting when aborted between checks', async () => {
260
- const controller = new AbortController();
261
- const check = vi.fn().mockResolvedValue( {} );
262
- const monitor = createMonitor( check, {}, controller.signal );
263
-
264
- const run = monitor.start();
265
- await flushPromises();
266
-
267
- expect( mockLog.info ).toHaveBeenCalledWith( 'Healthy' );
268
-
269
- controller.abort();
270
-
271
- await expect( run ).resolves.toBeUndefined();
272
- expect( check ).toHaveBeenCalledOnce();
273
- expect( mockLog.warn ).not.toHaveBeenCalled();
274
- } );
275
- } );
276
-
277
- describe( 'lifecycle', () => {
278
- it( 'returns the same watch when started more than once', async () => {
279
- const check = vi.fn().mockReturnValue( new Promise( () => {} ) );
280
- const monitor = createMonitor( check );
281
-
282
- const first = monitor.start();
283
- const second = monitor.start();
284
-
285
- expect( second ).toBe( first );
286
- expect( check ).toHaveBeenCalledOnce();
287
-
288
- await monitor.stop();
289
- await first;
290
- } );
291
-
292
- it( 'stops an in flight check without reporting a failure', async () => {
293
- const check = vi.fn().mockReturnValue( new Promise( () => {} ) );
294
- const monitor = createMonitor( check, { maxFailures: 1 } );
295
-
296
- const run = monitor.start();
297
-
298
- expect( monitor.running ).toBe( true );
299
-
300
- await monitor.stop();
301
- await run;
302
-
303
- expect( mockLog.warn ).not.toHaveBeenCalled();
304
- expect( monitor.running ).toBe( false );
305
- } );
306
-
307
- it( 'never watches when stopped before starting', async () => {
308
- const check = vi.fn();
309
- const monitor = createMonitor( check );
310
-
311
- await expect( monitor.stop() ).resolves.toBeUndefined();
312
- await expect( monitor.start() ).resolves.toBeUndefined();
313
-
314
- expect( check ).not.toHaveBeenCalled();
315
- expect( monitor.running ).toBe( false );
316
- } );
317
-
318
- it( 'applies the timing and failure threshold overrides', async () => {
319
- const check = vi.fn().mockRejectedValue( new Error( 'fast failure' ) );
320
- const monitor = createMonitor( check, { maxFailures: 1, checkIntervalMs: 7, checkTimeoutMs: 3 } );
321
-
322
- await expect( monitor.start() ).rejects.toThrow( ConnectionLostError );
323
-
324
- expect( delayMock ).toHaveBeenCalledWith( 3, 0, delayOptions );
325
- expect( delayMock ).not.toHaveBeenCalledWith( 7, true, delayOptions );
326
- } );
327
- } );
328
- } );