adaptive-memory-multi-model-router 2.6.0 → 2.7.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/README.md +149 -22
- package/dist/cache/semanticCache.d.ts +54 -22
- package/dist/cache/semanticCache.js +230 -86
- package/dist/cache/semanticCache.js.map +1 -1
- package/dist/cost/budgetEnforcer.d.ts +108 -0
- package/dist/cost/budgetEnforcer.js +295 -0
- package/dist/cost/budgetEnforcer.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/routing/providerHealth.d.ts +154 -0
- package/dist/routing/providerHealth.js +371 -0
- package/dist/routing/providerHealth.js.map +1 -0
- package/dist/sdk.d.ts +124 -0
- package/dist/sdk.js +109 -100
- package/package.json +3 -2
- package/src/cache/semanticCache.ts +293 -103
- package/src/cost/budgetEnforcer.ts +358 -0
- package/src/index.ts +2 -0
- package/src/routing/providerHealth.ts +483 -0
- package/test/test_budgetEnforcer.ts +310 -0
- package/test/test_providerHealth.ts +523 -0
- package/test/test_semanticCache.ts +507 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Health Manager Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for:
|
|
5
|
+
* - Health recording (success/failure)
|
|
6
|
+
* - Error tracking and consecutive errors
|
|
7
|
+
* - Circuit breaker trigger (3 consecutive errors → 60s cooldown)
|
|
8
|
+
* - Cooldown behavior
|
|
9
|
+
* - Fallback chain sorting by health score
|
|
10
|
+
* - Probe after cooldown
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
ProviderHealthManager,
|
|
15
|
+
ProviderHealth,
|
|
16
|
+
ProviderMetrics,
|
|
17
|
+
HealthEvent,
|
|
18
|
+
} from '../src/routing/providerHealth';
|
|
19
|
+
|
|
20
|
+
// Test configuration
|
|
21
|
+
const TEST_CONFIG = {
|
|
22
|
+
verbose: process.argv.includes('--verbose') || process.argv.includes('-v'),
|
|
23
|
+
timeout: 10000,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Test utilities
|
|
27
|
+
function log(message: string, level: 'info' | 'error' | 'debug' = 'info') {
|
|
28
|
+
if (level === 'error') console.error(message);
|
|
29
|
+
else if (TEST_CONFIG.verbose || level !== 'debug') console.log(message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let passed = 0;
|
|
33
|
+
let failed = 0;
|
|
34
|
+
let skipped = 0;
|
|
35
|
+
|
|
36
|
+
function test(name: string, fn: () => void | Promise<void>) {
|
|
37
|
+
try {
|
|
38
|
+
const result = fn();
|
|
39
|
+
if (result instanceof Promise) {
|
|
40
|
+
result.then(() => {
|
|
41
|
+
log(` ✅ ${name}`, 'success');
|
|
42
|
+
passed++;
|
|
43
|
+
}).catch((e) => {
|
|
44
|
+
log(` ❌ ${name}: ${e.message}`, 'error');
|
|
45
|
+
failed++;
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
log(` ✅ ${name}`, 'success');
|
|
49
|
+
passed++;
|
|
50
|
+
}
|
|
51
|
+
} catch (e: any) {
|
|
52
|
+
log(` ❌ ${name}: ${e.message}`, 'error');
|
|
53
|
+
failed++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assert(condition: boolean, message: string) {
|
|
58
|
+
if (!condition) throw new Error(message);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertEqual(actual: any, expected: any, message: string) {
|
|
62
|
+
if (actual !== expected) {
|
|
63
|
+
throw new Error(`${message}: expected ${expected}, got ${actual}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertApprox(actual: number, expected: number, tolerance: number, message: string) {
|
|
68
|
+
if (Math.abs(actual - expected) > tolerance) {
|
|
69
|
+
throw new Error(`${message}: expected ~${expected}, got ${actual}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================
|
|
74
|
+
// TEST SUITE
|
|
75
|
+
// ============================================================
|
|
76
|
+
|
|
77
|
+
console.log('\n═══════════════════════════════════════════════════════════════');
|
|
78
|
+
console.log('🧪 Provider Health Manager Tests');
|
|
79
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
80
|
+
|
|
81
|
+
// 1. Basic Health Recording
|
|
82
|
+
console.log('📊 1. Health Recording');
|
|
83
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
84
|
+
|
|
85
|
+
test('recordSuccess updates metrics correctly', () => {
|
|
86
|
+
const manager = new ProviderHealthManager({ windowSize: 10 });
|
|
87
|
+
|
|
88
|
+
manager.recordSuccess('openai/gpt-4o', 150);
|
|
89
|
+
manager.recordSuccess('openai/gpt-4o', 200);
|
|
90
|
+
|
|
91
|
+
const health = manager.getHealth('openai/gpt-4o');
|
|
92
|
+
assert(health !== undefined, 'health should exist');
|
|
93
|
+
assertEqual(health!.consecutiveErrors, 0, 'consecutive errors should be 0');
|
|
94
|
+
assert(health!.lastSuccess > 0, 'lastSuccess should be set');
|
|
95
|
+
assertApprox(health!.latency, 175, 5, 'latency should be average of 150 and 200');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('recordFailure updates metrics correctly', () => {
|
|
99
|
+
const manager = new ProviderHealthManager({ windowSize: 10 });
|
|
100
|
+
|
|
101
|
+
manager.recordFailure('openai/gpt-4o', 'timeout');
|
|
102
|
+
|
|
103
|
+
const health = manager.getHealth('openai/gpt-4o');
|
|
104
|
+
assert(health !== undefined, 'health should exist');
|
|
105
|
+
assertEqual(health!.consecutiveErrors, 1, 'consecutive errors should be 1');
|
|
106
|
+
assert(health!.lastError > 0, 'lastError should be set');
|
|
107
|
+
assertApprox(health!.errorRate, 1.0, 0.01, 'error rate should be 1.0');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('recordSuccess resets consecutive errors', () => {
|
|
111
|
+
const manager = new ProviderHealthManager({ windowSize: 10 });
|
|
112
|
+
|
|
113
|
+
manager.recordFailure('openai/gpt-4o', 'error1');
|
|
114
|
+
manager.recordFailure('openai/gpt-4o', 'error2');
|
|
115
|
+
manager.recordFailure('openai/gpt-4o', 'error3');
|
|
116
|
+
manager.recordSuccess('openai/gpt-4o', 200);
|
|
117
|
+
|
|
118
|
+
const health = manager.getHealth('openai/gpt-4o');
|
|
119
|
+
assertEqual(health!.consecutiveErrors, 0, 'consecutive errors should be reset');
|
|
120
|
+
assertEqual(health!.lastSuccess > 0, true, 'lastSuccess should be set');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('rolling window limits metrics to windowSize', () => {
|
|
124
|
+
const manager = new ProviderHealthManager({ windowSize: 5 });
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < 10; i++) {
|
|
127
|
+
manager.recordSuccess('openai/gpt-4o', 100 + i);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const health = manager.getHealth('openai/gpt-4o');
|
|
131
|
+
// After 10 successes with windowSize=5, we should have only 5 metrics
|
|
132
|
+
// Health score should be based on recent window
|
|
133
|
+
assert(health !== undefined, 'health should exist');
|
|
134
|
+
assert(health!.healthScore > 0, 'healthScore should be > 0');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 2. Error Tracking
|
|
138
|
+
console.log('\n❌ 2. Error Tracking');
|
|
139
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
140
|
+
|
|
141
|
+
test('error rate calculated correctly over window', () => {
|
|
142
|
+
const manager = new ProviderHealthManager({ windowSize: 10 });
|
|
143
|
+
|
|
144
|
+
// 3 successes, 1 failure
|
|
145
|
+
manager.recordSuccess('openai/gpt-4o', 100);
|
|
146
|
+
manager.recordSuccess('openai/gpt-4o', 100);
|
|
147
|
+
manager.recordFailure('openai/gpt-4o', 'error');
|
|
148
|
+
manager.recordSuccess('openai/gpt-4o', 100);
|
|
149
|
+
|
|
150
|
+
const health = manager.getHealth('openai/gpt-4o');
|
|
151
|
+
assertApprox(health!.errorRate, 0.25, 0.01, 'error rate should be 0.25 (1/4)');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('consecutive errors increment correctly', () => {
|
|
155
|
+
const manager = new ProviderHealthManager({ windowSize: 10 });
|
|
156
|
+
|
|
157
|
+
manager.recordFailure('openai/gpt-4o', 'error1');
|
|
158
|
+
let health = manager.getHealth('openai/gpt-4o');
|
|
159
|
+
assertEqual(health!.consecutiveErrors, 1, 'first failure');
|
|
160
|
+
|
|
161
|
+
manager.recordFailure('openai/gpt-4o', 'error2');
|
|
162
|
+
health = manager.getHealth('openai/gpt-4o');
|
|
163
|
+
assertEqual(health!.consecutiveErrors, 2, 'second failure');
|
|
164
|
+
|
|
165
|
+
manager.recordFailure('openai/gpt-4o', 'error3');
|
|
166
|
+
health = manager.getHealth('openai/gpt-4o');
|
|
167
|
+
assertEqual(health!.consecutiveErrors, 3, 'third failure');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// 3. Circuit Breaker
|
|
171
|
+
console.log('\n🔌 3. Circuit Breaker');
|
|
172
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
173
|
+
|
|
174
|
+
test('3 consecutive errors trigger circuit breaker', () => {
|
|
175
|
+
const manager = new ProviderHealthManager({
|
|
176
|
+
windowSize: 10,
|
|
177
|
+
circuitBreakerThreshold: 3,
|
|
178
|
+
cooldownMs: 60000,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
manager.recordFailure('openai/gpt-4o', 'error1');
|
|
182
|
+
manager.recordFailure('openai/gpt-4o', 'error2');
|
|
183
|
+
manager.recordFailure('openai/gpt-4o', 'error3');
|
|
184
|
+
|
|
185
|
+
const health = manager.getHealth('openai/gpt-4o');
|
|
186
|
+
assertEqual(health!.consecutiveErrors, 3, 'consecutive errors should be 3');
|
|
187
|
+
assert(health!.cooldownUntil > Date.now(), 'cooldownUntil should be set in future');
|
|
188
|
+
assertEqual(health!.isHealthy, false, 'provider should be unhealthy');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('provider unavailable during cooldown', () => {
|
|
192
|
+
const manager = new ProviderHealthManager({
|
|
193
|
+
circuitBreakerThreshold: 3,
|
|
194
|
+
cooldownMs: 60000,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
manager.recordFailure('openai/gpt-4o', 'error1');
|
|
198
|
+
manager.recordFailure('openai/gpt-4o', 'error2');
|
|
199
|
+
manager.recordFailure('openai/gpt-4o', 'error3');
|
|
200
|
+
|
|
201
|
+
const isAvailable = manager.isAvailable('openai/gpt-4o');
|
|
202
|
+
assertEqual(isAvailable, false, 'provider should be unavailable');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('circuit breaker emits events', () => {
|
|
206
|
+
const manager = new ProviderHealthManager({
|
|
207
|
+
circuitBreakerThreshold: 3,
|
|
208
|
+
cooldownMs: 60000,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
let circuitOpened = false;
|
|
212
|
+
let cooldownStarted = false;
|
|
213
|
+
|
|
214
|
+
manager.on(HealthEvent.CIRCUIT_OPENED, () => { circuitOpened = true; });
|
|
215
|
+
manager.on(HealthEvent.COOLDOWN_STARTED, () => { cooldownStarted = true; });
|
|
216
|
+
|
|
217
|
+
manager.recordFailure('openai/gpt-4o', 'error1');
|
|
218
|
+
manager.recordFailure('openai/gpt-4o', 'error2');
|
|
219
|
+
manager.recordFailure('openai/gpt-4o', 'error3');
|
|
220
|
+
|
|
221
|
+
assertEqual(circuitOpened, true, 'CIRCUIT_OPENED event should fire');
|
|
222
|
+
assertEqual(cooldownStarted, true, 'COOLDOWN_STARTED event should fire');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// 4. Cooldown
|
|
226
|
+
console.log('\n⏱️ 4. Cooldown');
|
|
227
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
228
|
+
|
|
229
|
+
test('cooldown expires after configured duration', async () => {
|
|
230
|
+
const manager = new ProviderHealthManager({
|
|
231
|
+
circuitBreakerThreshold: 3,
|
|
232
|
+
cooldownMs: 100, // 100ms for testing
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
manager.recordFailure('openai/gpt-4o', 'error1');
|
|
236
|
+
manager.recordFailure('openai/gpt-4o', 'error2');
|
|
237
|
+
manager.recordFailure('openai/gpt-4o', 'error3');
|
|
238
|
+
|
|
239
|
+
// Provider should be unavailable initially
|
|
240
|
+
assertEqual(manager.isAvailable('openai/gpt-4o'), false, 'unavailable during cooldown');
|
|
241
|
+
|
|
242
|
+
// Wait for cooldown to expire
|
|
243
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
244
|
+
|
|
245
|
+
// After cooldown, probe should be allowed
|
|
246
|
+
assertEqual(manager.isProbeAllowed('openai/gpt-4o'), true, 'probe should be allowed after cooldown');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('success after cooldown resets state', async () => {
|
|
250
|
+
const manager = new ProviderHealthManager({
|
|
251
|
+
circuitBreakerThreshold: 3,
|
|
252
|
+
cooldownMs: 100,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
manager.recordFailure('openai/gpt-4o', 'error1');
|
|
256
|
+
manager.recordFailure('openai/gpt-4o', 'error2');
|
|
257
|
+
manager.recordFailure('openai/gpt-4o', 'error3');
|
|
258
|
+
|
|
259
|
+
// Wait for cooldown
|
|
260
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
261
|
+
|
|
262
|
+
// Record success (probe request)
|
|
263
|
+
manager.recordSuccess('openai/gpt-4o', 150);
|
|
264
|
+
|
|
265
|
+
const health = manager.getHealth('openai/gpt-4o');
|
|
266
|
+
assertEqual(health!.consecutiveErrors, 0, 'consecutive errors should be reset');
|
|
267
|
+
assertEqual(health!.isHealthy, true, 'provider should be healthy');
|
|
268
|
+
assertEqual(health!.cooldownUntil, 0, 'cooldownUntil should be cleared');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// 5. Fallback Chain
|
|
272
|
+
console.log('\n🔀 5. Fallback Chain');
|
|
273
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
274
|
+
|
|
275
|
+
test('getFallbackChain sorts by health score', () => {
|
|
276
|
+
const manager = new ProviderHealthManager({ windowSize: 10 });
|
|
277
|
+
|
|
278
|
+
// Set up different health for each provider
|
|
279
|
+
manager.recordSuccess('fast-provider', 50); // Low latency
|
|
280
|
+
manager.recordSuccess('slow-provider', 500); // High latency
|
|
281
|
+
manager.recordFailure('broken-provider', 'error');
|
|
282
|
+
manager.recordFailure('broken-provider', 'error');
|
|
283
|
+
manager.recordFailure('broken-provider', 'error');
|
|
284
|
+
|
|
285
|
+
const chain = manager.getFallbackChain([
|
|
286
|
+
'fast-provider',
|
|
287
|
+
'slow-provider',
|
|
288
|
+
'broken-provider',
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
// fast-provider should be first (healthy, low latency)
|
|
292
|
+
assertEqual(chain[0], 'fast-provider', 'fast-provider should be first');
|
|
293
|
+
// broken-provider should be last (unhealthy)
|
|
294
|
+
assertEqual(chain[chain.length - 1], 'broken-provider', 'broken-provider should be last');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('getFallbackChain puts unavailable providers at end', () => {
|
|
298
|
+
const manager = new ProviderHealthManager({
|
|
299
|
+
circuitBreakerThreshold: 3,
|
|
300
|
+
cooldownMs: 60000,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
manager.recordSuccess('healthy', 100);
|
|
304
|
+
manager.recordFailure('unhealthy', 'error1');
|
|
305
|
+
manager.recordFailure('unhealthy', 'error2');
|
|
306
|
+
manager.recordFailure('unhealthy', 'error3');
|
|
307
|
+
|
|
308
|
+
const chain = manager.getFallbackChain(['healthy', 'unhealthy']);
|
|
309
|
+
|
|
310
|
+
// unhealthy should be at the end
|
|
311
|
+
assertEqual(chain[chain.length - 1], 'unhealthy', 'unhealthy should be last');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('getBestProvider returns highest scoring healthy provider', () => {
|
|
315
|
+
const manager = new ProviderHealthManager({ windowSize: 10 });
|
|
316
|
+
|
|
317
|
+
// Set up different latencies
|
|
318
|
+
manager.recordSuccess('slow', 2000);
|
|
319
|
+
manager.recordSuccess('fast', 100);
|
|
320
|
+
manager.recordSuccess('medium', 500);
|
|
321
|
+
|
|
322
|
+
const best = manager.getBestProvider(['slow', 'fast', 'medium']);
|
|
323
|
+
assertEqual(best, 'fast', 'fast provider should be best');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('getBestProvider returns null when all unavailable', () => {
|
|
327
|
+
const manager = new ProviderHealthManager({
|
|
328
|
+
circuitBreakerThreshold: 3,
|
|
329
|
+
cooldownMs: 60000,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
manager.recordFailure('provider1', 'error1');
|
|
333
|
+
manager.recordFailure('provider1', 'error2');
|
|
334
|
+
manager.recordFailure('provider1', 'error3');
|
|
335
|
+
|
|
336
|
+
manager.recordFailure('provider2', 'error1');
|
|
337
|
+
manager.recordFailure('provider2', 'error2');
|
|
338
|
+
manager.recordFailure('provider2', 'error3');
|
|
339
|
+
|
|
340
|
+
const best = manager.getBestProvider(['provider1', 'provider2']);
|
|
341
|
+
assertEqual(best, null, 'no provider should be available');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// 6. Probe After Cooldown
|
|
345
|
+
console.log('\n🔍 6. Probe After Cooldown');
|
|
346
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
347
|
+
|
|
348
|
+
test('probe allowed after cooldown expires', async () => {
|
|
349
|
+
const manager = new ProviderHealthManager({
|
|
350
|
+
circuitBreakerThreshold: 3,
|
|
351
|
+
cooldownMs: 50,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
manager.recordFailure('openai/gpt-4o', 'error1');
|
|
355
|
+
manager.recordFailure('openai/gpt-4o', 'error2');
|
|
356
|
+
manager.recordFailure('openai/gpt-4o', 'error3');
|
|
357
|
+
|
|
358
|
+
// Wait for cooldown
|
|
359
|
+
await new Promise(resolve => setTimeout(resolve, 75));
|
|
360
|
+
|
|
361
|
+
assertEqual(manager.isProbeAllowed('openai/gpt-4o'), true, 'probe should be allowed');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('probe success marks provider healthy', async () => {
|
|
365
|
+
const manager = new ProviderHealthManager({
|
|
366
|
+
circuitBreakerThreshold: 3,
|
|
367
|
+
cooldownMs: 50,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
manager.recordFailure('openai/gpt-4o', 'error1');
|
|
371
|
+
manager.recordFailure('openai/gpt-4o', 'error2');
|
|
372
|
+
manager.recordFailure('openai/gpt-4o', 'error3');
|
|
373
|
+
|
|
374
|
+
// Wait for cooldown
|
|
375
|
+
await new Promise(resolve => setTimeout(resolve, 75));
|
|
376
|
+
|
|
377
|
+
// Probe success
|
|
378
|
+
manager.recordSuccess('openai/gpt-4o', 100);
|
|
379
|
+
|
|
380
|
+
const health = manager.getHealth('openai/gpt-4o');
|
|
381
|
+
assertEqual(health!.isHealthy, true, 'should be healthy after probe success');
|
|
382
|
+
assertEqual(manager.isAvailable('openai/gpt-4o'), true, 'should be available after probe success');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('isAvailable checks manual disable', () => {
|
|
386
|
+
const manager = new ProviderHealthManager({});
|
|
387
|
+
|
|
388
|
+
manager.disableProvider('manual-disabled', 'testing');
|
|
389
|
+
|
|
390
|
+
assertEqual(manager.isAvailable('manual-disabled'), false, 'manually disabled provider unavailable');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// 7. Manual Enable/Disable
|
|
394
|
+
console.log('\n🔧 7. Manual Enable/Disable');
|
|
395
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
396
|
+
|
|
397
|
+
test('disableProvider marks provider unhealthy', () => {
|
|
398
|
+
const manager = new ProviderHealthManager({});
|
|
399
|
+
|
|
400
|
+
manager.recordSuccess('provider', 100);
|
|
401
|
+
manager.disableProvider('provider', 'maintenance');
|
|
402
|
+
|
|
403
|
+
const health = manager.getHealth('provider');
|
|
404
|
+
assertEqual(health!.isHealthy, false, 'provider should be unhealthy after disable');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('enableProvider re-enables provider', () => {
|
|
408
|
+
const manager = new ProviderHealthManager({});
|
|
409
|
+
|
|
410
|
+
manager.recordSuccess('provider', 100);
|
|
411
|
+
manager.disableProvider('provider', 'maintenance');
|
|
412
|
+
manager.enableProvider('provider');
|
|
413
|
+
|
|
414
|
+
const health = manager.getHealth('provider');
|
|
415
|
+
assertEqual(health!.isHealthy, true, 'provider should be healthy after enable');
|
|
416
|
+
assertEqual(health!.consecutiveErrors, 0, 'consecutive errors should be reset');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('enableProvider emits event', () => {
|
|
420
|
+
const manager = new ProviderHealthManager({});
|
|
421
|
+
|
|
422
|
+
manager.disableProvider('provider', 'maintenance');
|
|
423
|
+
|
|
424
|
+
let enabled = false;
|
|
425
|
+
manager.on(HealthEvent.PROVIDER_ENABLED, () => { enabled = true; });
|
|
426
|
+
|
|
427
|
+
manager.enableProvider('provider');
|
|
428
|
+
|
|
429
|
+
assertEqual(enabled, true, 'PROVIDER_ENABLED event should fire');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// 8. Health Score Calculation
|
|
433
|
+
console.log('\n📈 8. Health Score Calculation');
|
|
434
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
435
|
+
|
|
436
|
+
test('health score decreases with errors', () => {
|
|
437
|
+
const manager = new ProviderHealthManager({ windowSize: 10 });
|
|
438
|
+
|
|
439
|
+
manager.recordSuccess('provider', 100);
|
|
440
|
+
const healthyHealth = manager.getHealth('provider');
|
|
441
|
+
|
|
442
|
+
manager.recordFailure('provider', 'error');
|
|
443
|
+
manager.recordFailure('provider', 'error');
|
|
444
|
+
manager.recordFailure('provider', 'error');
|
|
445
|
+
manager.recordSuccess('provider', 100); // Reset consecutive errors
|
|
446
|
+
const errorHealth = manager.getHealth('provider');
|
|
447
|
+
|
|
448
|
+
// Health score should be lower with higher error rate
|
|
449
|
+
// Note: after reset, consecutive errors are 0, but error rate is still 3/4
|
|
450
|
+
assert(errorHealth!.healthScore < healthyHealth!.healthScore,
|
|
451
|
+
'health score should decrease with error rate');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('health score decreases with latency', () => {
|
|
455
|
+
const manager = new ProviderHealthManager({ windowSize: 10 });
|
|
456
|
+
|
|
457
|
+
manager.recordSuccess('fast', 50);
|
|
458
|
+
const fastHealth = manager.getHealth('fast');
|
|
459
|
+
|
|
460
|
+
manager.recordSuccess('slow', 5000);
|
|
461
|
+
const slowHealth = manager.getHealth('slow');
|
|
462
|
+
|
|
463
|
+
assert(fastHealth!.healthScore > slowHealth!.healthScore,
|
|
464
|
+
'fast provider should have higher health score');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// 9. getAllHealth
|
|
468
|
+
console.log('\n📋 9. Health Stats');
|
|
469
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
470
|
+
|
|
471
|
+
test('getAllHealth returns all providers', () => {
|
|
472
|
+
const manager = new ProviderHealthManager({});
|
|
473
|
+
|
|
474
|
+
manager.recordSuccess('provider1', 100);
|
|
475
|
+
manager.recordSuccess('provider2', 100);
|
|
476
|
+
manager.recordSuccess('provider3', 100);
|
|
477
|
+
|
|
478
|
+
const allHealth = manager.getAllHealth();
|
|
479
|
+
assertEqual(allHealth.size, 3, 'should have 3 providers');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('getStats returns correct counts', () => {
|
|
483
|
+
const manager = new ProviderHealthManager({});
|
|
484
|
+
|
|
485
|
+
manager.recordSuccess('healthy1', 100);
|
|
486
|
+
manager.recordSuccess('healthy2', 100);
|
|
487
|
+
manager.recordSuccess('healthy3', 100);
|
|
488
|
+
|
|
489
|
+
manager.recordFailure('cooldown1', 'error');
|
|
490
|
+
manager.recordFailure('cooldown1', 'error');
|
|
491
|
+
manager.recordFailure('cooldown1', 'error');
|
|
492
|
+
|
|
493
|
+
manager.disableProvider('disabled', 'test');
|
|
494
|
+
|
|
495
|
+
const stats = manager.getStats();
|
|
496
|
+
assertEqual(stats.totalProviders, 5, 'total providers should be 5');
|
|
497
|
+
assertEqual(stats.healthyProviders, 3, 'healthy should be 3');
|
|
498
|
+
assertEqual(stats.cooldownProviders, 1, 'cooldown should be 1');
|
|
499
|
+
assertEqual(stats.disabledProviders, 1, 'disabled should be 1');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// ============================================================
|
|
503
|
+
// SUMMARY
|
|
504
|
+
// ============================================================
|
|
505
|
+
|
|
506
|
+
setTimeout(() => {
|
|
507
|
+
console.log('\n═══════════════════════════════════════════════════════════════');
|
|
508
|
+
console.log('📊 Test Summary');
|
|
509
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
510
|
+
console.log(` Total: ${passed + failed + skipped}`);
|
|
511
|
+
console.log(` Passed: ${passed} ✅`);
|
|
512
|
+
console.log(` Failed: ${failed}${failed > 0 ? ' ❌' : ''}`);
|
|
513
|
+
console.log(` Skipped: ${skipped}${skipped > 0 ? ' ⏭️' : ''}`);
|
|
514
|
+
console.log('');
|
|
515
|
+
|
|
516
|
+
if (failed > 0) {
|
|
517
|
+
console.log('❌ Some tests failed');
|
|
518
|
+
process.exit(1);
|
|
519
|
+
} else {
|
|
520
|
+
console.log('✅ All tests passed!');
|
|
521
|
+
process.exit(0);
|
|
522
|
+
}
|
|
523
|
+
}, 100);
|