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:
Giancarmine Salucci
2025-12-22 03:00:29 +01:00
parent 35d6f6e40a
commit 8545744bb1
47 changed files with 12827 additions and 363 deletions

518
src/tests/queue-api.spec.ts Normal file
View File

@@ -0,0 +1,518 @@
/**
* Integration tests for Queue API endpoints
*
* Tests the HTTP API routes for queue operations by directly invoking the handlers.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { queueManager } from '$lib/server/queue/QueueManager';
import { POST as queuePOST, GET as queueGET } from '../routes/api/queue/+server.js';
import { GET as itemGET, DELETE as itemDELETE } from '../routes/api/queue/[id]/+server.js';
import { POST as retryPOST } from '../routes/api/queue/[id]/retry/+server.js';
describe('Queue API Endpoints', () => {
beforeEach(() => {
// Clear queue between tests
queueManager.getAll().forEach(item => queueManager.remove(item.id));
});
afterEach(() => {
// Clean up after tests
queueManager.getAll().forEach(item => queueManager.remove(item.id));
});
describe('POST /api/queue', () => {
it('should enqueue valid Instagram URL', async () => {
const request = new Request('http://localhost/api/queue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://instagram.com/p/ABC123'
})
});
const response = await queuePOST({ request } as any);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.id).toBeTruthy();
expect(data.url).toBe('https://instagram.com/p/ABC123');
expect(data.status).toBe('pending');
expect(data.enqueuedAt).toBeTruthy();
// Verify item exists in queue
const item = queueManager.get(data.id);
expect(item).toBeTruthy();
expect(item?.url).toBe('https://instagram.com/p/ABC123');
});
it('should accept Instagram URLs with www', async () => {
const request = new Request('http://localhost/api/queue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://www.instagram.com/p/XYZ789'
})
});
const response = await queuePOST({ request } as any);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.url).toBe('https://www.instagram.com/p/XYZ789');
// Verify item exists in queue
const item = queueManager.get(data.id);
expect(item).toBeTruthy();
expect(item?.url).toBe('https://www.instagram.com/p/XYZ789');
});
it('should reject invalid Instagram URL formats', async () => {
const invalidUrls = [
'https://facebook.com/post/123',
'https://instagram.com/user/profile',
'not-a-url',
'https://other-site.com'
];
for (const url of invalidUrls) {
const request = new Request('http://localhost/api/queue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url })
});
try {
const response = await queuePOST({ request } as any);
// If we get here, check the response status
expect(response.status).toBe(400);
const data = await response.json();
expect(data.message).toBe('Invalid Instagram URL format. Expected: https://instagram.com/p/{post-id}');
} catch (err: any) {
// SvelteKit's error() throws - check the error
expect(err.status).toBe(400);
expect(err.body.message).toBe('Invalid Instagram URL format. Expected: https://instagram.com/p/{post-id}');
}
}
// Verify no items were added to queue
expect(queueManager.getAll()).toHaveLength(0);
});
it('should reject missing URL', async () => {
const request = new Request('http://localhost/api/queue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({})
});
try {
const response = await queuePOST({ request } as any);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.message).toBe('URL is required and must be a string');
} catch (err: any) {
expect(err.status).toBe(400);
expect(err.body.message).toBe('URL is required and must be a string');
}
});
it('should reject non-JSON body', async () => {
const request = new Request('http://localhost/api/queue', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
body: 'not json'
});
try {
const response = await queuePOST({ request } as any);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.message).toBe('Invalid JSON in request body');
} catch (err: any) {
expect(err.status).toBe(400);
expect(err.body.message).toBe('Invalid JSON in request body');
}
});
});
describe('GET /api/queue', () => {
it('should return empty list when no items', async () => {
const url = new URL('http://localhost/api/queue');
const request = new Request(url);
const response = await queueGET({ request, url } as any);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.items).toEqual([]);
expect(data.total).toBe(0);
expect(data.pagination.offset).toBe(0);
expect(data.pagination.limit).toBe(50);
});
it('should return queued items', async () => {
// Add test items
const item1 = queueManager.enqueue('https://instagram.com/p/TEST1');
const item2 = queueManager.enqueue('https://instagram.com/p/TEST2');
const url = new URL('http://localhost/api/queue');
const request = new Request(url);
const response = await queueGET({ request, url } as any);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.total).toBe(2);
expect(data.items).toHaveLength(2);
expect(data.items[0].url).toBe('https://instagram.com/p/TEST1');
expect(data.items[1].url).toBe('https://instagram.com/p/TEST2');
});
it('should filter by status', async () => {
// Add test items with different statuses
const item1 = queueManager.enqueue('https://instagram.com/p/PENDING');
const item2 = queueManager.enqueue('https://instagram.com/p/ERROR');
// Set one to error status
queueManager.updateStatus(item2.id, 'error', { message: 'Test error' });
const url = new URL('http://localhost/api/queue?status=error');
const request = new Request(url);
const response = await queueGET({ request, url } as any);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.total).toBe(1);
expect(data.items).toHaveLength(1);
expect(data.items[0].status).toBe('error');
expect(data.items[0].url).toBe('https://instagram.com/p/ERROR');
});
it('should handle pagination', async () => {
// Add multiple test items
for (let i = 1; i <= 5; i++) {
queueManager.enqueue(`https://instagram.com/p/TEST${i}`);
}
const url = new URL('http://localhost/api/queue?limit=2&offset=1');
const request = new Request(url);
const response = await queueGET({ request, url } as any);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.total).toBe(5);
expect(data.items).toHaveLength(2);
expect(data.pagination.offset).toBe(1);
expect(data.pagination.limit).toBe(2);
// Items are sorted by enqueued time (newest first), so with offset=1, limit=2 we get items 2-3 from the sorted list
});
it('should validate query parameters', async () => {
// Invalid status
try {
let url = new URL('http://localhost/api/queue?status=invalid');
let request = new Request(url);
let response = await queueGET({ request, url } as any);
expect(response.status).toBe(400);
let data = await response.json();
expect(data.message).toBe('Invalid status filter. Must be one of: pending, in_progress, success, unhealthy, error');
} catch (err: any) {
expect(err.status).toBe(400);
expect(err.body.message).toBe('Invalid status filter. Must be one of: pending, in_progress, success, unhealthy, error');
}
// Invalid limit (negative)
try {
let url = new URL('http://localhost/api/queue?limit=-1');
let request = new Request(url);
let response = await queueGET({ request, url } as any);
expect(response.status).toBe(400);
let data = await response.json();
expect(data.message).toBe('Limit must be a positive integer');
} catch (err: any) {
expect(err.status).toBe(400);
expect(err.body.message).toBe('Limit must be a positive integer');
}
// Invalid offset (negative)
try {
let url = new URL('http://localhost/api/queue?offset=-1');
let request = new Request(url);
let response = await queueGET({ request, url } as any);
expect(response.status).toBe(400);
let data = await response.json();
expect(data.message).toBe('Offset must be a non-negative integer');
} catch (err: any) {
expect(err.status).toBe(400);
expect(err.body.message).toBe('Offset must be a non-negative integer');
}
// Limit too large
try {
let url = new URL('http://localhost/api/queue?limit=999');
let request = new Request(url);
let response = await queueGET({ request, url } as any);
expect(response.status).toBe(400);
let data = await response.json();
expect(data.message).toBe('Limit cannot exceed 200');
} catch (err: any) {
expect(err.status).toBe(400);
expect(err.body.message).toBe('Limit cannot exceed 200');
}
});
});
describe('GET /api/queue/[id]', () => {
it('should return queue item by ID', async () => {
// Add test item
const item = queueManager.enqueue('https://instagram.com/p/DETAIL');
const response = await itemGET({
params: { id: item.id }
} as any);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.id).toBe(item.id);
expect(data.url).toBe('https://instagram.com/p/DETAIL');
expect(data.status).toBe('pending');
});
it('should return 404 for non-existent ID', async () => {
const fakeId = '550e8400-e29b-41d4-a716-446655440000'; // Valid v4 UUID format but non-existent
try {
const response = await itemGET({
params: { id: fakeId }
} as any);
expect(response.status).toBe(404);
const data = await response.json();
expect(data.message).toBe('Queue item not found');
} catch (err: any) {
expect(err.status).toBe(404);
expect(err.body.message).toBe('Queue item not found');
}
});
it('should validate ID format', async () => {
try {
const response = await itemGET({
params: { id: 'invalid-id' }
} as any);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.message).toBe('Invalid queue item ID format');
} catch (err: any) {
expect(err.status).toBe(400);
expect(err.body.message).toBe('Invalid queue item ID format');
}
});
});
describe('POST /api/queue/[id]/retry', () => {
it('should retry error item', async () => {
// Add test item and set to error
const item = queueManager.enqueue('https://instagram.com/p/RETRY');
queueManager.updateStatus(item.id, 'error', { message: 'Test error' });
const request = new Request(`http://localhost/api/queue/${item.id}/retry`, {
method: 'POST'
});
const response = await retryPOST({
request,
params: { id: item.id }
} as any);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.message).toBe('Queue item has been reset and will be reprocessed');
expect(data.success).toBe(true);
// Verify item status was reset
const updatedItem = queueManager.get(item.id);
expect(updatedItem?.status).toBe('pending');
expect(updatedItem?.error).toBeUndefined(); // error field is cleared (undefined, not null)
});
it('should retry unhealthy item', async () => {
// Add test item and set to unhealthy
const item = queueManager.enqueue('https://instagram.com/p/UNHEALTHY');
queueManager.updateStatus(item.id, 'unhealthy', {
phase: 'extraction',
attempts: 3,
lastAttempt: new Date(),
message: 'Max retries exceeded'
});
const request = new Request(`http://localhost/api/queue/${item.id}/retry`, {
method: 'POST'
});
const response = await retryPOST({
request,
params: { id: item.id }
} as any);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.message).toBe('Queue item has been reset and will be reprocessed');
expect(data.success).toBe(true);
// Verify item status was reset
const updatedItem = queueManager.get(item.id);
expect(updatedItem?.status).toBe('pending');
});
it('should reject retry for non-retryable statuses', async () => {
// Add test item (default status is 'pending')
const item = queueManager.enqueue('https://instagram.com/p/PENDING');
const request = new Request(`http://localhost/api/queue/${item.id}/retry`, {
method: 'POST'
});
// Item is pending (cannot retry)
try {
const response = await retryPOST({
request,
params: { id: item.id }
} as any);
expect(response.status).toBe(409);
const data = await response.json();
expect(data.message).toBe("Cannot retry item with status 'pending'. Only 'error' and 'unhealthy' items can be retried.");
} catch (err: any) {
expect(err.status).toBe(409);
expect(err.body.message).toBe("Cannot retry item with status 'pending'. Only 'error' and 'unhealthy' items can be retried.");
}
});
it('should return 404 for non-existent item', async () => {
const fakeId = '550e8400-e29b-41d4-a716-446655440000'; // Valid v4 UUID format but non-existent
const request = new Request(`http://localhost/api/queue/${fakeId}/retry`, {
method: 'POST'
});
try {
const response = await retryPOST({
request,
params: { id: fakeId }
} as any);
expect(response.status).toBe(404);
const data = await response.json();
expect(data.message).toBe('Queue item not found');
} catch (err: any) {
expect(err.status).toBe(404);
expect(err.body.message).toBe('Queue item not found');
}
});
});
describe('DELETE /api/queue/[id]', () => {
it('should delete queue item successfully', async () => {
// Create an item
const item = queueManager.enqueue('https://instagram.com/p/DELETE123');
// Mark it as success (completed)
queueManager.updateStatus(item.id, 'success');
const request = new Request(`http://localhost/api/queue/${item.id}`, {
method: 'DELETE'
});
const response = await itemDELETE({
request,
params: { id: item.id }
} as any);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
expect(data.message).toBe('Queue item removed successfully');
// Verify item no longer exists
expect(queueManager.get(item.id)).toBeUndefined();
});
it('should return 404 for non-existent item', async () => {
const fakeId = '550e8400-e29b-41d4-a716-446655440000';
const request = new Request(`http://localhost/api/queue/${fakeId}`, {
method: 'DELETE'
});
try {
const response = await itemDELETE({
request,
params: { id: fakeId }
} as any);
expect(response.status).toBe(404);
const data = await response.json();
expect(data.message).toBe('Queue item not found');
} catch (err: any) {
expect(err.status).toBe(404);
expect(err.body.message).toBe('Queue item not found');
}
});
it('should return 409 for in-progress items', async () => {
// Create an item and mark it as in progress
const item = queueManager.enqueue('https://instagram.com/p/PROCESSING');
queueManager.updateStatus(item.id, 'in_progress', { phase: 'extraction' });
const request = new Request(`http://localhost/api/queue/${item.id}`, {
method: 'DELETE'
});
try {
const response = await itemDELETE({
request,
params: { id: item.id }
} as any);
expect(response.status).toBe(409);
const data = await response.json();
expect(data.message).toBe('Cannot delete item that is currently being processed');
} catch (err: any) {
expect(err.status).toBe(409);
expect(err.body.message).toBe('Cannot delete item that is currently being processed');
}
// Verify item still exists
expect(queueManager.get(item.id)).toBeTruthy();
});
it('should validate ID format', async () => {
const invalidIds = ['not-a-uuid', '12345', 'abc-def-ghi'];
for (const invalidId of invalidIds) {
const request = new Request(`http://localhost/api/queue/${invalidId}`, {
method: 'DELETE'
});
try {
const response = await itemDELETE({
request,
params: { id: invalidId }
} as any);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.message).toBe('Invalid queue item ID format');
} catch (err: any) {
expect(err.status).toBe(400);
expect(err.body.message).toBe('Invalid queue item ID format');
}
}
});
});
});