Skip to content

Commit 904ec3d

Browse files
committed
fix(testing): scope mock Redis listeners to the client that registered them
The listener registry outlived the spies: `vi.clearAllMocks()` and `clearRedisMocks` reset call history but left handlers registered, so they accumulated across tests and a later `emit` could reach handlers belonging to a client the test under way never created. Adds `removeAllListeners`, which real clients have, and drops listeners in `clearRedisMocks` alongside spy history. Where one mock instance stands in for every client a module constructs, the registry is now emptied per construction — a real client starts with none, so binding listener lifetime to construction makes the isolation automatic rather than something each test has to remember. Covers the mock's event behavior in the testing package, where it lives.
1 parent fd8a45f commit 904ec3d

3 files changed

Lines changed: 112 additions & 14 deletions

File tree

apps/sim/lib/core/config/redis.test.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ const { mockEnv, MockRedisConstructor, mockLogger } = vi.hoisted(() => ({
1818
}))
1919

2020
const mockRedisInstance = createMockRedis()
21-
MockRedisConstructor.mockImplementation(
22-
class {
23-
constructor() {
24-
Object.assign(this, mockRedisInstance)
25-
}
26-
}
27-
)
21+
/** One mock instance stands in for every client the module constructs, so its
22+
* listener registry has to be emptied per construction — a real client starts
23+
* with none, and keeping them would let an `emit` reach handlers registered by
24+
* a client that no longer exists. */
25+
function newMockClient(this: object) {
26+
mockRedisInstance.removeAllListeners()
27+
Object.assign(this, mockRedisInstance)
28+
}
29+
30+
MockRedisConstructor.mockImplementation(newMockClient)
2831

2932
vi.unmock('@/lib/core/config/redis')
3033
vi.mock('@/lib/core/config/env', () => ({ env: mockEnv }))
@@ -60,13 +63,7 @@ describe('redis config', () => {
6063
mockRedisInstance.status = 'ready'
6164
mockEnv.REDIS_URL = 'redis://localhost:6379'
6265
mockEnv.REDIS_TLS_SERVERNAME = undefined
63-
MockRedisConstructor.mockImplementation(
64-
class {
65-
constructor() {
66-
Object.assign(this, mockRedisInstance)
67-
}
68-
}
69-
)
66+
MockRedisConstructor.mockImplementation(newMockClient)
7067
})
7168

7269
afterEach(() => {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { clearRedisMocks, createMockRedis } from './redis.mock'
3+
4+
describe('createMockRedis events', () => {
5+
it('dispatches an emitted event to its registered listeners', () => {
6+
const redis = createMockRedis()
7+
const onReady = vi.fn()
8+
redis.on('ready', onReady)
9+
10+
expect(redis.emit('ready')).toBe(true)
11+
expect(onReady).toHaveBeenCalledOnce()
12+
})
13+
14+
it('reports no delivery when nothing is listening', () => {
15+
expect(createMockRedis().emit('ready')).toBe(false)
16+
})
17+
18+
it('stops delivering to a removed listener', () => {
19+
const redis = createMockRedis()
20+
const onReady = vi.fn()
21+
redis.on('ready', onReady)
22+
redis.removeListener('ready', onReady)
23+
24+
redis.emit('ready')
25+
expect(onReady).not.toHaveBeenCalled()
26+
})
27+
28+
it('lets a listener remove itself while the event is dispatching', () => {
29+
const redis = createMockRedis()
30+
const onReady = vi.fn(() => redis.removeListener('ready', onReady))
31+
redis.on('ready', onReady)
32+
33+
expect(() => redis.emit('ready')).not.toThrow()
34+
redis.emit('ready')
35+
expect(onReady).toHaveBeenCalledOnce()
36+
})
37+
38+
it('drops every listener on removeAllListeners', () => {
39+
const redis = createMockRedis()
40+
const onReady = vi.fn()
41+
const onError = vi.fn()
42+
redis.on('ready', onReady)
43+
redis.on('error', onError)
44+
45+
redis.removeAllListeners()
46+
47+
redis.emit('ready')
48+
redis.emit('error')
49+
expect(onReady).not.toHaveBeenCalled()
50+
expect(onError).not.toHaveBeenCalled()
51+
})
52+
53+
it('drops only the named event when one is given', () => {
54+
const redis = createMockRedis()
55+
const onReady = vi.fn()
56+
const onError = vi.fn()
57+
redis.on('ready', onReady)
58+
redis.on('error', onError)
59+
60+
redis.removeAllListeners('ready')
61+
62+
redis.emit('ready')
63+
redis.emit('error')
64+
expect(onReady).not.toHaveBeenCalled()
65+
expect(onError).toHaveBeenCalledOnce()
66+
})
67+
68+
it('clears listeners alongside spy history, not just spy history', () => {
69+
// Handlers left behind would be invoked by a later emit on behalf of a
70+
// client the test under way never created.
71+
const redis = createMockRedis()
72+
const onReady = vi.fn()
73+
redis.on('ready', onReady)
74+
75+
clearRedisMocks(redis)
76+
77+
expect(redis.emit('ready')).toBe(false)
78+
expect(onReady).not.toHaveBeenCalled()
79+
})
80+
81+
it('keeps listeners scoped to the instance that registered them', () => {
82+
const a = createMockRedis()
83+
const b = createMockRedis()
84+
const onA = vi.fn()
85+
a.on('ready', onA)
86+
87+
b.emit('ready')
88+
expect(onA).not.toHaveBeenCalled()
89+
})
90+
})

packages/testing/src/mocks/redis.mock.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ export function createMockRedis() {
6262
removeListener: vi.fn((event: string, listener: (...args: unknown[]) => void) => {
6363
listeners.get(event)?.delete(listener)
6464
}),
65+
/** Listeners belong to a client, so a caller reusing this instance as a new
66+
* client clears them the way a real one starts empty. */
67+
removeAllListeners: vi.fn((event?: string) => {
68+
if (event === undefined) listeners.clear()
69+
else listeners.delete(event)
70+
}),
6571
/** Drives the lifecycle events a real client emits (`connect`, `ready`, `error`). */
6672
emit: vi.fn((event: string, ...args: unknown[]) => {
6773
const registered = listeners.get(event)
@@ -93,8 +99,13 @@ export type MockRedis = ReturnType<typeof createMockRedis>
9399

94100
/**
95101
* Clears all Redis mock calls.
102+
*
103+
* Also drops registered listeners: spy history and the listener registry are
104+
* separate state, and handlers left behind would be invoked by a later `emit`
105+
* on behalf of a client the test under way never created.
96106
*/
97107
export function clearRedisMocks(redis: MockRedis) {
108+
redis.removeAllListeners()
98109
Object.values(redis).forEach((value) => {
99110
if (typeof value === 'function' && 'mockClear' in value) {
100111
value.mockClear()

0 commit comments

Comments
 (0)