const TaskScheduler = require('./async-scheduler'); // Suppress unhandled rejection warnings in tests process.on('unhandledRejection', () => {}); describe('TaskScheduler', () => { describe('constructor', () => { it('should create a scheduler with custom concurrency', () => { const scheduler = new TaskScheduler(5); expect(scheduler.concurrencyLimit).toBe(5); }); it('should start in paused mode by default', () => { const scheduler = new TaskScheduler(); expect(scheduler.isProcessing).toBe(false); }); }); describe('addTask', () => { it('should return a promise', () => { const scheduler = new TaskScheduler(); const promise = scheduler.addTask(() => Promise.resolve('result')); expect(promise).toBeInstanceOf(Promise); }); it('should resolve when task completes successfully', async () => { const scheduler = new TaskScheduler(); scheduler.play(); const promise = scheduler.addTask(() => Promise.resolve('success')); await expect(promise).resolves.toBe('success'); }); it('should reject when task fails', async () => { const scheduler = new TaskScheduler(); scheduler.play(); const promise = scheduler.addTask(() => Promise.reject(new Error('task failed'))); await expect(promise).rejects.toThrow('task failed'); }); it('should capture synchronous throws as rejections', async () => { const scheduler = new TaskScheduler(); scheduler.play(); const promise = scheduler.addTask(() => { throw new Error('sync error'); }); await expect(promise).rejects.toThrow('sync error'); }); it('should queue task if scheduler is paused', async () => { const scheduler = new TaskScheduler(); const mockFn = jest.fn(() => Promise.resolve()); scheduler.addTask(mockFn); await new Promise(r => setTimeout(r, 50)); expect(mockFn).not.toHaveBeenCalled(); }); }); describe('play and pause', () => { it('should start processing when play() is called', async () => { const scheduler = new TaskScheduler(); const mockFn = jest.fn(() => Promise.resolve()); scheduler.addTask(mockFn); expect(mockFn).not.toHaveBeenCalled(); scheduler.play(); await new Promise(r => setTimeout(r, 10)); expect(mockFn).toHaveBeenCalled(); }); it('should stop starting new tasks when pause() is called', async () => { const scheduler = new TaskScheduler(1); const mockFn1 = jest.fn(() => new Promise(r => setTimeout(r, 50))); const mockFn2 = jest.fn(() => Promise.resolve()); scheduler.play(); scheduler.addTask(mockFn1); scheduler.addTask(mockFn2); scheduler.pause(); await new Promise(r => setTimeout(r, 100)); expect(mockFn2).not.toHaveBeenCalled(); }); it('should resume processing when play() is called again', async () => { const scheduler = new TaskScheduler(1); const mockFn1 = jest.fn(() => new Promise(r => setTimeout(() => r(), 50))); const mockFn2 = jest.fn(() => Promise.resolve()); scheduler.play(); scheduler.addTask(mockFn1); scheduler.pause(); scheduler.addTask(mockFn2); await new Promise(r => setTimeout(r, 60)); expect(mockFn2).not.toHaveBeenCalled(); scheduler.play(); await new Promise(r => setTimeout(r, 60)); expect(mockFn2).toHaveBeenCalled(); }); }); describe('concurrency', () => { it('should execute up to concurrency limit simultaneously', async () => { const scheduler = new TaskScheduler(2); const executionTimes = []; const createTask = (id) => async () => { executionTimes.push({ id, event: "start" }); await new Promise(r => setTimeout(r, 50)); executionTimes.push({ id, event: "end" }); }; scheduler.play(); await Promise.all([ scheduler.addTask(createTask(1)), scheduler.addTask(createTask(2)), scheduler.addTask(createTask(3)), ]); // Expected order: // 1. Task 1 starts // 2. Task 2 starts (parallel, within limit) // 3. Task 1 or 2 ends // 4. Task 3 starts (frees up a slot) // 5. Task 3 ends expect(executionTimes[0]).toEqual({id: 1, event: "start"}); expect(executionTimes[1]).toEqual({id: 2, event: "start"}); expect(executionTimes[2]).toEqual({id: 1, event: "end"}); expect(executionTimes[3]).toEqual({id: 3, event: "start"}); expect(executionTimes[4]).toEqual({id: 2, event: "end"}); expect(executionTimes[5]).toEqual({id: 3, event: "end"}); }); it('should queue tasks when concurrency limit is reached', async () => { const scheduler = new TaskScheduler(1); const execOrder = []; const createTask = (id) => async () => { execOrder.push(`start-${id}`); await new Promise(r => setTimeout(r, 10)); execOrder.push(`end-${id}`); }; scheduler.play(); await Promise.all([ scheduler.addTask(createTask(1)), scheduler.addTask(createTask(2)), scheduler.addTask(createTask(3)), ]); expect(execOrder).toEqual([ 'start-1', 'end-1', 'start-2', 'end-2', 'start-3', 'end-3', ]); }); }); describe('FIFO ordering', () => { it('should execute tasks in FIFO order', async () => { const scheduler = new TaskScheduler(1); const execOrder = []; const createTask = (id) => () => { execOrder.push(id); return Promise.resolve(); }; scheduler.play(); await Promise.all([ scheduler.addTask(createTask(1)), scheduler.addTask(createTask(2)), scheduler.addTask(createTask(3)), scheduler.addTask(createTask(4)), ]); expect(execOrder).toEqual([1, 2, 3, 4]); }); it('should maintain FIFO order even with variable task durations', async () => { const scheduler = new TaskScheduler(1); const execOrder = []; const createTask = (id, duration) => async () => { execOrder.push(`start-${id}`); await new Promise(r => setTimeout(r, duration)); execOrder.push(`end-${id}`); }; scheduler.play(); await Promise.all([ scheduler.addTask(createTask(1, 30)), scheduler.addTask(createTask(2, 10)), scheduler.addTask(createTask(3, 20)), ]); expect(execOrder).toEqual([ 'start-1', 'end-1', 'start-2', 'end-2', 'start-3', 'end-3', ]); }); }); describe('automatic task pickup after completion', () => { it('should pick next task from queue when running task completes', async () => { const scheduler = new TaskScheduler(1); const mockFn2 = jest.fn(() => Promise.resolve()); scheduler.play(); await scheduler.addTask(() => new Promise(r => setTimeout(r, 20))); const p2 = scheduler.addTask(mockFn2); await p2; expect(mockFn2).toHaveBeenCalled(); }); // it('should pick next task even if current task rejects', async () => { // const scheduler = new TaskScheduler(1); // const mockFn2 = jest.fn(() => Promise.resolve()); // const p1 = scheduler.addTask(() => Promise.reject(new Error("this should fail"))); // p1.catch(() => {}); // suppress unhandled rejection // scheduler.play(); // try { // await p1; // } catch (e) { // // expected // } // const p2 = scheduler.addTask(mockFn2); // await p2; // expect(mockFn2).toHaveBeenCalled(); // }); }); describe('edge cases', () => { it('should handle concurrency of 1', async () => { const scheduler = new TaskScheduler(1); const execOrder = []; scheduler.play(); await Promise.all([ scheduler.addTask(() => { execOrder.push(1); return Promise.resolve(); }), scheduler.addTask(() => { execOrder.push(2); return Promise.resolve(); }), ]); expect(execOrder).toEqual([1, 2]); }); it('should handle large concurrency', async () => { const scheduler = new TaskScheduler(100); const promises = []; scheduler.play(); for (let i = 0; i < 50; i++) { promises.push(scheduler.addTask(() => Promise.resolve(i))); } const results = await Promise.all(promises); expect(results.length).toBe(50); }); it('should handle empty queue gracefully', () => { const scheduler = new TaskScheduler(); scheduler.play(); // Should not throw expect(() => scheduler._processQueue()).not.toThrow(); }); }); });