import { beforeEach, describe, expect, it, mock, type Mock } from 'bun:test'; import type { Logger } from '@stock-bot/logger'; import { processBatchJob, processItems } from '../src/batch-processor'; import type { BatchJobData, ProcessOptions, Queue, QueueManager } from '../src/types'; describe('Batch Processor', () => { type MockLogger = { info: Mock<(message: string, meta?: any) => void>; error: Mock<(message: string, meta?: any) => void>; warn: Mock<(message: string, meta?: any) => void>; debug: Mock<(message: string, meta?: any) => void>; trace: Mock<(message: string, meta?: any) => void>; }; type MockQueue = { add: Mock<(name: string, data: any, options?: any) => Promise<{ id: string }>>; addBulk: Mock< (jobs: Array<{ name: string; data: any; opts?: any }>) => Promise> >; createChildLogger: Mock<(component: string, meta?: any) => MockLogger>; getName: Mock<() => string>; }; type MockQueueManager = { getQueue: Mock<(name: string) => MockQueue>; getCache: Mock< (name: string) => { get: Mock<(key: string) => Promise>; set: Mock<(key: string, value: any, ttl?: number) => Promise>; del: Mock<(key: string) => Promise>; } >; }; let mockLogger: MockLogger; let mockQueue: MockQueue; let mockQueueManager: MockQueueManager; let mockCache: { get: Mock<(key: string) => Promise>; set: Mock<(key: string, value: any, ttl?: number) => Promise>; del: Mock<(key: string) => Promise>; }; beforeEach(() => { mockLogger = { info: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), debug: mock(() => {}), trace: mock(() => {}), }; mockQueue = { add: mock(async () => ({ id: 'job-123' })), addBulk: mock(async jobs => jobs.map((_, i) => ({ id: `job-${i + 1}` }))), createChildLogger: mock(() => mockLogger), getName: mock(() => 'test-queue'), }; mockCache = { get: mock(async () => null), set: mock(async () => {}), del: mock(async () => {}), }; mockQueueManager = { getQueue: mock(() => mockQueue), getCache: mock(() => mockCache), }; }); describe('processBatchJob', () => { it('should process all items successfully', async () => { const batchData: BatchJobData = { payloadKey: 'test-payload-key', batchIndex: 0, totalBatches: 1, itemCount: 3, totalDelayHours: 0, }; // Mock the cached payload const cachedPayload = { items: ['item1', 'item2', 'item3'], options: { batchSize: 2, concurrency: 1, }, }; mockCache.get.mockImplementation(async () => cachedPayload); const result = await processBatchJob( batchData, 'test-queue', mockQueueManager as unknown as QueueManager ); expect(mockCache.get).toHaveBeenCalledWith('test-payload-key'); expect(mockQueue.addBulk).toHaveBeenCalled(); expect(result).toBeDefined(); }); it('should handle partial failures', async () => { const batchData: BatchJobData = { payloadKey: 'test-payload-key', batchIndex: 0, totalBatches: 1, itemCount: 3, totalDelayHours: 0, }; // Mock the cached payload const cachedPayload = { items: ['item1', 'item2', 'item3'], options: {}, }; mockCache.get.mockImplementation(async () => cachedPayload); // Make addBulk throw an error to simulate failure mockQueue.addBulk.mockImplementation(async () => { throw new Error('Failed to add jobs'); }); // processBatchJob should still complete even if addBulk fails const result = await processBatchJob( batchData, 'test-queue', mockQueueManager as unknown as QueueManager ); expect(mockQueue.addBulk).toHaveBeenCalled(); // The error is logged in addJobsInChunks, not in processBatchJob expect(mockLogger.error).toHaveBeenCalledWith('Failed to add job chunk', expect.any(Object)); }); it('should handle empty items', async () => { const batchData: BatchJobData = { payloadKey: 'test-payload-key', batchIndex: 0, totalBatches: 1, itemCount: 0, totalDelayHours: 0, }; // Mock the cached payload with empty items const cachedPayload = { items: [], options: {}, }; mockCache.get.mockImplementation(async () => cachedPayload); const result = await processBatchJob( batchData, 'test-queue', mockQueueManager as unknown as QueueManager ); expect(mockQueue.addBulk).not.toHaveBeenCalled(); expect(result).toBeDefined(); }); it('should track duration', async () => { const batchData: BatchJobData = { payloadKey: 'test-payload-key', batchIndex: 0, totalBatches: 1, itemCount: 1, totalDelayHours: 0, }; // Mock the cached payload const cachedPayload = { items: ['item1'], options: {}, }; mockCache.get.mockImplementation(async () => cachedPayload); // Add delay to queue.add mockQueue.add.mockImplementation( () => new Promise(resolve => setTimeout(() => resolve({ id: 'job-1' }), 10)) ); const result = await processBatchJob( batchData, 'test-queue', mockQueueManager as unknown as QueueManager ); expect(result).toBeDefined(); // The function doesn't return duration in its result }); }); describe('processItems', () => { it('should process items with default options', async () => { const items = [1, 2, 3, 4, 5]; const options: ProcessOptions = { totalDelayHours: 0 }; const result = await processItems( items, 'test-queue', options, mockQueueManager as unknown as QueueManager ); expect(result.totalItems).toBe(5); expect(result.jobsCreated).toBe(5); expect(result.mode).toBe('direct'); expect(mockQueue.addBulk).toHaveBeenCalled(); }); it('should process items in batches', async () => { const items = [1, 2, 3, 4, 5]; const options: ProcessOptions = { totalDelayHours: 0, useBatching: true, batchSize: 2, }; const result = await processItems( items, 'test-queue', options, mockQueueManager as unknown as QueueManager ); expect(result.totalItems).toBe(5); expect(result.mode).toBe('batch'); // When batching is enabled, it creates batch jobs instead of individual jobs expect(mockQueue.addBulk).toHaveBeenCalled(); }); it('should handle concurrent processing', async () => { const items = [1, 2, 3, 4]; const options: ProcessOptions = { totalDelayHours: 0, }; const result = await processItems( items, 'test-queue', options, mockQueueManager as unknown as QueueManager ); expect(result.totalItems).toBe(4); expect(result.jobsCreated).toBe(4); expect(mockQueue.addBulk).toHaveBeenCalled(); }); it('should handle empty array', async () => { const items: number[] = []; const options: ProcessOptions = { totalDelayHours: 0 }; const result = await processItems( items, 'test-queue', options, mockQueueManager as unknown as QueueManager ); expect(result.totalItems).toBe(0); expect(result.jobsCreated).toBe(0); expect(result.mode).toBe('direct'); expect(mockQueue.addBulk).not.toHaveBeenCalled(); }); it('should propagate errors', async () => { const items = [1, 2, 3]; const options: ProcessOptions = { totalDelayHours: 0 }; // Make queue.addBulk throw an error mockQueue.addBulk.mockImplementation(async () => { throw new Error('Process error'); }); // processItems catches errors and continues, so it won't reject const result = await processItems( items, 'test-queue', options, mockQueueManager as unknown as QueueManager ); expect(result.jobsCreated).toBe(0); expect(mockQueue.addBulk).toHaveBeenCalled(); expect(mockLogger.error).toHaveBeenCalledWith('Failed to add job chunk', expect.any(Object)); }); it('should process large batches efficiently', async () => { const items = Array.from({ length: 100 }, (_, i) => i); const options: ProcessOptions = { totalDelayHours: 0, useBatching: true, batchSize: 20, }; const result = await processItems( items, 'test-queue', options, mockQueueManager as unknown as QueueManager ); expect(result.totalItems).toBe(100); expect(result.mode).toBe('batch'); // With batching enabled and batch size 20, we should have 5 batch jobs expect(mockQueue.addBulk).toHaveBeenCalled(); }); }); });