Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
/**
* Unit tests for APIClient
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { APIClient } from '../../../src/api/client.js';
// Mock fetch
global.fetch = vi.fn();
describe('APIClient', () => {
let client;
beforeEach(() => {
client = new APIClient({
baseUrl: 'https://api.test.com',
apiKey: 'test-key-123',
timeout: 5000,
retries: 3
});
vi.clearAllMocks();
});
describe('constructor', () => {
it('should create client with default options', () => {
const defaultClient = new APIClient();
expect(defaultClient.baseUrl).toBe('https://api.example.com');
expect(defaultClient.timeout).toBe(5000);
expect(defaultClient.retries).toBe(3);
});
it('should accept custom options', () => {
expect(client.baseUrl).toBe('https://api.test.com');
expect(client.apiKey).toBe('test-key-123');
expect(client.timeout).toBe(5000);
expect(client.retries).toBe(3);
});
});
describe('request', () => {
it('should make successful request', async () => {
const mockResponse = { data: 'test' };
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const result = await client.request('/test');
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockResponse);
});
it('should include authorization header', async () => {
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({})
});
await client.request('/test');
const callArgs = global.fetch.mock.calls[0];
expect(callArgs[1].headers.Authorization).toBe('Bearer test-key-123');
});
it('should handle API errors', async () => {
// Mock must return error for all retry attempts
global.fetch.mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found'
});
await expect(client.request('/test')).rejects.toThrow('API error: 404 Not Found');
});
it('should retry on failure', async () => {
global.fetch
.mockRejectedValueOnce(new Error('Network error'))
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true })
});
const result = await client.request('/test');
expect(global.fetch).toHaveBeenCalledTimes(3);
expect(result).toEqual({ success: true });
});
it('should fail after max retries', async () => {
global.fetch.mockRejectedValue(new Error('Network error'));
await expect(client.request('/test')).rejects.toThrow('Network error');
expect(global.fetch).toHaveBeenCalledTimes(3);
});
it('should respect timeout', async () => {
const shortTimeoutClient = new APIClient({ timeout: 100 });
global.fetch.mockImplementationOnce(() =>
new Promise(resolve => setTimeout(resolve, 200))
);
// Note: This test depends on AbortController implementation
// May need adjustment based on test environment
});
});
describe('get', () => {
it('should make GET request', async () => {
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ result: 'success' })
});
const result = await client.get('/users');
expect(result).toEqual({ result: 'success' });
expect(global.fetch.mock.calls[0][1].method).toBe('GET');
});
it('should append query parameters', async () => {
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({})
});
await client.get('/users', { page: 1, limit: 10 });
const url = global.fetch.mock.calls[0][0];
expect(url).toContain('?page=1&limit=10');
});
});
describe('post', () => {
it('should make POST request', async () => {
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: 123 })
});
const data = { name: 'Test User' };
const result = await client.post('/users', data);
expect(result).toEqual({ id: 123 });
const callArgs = global.fetch.mock.calls[0];
expect(callArgs[1].method).toBe('POST');
expect(callArgs[1].body).toBe(JSON.stringify(data));
});
it('should include content-type header', async () => {
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({})
});
await client.post('/test', {});
const headers = global.fetch.mock.calls[0][1].headers;
expect(headers['Content-Type']).toBe('application/json');
});
});
describe('error handling', () => {
it('should handle network errors', async () => {
global.fetch.mockRejectedValue(new Error('Failed to fetch'));
await expect(client.get('/test')).rejects.toThrow();
});
it('should handle timeout errors', async () => {
global.fetch.mockImplementationOnce(() =>
new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), 100);
})
);
await expect(client.request('/test')).rejects.toThrow();
});
});
});

View File

@@ -0,0 +1,256 @@
/**
* Unit tests for ContextCache
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ContextCache } from '../../../src/cache/context-cache.js';
describe('ContextCache', () => {
let cache;
beforeEach(() => {
cache = new ContextCache({
maxSize: 5,
ttl: 1000 // 1 second for testing
});
});
describe('constructor', () => {
it('should create cache with default options', () => {
const defaultCache = new ContextCache();
expect(defaultCache.maxSize).toBe(100);
expect(defaultCache.ttl).toBe(3600000);
});
it('should accept custom options', () => {
expect(cache.maxSize).toBe(5);
expect(cache.ttl).toBe(1000);
});
it('should initialize empty cache', () => {
expect(cache.cache.size).toBe(0);
});
it('should initialize stats', () => {
const stats = cache.getStats();
expect(stats.hits).toBe(0);
expect(stats.misses).toBe(0);
expect(stats.evictions).toBe(0);
});
});
describe('set and get', () => {
it('should store and retrieve value', () => {
cache.set('key1', 'value1');
const result = cache.get('key1');
expect(result).toBe('value1');
});
it('should return null for non-existent key', () => {
const result = cache.get('nonexistent');
expect(result).toBeNull();
});
it('should update existing key', () => {
cache.set('key1', 'value1');
cache.set('key1', 'value2');
expect(cache.get('key1')).toBe('value2');
expect(cache.cache.size).toBe(1);
});
it('should store complex objects', () => {
const obj = { nested: { data: [1, 2, 3] } };
cache.set('complex', obj);
expect(cache.get('complex')).toEqual(obj);
});
});
describe('TTL (Time To Live)', () => {
it('should return null for expired entries', async () => {
cache.set('key1', 'value1');
// Wait for TTL to expire
await new Promise(resolve => setTimeout(resolve, 1100));
const result = cache.get('key1');
expect(result).toBeNull();
});
it('should not return expired entries in has()', async () => {
cache.set('key1', 'value1');
expect(cache.has('key1')).toBe(true);
await new Promise(resolve => setTimeout(resolve, 1100));
expect(cache.has('key1')).toBe(false);
});
it('should delete expired entries', async () => {
cache.set('key1', 'value1');
expect(cache.cache.size).toBe(1);
await new Promise(resolve => setTimeout(resolve, 1100));
cache.get('key1'); // Triggers cleanup
expect(cache.cache.size).toBe(0);
});
});
describe('eviction', () => {
it('should evict LRU entry when at capacity', () => {
// Fill cache to capacity
for (let i = 0; i < 5; i++) {
cache.set(`key${i}`, `value${i}`);
}
expect(cache.cache.size).toBe(5);
// Access key1 to make key0 the LRU
cache.get('key1');
// Add new entry, should evict key0
cache.set('key5', 'value5');
expect(cache.cache.size).toBe(5);
expect(cache.get('key0')).toBeNull();
expect(cache.get('key5')).toBe('value5');
});
it('should track eviction stats', () => {
for (let i = 0; i < 6; i++) {
cache.set(`key${i}`, `value${i}`);
}
const stats = cache.getStats();
expect(stats.evictions).toBeGreaterThan(0);
});
});
describe('has', () => {
it('should return true for existing key', () => {
cache.set('key1', 'value1');
expect(cache.has('key1')).toBe(true);
});
it('should return false for non-existent key', () => {
expect(cache.has('nonexistent')).toBe(false);
});
});
describe('clear', () => {
it('should remove all entries', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.clear();
expect(cache.cache.size).toBe(0);
expect(cache.get('key1')).toBeNull();
});
it('should reset statistics', () => {
cache.set('key1', 'value1');
cache.get('key1');
cache.get('nonexistent');
cache.clear();
const stats = cache.getStats();
expect(stats.hits).toBe(0);
expect(stats.misses).toBe(0);
});
});
describe('getStats', () => {
it('should track cache hits', () => {
cache.set('key1', 'value1');
cache.get('key1');
cache.get('key1');
const stats = cache.getStats();
expect(stats.hits).toBe(2);
});
it('should track cache misses', () => {
cache.get('nonexistent1');
cache.get('nonexistent2');
const stats = cache.getStats();
expect(stats.misses).toBe(2);
});
it('should calculate hit rate', () => {
cache.set('key1', 'value1');
cache.get('key1'); // hit
cache.get('key1'); // hit
cache.get('nonexistent'); // miss
const stats = cache.getStats();
expect(stats.hitRate).toBeCloseTo(0.666, 2);
});
it('should include cache size', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
const stats = cache.getStats();
expect(stats.size).toBe(2);
});
it('should handle zero hit rate', () => {
cache.get('nonexistent');
const stats = cache.getStats();
expect(stats.hitRate).toBe(0);
});
});
describe('access tracking', () => {
it('should update access count', () => {
cache.set('key1', 'value1');
cache.get('key1');
cache.get('key1');
const entry = cache.cache.get('key1');
expect(entry.accessCount).toBe(2);
});
it('should update last access time', () => {
cache.set('key1', 'value1');
const initialAccess = cache.cache.get('key1').lastAccess;
// Small delay
setTimeout(() => {
cache.get('key1');
const laterAccess = cache.cache.get('key1').lastAccess;
expect(laterAccess).toBeGreaterThan(initialAccess);
}, 10);
});
});
describe('performance', () => {
it('should handle 1000 operations quickly', () => {
const start = Date.now();
for (let i = 0; i < 1000; i++) {
cache.set(`key${i}`, `value${i}`);
cache.get(`key${i}`);
}
const duration = Date.now() - start;
expect(duration).toBeLessThan(100); // Less than 100ms
});
it('should maintain performance with large values', () => {
const largeValue = { data: new Array(1000).fill('x'.repeat(100)) };
const start = Date.now();
for (let i = 0; i < 100; i++) {
cache.set(`key${i}`, largeValue);
}
const duration = Date.now() - start;
expect(duration).toBeLessThan(100);
});
});
});

View File

@@ -0,0 +1,275 @@
/**
* Unit tests for Config
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { Config } from '../../../src/config/config.js';
import { writeFileSync, unlinkSync, existsSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
describe('Config', () => {
let testConfigPath;
let originalEnv;
beforeEach(() => {
originalEnv = { ...process.env };
testConfigPath = join(tmpdir(), `test-config-${Date.now()}.json`);
});
afterEach(() => {
process.env = originalEnv;
if (existsSync(testConfigPath)) {
unlinkSync(testConfigPath);
}
});
describe('constructor', () => {
it('should create config with defaults', () => {
const config = new Config({ loadEnv: false });
expect(config.values).toBeDefined();
expect(config.get('api.baseUrl')).toBeDefined();
});
it('should accept custom options', () => {
const config = new Config({
loadEnv: false,
api: { baseUrl: 'https://custom.com' }
});
expect(config.get('api.baseUrl')).toBe('https://custom.com');
});
it('should load from file if provided', () => {
writeFileSync(testConfigPath, JSON.stringify({
custom: { value: 'test' }
}));
const config = new Config({
loadEnv: false,
configPath: testConfigPath
});
expect(config.get('custom.value')).toBe('test');
});
});
describe('get', () => {
let config;
beforeEach(() => {
config = new Config({
loadEnv: false,
api: {
baseUrl: 'https://test.com',
timeout: 5000
},
nested: {
deep: {
value: 'found'
}
}
});
});
it('should get top-level value', () => {
expect(config.get('api')).toEqual({
baseUrl: 'https://test.com',
timeout: 5000
});
});
it('should get nested value with dot notation', () => {
expect(config.get('api.baseUrl')).toBe('https://test.com');
expect(config.get('nested.deep.value')).toBe('found');
});
it('should return default for non-existent key', () => {
expect(config.get('nonexistent', 'default')).toBe('default');
});
it('should return undefined for non-existent key without default', () => {
expect(config.get('nonexistent')).toBeUndefined();
});
it('should read from environment variables', () => {
process.env.AGENTIC_SYNTH_API_KEY = 'env-key-123';
const config = new Config({ loadEnv: false });
expect(config.get('api.key')).toBe('env-key-123');
});
it('should prioritize environment over config file', () => {
process.env.AGENTIC_SYNTH_CUSTOM_VALUE = 'from-env';
const config = new Config({
loadEnv: false,
custom: { value: 'from-config' }
});
expect(config.get('custom.value')).toBe('from-env');
});
});
describe('set', () => {
let config;
beforeEach(() => {
config = new Config({ loadEnv: false });
});
it('should set top-level value', () => {
config.set('newKey', 'newValue');
expect(config.get('newKey')).toBe('newValue');
});
it('should set nested value with dot notation', () => {
config.set('nested.deep.value', 'test');
expect(config.get('nested.deep.value')).toBe('test');
});
it('should create nested structure if not exists', () => {
config.set('a.b.c.d', 'deep');
expect(config.get('a.b.c.d')).toBe('deep');
});
it('should update existing value', () => {
config.set('api.baseUrl', 'https://new.com');
expect(config.get('api.baseUrl')).toBe('https://new.com');
});
});
describe('loadFromFile', () => {
it('should load JSON config', () => {
const configData = {
api: { baseUrl: 'https://json.com' }
};
writeFileSync(testConfigPath, JSON.stringify(configData));
const config = new Config({ loadEnv: false });
config.loadFromFile(testConfigPath);
expect(config.get('api.baseUrl')).toBe('https://json.com');
});
it('should load YAML config', () => {
const yamlPath = testConfigPath.replace('.json', '.yaml');
writeFileSync(yamlPath, 'api:\n baseUrl: https://yaml.com');
const config = new Config({ loadEnv: false });
config.loadFromFile(yamlPath);
expect(config.get('api.baseUrl')).toBe('https://yaml.com');
unlinkSync(yamlPath);
});
it('should throw error for invalid JSON', () => {
writeFileSync(testConfigPath, 'invalid json');
const config = new Config({ loadEnv: false });
expect(() => config.loadFromFile(testConfigPath)).toThrow();
});
it('should throw error for unsupported format', () => {
const txtPath = testConfigPath.replace('.json', '.txt');
writeFileSync(txtPath, 'text');
const config = new Config({ loadEnv: false });
expect(() => config.loadFromFile(txtPath)).toThrow('Unsupported config file format');
unlinkSync(txtPath);
});
it('should throw error for non-existent file', () => {
const config = new Config({ loadEnv: false });
expect(() => config.loadFromFile('/nonexistent/file.json')).toThrow();
});
});
describe('validate', () => {
let config;
beforeEach(() => {
config = new Config({
loadEnv: false,
api: { baseUrl: 'https://test.com' },
cache: { maxSize: 100 }
});
});
it('should pass validation for existing keys', () => {
expect(() => config.validate(['api.baseUrl', 'cache.maxSize'])).not.toThrow();
});
it('should throw error for missing required keys', () => {
expect(() => config.validate(['nonexistent'])).toThrow('Missing required configuration: nonexistent');
});
it('should list all missing keys', () => {
expect(() => config.validate(['missing1', 'missing2'])).toThrow('missing1, missing2');
});
it('should return true on successful validation', () => {
expect(config.validate(['api.baseUrl'])).toBe(true);
});
});
describe('getAll', () => {
it('should return all configuration', () => {
const config = new Config({
loadEnv: false,
custom: { value: 'test' }
});
const all = config.getAll();
expect(all).toHaveProperty('custom');
expect(all.custom.value).toBe('test');
});
it('should return copy not reference', () => {
const config = new Config({ loadEnv: false });
const all = config.getAll();
all.modified = true;
expect(config.get('modified')).toBeUndefined();
});
});
describe('_parseValue', () => {
let config;
beforeEach(() => {
config = new Config({ loadEnv: false });
});
it('should parse JSON strings', () => {
expect(config._parseValue('{"key":"value"}')).toEqual({ key: 'value' });
expect(config._parseValue('[1,2,3]')).toEqual([1, 2, 3]);
});
it('should parse booleans', () => {
expect(config._parseValue('true')).toBe(true);
expect(config._parseValue('false')).toBe(false);
});
it('should parse numbers', () => {
expect(config._parseValue('123')).toBe(123);
expect(config._parseValue('45.67')).toBe(45.67);
});
it('should return string for unparseable values', () => {
expect(config._parseValue('plain text')).toBe('plain text');
});
});
describe('default configuration', () => {
it('should have sensible defaults', () => {
const config = new Config({ loadEnv: false });
expect(config.get('api.timeout')).toBe(5000);
expect(config.get('cache.maxSize')).toBe(100);
expect(config.get('cache.ttl')).toBe(3600000);
expect(config.get('router.strategy')).toBe('round-robin');
});
});
});

View File

@@ -0,0 +1,161 @@
/**
* Unit tests for DataGenerator
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { DataGenerator } from '../../../src/generators/data-generator.js';
describe('DataGenerator', () => {
let generator;
beforeEach(() => {
generator = new DataGenerator({
seed: 12345,
schema: {
name: { type: 'string', length: 10 },
age: { type: 'number', min: 18, max: 65 },
active: { type: 'boolean' },
tags: { type: 'array', items: 5 },
embedding: { type: 'vector', dimensions: 128 }
}
});
});
describe('constructor', () => {
it('should create generator with default options', () => {
const gen = new DataGenerator();
expect(gen).toBeDefined();
expect(gen.format).toBe('json');
});
it('should accept custom options', () => {
const gen = new DataGenerator({
seed: 99999,
format: 'csv',
schema: { test: { type: 'string' } }
});
expect(gen.seed).toBe(99999);
expect(gen.format).toBe('csv');
expect(gen.schema).toHaveProperty('test');
});
});
describe('generate', () => {
it('should generate specified number of records', () => {
const data = generator.generate(5);
expect(data).toHaveLength(5);
});
it('should generate single record by default', () => {
const data = generator.generate();
expect(data).toHaveLength(1);
});
it('should throw error for invalid count', () => {
expect(() => generator.generate(0)).toThrow('Count must be at least 1');
expect(() => generator.generate(-5)).toThrow('Count must be at least 1');
});
it('should generate records with correct schema fields', () => {
const data = generator.generate(1);
const record = data[0];
expect(record).toHaveProperty('id');
expect(record).toHaveProperty('name');
expect(record).toHaveProperty('age');
expect(record).toHaveProperty('active');
expect(record).toHaveProperty('tags');
expect(record).toHaveProperty('embedding');
});
it('should generate unique IDs', () => {
const data = generator.generate(10);
const ids = data.map(r => r.id);
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(10);
});
});
describe('field generation', () => {
it('should generate strings of correct length', () => {
const data = generator.generate(1);
expect(data[0].name).toHaveLength(10);
expect(typeof data[0].name).toBe('string');
});
it('should generate numbers within range', () => {
const data = generator.generate(100);
data.forEach(record => {
expect(record.age).toBeGreaterThanOrEqual(18);
expect(record.age).toBeLessThanOrEqual(65);
});
});
it('should generate boolean values', () => {
const data = generator.generate(1);
expect(typeof data[0].active).toBe('boolean');
});
it('should generate arrays of correct length', () => {
const data = generator.generate(1);
expect(Array.isArray(data[0].tags)).toBe(true);
expect(data[0].tags).toHaveLength(5);
});
it('should generate vectors with correct dimensions', () => {
const data = generator.generate(1);
expect(Array.isArray(data[0].embedding)).toBe(true);
expect(data[0].embedding).toHaveLength(128);
// Check all values are numbers between 0 and 1
data[0].embedding.forEach(val => {
expect(typeof val).toBe('number');
expect(val).toBeGreaterThanOrEqual(0);
expect(val).toBeLessThanOrEqual(1);
});
});
});
describe('setSeed', () => {
it('should allow updating seed', () => {
generator.setSeed(54321);
expect(generator.seed).toBe(54321);
});
it('should produce same results with same seed', () => {
const gen1 = new DataGenerator({ seed: 12345, schema: { val: { type: 'number' } } });
const gen2 = new DataGenerator({ seed: 12345, schema: { val: { type: 'number' } } });
// Note: This test may be flaky due to random number generation
// In real implementation, you'd want seeded random number generation
expect(gen1.seed).toBe(gen2.seed);
});
});
describe('performance', () => {
it('should generate 1000 records quickly', () => {
const start = Date.now();
const data = generator.generate(1000);
const duration = Date.now() - start;
expect(data).toHaveLength(1000);
expect(duration).toBeLessThan(1000); // Less than 1 second
});
it('should handle large vector dimensions efficiently', () => {
const largeVectorGen = new DataGenerator({
schema: {
embedding: { type: 'vector', dimensions: 4096 }
}
});
const start = Date.now();
const data = largeVectorGen.generate(100);
const duration = Date.now() - start;
expect(data).toHaveLength(100);
expect(data[0].embedding).toHaveLength(4096);
expect(duration).toBeLessThan(2000); // Less than 2 seconds
});
});
});

View File

@@ -0,0 +1,257 @@
/**
* Unit tests for ModelRouter
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { ModelRouter } from '../../../src/routing/model-router.js';
describe('ModelRouter', () => {
let router;
let models;
beforeEach(() => {
models = [
{ id: 'model-1', endpoint: 'http://api1.com', capabilities: ['general', 'code'] },
{ id: 'model-2', endpoint: 'http://api2.com', capabilities: ['general'] },
{ id: 'model-3', endpoint: 'http://api3.com', capabilities: ['math', 'reasoning'] }
];
router = new ModelRouter({
models,
strategy: 'round-robin'
});
});
describe('constructor', () => {
it('should create router with default options', () => {
const defaultRouter = new ModelRouter();
expect(defaultRouter.models).toEqual([]);
expect(defaultRouter.strategy).toBe('round-robin');
});
it('should accept custom options', () => {
expect(router.models).toEqual(models);
expect(router.strategy).toBe('round-robin');
});
it('should initialize model stats', () => {
models.forEach(model => {
const stats = router.getStats(model.id);
expect(stats).toBeDefined();
expect(stats.requests).toBe(0);
expect(stats.errors).toBe(0);
});
});
});
describe('registerModel', () => {
it('should register new model', () => {
const newModel = { id: 'model-4', endpoint: 'http://api4.com' };
router.registerModel(newModel);
expect(router.models).toContain(newModel);
expect(router.getStats('model-4')).toBeDefined();
});
it('should throw error for invalid model', () => {
expect(() => router.registerModel({})).toThrow('Model must have id and endpoint');
expect(() => router.registerModel({ id: 'test' })).toThrow('Model must have id and endpoint');
});
it('should initialize stats for new model', () => {
const newModel = { id: 'model-4', endpoint: 'http://api4.com' };
router.registerModel(newModel);
const stats = router.getStats('model-4');
expect(stats.requests).toBe(0);
expect(stats.errors).toBe(0);
expect(stats.avgLatency).toBe(0);
});
});
describe('route - round-robin', () => {
it('should distribute requests evenly', () => {
const results = [];
for (let i = 0; i < 6; i++) {
results.push(router.route({}));
}
expect(results[0]).toBe('model-1');
expect(results[1]).toBe('model-2');
expect(results[2]).toBe('model-3');
expect(results[3]).toBe('model-1');
expect(results[4]).toBe('model-2');
expect(results[5]).toBe('model-3');
});
it('should wrap around after reaching end', () => {
for (let i = 0; i < 3; i++) {
router.route({});
}
expect(router.route({})).toBe('model-1');
});
});
describe('route - least-latency', () => {
beforeEach(() => {
router.strategy = 'least-latency';
// Record some metrics
router.recordMetrics('model-1', 100);
router.recordMetrics('model-2', 50);
router.recordMetrics('model-3', 150);
});
it('should route to model with lowest latency', () => {
const modelId = router.route({});
expect(modelId).toBe('model-2');
});
it('should update as latencies change', () => {
router.recordMetrics('model-1', 20);
router.recordMetrics('model-1', 20);
const modelId = router.route({});
expect(modelId).toBe('model-1');
});
});
describe('route - cost-optimized', () => {
beforeEach(() => {
router.strategy = 'cost-optimized';
});
it('should route small requests to first model', () => {
const smallRequest = { data: 'test' };
const modelId = router.route(smallRequest);
expect(modelId).toBe('model-1');
});
it('should route large requests to last model', () => {
const largeRequest = { data: 'x'.repeat(2000) };
const modelId = router.route(largeRequest);
expect(modelId).toBe('model-3');
});
});
describe('route - capability-based', () => {
beforeEach(() => {
router.strategy = 'capability-based';
});
it('should route to model with required capability', () => {
const request = { capability: 'code' };
const modelId = router.route(request);
expect(modelId).toBe('model-1');
});
it('should route math requests to capable model', () => {
const request = { capability: 'math' };
const modelId = router.route(request);
expect(modelId).toBe('model-3');
});
it('should fallback to first model if no match', () => {
const request = { capability: 'unsupported' };
const modelId = router.route(request);
expect(modelId).toBe('model-1');
});
});
describe('route - error handling', () => {
it('should throw error when no models available', () => {
const emptyRouter = new ModelRouter();
expect(() => emptyRouter.route({})).toThrow('No models available for routing');
});
});
describe('recordMetrics', () => {
it('should record successful requests', () => {
router.recordMetrics('model-1', 100, true);
const stats = router.getStats('model-1');
expect(stats.requests).toBe(1);
expect(stats.errors).toBe(0);
expect(stats.avgLatency).toBe(100);
});
it('should record failed requests', () => {
router.recordMetrics('model-1', 100, false);
const stats = router.getStats('model-1');
expect(stats.requests).toBe(1);
expect(stats.errors).toBe(1);
});
it('should calculate average latency', () => {
router.recordMetrics('model-1', 100);
router.recordMetrics('model-1', 200);
router.recordMetrics('model-1', 300);
const stats = router.getStats('model-1');
expect(stats.avgLatency).toBe(200);
});
it('should handle non-existent model gracefully', () => {
router.recordMetrics('nonexistent', 100);
expect(router.getStats('nonexistent')).toBeUndefined();
});
});
describe('getStats', () => {
it('should return stats for specific model', () => {
router.recordMetrics('model-1', 100);
const stats = router.getStats('model-1');
expect(stats).toHaveProperty('requests');
expect(stats).toHaveProperty('errors');
expect(stats).toHaveProperty('avgLatency');
});
it('should return all stats when no model specified', () => {
const allStats = router.getStats();
expect(allStats).toHaveProperty('model-1');
expect(allStats).toHaveProperty('model-2');
expect(allStats).toHaveProperty('model-3');
});
it('should track multiple models independently', () => {
router.recordMetrics('model-1', 100);
router.recordMetrics('model-2', 200);
expect(router.getStats('model-1').avgLatency).toBe(100);
expect(router.getStats('model-2').avgLatency).toBe(200);
});
});
describe('performance', () => {
it('should handle 1000 routing decisions quickly', () => {
const start = Date.now();
for (let i = 0; i < 1000; i++) {
router.route({});
}
const duration = Date.now() - start;
expect(duration).toBeLessThan(100); // Less than 100ms
});
it('should efficiently handle many models', () => {
const manyModels = Array.from({ length: 100 }, (_, i) => ({
id: `model-${i}`,
endpoint: `http://api${i}.com`
}));
const largeRouter = new ModelRouter({ models: manyModels });
const start = Date.now();
for (let i = 0; i < 1000; i++) {
largeRouter.route({});
}
const duration = Date.now() - start;
expect(duration).toBeLessThan(200);
});
});
});