import { afterEach, describe, expect, it, vi } from "vitest"; import { RedisService } from "../redis/redis.service.js"; import { ChatRateLimiter } from "./chat-rate-limiter.js"; type FakeRedisClient = { isReady: boolean; isOpen: boolean; on: ReturnType; connect: ReturnType; eval: ReturnType; destroy: ReturnType; quit: ReturnType; }; function fakeClient(evalResult: Promise): FakeRedisClient { return { isReady: true, isOpen: true, on: vi.fn(), connect: vi.fn(() => Promise.resolve()), eval: vi.fn(() => evalResult), destroy: vi.fn(), quit: vi.fn(() => Promise.resolve()), }; } describe("ChatRateLimiter Redis command deadline", () => { const previousTimeout = process.env.REDIS_OPERATION_TIMEOUT_MS; afterEach(() => { if (previousTimeout === undefined) delete process.env.REDIS_OPERATION_TIMEOUT_MS; else process.env.REDIS_OPERATION_TIMEOUT_MS = previousTimeout; }); it("destroys a client whose eval never settles and recovers with a new client", async () => { process.env.REDIS_OPERATION_TIMEOUT_MS = "20"; const stuck = fakeClient(new Promise(() => undefined)); const healthy = fakeClient(Promise.resolve(1)); const factory = vi .fn() .mockReturnValueOnce(stuck) .mockReturnValueOnce(healthy); const redis = new RedisService(factory as never); const limiter = new ChatRateLimiter(redis); await expect(limiter.consume("account", "session")).rejects.toThrow( "Auth Redis operation timed out", ); expect(stuck.destroy).toHaveBeenCalledOnce(); await expect(limiter.consume("account", "session")).resolves.toBe(true); expect(healthy.eval).toHaveBeenCalledOnce(); expect(factory).toHaveBeenCalledTimes(2); }); });