WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at root/atb-56-theme-caching-layer 258 lines 8.8 kB view raw
1import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; 2import { ReconnectionManager } from "../reconnection-manager.js"; 3import { createMockLogger } from "./mock-logger.js"; 4 5describe("ReconnectionManager", () => { 6 let reconnectionManager: ReconnectionManager; 7 let reconnectFn: ReturnType<typeof vi.fn>; 8 let mockLogger: ReturnType<typeof createMockLogger>; 9 10 beforeEach(() => { 11 vi.useFakeTimers(); 12 reconnectFn = vi.fn().mockResolvedValue(undefined); 13 mockLogger = createMockLogger(); 14 }); 15 16 afterEach(() => { 17 vi.clearAllMocks(); 18 vi.useRealTimers(); 19 }); 20 21 describe("Construction", () => { 22 it("should initialize with maxAttempts and baseDelayMs", () => { 23 expect(() => { 24 reconnectionManager = new ReconnectionManager(5, 1000, mockLogger); 25 }).not.toThrow(); 26 27 expect(reconnectionManager.getAttemptCount()).toBe(0); 28 }); 29 }); 30 31 describe("attemptReconnect", () => { 32 beforeEach(() => { 33 reconnectionManager = new ReconnectionManager(3, 1000, mockLogger); 34 }); 35 36 it("should attempt reconnection with exponential backoff", async () => { 37 // First attempt: 1000ms delay (1000 * 2^0) 38 const promise1 = reconnectionManager.attemptReconnect(reconnectFn); 39 expect(mockLogger.info).toHaveBeenCalledWith( 40 "Attempting to reconnect", 41 expect.objectContaining({ attempt: 1, maxAttempts: 3, delayMs: 1000 }) 42 ); 43 await vi.advanceTimersByTimeAsync(1000); 44 await promise1; 45 expect(reconnectFn).toHaveBeenCalledTimes(1); 46 47 // Second attempt: 2000ms delay (1000 * 2^1) 48 const promise2 = reconnectionManager.attemptReconnect(reconnectFn); 49 expect(mockLogger.info).toHaveBeenCalledWith( 50 "Attempting to reconnect", 51 expect.objectContaining({ attempt: 2, maxAttempts: 3, delayMs: 2000 }) 52 ); 53 await vi.advanceTimersByTimeAsync(2000); 54 await promise2; 55 expect(reconnectFn).toHaveBeenCalledTimes(2); 56 57 // Third attempt: 4000ms delay (1000 * 2^2) 58 const promise3 = reconnectionManager.attemptReconnect(reconnectFn); 59 expect(mockLogger.info).toHaveBeenCalledWith( 60 "Attempting to reconnect", 61 expect.objectContaining({ attempt: 3, maxAttempts: 3, delayMs: 4000 }) 62 ); 63 await vi.advanceTimersByTimeAsync(4000); 64 await promise3; 65 expect(reconnectFn).toHaveBeenCalledTimes(3); 66 }); 67 68 it("should throw error when max attempts exceeded", async () => { 69 // Exhaust all attempts 70 const promise1 = reconnectionManager.attemptReconnect(reconnectFn); 71 await vi.runAllTimersAsync(); 72 await promise1; 73 74 const promise2 = reconnectionManager.attemptReconnect(reconnectFn); 75 await vi.runAllTimersAsync(); 76 await promise2; 77 78 const promise3 = reconnectionManager.attemptReconnect(reconnectFn); 79 await vi.runAllTimersAsync(); 80 await promise3; 81 82 // Fourth attempt should throw 83 await expect(reconnectionManager.attemptReconnect(reconnectFn)).rejects.toThrow( 84 "Max reconnection attempts exceeded" 85 ); 86 87 expect(mockLogger.fatal).toHaveBeenCalledWith( 88 expect.stringContaining("Max reconnect attempts reached"), 89 expect.objectContaining({ maxAttempts: 3 }) 90 ); 91 }); 92 93 it("should log reconnection attempts", async () => { 94 const promise = reconnectionManager.attemptReconnect(reconnectFn); 95 await vi.runAllTimersAsync(); 96 await promise; 97 98 expect(mockLogger.info).toHaveBeenCalledWith( 99 "Attempting to reconnect", 100 expect.any(Object) 101 ); 102 }); 103 104 it("should propagate errors from reconnectFn", async () => { 105 const error = new Error("Reconnection failed"); 106 reconnectFn.mockRejectedValueOnce(error); 107 108 // Attach rejection handler BEFORE advancing timers 109 const promise = reconnectionManager.attemptReconnect(reconnectFn); 110 const assertion = expect(promise).rejects.toThrow("Reconnection failed"); 111 await vi.runAllTimersAsync(); 112 await assertion; 113 }); 114 }); 115 116 describe("reset", () => { 117 beforeEach(() => { 118 reconnectionManager = new ReconnectionManager(3, 1000, mockLogger); 119 }); 120 121 it("should reset attempt counter", async () => { 122 // Make one attempt 123 const promise = reconnectionManager.attemptReconnect(reconnectFn); 124 await vi.runAllTimersAsync(); 125 await promise; 126 127 expect(reconnectionManager.getAttemptCount()).toBe(1); 128 129 // Reset 130 reconnectionManager.reset(); 131 expect(reconnectionManager.getAttemptCount()).toBe(0); 132 }); 133 134 it("should allow reconnection after reset", async () => { 135 // Exhaust all attempts 136 const promise1 = reconnectionManager.attemptReconnect(reconnectFn); 137 await vi.runAllTimersAsync(); 138 await promise1; 139 140 const promise2 = reconnectionManager.attemptReconnect(reconnectFn); 141 await vi.runAllTimersAsync(); 142 await promise2; 143 144 const promise3 = reconnectionManager.attemptReconnect(reconnectFn); 145 await vi.runAllTimersAsync(); 146 await promise3; 147 148 expect(reconnectionManager.getAttemptCount()).toBe(3); 149 150 // Reset should allow new attempts 151 reconnectionManager.reset(); 152 153 const promise4 = reconnectionManager.attemptReconnect(reconnectFn); 154 expect(mockLogger.info).toHaveBeenCalledWith( 155 "Attempting to reconnect", 156 expect.objectContaining({ attempt: 1, maxAttempts: 3, delayMs: 1000 }) 157 ); 158 await vi.runAllTimersAsync(); 159 await promise4; 160 }); 161 }); 162 163 describe("getAttemptCount", () => { 164 beforeEach(() => { 165 reconnectionManager = new ReconnectionManager(3, 1000, mockLogger); 166 }); 167 168 it("should return current attempt count", async () => { 169 expect(reconnectionManager.getAttemptCount()).toBe(0); 170 171 const promise1 = reconnectionManager.attemptReconnect(reconnectFn); 172 await vi.runAllTimersAsync(); 173 await promise1; 174 expect(reconnectionManager.getAttemptCount()).toBe(1); 175 176 const promise2 = reconnectionManager.attemptReconnect(reconnectFn); 177 await vi.runAllTimersAsync(); 178 await promise2; 179 expect(reconnectionManager.getAttemptCount()).toBe(2); 180 }); 181 }); 182 183 describe("Edge Cases", () => { 184 it("should handle maxAttempts of 1", async () => { 185 reconnectionManager = new ReconnectionManager(1, 1000, mockLogger); 186 187 const promise = reconnectionManager.attemptReconnect(reconnectFn); 188 await vi.runAllTimersAsync(); 189 await promise; 190 191 expect(reconnectionManager.getAttemptCount()).toBe(1); 192 193 // Second attempt should fail 194 await expect(reconnectionManager.attemptReconnect(reconnectFn)).rejects.toThrow( 195 "Max reconnection attempts exceeded" 196 ); 197 }); 198 199 it("should handle very small base delays", async () => { 200 reconnectionManager = new ReconnectionManager(2, 10, mockLogger); 201 202 const promise = reconnectionManager.attemptReconnect(reconnectFn); 203 await vi.advanceTimersByTimeAsync(10); 204 await promise; 205 206 expect(reconnectFn).toHaveBeenCalled(); 207 }); 208 209 it("should calculate exponential backoff correctly for many attempts", async () => { 210 reconnectionManager = new ReconnectionManager(5, 100, mockLogger); 211 212 // Attempt 1: 100ms (100 * 2^0) 213 let promise = reconnectionManager.attemptReconnect(reconnectFn); 214 expect(mockLogger.info).toHaveBeenCalledWith( 215 "Attempting to reconnect", 216 expect.objectContaining({ delayMs: 100 }) 217 ); 218 await vi.runAllTimersAsync(); 219 await promise; 220 221 // Attempt 2: 200ms (100 * 2^1) 222 promise = reconnectionManager.attemptReconnect(reconnectFn); 223 expect(mockLogger.info).toHaveBeenCalledWith( 224 "Attempting to reconnect", 225 expect.objectContaining({ delayMs: 200 }) 226 ); 227 await vi.runAllTimersAsync(); 228 await promise; 229 230 // Attempt 3: 400ms (100 * 2^2) 231 promise = reconnectionManager.attemptReconnect(reconnectFn); 232 expect(mockLogger.info).toHaveBeenCalledWith( 233 "Attempting to reconnect", 234 expect.objectContaining({ delayMs: 400 }) 235 ); 236 await vi.runAllTimersAsync(); 237 await promise; 238 239 // Attempt 4: 800ms (100 * 2^3) 240 promise = reconnectionManager.attemptReconnect(reconnectFn); 241 expect(mockLogger.info).toHaveBeenCalledWith( 242 "Attempting to reconnect", 243 expect.objectContaining({ delayMs: 800 }) 244 ); 245 await vi.runAllTimersAsync(); 246 await promise; 247 248 // Attempt 5: 1600ms (100 * 2^4) 249 promise = reconnectionManager.attemptReconnect(reconnectFn); 250 expect(mockLogger.info).toHaveBeenCalledWith( 251 "Attempting to reconnect", 252 expect.objectContaining({ delayMs: 1600 }) 253 ); 254 await vi.runAllTimersAsync(); 255 await promise; 256 }); 257 }); 258});