/** * Unit tests for QueueManager * * Tests core queue operations, status management, and pub/sub functionality. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { QueueManager } from '$lib/server/queue/QueueManager'; describe('QueueManager', () => { let queueManager: QueueManager; beforeEach(() => { // Create fresh instance for each test queueManager = new QueueManager(); }); describe('enqueue', () => { it('should enqueue items with unique IDs', () => { const item1 = queueManager.enqueue('https://instagram.com/p/test1'); const item2 = queueManager.enqueue('https://instagram.com/p/test2'); expect(item1.id).toBeTruthy(); expect(item2.id).toBeTruthy(); expect(item1.id).not.toBe(item2.id); }); it('should create items with pending status', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); expect(item.status).toBe('pending'); expect(item.enqueuedAt).toBeTruthy(); expect(item.logs).toEqual([]); expect(item.progressEvents).toEqual([]); expect(item.retryCount).toBe(0); expect(item.maxRetries).toBe(3); }); it('should notify subscribers when enqueueing', () => { const callback = vi.fn(); queueManager.subscribe(callback); const item = queueManager.enqueue('https://instagram.com/p/test'); expect(callback).toHaveBeenCalledWith( expect.objectContaining({ itemId: item.id, status: 'pending' }) ); }); }); describe('dequeue', () => { it('should dequeue oldest pending item first (FIFO)', () => { const item1 = queueManager.enqueue('https://instagram.com/p/test1'); const item2 = queueManager.enqueue('https://instagram.com/p/test2'); const dequeued1 = queueManager.dequeue(); expect(dequeued1?.id).toBe(item1.id); const dequeued2 = queueManager.dequeue(); expect(dequeued2?.id).toBe(item2.id); }); it('should return null when queue is empty', () => { const item = queueManager.dequeue(); expect(item).toBeNull(); }); it('should mark dequeued item as in_progress', () => { const enqueuedItem = queueManager.enqueue('https://instagram.com/p/test'); const dequeuedItem = queueManager.dequeue(); expect(dequeuedItem?.status).toBe('in_progress'); expect(dequeuedItem?.currentPhase).toBe('extraction'); expect(dequeuedItem?.startedAt).toBeTruthy(); }); it('should skip non-pending items', () => { const item1 = queueManager.enqueue('https://instagram.com/p/test1'); const item2 = queueManager.enqueue('https://instagram.com/p/test2'); // Dequeue first item queueManager.dequeue(); // Second item should be next const dequeued = queueManager.dequeue(); expect(dequeued?.id).toBe(item2.id); }); }); describe('updateStatus', () => { it('should update item status', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); queueManager.updateStatus(item.id, 'in_progress', { phase: 'parsing' }); const updated = queueManager.get(item.id); expect(updated?.status).toBe('in_progress'); expect(updated?.currentPhase).toBe('parsing'); }); it('should set completedAt for terminal statuses', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); queueManager.updateStatus(item.id, 'success'); const updated = queueManager.get(item.id); expect(updated?.completedAt).toBeTruthy(); }); it('should merge additional data into item', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); queueManager.updateStatus(item.id, 'success', { recipe: { name: 'Test Recipe' }, tandoorRecipeId: 123 }); const updated = queueManager.get(item.id); expect(updated?.recipe).toEqual({ name: 'Test Recipe' }); expect(updated?.tandoorRecipeId).toBe(123); }); it('should handle error data', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); const errorData = { error: { phase: 'extraction' as const, message: 'Failed to load page', recoverable: true, timestamp: new Date().toISOString() } }; queueManager.updateStatus(item.id, 'unhealthy', errorData); const updated = queueManager.get(item.id); expect(updated?.error).toEqual(errorData.error); }); }); describe('addProgressEvent', () => { it('should add progress events to item', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); const event = { type: 'status', message: 'Extracting...', timestamp: new Date().toISOString() }; queueManager.addProgressEvent(item.id, event); const updated = queueManager.get(item.id); expect(updated?.progressEvents).toHaveLength(1); expect(updated?.progressEvents[0]).toEqual(event); }); it('should add event message to logs', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); queueManager.addProgressEvent(item.id, { type: 'status', message: 'Test message', timestamp: new Date().toISOString() }); const updated = queueManager.get(item.id); expect(updated?.logs).toContain('Test message'); }); it('should notify subscribers with event data', () => { const callback = vi.fn(); queueManager.subscribe(callback); const item = queueManager.enqueue('https://instagram.com/p/test'); callback.mockClear(); // Clear enqueue notification const event = { type: 'status', message: 'Test', timestamp: new Date().toISOString() }; queueManager.addProgressEvent(item.id, event); expect(callback).toHaveBeenCalledWith( expect.objectContaining({ itemId: item.id, data: { event } }) ); }); }); describe('remove', () => { it('should remove items by ID', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); const removed = queueManager.remove(item.id); expect(removed).toBe(true); expect(queueManager.get(item.id)).toBeUndefined(); }); it('should return false for non-existent items', () => { const removed = queueManager.remove('non-existent-id'); expect(removed).toBe(false); }); it('should notify subscribers when removing', () => { const callback = vi.fn(); queueManager.subscribe(callback); const item = queueManager.enqueue('https://instagram.com/p/test'); callback.mockClear(); queueManager.remove(item.id); expect(callback).toHaveBeenCalledWith( expect.objectContaining({ itemId: item.id, data: { removed: true } }) ); }); }); describe('retry', () => { it('should retry failed items', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); queueManager.updateStatus(item.id, 'error'); const retried = queueManager.retry(item.id); expect(retried).toBe(true); const updated = queueManager.get(item.id); expect(updated?.status).toBe('pending'); expect(updated?.retryCount).toBe(1); expect(updated?.error).toBeUndefined(); expect(updated?.currentPhase).toBeUndefined(); }); it('should not retry items in progress', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); queueManager.updateStatus(item.id, 'in_progress'); const retried = queueManager.retry(item.id); expect(retried).toBe(false); expect(queueManager.get(item.id)?.status).toBe('in_progress'); }); it('should increment retry count', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); queueManager.updateStatus(item.id, 'error'); queueManager.retry(item.id); queueManager.retry(item.id); expect(queueManager.get(item.id)?.retryCount).toBe(2); }); }); describe('getAll', () => { it('should return all queue items', () => { queueManager.enqueue('https://instagram.com/p/test1'); queueManager.enqueue('https://instagram.com/p/test2'); queueManager.enqueue('https://instagram.com/p/test3'); const items = queueManager.getAll(); expect(items).toHaveLength(3); }); it('should return empty array when queue is empty', () => { const items = queueManager.getAll(); expect(items).toEqual([]); }); }); describe('get', () => { it('should return item by ID', () => { const item = queueManager.enqueue('https://instagram.com/p/test'); const retrieved = queueManager.get(item.id); expect(retrieved?.id).toBe(item.id); expect(retrieved?.url).toBe(item.url); }); it('should return undefined for non-existent ID', () => { const item = queueManager.get('non-existent-id'); expect(item).toBeUndefined(); }); }); describe('subscribe', () => { it('should notify subscribers of updates', () => { const callback = vi.fn(); queueManager.subscribe(callback); queueManager.enqueue('https://instagram.com/p/test'); expect(callback).toHaveBeenCalled(); }); it('should return unsubscribe function', () => { const callback = vi.fn(); const unsubscribe = queueManager.subscribe(callback); queueManager.enqueue('https://instagram.com/p/test1'); expect(callback).toHaveBeenCalledTimes(1); unsubscribe(); callback.mockClear(); queueManager.enqueue('https://instagram.com/p/test2'); expect(callback).not.toHaveBeenCalled(); }); it('should handle subscriber errors gracefully', () => { const goodCallback = vi.fn(); const badCallback = vi.fn(() => { throw new Error('Subscriber error'); }); queueManager.subscribe(goodCallback); queueManager.subscribe(badCallback); // Should not throw despite bad callback expect(() => { queueManager.enqueue('https://instagram.com/p/test'); }).not.toThrow(); // Good callback should still be called expect(goodCallback).toHaveBeenCalled(); }); it('should support multiple subscribers', () => { const callback1 = vi.fn(); const callback2 = vi.fn(); const callback3 = vi.fn(); queueManager.subscribe(callback1); queueManager.subscribe(callback2); queueManager.subscribe(callback3); queueManager.enqueue('https://instagram.com/p/test'); expect(callback1).toHaveBeenCalled(); expect(callback2).toHaveBeenCalled(); expect(callback3).toHaveBeenCalled(); }); }); describe('deduplication', () => { it('should return existing item when enqueueing duplicate URL', () => { const url = 'https://instagram.com/p/ABC123'; const firstItem = queueManager.enqueue(url); const secondItem = queueManager.enqueue(url); expect(secondItem.id).toBe(firstItem.id); expect(queueManager.getAll()).toHaveLength(1); }); it('should find item by URL', () => { const url = 'https://instagram.com/p/TEST123'; const item = queueManager.enqueue(url); const found = queueManager.findByUrl(url); expect(found?.id).toBe(item.id); }); it('should return undefined when URL not found', () => { const found = queueManager.findByUrl('https://instagram.com/p/NOTFOUND'); expect(found).toBeUndefined(); }); }); });