2026-01-29 07:04:30 -08:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
/**
|
|
|
|
|
* End-to-end test script for queue worker.
|
|
|
|
|
*
|
|
|
|
|
* This script:
|
|
|
|
|
* 1. Connects to Redis
|
|
|
|
|
* 2. Adds test jobs to the test queue
|
|
|
|
|
* 3. Waits for them to complete or fail
|
|
|
|
|
* 4. Reports success/failure
|
|
|
|
|
*
|
|
|
|
|
* Usage:
|
|
|
|
|
* npm run test
|
2026-01-31 17:20:48 -08:00
|
|
|
* DATABASE_REDIS_URL=redis://:password@host:port npm run test
|
2026-01-29 07:04:30 -08:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { Queue } from 'bullmq';
|
|
|
|
|
import Redis from 'ioredis';
|
2026-01-31 17:20:48 -08:00
|
|
|
import { getServiceRegistry } from '@lilith/service-registry';
|
2026-01-29 07:04:30 -08:00
|
|
|
|
|
|
|
|
interface TestJobData {
|
|
|
|
|
testId: string;
|
|
|
|
|
message: string;
|
|
|
|
|
shouldFail?: boolean;
|
|
|
|
|
delayMs?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface TestResult {
|
|
|
|
|
jobId: string;
|
|
|
|
|
status: 'completed' | 'failed' | 'timeout';
|
|
|
|
|
result?: unknown;
|
|
|
|
|
error?: string;
|
|
|
|
|
durationMs: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class QueueTester {
|
|
|
|
|
private redis: Redis;
|
|
|
|
|
private queue: Queue;
|
|
|
|
|
private readonly redisUrl: string;
|
|
|
|
|
private readonly queueName = 'test';
|
|
|
|
|
|
|
|
|
|
constructor() {
|
2026-01-31 17:20:48 -08:00
|
|
|
// Use env var override or service registry
|
|
|
|
|
let redisUrl = process.env['DATABASE_REDIS_URL'];
|
|
|
|
|
if (!redisUrl) {
|
|
|
|
|
const registry = getServiceRegistry();
|
|
|
|
|
redisUrl = registry.getRedisUrl('queue-worker');
|
|
|
|
|
if (!redisUrl) {
|
|
|
|
|
throw new Error('Redis URL not found for queue-worker service in service registry and DATABASE_REDIS_URL not set');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.redisUrl = redisUrl;
|
2026-01-29 07:04:30 -08:00
|
|
|
|
|
|
|
|
// Create Redis connection
|
|
|
|
|
this.redis = new Redis(this.redisUrl, {
|
|
|
|
|
maxRetriesPerRequest: null,
|
|
|
|
|
enableReadyCheck: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create test queue
|
|
|
|
|
this.queue = new Queue(this.queueName, {
|
|
|
|
|
connection: this.redis,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async checkConnection(): Promise<void> {
|
|
|
|
|
console.log('🔌 Connecting to Redis...');
|
|
|
|
|
try {
|
|
|
|
|
await this.redis.ping();
|
|
|
|
|
console.log('✅ Redis connection successful');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
throw new Error(`Failed to connect to Redis: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async addJob(data: TestJobData): Promise<string> {
|
|
|
|
|
const job = await this.queue.add('test-job', data);
|
|
|
|
|
return job.id!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async waitForJob(jobId: string, timeoutMs: number = 10000): Promise<TestResult> {
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const checkInterval = setInterval(async () => {
|
|
|
|
|
const job = await this.queue.getJob(jobId);
|
|
|
|
|
|
|
|
|
|
if (!job) {
|
|
|
|
|
clearInterval(checkInterval);
|
|
|
|
|
resolve({
|
|
|
|
|
jobId,
|
|
|
|
|
status: 'failed',
|
|
|
|
|
error: 'Job not found',
|
|
|
|
|
durationMs: Date.now() - startTime,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state = await job.getState();
|
|
|
|
|
|
|
|
|
|
if (state === 'completed') {
|
|
|
|
|
clearInterval(checkInterval);
|
|
|
|
|
resolve({
|
|
|
|
|
jobId,
|
|
|
|
|
status: 'completed',
|
|
|
|
|
result: job.returnvalue,
|
|
|
|
|
durationMs: Date.now() - startTime,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state === 'failed') {
|
|
|
|
|
clearInterval(checkInterval);
|
|
|
|
|
resolve({
|
|
|
|
|
jobId,
|
|
|
|
|
status: 'failed',
|
|
|
|
|
error: job.failedReason ?? 'Unknown error',
|
|
|
|
|
durationMs: Date.now() - startTime,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Date.now() - startTime > timeoutMs) {
|
|
|
|
|
clearInterval(checkInterval);
|
|
|
|
|
resolve({
|
|
|
|
|
jobId,
|
|
|
|
|
status: 'timeout',
|
|
|
|
|
error: `Job did not complete within ${timeoutMs}ms`,
|
|
|
|
|
durationMs: Date.now() - startTime,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async runTests(): Promise<boolean> {
|
|
|
|
|
const results: TestResult[] = [];
|
|
|
|
|
|
|
|
|
|
console.log('\n🧪 Running queue worker tests...\n');
|
|
|
|
|
|
|
|
|
|
// Test 1: Simple successful job
|
|
|
|
|
try {
|
|
|
|
|
console.log('Test 1: Simple successful job');
|
|
|
|
|
const jobId = await this.addJob({
|
|
|
|
|
testId: 'test-1',
|
|
|
|
|
message: 'Hello, queue worker!',
|
|
|
|
|
});
|
|
|
|
|
console.log(` Added job ${jobId}`);
|
|
|
|
|
|
|
|
|
|
const result = await this.waitForJob(jobId);
|
|
|
|
|
results.push(result);
|
|
|
|
|
|
|
|
|
|
if (result.status === 'completed') {
|
|
|
|
|
console.log(` ✅ Job completed in ${result.durationMs}ms`);
|
|
|
|
|
console.log(` Result:`, JSON.stringify(result.result, null, 2));
|
|
|
|
|
} else {
|
|
|
|
|
console.log(` ❌ Job ${result.status}: ${result.error}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(` ❌ Test failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
|
results.push({
|
|
|
|
|
jobId: 'test-1',
|
|
|
|
|
status: 'failed',
|
|
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
durationMs: 0,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
|
|
|
|
// Test 2: Job with delay
|
|
|
|
|
try {
|
|
|
|
|
console.log('Test 2: Job with delay');
|
|
|
|
|
const jobId = await this.addJob({
|
|
|
|
|
testId: 'test-2',
|
|
|
|
|
message: 'Delayed job',
|
|
|
|
|
delayMs: 1000,
|
|
|
|
|
});
|
|
|
|
|
console.log(` Added job ${jobId}`);
|
|
|
|
|
|
|
|
|
|
const result = await this.waitForJob(jobId, 15000);
|
|
|
|
|
results.push(result);
|
|
|
|
|
|
|
|
|
|
if (result.status === 'completed') {
|
|
|
|
|
console.log(` ✅ Job completed in ${result.durationMs}ms`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(` ❌ Job ${result.status}: ${result.error}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(` ❌ Test failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
|
results.push({
|
|
|
|
|
jobId: 'test-2',
|
|
|
|
|
status: 'failed',
|
|
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
durationMs: 0,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
|
|
|
|
// Test 3: Intentional failure
|
|
|
|
|
try {
|
|
|
|
|
console.log('Test 3: Intentional failure (should fail)');
|
|
|
|
|
const jobId = await this.addJob({
|
|
|
|
|
testId: 'test-3',
|
|
|
|
|
message: 'This should fail',
|
|
|
|
|
shouldFail: true,
|
|
|
|
|
});
|
|
|
|
|
console.log(` Added job ${jobId}`);
|
|
|
|
|
|
|
|
|
|
const result = await this.waitForJob(jobId);
|
|
|
|
|
results.push(result);
|
|
|
|
|
|
|
|
|
|
if (result.status === 'failed') {
|
|
|
|
|
console.log(` ✅ Job failed as expected in ${result.durationMs}ms`);
|
|
|
|
|
console.log(` Error: ${result.error}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(` ❌ Job should have failed but got status: ${result.status}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(` ❌ Test failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
|
results.push({
|
|
|
|
|
jobId: 'test-3',
|
|
|
|
|
status: 'failed',
|
|
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
durationMs: 0,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
|
|
|
|
// Print summary
|
|
|
|
|
console.log('━'.repeat(60));
|
|
|
|
|
console.log('📊 Test Summary');
|
|
|
|
|
console.log('━'.repeat(60));
|
|
|
|
|
|
|
|
|
|
const completedCount = results.filter((r) => r.status === 'completed').length;
|
|
|
|
|
const failedCount = results.filter((r) => r.status === 'failed').length;
|
|
|
|
|
const timeoutCount = results.filter((r) => r.status === 'timeout').length;
|
|
|
|
|
|
|
|
|
|
console.log(`Total tests: ${results.length}`);
|
|
|
|
|
console.log(`✅ Completed: ${completedCount}`);
|
|
|
|
|
console.log(`❌ Failed: ${failedCount}`);
|
|
|
|
|
console.log(`⏱️ Timeout: ${timeoutCount}`);
|
|
|
|
|
console.log();
|
|
|
|
|
|
|
|
|
|
// Test 3 should fail, so we expect 2 completed and 1 failed
|
|
|
|
|
const success = completedCount === 2 && failedCount === 1 && timeoutCount === 0;
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
console.log('🎉 All tests passed!');
|
|
|
|
|
} else {
|
|
|
|
|
console.log('💥 Some tests failed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log();
|
|
|
|
|
return success;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async cleanup(): Promise<void> {
|
|
|
|
|
await this.queue.close();
|
|
|
|
|
await this.redis.quit();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function main(): Promise<void> {
|
|
|
|
|
const tester = new QueueTester();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await tester.checkConnection();
|
|
|
|
|
const success = await tester.runTests();
|
|
|
|
|
await tester.cleanup();
|
|
|
|
|
|
|
|
|
|
process.exit(success ? 0 : 1);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('❌ Test suite failed:', error instanceof Error ? error.message : String(error));
|
|
|
|
|
await tester.cleanup();
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main().catch((error: unknown) => {
|
|
|
|
|
console.error('Fatal error:', error);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|