Files
insta-recipe/src/tests/queue-manager.spec.ts

381 lines
11 KiB
TypeScript

/**
* 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();
});
});
});