import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { EventEmitter } from 'events';
import {
RequestBatcher,
getBatcher,
resetBatcher,
BatchRequest,
BatchResult,
BatchMetrics,
BatchPriority,
BatchConfig,
ToolCompatibilityGroup
} from '../../src/core/request-batcher.js';
import { ToolRegistry } from '../../src/core/tool-registry.js';
// Mock dependencies
vi.mock('../../src/logger.js', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn()
}
}));
vi.mock('../../src/core/performance-monitor.js', () => ({
getPerformanceMonitor: vi.fn(() => ({
recordQueueMetrics: vi.fn()
}))
}));
vi.mock('../../src/core/tool-registry.js', () => ({
ToolRegistry: {
getInstance: vi.fn(() => ({
getHandler: vi.fn()
}))
}
}));
vi.mock('../../src/core/execution-context.js', () => ({
ExecutionContextImpl: class {
recordToolExecution = vi.fn();
cleanup = vi.fn();
}
}));
// Mock crypto for deterministic IDs in tests
vi.mock('crypto', () => ({
randomUUID: vi.fn(() => 'test-uuid-' + Math.random().toString(36).substr(2, 9))
}));
describe('RequestBatcher - Simple Tests', () => {
let batcher: RequestBatcher;
let mockToolRegistry: any;
beforeEach(() => {
vi.clearAllMocks();
resetBatcher();
// Setup mock tool registry
mockToolRegistry = {
getHandler: vi.fn()
};
vi.mocked(ToolRegistry.getInstance).mockReturnValue(mockToolRegistry);
batcher = new RequestBatcher({
batchTimeout: 10, // Very short timeout for tests
maxBatchSize: 5,
maxConcurrentBatches: 2
});
});
afterEach(async () => {
if (batcher) {
batcher.clearQueue();
batcher.cleanup();
}
await new Promise(resolve => setTimeout(resolve, 0));
});
describe('Basic Functionality', () => {
it('should initialize with default configuration', () => {
expect(batcher).toBeInstanceOf(RequestBatcher);
expect(batcher).toBeInstanceOf(EventEmitter);
});
it('should accept custom configuration', () => {
const customConfig: Partial<BatchConfig> = {
maxBatchSize: 20,
batchTimeout: 200,
maxConcurrentBatches: 5,
enableParallelExecution: false,
priorityWeighting: false
};
const customBatcher = new RequestBatcher(customConfig);
expect(customBatcher).toBeInstanceOf(RequestBatcher);
customBatcher.cleanup();
});
it('should create singleton instance with getBatcher', () => {
const batcher1 = getBatcher();
const batcher2 = getBatcher();
expect(batcher1).toBe(batcher2);
resetBatcher();
});
it('should provide queue status', () => {
const initialStatus = batcher.getQueueStatus();
expect(initialStatus).toMatchObject({
queueLength: 0,
activeBatches: 0,
priorityDistribution: {
[BatchPriority.LOW]: 0,
[BatchPriority.NORMAL]: 0,
[BatchPriority.HIGH]: 0,
[BatchPriority.CRITICAL]: 0
},
oldestRequestAge: 0
});
});
});
describe('Request Processing', () => {
it('should process a single request successfully', async () => {
const mockHandler = {
execute: vi.fn().mockResolvedValue({
content: [{ type: 'text', text: 'result' }],
isError: false
})
};
mockToolRegistry.getHandler.mockReturnValue(mockHandler);
const result = await batcher.addRequest('test-tool', { arg: 'value' });
expect(result.success).toBe(true);
expect(mockHandler.execute).toHaveBeenCalledWith({ arg: 'value' }, expect.any(Object));
});
it('should handle request with custom priority', async () => {
const mockHandler = {
execute: vi.fn().mockResolvedValue({ content: [{ type: 'text', text: 'result' }] })
};
mockToolRegistry.getHandler.mockReturnValue(mockHandler);
const promise = batcher.addRequest('test-tool', { arg: 'value' }, {
priority: BatchPriority.HIGH
});
const status = batcher.getQueueStatus();
expect(status.priorityDistribution[BatchPriority.HIGH]).toBe(1);
batcher.clearQueue();
await expect(promise).rejects.toThrow('Queue cleared');
});
it('should handle multiple requests', async () => {
const mockHandler = {
execute: vi.fn().mockResolvedValue({
content: [{ type: 'text', text: 'result' }],
isError: false
})
};
mockToolRegistry.getHandler.mockReturnValue(mockHandler);
const promises = [
batcher.addRequest('tool1', {}),
batcher.addRequest('tool2', {}),
batcher.addRequest('tool3', {})
];
const results = await Promise.allSettled(promises);
expect(results.every(r => r.status === 'fulfilled')).toBe(true);
expect(mockHandler.execute).toHaveBeenCalledTimes(3);
});
it('should handle execution failure', async () => {
const mockHandler = {
execute: vi.fn().mockRejectedValue(new Error('Tool execution failed'))
};
mockToolRegistry.getHandler.mockReturnValue(mockHandler);
const result = await batcher.addRequest('failing-tool', {});
expect(result.success).toBe(false);
expect(result.error?.message).toBe('Tool execution failed');
});
it('should handle tool not found error', async () => {
mockToolRegistry.getHandler.mockReturnValue(null);
const result = await batcher.addRequest('nonexistent-tool', {});
expect(result.success).toBe(false);
expect(result.error?.message).toContain('Tool not found');
});
});
describe('Performance Tracking', () => {
it('should track tool performance metrics', async () => {
const mockHandler = {
execute: vi.fn().mockResolvedValue({
content: [{ type: 'text', text: 'result' }],
isError: false
})
};
mockToolRegistry.getHandler.mockReturnValue(mockHandler);
await batcher.addRequest('perf-tool', {});
const metrics = batcher.getToolPerformanceMetrics('perf-tool');
expect(metrics).toBeTruthy();
expect(metrics?.count).toBe(1);
expect(metrics?.averageTime).toBeGreaterThanOrEqual(0);
});
it('should return null for unknown tool metrics', () => {
const metrics = batcher.getToolPerformanceMetrics('unknown-tool');
expect(metrics).toBeNull();
});
it('should track batch metrics', async () => {
const mockHandler = {
execute: vi.fn().mockResolvedValue({
content: [{ type: 'text', text: 'result' }],
isError: false
})
};
mockToolRegistry.getHandler.mockReturnValue(mockHandler);
await batcher.addRequest('test-tool', {});
const batchMetrics = batcher.getBatchMetrics();
expect(batchMetrics).toHaveLength(1);
expect(batchMetrics[0]).toMatchObject({
requestCount: 1,
successCount: 1,
failureCount: 0
});
});
});
describe('Queue Management', () => {
it('should clear queue and reject pending requests', async () => {
const promises = [
batcher.addRequest('tool1', {}),
batcher.addRequest('tool2', {}),
batcher.addRequest('tool3', {})
];
const statusBefore = batcher.getQueueStatus();
expect(statusBefore.queueLength).toBe(3);
batcher.clearQueue();
const statusAfter = batcher.getQueueStatus();
expect(statusAfter.queueLength).toBe(0);
// All promises should be rejected
await Promise.all(
promises.map(p => expect(p).rejects.toThrow('Queue cleared'))
);
});
it('should update configuration dynamically', () => {
const newConfig: Partial<BatchConfig> = {
maxBatchSize: 25,
batchTimeout: 300
};
batcher.updateConfig(newConfig);
// Configuration updated internally - verify through behavior
expect(() => batcher.updateConfig(newConfig)).not.toThrow();
});
});
describe('Cleanup', () => {
it('should cleanup all resources', async () => {
// Add some data and capture promises to handle rejections
const promises = [
batcher.addRequest('tool1', {}),
batcher.addRequest('tool2', {})
];
const cleanupSpy = vi.spyOn(batcher, 'removeAllListeners');
batcher.cleanup();
expect(cleanupSpy).toHaveBeenCalled();
expect(batcher.getQueueStatus().queueLength).toBe(0);
// Handle the rejected promises
await Promise.allSettled(promises);
});
});
describe('Singleton Management', () => {
it('should reset singleton instance', () => {
const batcher1 = getBatcher();
resetBatcher();
const batcher2 = getBatcher();
expect(batcher1).not.toBe(batcher2);
resetBatcher();
});
it('should cleanup before resetting', () => {
const batcher = getBatcher();
const cleanupSpy = vi.spyOn(batcher, 'cleanup');
resetBatcher();
expect(cleanupSpy).toHaveBeenCalled();
});
});
});