fix(ssr): resolve EventSource SSR violations and implement best practices
- Fix EventSource is not defined error in queue dashboard - Add browser guards for all EventSource usage - Replace static constants (EventSource.OPEN/CLOSED) with numeric values - Fix setInterval SSR violation in LLM health indicator - Replace $effect anti-pattern with onMount in share page - Add comprehensive SvelteKit SSR best practices documentation - Add SSR audit and testing verification All changes follow SvelteKit best practices and are verified against official documentation. Production build succeeds with no SSR errors. Closes: FixEventSourceSSR See: docs/outcomes/FixEventSourceSSR.md
This commit is contained in:
356
src/tests/queue-manager.spec.ts
Normal file
356
src/tests/queue-manager.spec.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user