adaptive-memory-multi-model-router 1.9.0 → 1.9.2

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.
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * A3M Router - Provider Test Framework
4
+ *
5
+ * Comprehensive tests for the generic provider system.
6
+ * Tests work with whatever providers the user has configured.
7
+ */
8
+
9
+ const {
10
+ createA3MRouter,
11
+ getAvailableProviders,
12
+ registerProvider,
13
+ deregisterProvider,
14
+ DEFAULT_PROVIDERS,
15
+ providerConfig,
16
+ routeQuery,
17
+ routeBatch,
18
+ recommendForTask,
19
+ extractQueryFeatures,
20
+ MODEL_PROFILES,
21
+ countTokens,
22
+ estimateCost,
23
+ MemoryTree,
24
+ CostTracker,
25
+ ResponseCache,
26
+ ProviderRegistry,
27
+ compressText,
28
+ isonEncode,
29
+ isonDecode,
30
+ } = require('../dist/index.js');
31
+
32
+ // Test configuration
33
+ const TEST_CONFIG = {
34
+ verbose: process.argv.includes('--verbose') || process.argv.includes('-v'),
35
+ skipLive: process.argv.includes('--skip-live'),
36
+ timeout: 30000,
37
+ };
38
+
39
+ // Test state
40
+ let passed = 0;
41
+ let failed = 0;
42
+ let skipped = 0;
43
+
44
+ // Test utilities
45
+ function log(message, level = 'info') {
46
+ if (level === 'error') console.error(message);
47
+ else if (TEST_CONFIG.verbose || level !== 'debug') console.log(message);
48
+ }
49
+
50
+ function test(name, fn) {
51
+ try {
52
+ fn();
53
+ log(` ✅ ${name}`, 'success');
54
+ passed++;
55
+ } catch (e) {
56
+ log(` ❌ ${name}: ${e.message}`, 'error');
57
+ failed++;
58
+ }
59
+ }
60
+
61
+ async function asyncTest(name, fn) {
62
+ try {
63
+ await fn();
64
+ log(` ✅ ${name}`, 'success');
65
+ passed++;
66
+ } catch (e) {
67
+ log(` ❌ ${name}: ${e.message}`, 'error');
68
+ failed++;
69
+ }
70
+ }
71
+
72
+ function skip(name, reason) {
73
+ log(` ⏭️ ${name} (skipped: ${reason})`, 'warn');
74
+ skipped++;
75
+ }
76
+
77
+ // ============================================================
78
+ // TEST SUITE
79
+ // ============================================================
80
+
81
+ console.log('\n═══════════════════════════════════════════════════════════════');
82
+ console.log('🧪 A3M Router - Provider Test Framework');
83
+ console.log('═══════════════════════════════════════════════════════════════\n');
84
+
85
+ // 1. Provider Configuration Tests
86
+ console.log('📦 1. Provider Configuration');
87
+ console.log('─────────────────────────────────────────────────────────────');
88
+
89
+ test('providerConfig module loads', () => {
90
+ if (!providerConfig) throw new Error('providerConfig not exported');
91
+ if (typeof providerConfig.loadConfig !== 'function') throw new Error('loadConfig not a function');
92
+ if (typeof providerConfig.getAvailableProviders !== 'function') throw new Error('getAvailableProviders not a function');
93
+ });
94
+
95
+ test('DEFAULT_PROVIDERS has expected structure', () => {
96
+ if (!DEFAULT_PROVIDERS) throw new Error('DEFAULT_PROVIDERS not defined');
97
+
98
+ // Check at least some providers exist
99
+ const providerCount = Object.keys(DEFAULT_PROVIDERS).length;
100
+ if (providerCount < 5) throw new Error(`Expected at least 5 providers, got ${providerCount}`);
101
+
102
+ // Check provider structure
103
+ for (const [id, provider] of Object.entries(DEFAULT_PROVIDERS)) {
104
+ if (!provider.id) throw new Error(`${id}: missing id`);
105
+ if (!provider.name) throw new Error(`${id}: missing name`);
106
+ if (!provider.type) throw new Error(`${id}: missing type`);
107
+ if (!['api', 'cli', 'local'].includes(provider.type)) throw new Error(`${id}: invalid type ${provider.type}`);
108
+ if (typeof provider.priority !== 'number') throw new Error(`${id}: missing priority`);
109
+ }
110
+ });
111
+
112
+ test('getAvailableProviders returns configured providers', () => {
113
+ const available = getAvailableProviders();
114
+ if (!available) throw new Error('getAvailableProviders returned null');
115
+
116
+ // Should return object with providers that have API keys
117
+ for (const [id, provider] of Object.entries(available)) {
118
+ if (!provider.id) throw new Error(`${id}: missing id`);
119
+ if (!provider.name) throw new Error(`${id}: missing name`);
120
+ if (!provider.models) throw new Error(`${id}: missing models`);
121
+ if (!Array.isArray(provider.models)) throw new Error(`${id}: models not an array`);
122
+ }
123
+ });
124
+
125
+ test('Provider types are correct', () => {
126
+ const available = getAvailableProviders();
127
+
128
+ for (const [id, provider] of Object.entries(available)) {
129
+ if (!['api', 'cli', 'local'].includes(provider.type)) {
130
+ throw new Error(`${id}: invalid type ${provider.type}`);
131
+ }
132
+
133
+ // API providers should have baseUrl and apiKeyEnv
134
+ if (provider.type === 'api') {
135
+ if (!provider.baseUrl) throw new Error(`${id}: API provider missing baseUrl`);
136
+ if (!provider.apiKeyEnv) throw new Error(`${id}: API provider missing apiKeyEnv`);
137
+ }
138
+
139
+ // CLI providers should have cliCommand
140
+ if (provider.type === 'cli') {
141
+ if (!provider.cliCommand) throw new Error(`${id}: CLI provider missing cliCommand`);
142
+ }
143
+ }
144
+ });
145
+
146
+ // 2. Routing Tests
147
+ console.log('\n🔀 2. Routing');
148
+ console.log('─────────────────────────────────────────────────────────────');
149
+
150
+ test('routeQuery returns valid result', () => {
151
+ const result = routeQuery('What is 2+2?');
152
+ if (!result) throw new Error('routeQuery returned null');
153
+ if (!result.primary_model) throw new Error('missing primary_model');
154
+ if (!Array.isArray(result.fallback_models)) throw new Error('fallback_models not an array');
155
+ if (typeof result.estimated_cost !== 'number') throw new Error('estimated_cost not a number');
156
+ if (typeof result.confidence !== 'number') throw new Error('confidence not a number');
157
+ });
158
+
159
+ test('routeQuery selects appropriate provider for code', () => {
160
+ const result = routeQuery('Write a Python function to sort an array');
161
+ if (!result.primary_model) throw new Error('missing primary_model');
162
+ if (!result.reasoning) throw new Error('missing reasoning');
163
+
164
+ // Should detect code
165
+ const features = extractQueryFeatures('Write a Python function to sort an array');
166
+ if (!features.has_code) throw new Error('should detect code');
167
+ });
168
+
169
+ test('routeQuery selects appropriate provider for math', () => {
170
+ const result = routeQuery('Calculate the integral of x^2');
171
+ if (!result.primary_model) throw new Error('missing primary_model');
172
+
173
+ const features = extractQueryFeatures('Calculate the integral of x^2');
174
+ if (!features.has_math) throw new Error('should detect math');
175
+ });
176
+
177
+ test('routeQuery selects appropriate provider for translation', () => {
178
+ const result = routeQuery('Translate hello to French');
179
+ if (!result.primary_model) throw new Error('missing primary_model');
180
+
181
+ const features = extractQueryFeatures('Translate hello to French');
182
+ if (!features.is_translation) throw new Error('should detect translation');
183
+ });
184
+
185
+ test('routeBatch returns array of results', () => {
186
+ const queries = ['Hello', 'What is 2+2?', 'Write Python code'];
187
+ const results = routeBatch(queries);
188
+
189
+ if (!Array.isArray(results)) throw new Error('routeBatch should return array');
190
+ if (results.length !== queries.length) throw new Error(`Expected ${queries.length} results, got ${results.length}`);
191
+
192
+ results.forEach((r, i) => {
193
+ if (!r.primary_model) throw new Error(`result ${i}: missing primary_model`);
194
+ });
195
+ });
196
+
197
+ test('recommendForTask returns recommendation', () => {
198
+ const rec = recommendForTask('coding');
199
+ if (!rec) throw new Error('recommendForTask returned null');
200
+ if (!rec.primary) throw new Error('missing primary');
201
+ if (!Array.isArray(rec.fallbacks)) throw new Error('fallbacks not an array');
202
+ if (!rec.reason) throw new Error('missing reason');
203
+ });
204
+
205
+ // 3. Model Profile Tests
206
+ console.log('\n📊 3. Model Profiles');
207
+ console.log('─────────────────────────────────────────────────────────────');
208
+
209
+ test('MODEL_PROFILES is populated', () => {
210
+ if (!MODEL_PROFILES) throw new Error('MODEL_PROFILES not defined');
211
+ if (Object.keys(MODEL_PROFILES).length === 0) throw new Error('MODEL_PROFILES is empty');
212
+ });
213
+
214
+ test('Model profiles have required fields', () => {
215
+ for (const [name, profile] of Object.entries(MODEL_PROFILES)) {
216
+ if (!profile.name) throw new Error(`${name}: missing name`);
217
+ if (!profile.provider) throw new Error(`${name}: missing provider`);
218
+ if (typeof profile.cost_per_1k_input !== 'number') throw new Error(`${name}: missing cost_per_1k_input`);
219
+ if (typeof profile.cost_per_1k_output !== 'number') throw new Error(`${name}: missing cost_per_1k_output`);
220
+ if (typeof profile.quality_score !== 'number') throw new Error(`${name}: missing quality_score`);
221
+ if (!Array.isArray(profile.strengths)) throw new Error(`${name}: strengths not an array`);
222
+ }
223
+ });
224
+
225
+ // 4. Token Utility Tests
226
+ console.log('\n🔢 4. Token Utilities');
227
+ console.log('─────────────────────────────────────────────────────────────');
228
+
229
+ test('countTokens returns number', () => {
230
+ const tokens = countTokens('Hello world');
231
+ if (typeof tokens !== 'number') throw new Error('should return number');
232
+ if (tokens <= 0) throw new Error('should return positive number');
233
+ });
234
+
235
+ test('countTokens counts correctly', () => {
236
+ const tokens = countTokens('Hello world');
237
+ if (tokens < 2) throw new Error('should count at least 2 tokens for 2 words');
238
+ });
239
+
240
+ test('estimateCost returns number', () => {
241
+ const cost = estimateCost(100, 50, 'gpt-4o');
242
+ if (typeof cost !== 'number') throw new Error('should return number');
243
+ if (cost < 0) throw new Error('should return non-negative');
244
+ });
245
+
246
+ // 5. A3M Router Factory Tests
247
+ console.log('\n🏭 5. A3M Router Factory');
248
+ console.log('─────────────────────────────────────────────────────────────');
249
+
250
+ test('createA3MRouter returns router object', () => {
251
+ const router = createA3MRouter({});
252
+ if (!router) throw new Error('createA3MRouter returned null');
253
+ if (typeof router.route !== 'function') throw new Error('missing route function');
254
+ if (typeof router.routeBatch !== 'function') throw new Error('missing routeBatch function');
255
+ if (typeof router.recommend !== 'function') throw new Error('missing recommend function');
256
+ });
257
+
258
+ test('createA3MRouter has memory', () => {
259
+ const router = createA3MRouter({});
260
+ if (!router.memory) throw new Error('missing memory');
261
+ if (typeof router.memory.add !== 'function') throw new Error('memory missing add');
262
+ if (typeof router.memory.search !== 'function') throw new Error('memory missing search');
263
+ });
264
+
265
+ test('createA3MRouter has cache', () => {
266
+ const router = createA3MRouter({});
267
+ if (!router.cache) throw new Error('missing cache');
268
+ });
269
+
270
+ test('createA3MRouter has costTracker', () => {
271
+ const router = createA3MRouter({});
272
+ if (!router.costTracker) throw new Error('missing costTracker');
273
+ });
274
+
275
+ test('createA3MRouter has providers registry', () => {
276
+ const router = createA3MRouter({});
277
+ if (!router.providers) throw new Error('missing providers');
278
+ });
279
+
280
+ test('createA3MRouter has compression', () => {
281
+ const router = createA3MRouter({});
282
+ if (!router.compression) throw new Error('missing compression');
283
+ });
284
+
285
+ test('createA3MRouter has vault', () => {
286
+ const router = createA3MRouter({});
287
+ if (!router.vault) throw new Error('missing vault');
288
+ });
289
+
290
+ test('createA3MRouter has autoFetch', () => {
291
+ const router = createA3MRouter({});
292
+ if (!router.autoFetch) throw new Error('missing autoFetch');
293
+ });
294
+
295
+ test('createA3MRouter has oauth', () => {
296
+ const router = createA3MRouter({});
297
+ if (!router.oauth) throw new Error('missing oauth');
298
+ });
299
+
300
+ // 6. Memory Tree Tests
301
+ console.log('\n🧠 6. Memory Tree');
302
+ console.log('─────────────────────────────────────────────────────────────');
303
+
304
+ test('MemoryTree can add and search', () => {
305
+ const memory = new MemoryTree({ maxSize: 100 });
306
+ memory.add('Python is great for data science', { tags: ['python', 'data'] });
307
+ memory.add('JavaScript is great for web', { tags: ['js', 'web'] });
308
+
309
+ const results = memory.search('python data');
310
+ if (!Array.isArray(results)) throw new Error('search should return array');
311
+ });
312
+
313
+ test('MemoryTree getStats returns stats', () => {
314
+ const memory = new MemoryTree({ maxSize: 100 });
315
+ memory.add('Test entry', { tags: ['test'] });
316
+
317
+ const stats = memory.getStats();
318
+ if (!stats) throw new Error('getStats returned null');
319
+ if (typeof stats.totalChunks !== 'number') throw new Error('missing totalChunks');
320
+ });
321
+
322
+ // 7. Provider Registry Tests
323
+ console.log('\n📋 7. Provider Registry');
324
+ console.log('─────────────────────────────────────────────────────────────');
325
+
326
+ test('ProviderRegistry can be instantiated', () => {
327
+ const registry = new ProviderRegistry();
328
+ if (!registry) throw new Error('failed to create registry');
329
+ if (typeof registry.getReadyProviders !== 'function') throw new Error('missing getReadyProviders');
330
+ if (typeof registry.selectModel !== 'function') throw new Error('missing selectModel');
331
+ });
332
+
333
+ test('ProviderRegistry getStatus returns status', () => {
334
+ const registry = new ProviderRegistry();
335
+ const status = registry.getStatus();
336
+ if (!status) throw new Error('getStatus returned null');
337
+ if (!Array.isArray(status.providers)) throw new Error('providers not an array');
338
+ });
339
+
340
+ // 8. Dynamic Provider Registration Tests
341
+ console.log('\n🔧 8. Dynamic Provider Registration');
342
+ console.log('─────────────────────────────────────────────────────────────');
343
+
344
+ test('registerProvider adds new provider', () => {
345
+ const testProvider = {
346
+ name: 'TestProvider',
347
+ type: 'api',
348
+ baseUrl: 'https://test.example.com',
349
+ models: ['test-model'],
350
+ priority: 99,
351
+ };
352
+
353
+ registerProvider('test-provider', testProvider);
354
+
355
+ // Check it was added
356
+ if (!providerConfig._providers['test-provider']) throw new Error('provider not added');
357
+ if (providerConfig._providers['test-provider'].name !== 'TestProvider') {
358
+ throw new Error('provider name mismatch');
359
+ }
360
+
361
+ // Clean up
362
+ deregisterProvider('test-provider');
363
+ });
364
+
365
+ test('deregisterProvider removes provider', () => {
366
+ // First add
367
+ registerProvider('temp-provider', { name: 'Temp', type: 'api', models: [] });
368
+ if (!providerConfig._providers['temp-provider']) throw new Error('provider not added');
369
+
370
+ // Then remove
371
+ deregisterProvider('temp-provider');
372
+ if (providerConfig._providers['temp-provider']) throw new Error('provider not removed');
373
+ });
374
+
375
+ // 9. Compression Tests
376
+ console.log('\n🗜️ 9. Compression');
377
+ console.log('─────────────────────────────────────────────────────────────');
378
+
379
+ test('compressText reduces size', () => {
380
+ const text = 'This is a test message that should be compressed to save tokens.';
381
+ const compressed = compressText(text, 0.5);
382
+ if (!compressed) throw new Error('compressText returned null');
383
+ if (compressed.length >= text.length) throw new Error('compression did not reduce size');
384
+ });
385
+
386
+ test('isonEncode/Decode roundtrip', () => {
387
+ const text = 'function test() { return "hello world"; }';
388
+ const encoded = isonEncode(text);
389
+ if (!encoded) throw new Error('isonEncode returned null');
390
+ if (typeof encoded !== 'string') throw new Error('isonEncode should return string');
391
+
392
+ const decoded = isonDecode(encoded);
393
+ if (!decoded) throw new Error('isonDecode returned null');
394
+ if (typeof decoded !== 'string') throw new Error('isonDecode should return string');
395
+ // Decoded might not be identical due to compression, but should be similar
396
+ if (decoded.length < 5) throw new Error('decoded text too short');
397
+ });
398
+
399
+ // 10. End-to-End Pipeline Test
400
+ console.log('\n🔄 10. End-to-End Pipeline');
401
+ console.log('─────────────────────────────────────────────────────────────');
402
+
403
+ test('Full pipeline: route → track → remember', () => {
404
+ const router = createA3MRouter({ memory: { maxSize: 100 } });
405
+
406
+ // Route
407
+ const route = router.route('Test query');
408
+ if (!route.primary_model) throw new Error('routing failed');
409
+
410
+ // Track (via costTracker)
411
+ if (!router.costTracker) throw new Error('costTracker not available');
412
+
413
+ // Remember
414
+ router.memory.add('Test query result', { route: route.primary_model });
415
+ const search = router.memory.search('test');
416
+ if (!Array.isArray(search)) throw new Error('memory search failed');
417
+ });
418
+
419
+ // ============================================================
420
+ // LIVE PROVIDER TESTS (if not skipped)
421
+ // ============================================================
422
+
423
+ if (!TEST_CONFIG.skipLive) {
424
+ console.log('\n🌐 Live Provider Tests');
425
+ console.log('─────────────────────────────────────────────────────────────');
426
+
427
+ const available = getAvailableProviders();
428
+
429
+ if (Object.keys(available).length === 0) {
430
+ skip('No providers configured', 'No API keys found in environment');
431
+ } else {
432
+ for (const [id, provider] of Object.entries(available)) {
433
+ asyncTest(`Health check: ${provider.name}`, async () => {
434
+ const health = await providerConfig.healthCheck(id);
435
+ if (!health) throw new Error('healthCheck returned null');
436
+
437
+ // CLI providers may not have traditional health checks
438
+ if (provider.type === 'cli') {
439
+ log(` ${id}: CLI provider (type: ${health.type || 'unknown'})`, 'debug');
440
+ return;
441
+ }
442
+
443
+ if (!health.healthy) {
444
+ throw new Error(`unhealthy: ${health.error || 'unknown error'}`);
445
+ }
446
+
447
+ log(` ${id}: healthy (${health.latency}ms)`, 'debug');
448
+ });
449
+ }
450
+ }
451
+ }
452
+
453
+ // ============================================================
454
+ // SUMMARY
455
+ // ============================================================
456
+
457
+ console.log('\n═══════════════════════════════════════════════════════════════');
458
+ console.log('📊 Test Summary');
459
+ console.log('═══════════════════════════════════════════════════════════════');
460
+ console.log(` Total: ${passed + failed + skipped}`);
461
+ console.log(` Passed: ${passed} ✅`);
462
+ console.log(` Failed: ${failed}${failed > 0 ? ' ❌' : ''}`);
463
+ console.log(` Skipped: ${skipped}${skipped > 0 ? ' ⏭️' : ''}`);
464
+ console.log('');
465
+
466
+ if (failed > 0) {
467
+ console.log('❌ Some tests failed');
468
+ process.exit(1);
469
+ } else {
470
+ console.log('✅ All tests passed!');
471
+ process.exit(0);
472
+ }