|
1 | 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
2 | 2 |
|
3 | 3 | const { createClient, redisClient } = vi.hoisted(() => { |
| 4 | + const handlers: Record<string, (arg?: unknown) => void> = {} |
4 | 5 | const client = { |
5 | 6 | isReady: false, |
6 | | - on: vi.fn(), |
| 7 | + on: vi.fn((event: string, handler: (arg?: unknown) => void) => { |
| 8 | + handlers[event] = handler |
| 9 | + return client |
| 10 | + }), |
| 11 | + emit: (event: string, arg?: unknown) => handlers[event]?.(arg), |
7 | 12 | connect: vi.fn(), |
8 | 13 | ping: vi.fn(), |
9 | 14 | get: vi.fn(), |
@@ -97,12 +102,55 @@ describe('optional KV client', () => { |
97 | 102 | }) |
98 | 103 | expect(createClient).toHaveBeenCalledWith({ |
99 | 104 | url: 'redis://localhost:6379', |
| 105 | + disableOfflineQueue: true, |
100 | 106 | }) |
101 | 107 | expect(redisClient.on).toHaveBeenCalledWith('error', expect.any(Function)) |
| 108 | + expect(redisClient.on).toHaveBeenCalledWith('end', expect.any(Function)) |
102 | 109 | expect(redisClient.connect).toHaveBeenCalledTimes(1) |
103 | 110 | expect(redisClient.ping).toHaveBeenCalledTimes(1) |
104 | 111 | }) |
105 | 112 |
|
| 113 | + it('fails fast when the client disconnects mid-session', async () => { |
| 114 | + process.env.REDIS_URL = 'redis://localhost:6379' |
| 115 | + redisClient.ping.mockResolvedValueOnce('PONG') |
| 116 | + const { pingKv } = await loadKvClient() |
| 117 | + |
| 118 | + // First ping: healthy |
| 119 | + await expect(pingKv()).resolves.toMatchObject({ status: 'ok' }) |
| 120 | + |
| 121 | + // Simulate disconnect: isReady flips and the next ping rejects fast |
| 122 | + // because the singleton was created with disableOfflineQueue: true. |
| 123 | + redisClient.isReady = false |
| 124 | + const closedError = new Error('The client is closed') |
| 125 | + redisClient.ping.mockRejectedValueOnce(closedError) |
| 126 | + |
| 127 | + await expect(pingKv()).resolves.toEqual({ |
| 128 | + configured: true, |
| 129 | + available: false, |
| 130 | + status: 'error', |
| 131 | + error: closedError, |
| 132 | + }) |
| 133 | + }) |
| 134 | + |
| 135 | + it('rebuilds the singleton after the client emits end', async () => { |
| 136 | + process.env.REDIS_URL = 'redis://localhost:6379' |
| 137 | + redisClient.ping.mockResolvedValue('PONG') |
| 138 | + const { pingKv } = await loadKvClient() |
| 139 | + |
| 140 | + await pingKv() |
| 141 | + expect(createClient).toHaveBeenCalledTimes(1) |
| 142 | + expect(redisClient.connect).toHaveBeenCalledTimes(1) |
| 143 | + |
| 144 | + // Permanent disconnect — drives the 'end' handler, which should null out |
| 145 | + // the cached singletons so the next call rebuilds from scratch. |
| 146 | + redisClient.isReady = false |
| 147 | + redisClient.emit('end') |
| 148 | + |
| 149 | + await pingKv() |
| 150 | + expect(createClient).toHaveBeenCalledTimes(2) |
| 151 | + expect(redisClient.connect).toHaveBeenCalledTimes(2) |
| 152 | + }) |
| 153 | + |
106 | 154 | it('reports KV errors when Redis connection fails', async () => { |
107 | 155 | process.env.REDIS_URL = 'redis://localhost:6379' |
108 | 156 | const error = new Error('redis unavailable') |
|
0 commit comments