返回 AiToEarn
event-stream.explorer.spec.ts
根目录 / project / aitoearn-backend / libs / redis / src / event-stream.explorer.spec.ts
1 import type { EventEnvelope } from './event-stream.service'
2 import { describe, expect, it, vi } from 'vitest'
3 import { EventStream } from './enum/event-stream.enum'
4 import { EventTopic } from './enum/event-topic.enum'
5 import { OnEventStream } from './event-stream.decorator'
6 import { EventStreamExplorer } from './event-stream.explorer'
7 import { EventStreamService } from './event-stream.service'
8
9 function createModulesContainer(instance: object) {
10 return new Map([
11 ['test-module', {
12 providers: new Map([
13 ['handler', { instance }],
14 ]),
15 controllers: new Map(),
16 }],
17 ])
18 }
19
20 function createEnvelope(topic: EventTopic): EventEnvelope {
21 return {
22 eventId: 'event_1',
23 topic,
24 version: 1,
25 occurredAt: '2026-06-09T00:00:00.000Z',
26 source: 'spec',
27 idempotencyKey: 'event_1',
28 payload: {},
29 } as EventEnvelope
30 }
31
32 describe('eventStreamExplorer', () => {
33 it('registers decorated handlers with an inferred stream and generated group', async () => {
34 class AutoGroupHandler {
35 received: EventEnvelope[] = []
36
37 @OnEventStream(EventTopic.UserCreated)
38 async handle(envelope: EventEnvelope) {
39 this.received.push(envelope)
40 }
41 }
42 const instance = new AutoGroupHandler()
43 const eventStreamService = { subscribe: vi.fn() }
44
45 new EventStreamExplorer(
46 createModulesContainer(instance) as never,
47 eventStreamService as unknown as EventStreamService,
48 ).onModuleInit()
49
50 const expectedGroup = 'event-stream:AutoGroupHandler.handle:user.created'
51 expect(eventStreamService.subscribe).toHaveBeenCalledWith(expect.objectContaining({
52 group: expectedGroup,
53 consumer: expect.stringContaining(`${expectedGroup}:`),
54 streams: [EventStream.User],
55 topics: [EventTopic.UserCreated],
56 }))
57
58 await eventStreamService.subscribe.mock.calls[0][0].handler(createEnvelope(EventTopic.UserCreated))
59
60 expect(instance.received).toHaveLength(1)
61 })
62
63 it('keeps explicit consumer group options', () => {
64 class ExplicitGroupHandler {
65 @OnEventStream(EventTopic.ChannelsAccountConnected, {
66 streams: [EventStream.Channels],
67 group: 'channels-account-connected-spec',
68 consumer: 'consumer-1',
69 maxRetries: 2,
70 pollInterval: 300,
71 })
72 async handle() {
73 return undefined
74 }
75 }
76 const eventStreamService = { subscribe: vi.fn() }
77
78 new EventStreamExplorer(
79 createModulesContainer(new ExplicitGroupHandler()) as never,
80 eventStreamService as unknown as EventStreamService,
81 ).onModuleInit()
82
83 expect(eventStreamService.subscribe).toHaveBeenCalledWith(expect.objectContaining({
84 group: 'channels-account-connected-spec',
85 consumer: 'consumer-1',
86 streams: [EventStream.Channels],
87 topics: [EventTopic.ChannelsAccountConnected],
88 maxRetries: 2,
89 pollInterval: 300,
90 }))
91 })
92 })
93
94 describe('eventStreamService topic filtering', () => {
95 it('acks messages without invoking handlers when the topic does not match', async () => {
96 const redis = {
97 xack: vi.fn(),
98 xadd: vi.fn(),
99 }
100 const service = new EventStreamService(redis as never)
101 const handler = vi.fn()
102
103 await (service as unknown as {
104 processMessage: (
105 stream: string,
106 id: string,
107 fields: string[],
108 config: {
109 group: string
110 consumer: string
111 streams: EventStream[]
112 topics: EventTopic[]
113 handler: (envelope: EventEnvelope) => Promise<void>
114 },
115 maxRetries: number,
116 ) => Promise<void>
117 }).processMessage(
118 EventStream.User,
119 '1-0',
120 ['data', JSON.stringify(createEnvelope(EventTopic.UserCreated))],
121 {
122 group: 'group-1',
123 consumer: 'consumer-1',
124 streams: [EventStream.User],
125 topics: [EventTopic.ChannelsAccountConnected],
126 handler,
127 },
128 0,
129 )
130
131 expect(handler).not.toHaveBeenCalled()
132 expect(redis.xack).toHaveBeenCalledWith(EventStream.User, 'group-1', '1-0')
133 })
134 })
135
135 lines TYPESCRIPT