Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user