import type { Job, Queue, QueueEvents } from 'bullmq'; import { beforeEach, describe, expect, it, mock, type Mock } from 'bun:test'; import { QueueMetricsCollector } from '../src/queue-metrics'; describe('QueueMetricsCollector', () => { let metrics: QueueMetricsCollector; let mockQueue: { name: string; getWaitingCount: Mock<() => Promise>; getActiveCount: Mock<() => Promise>; getCompletedCount: Mock<() => Promise>; getFailedCount: Mock<() => Promise>; getDelayedCount: Mock<() => Promise>; isPaused: Mock<() => Promise>; getWaiting: Mock<() => Promise>; }; let mockQueueEvents: { on: Mock<(event: string, handler: Function) => void>; }; beforeEach(() => { mockQueue = { name: 'test-queue', getWaitingCount: mock(() => Promise.resolve(0)), getActiveCount: mock(() => Promise.resolve(0)), getCompletedCount: mock(() => Promise.resolve(0)), getFailedCount: mock(() => Promise.resolve(0)), getDelayedCount: mock(() => Promise.resolve(0)), isPaused: mock(() => Promise.resolve(false)), getWaiting: mock(() => Promise.resolve([])), }; mockQueueEvents = { on: mock(() => {}), }; metrics = new QueueMetricsCollector( mockQueue as unknown as Queue, mockQueueEvents as unknown as QueueEvents ); }); describe('collect metrics', () => { it('should collect current metrics', async () => { mockQueue.getWaitingCount.mockImplementation(() => Promise.resolve(5)); mockQueue.getActiveCount.mockImplementation(() => Promise.resolve(2)); mockQueue.getCompletedCount.mockImplementation(() => Promise.resolve(100)); mockQueue.getFailedCount.mockImplementation(() => Promise.resolve(3)); mockQueue.getDelayedCount.mockImplementation(() => Promise.resolve(1)); // Add some completed timestamps to avoid 100% failure rate const completedHandler = mockQueueEvents.on.mock.calls.find( call => call[0] === 'completed' )?.[1]; if (completedHandler) { for (let i = 0; i < 50; i++) { completedHandler(); } } const result = await metrics.collect(); expect(result.waiting).toBe(5); expect(result.active).toBe(2); expect(result.completed).toBe(100); expect(result.failed).toBe(3); expect(result.delayed).toBe(1); expect(result.isHealthy).toBe(true); }); it('should detect health issues', async () => { mockQueue.getWaitingCount.mockImplementation(() => Promise.resolve(2000)); // High backlog mockQueue.getActiveCount.mockImplementation(() => Promise.resolve(150)); // High active mockQueue.getFailedCount.mockImplementation(() => Promise.resolve(50)); const result = await metrics.collect(); expect(result.isHealthy).toBe(false); expect(result.healthIssues.length).toBeGreaterThan(0); expect(result.healthIssues.some(issue => issue.includes('queue backlog'))).toBe(true); expect(result.healthIssues.some(issue => issue.includes('active jobs'))).toBe(true); }); it('should handle paused queue', async () => { mockQueue.getWaitingCount.mockImplementation(() => Promise.resolve(10)); mockQueue.isPaused.mockImplementation(() => Promise.resolve(true)); const result = await metrics.collect(); expect(result.paused).toBe(10); }); }); describe('processing time metrics', () => { it('should calculate processing time metrics', async () => { // Access private property for testing const metricsWithPrivate = metrics as QueueMetricsCollector & { processingTimes: number[]; }; metricsWithPrivate.processingTimes = [1000, 2000, 3000, 4000, 5000]; const result = await metrics.collect(); expect(result.processingTime.avg).toBe(3000); expect(result.processingTime.min).toBe(1000); expect(result.processingTime.max).toBe(5000); expect(result.processingTime.p95).toBe(5000); }); it('should handle no processing times', async () => { const result = await metrics.collect(); expect(result.processingTime.avg).toBe(0); expect(result.processingTime.min).toBe(0); expect(result.processingTime.max).toBe(0); }); }); describe('throughput metrics', () => { it('should calculate throughput', async () => { // Access private properties for testing const metricsWithPrivate = metrics as QueueMetricsCollector & { completedTimestamps: number[]; failedTimestamps: number[]; }; const now = Date.now(); metricsWithPrivate.completedTimestamps = [ now - 30000, // 30 seconds ago now - 20000, now - 10000, ]; metricsWithPrivate.failedTimestamps = [now - 25000, now - 5000]; const result = await metrics.collect(); expect(result.throughput.completedPerMinute).toBe(3); expect(result.throughput.failedPerMinute).toBe(2); expect(result.throughput.totalPerMinute).toBe(5); }); }); describe('getReport', () => { it('should generate formatted report', async () => { mockQueue.getWaitingCount.mockImplementation(() => Promise.resolve(5)); mockQueue.getActiveCount.mockImplementation(() => Promise.resolve(2)); mockQueue.getCompletedCount.mockImplementation(() => Promise.resolve(100)); mockQueue.getFailedCount.mockImplementation(() => Promise.resolve(3)); // Add some completed timestamps to make it healthy const completedHandler = mockQueueEvents.on.mock.calls.find( call => call[0] === 'completed' )?.[1]; if (completedHandler) { for (let i = 0; i < 50; i++) { completedHandler(); } } const report = await metrics.getReport(); expect(report).toContain('Queue Metrics Report'); expect(report).toContain('✅ Healthy'); expect(report).toContain('Waiting: 5'); expect(report).toContain('Active: 2'); expect(report).toContain('Completed: 100'); expect(report).toContain('Failed: 3'); }); }); describe('getPrometheusMetrics', () => { it('should generate Prometheus formatted metrics', async () => { mockQueue.getWaitingCount.mockImplementation(() => Promise.resolve(5)); mockQueue.getActiveCount.mockImplementation(() => Promise.resolve(2)); mockQueue.getCompletedCount.mockImplementation(() => Promise.resolve(100)); mockQueue.getFailedCount.mockImplementation(() => Promise.resolve(3)); const prometheusMetrics = await metrics.getPrometheusMetrics(); expect(prometheusMetrics).toContain('# HELP queue_jobs_total'); expect(prometheusMetrics).toContain( 'queue_jobs_total{queue="test-queue",status="waiting"} 5' ); expect(prometheusMetrics).toContain('queue_jobs_total{queue="test-queue",status="active"} 2'); expect(prometheusMetrics).toContain( 'queue_jobs_total{queue="test-queue",status="completed"} 100' ); expect(prometheusMetrics).toContain('# HELP queue_processing_time_seconds'); expect(prometheusMetrics).toContain('# HELP queue_throughput_per_minute'); expect(prometheusMetrics).toContain('# HELP queue_health'); }); }); describe('event listeners', () => { it('should setup event listeners on construction', () => { const newMockQueueEvents = { on: mock<(event: string, handler: Function) => void>(() => {}), }; new QueueMetricsCollector( mockQueue as unknown as Queue, newMockQueueEvents as unknown as QueueEvents ); expect(newMockQueueEvents.on).toHaveBeenCalledWith('completed', expect.any(Function)); expect(newMockQueueEvents.on).toHaveBeenCalledWith('failed', expect.any(Function)); expect(newMockQueueEvents.on).toHaveBeenCalledWith('active', expect.any(Function)); }); }); describe('oldest waiting job', () => { it('should get oldest waiting job date', async () => { const oldJob = { timestamp: Date.now() - 60000, // 1 minute ago } as Job; mockQueue.getWaiting.mockImplementation(() => Promise.resolve([oldJob])); const result = await metrics.collect(); expect(result.oldestWaitingJob).toBeDefined(); expect(result.oldestWaitingJob).toBeInstanceOf(Date); }); it('should return null when no waiting jobs', async () => { mockQueue.getWaiting.mockImplementation(() => Promise.resolve([])); const result = await metrics.collect(); expect(result.oldestWaitingJob).toBeNull(); }); }); });