/** * Download Manager Tests * Tests for parallel download queue, priority system, retry logic */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import DownloadManager from '../src/download-manager.js' const { PRIORITY } = DownloadManager describe('DownloadManager - Parallel Processing', () => { let manager let mockDownloadFn beforeEach(() => { manager = new DownloadManager({ maxConcurrent: 2, maxRetries: 2 }) // Mock download function mockDownloadFn = vi.fn(async ({ url }) => { // Simulate download delay await new Promise(resolve => setTimeout(resolve, 100)) return { success: true, url } }) }) afterEach(() => { if (manager) { manager.cancelAll() } }) describe('Queue Management', () => { it('should initialize with correct settings', () => { expect(manager.maxConcurrent).toBeGreaterThan(0) expect(manager.maxRetries).toBe(2) expect(manager.activeDownloads.size).toBe(0) expect(manager.queuedDownloads.length).toBe(0) }) it('should get stats correctly', () => { const stats = manager.getStats() expect(stats).toHaveProperty('active') expect(stats).toHaveProperty('queued') expect(stats).toHaveProperty('maxConcurrent') expect(stats).toHaveProperty('completed') expect(stats).toHaveProperty('canAcceptMore') expect(stats.active).toBe(0) expect(stats.queued).toBe(0) }) it('should detect if video is already downloading', async () => { const downloadPromise = manager.addDownload({ videoId: 'test1', url: 'https://youtube.com/watch?v=test1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }) expect(manager.isDownloading('test1')).toBe(true) await downloadPromise expect(manager.isDownloading('test1')).toBe(false) }) it('should prevent duplicate video downloads', async () => { const downloadPromise = manager.addDownload({ videoId: 'test1', url: 'https://youtube.com/watch?v=test1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }) await expect(async () => { await manager.addDownload({ videoId: 'test1', url: 'https://youtube.com/watch?v=test1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }) }).rejects.toThrow('already being downloaded') await downloadPromise }) }) describe('Priority System', () => { it('should add downloads with default NORMAL priority', async () => { // Fill up active downloads first const slowDownload = vi.fn(async () => { await new Promise(resolve => setTimeout(resolve, 500)) return { success: true } }) manager.addDownload({ videoId: 'active1', url: 'https://youtube.com/watch?v=active1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: slowDownload }) manager.addDownload({ videoId: 'active2', url: 'https://youtube.com/watch?v=active2', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: slowDownload }) // Now this one goes to queue manager.addDownload({ videoId: 'test1', url: 'https://youtube.com/watch?v=test1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }).catch(() => {}) // Suppress cancellation errors expect(manager.queuedDownloads[0].priority).toBe(PRIORITY.NORMAL) manager.cancelAll() }) it('should accept custom priority', async () => { // Fill up active downloads first const slowDownload = vi.fn(async () => { await new Promise(resolve => setTimeout(resolve, 500)) return { success: true } }) manager.addDownload({ videoId: 'active1', url: 'https://youtube.com/watch?v=active1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: slowDownload }) manager.addDownload({ videoId: 'active2', url: 'https://youtube.com/watch?v=active2', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: slowDownload }) // Now this one goes to queue with HIGH priority manager.addDownload({ videoId: 'test1', url: 'https://youtube.com/watch?v=test1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }, PRIORITY.HIGH).catch(() => {}) // Suppress cancellation errors expect(manager.queuedDownloads[0].priority).toBe(PRIORITY.HIGH) manager.cancelAll() }) it('should sort queue by priority', async () => { // Fill up active downloads first (maxConcurrent = 2) const slowDownload = vi.fn(async () => { await new Promise(resolve => setTimeout(resolve, 500)) return { success: true } }) manager.addDownload({ videoId: 'active1', url: 'https://youtube.com/watch?v=active1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: slowDownload }) manager.addDownload({ videoId: 'active2', url: 'https://youtube.com/watch?v=active2', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: slowDownload }) // Now add to queue with different priorities manager.addDownload({ videoId: 'low', url: 'https://youtube.com/watch?v=low', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }, PRIORITY.LOW).catch(() => {}) // Suppress cancellation errors manager.addDownload({ videoId: 'high', url: 'https://youtube.com/watch?v=high', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }, PRIORITY.HIGH).catch(() => {}) // Suppress cancellation errors manager.addDownload({ videoId: 'normal', url: 'https://youtube.com/watch?v=normal', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }, PRIORITY.NORMAL).catch(() => {}) // Suppress cancellation errors // Check queue order expect(manager.queuedDownloads[0].videoId).toBe('high') expect(manager.queuedDownloads[1].videoId).toBe('normal') expect(manager.queuedDownloads[2].videoId).toBe('low') // Clean up manager.cancelAll() }) it('should allow changing priority of queued download', async () => { // Fill active downloads const slowDownload = vi.fn(async () => { await new Promise(resolve => setTimeout(resolve, 500)) return { success: true } }) manager.addDownload({ videoId: 'active1', url: 'https://youtube.com/watch?v=active1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: slowDownload }) manager.addDownload({ videoId: 'active2', url: 'https://youtube.com/watch?v=active2', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: slowDownload }) // Add low priority download manager.addDownload({ videoId: 'test1', url: 'https://youtube.com/watch?v=test1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }, PRIORITY.LOW).catch(() => {}) // Suppress cancellation errors // Change to high priority const changed = manager.setPriority('test1', PRIORITY.HIGH) expect(changed).toBe(true) const request = manager.queuedDownloads.find(r => r.videoId === 'test1') expect(request.priority).toBe(PRIORITY.HIGH) // Clean up manager.cancelAll() }) }) describe('Retry Logic', () => { it('should have retry configuration', () => { expect(manager.maxRetries).toBe(2) }) it('should identify retryable errors', () => { const retryableErrors = [ new Error('network timeout'), new Error('ECONNRESET'), new Error('ETIMEDOUT'), new Error('ENOTFOUND'), new Error('503 Service Unavailable') ] retryableErrors.forEach(error => { expect(manager.isRetryableError(error)).toBe(true) }) }) it('should identify non-retryable errors', () => { const nonRetryableErrors = [ new Error('Video unavailable'), new Error('Permission denied'), new Error('Invalid URL') ] nonRetryableErrors.forEach(error => { expect(manager.isRetryableError(error)).toBe(false) }) }) }) describe('Cancellation', () => { it('should cancel queued download', async () => { // Fill active slots const slowDownload = vi.fn(async () => { await new Promise(resolve => setTimeout(resolve, 500)) return { success: true } }) manager.addDownload({ videoId: 'active1', url: 'https://youtube.com/watch?v=active1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: slowDownload }) manager.addDownload({ videoId: 'active2', url: 'https://youtube.com/watch?v=active2', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: slowDownload }) // Add to queue const queuedPromise = manager.addDownload({ videoId: 'queued1', url: 'https://youtube.com/watch?v=queued1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }) // Cancel it const cancelled = manager.cancelDownload('queued1') expect(cancelled).toBe(true) expect(manager.queuedDownloads.find(r => r.videoId === 'queued1')).toBeUndefined() await expect(queuedPromise).rejects.toThrow('cancelled') // Clean up manager.cancelAll() }) it('should cancel all downloads', () => { // Add some downloads (catch rejections to avoid unhandled errors) manager.addDownload({ videoId: 'test1', url: 'https://youtube.com/watch?v=test1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }).catch(() => {}) manager.addDownload({ videoId: 'test2', url: 'https://youtube.com/watch?v=test2', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }).catch(() => {}) const result = manager.cancelAll() expect(result.total).toBeGreaterThanOrEqual(0) expect(manager.queuedDownloads.length).toBe(0) expect(manager.activeDownloads.size).toBe(0) }) }) describe('Event Emission', () => { it('should emit queueUpdated event', async () => { return new Promise((resolve) => { manager.on('queueUpdated', (stats) => { expect(stats).toHaveProperty('active') expect(stats).toHaveProperty('queued') resolve() }) manager.addDownload({ videoId: 'test1', url: 'https://youtube.com/watch?v=test1', quality: '720p', format: 'mp4', savePath: '/tmp', downloadFn: mockDownloadFn }).catch(() => {}) }) }) }) })