/** * Zulip事件处理服务测试 * * 功能描述: * - 测试ZulipEventProcessorService的核心功能 * - 包含属性测试验证消息格式转换完整性 * - 包含属性测试验证消息接收和分发 * * **Feature: zulip-integration, Property 4: 消息格式转换完整性** * **Validates: Requirements 5.3, 5.4** * * **Feature: zulip-integration, Property 5: 消息接收和分发** * **Validates: Requirements 5.1, 5.2, 5.5** * * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ import { Test, TestingModule } from '@nestjs/testing'; import * as fc from 'fast-check'; import { ZulipEventProcessorService, ZulipMessage, GameMessage, MessageDistributor, } from './zulip_event_processor.service'; import { SessionManagerService, GameSession } from './session_manager.service'; import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces'; import { AppLoggerService } from '../../../core/utils/logger/logger.service'; describe('ZulipEventProcessorService', () => { let service: ZulipEventProcessorService; let mockLogger: jest.Mocked; let mockSessionManager: jest.Mocked; let mockConfigManager: jest.Mocked; let mockClientPool: jest.Mocked; let mockDistributor: jest.Mocked; // 创建模拟Zulip消息 const createMockZulipMessage = (overrides: Partial = {}): ZulipMessage => ({ id: Math.floor(Math.random() * 1000000), sender_email: 'test@example.com', sender_full_name: 'Test User', content: 'Hello, World!', stream_id: 1, subject: 'General', timestamp: Math.floor(Date.now() / 1000), display_recipient: 'Tavern', type: 'stream', ...overrides, }); // 创建模拟会话 const createMockSession = (overrides: Partial = {}): GameSession => ({ socketId: `socket_${Math.random().toString(36).substr(2, 9)}`, userId: `user_${Math.random().toString(36).substr(2, 9)}`, username: 'TestPlayer', zulipQueueId: `queue_${Math.random().toString(36).substr(2, 9)}`, currentMap: 'tavern', position: { x: 100, y: 200 }, lastActivity: new Date(), createdAt: new Date(), ...overrides, }); beforeEach(async () => { jest.clearAllMocks(); mockLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn(), } as any; mockSessionManager = { getSession: jest.fn(), getSocketsInMap: jest.fn(), createSession: jest.fn(), destroySession: jest.fn(), updatePlayerPosition: jest.fn(), injectContext: jest.fn(), } as any; mockConfigManager = { getMapIdByStream: jest.fn(), getStreamByMap: jest.fn(), getTopicByObject: jest.fn(), getZulipConfig: jest.fn(), hasMap: jest.fn(), hasStream: jest.fn(), getAllMapIds: jest.fn(), getAllStreams: jest.fn(), reloadConfig: jest.fn(), validateConfig: jest.fn(), } as any; mockClientPool = { getUserClient: jest.fn(), createUserClient: jest.fn(), destroyUserClient: jest.fn(), hasUserClient: jest.fn(), sendMessage: jest.fn(), registerEventQueue: jest.fn(), deregisterEventQueue: jest.fn(), getPoolStats: jest.fn(), cleanupIdleClients: jest.fn(), } as any; mockDistributor = { sendChatRender: jest.fn(), broadcastToMap: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ ZulipEventProcessorService, { provide: AppLoggerService, useValue: mockLogger, }, { provide: SessionManagerService, useValue: mockSessionManager, }, { provide: 'ZULIP_CONFIG_SERVICE', useValue: mockConfigManager, }, { provide: 'ZULIP_CLIENT_POOL_SERVICE', useValue: mockClientPool, }, ], }).compile(); service = module.get(ZulipEventProcessorService); service.setMessageDistributor(mockDistributor); }); afterEach(async () => { await service.stopEventProcessing(); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('convertMessageFormat - 消息格式转换', () => { it('应该正确转换基本的Zulip消息', async () => { const zulipMessage = createMockZulipMessage({ sender_full_name: 'Alice', content: 'Hello everyone!', }); const result = await service.convertMessageFormat(zulipMessage); expect(result.t).toBe('chat_render'); expect(result.from).toBe('Alice'); expect(result.txt).toBe('Hello everyone!'); expect(result.bubble).toBe(true); }); it('应该从邮箱提取用户名当sender_full_name为空时', async () => { const zulipMessage = createMockZulipMessage({ sender_full_name: '', sender_email: 'bob@example.com', content: 'Test message', }); const result = await service.convertMessageFormat(zulipMessage); expect(result.from).toBe('bob'); }); it('应该移除Markdown格式', async () => { const zulipMessage = createMockZulipMessage({ content: '**bold** and *italic* and `code`', }); const result = await service.convertMessageFormat(zulipMessage); expect(result.txt).toBe('bold and italic and code'); }); it('应该截断过长的消息', async () => { const longContent = 'A'.repeat(300); const zulipMessage = createMockZulipMessage({ content: longContent, }); const result = await service.convertMessageFormat(zulipMessage); expect(result.txt.length).toBeLessThanOrEqual(200); expect(result.txt.endsWith('...')).toBe(true); }); }); /** * 属性测试: 消息格式转换完整性 * * **Feature: zulip-integration, Property 4: 消息格式转换完整性** * **Validates: Requirements 5.3, 5.4** * * 对于任何在Zulip和游戏之间转发的消息,转换后的消息应该包含所有必需的信息 * (发送者、内容、时间戳),并符合目标协议格式 */ describe('Property 4: 消息格式转换完整性', () => { /** * 属性: 对于任何有效的Zulip消息,转换后应该包含发送者信息 * 验证需求 5.3: 确定目标玩家时系统应将消息转换为游戏协议格式 * 验证需求 5.4: 转换消息格式时系统应包含发送者信息、消息内容和时间戳 */ it('对于任何有效的Zulip消息,转换后应该包含发送者信息', async () => { await fc.assert( fc.asyncProperty( // 生成有效的发送者全名 fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // 生成有效的发送者邮箱 fc.emailAddress(), // 生成有效的消息内容 fc.string({ minLength: 1, maxLength: 500 }).filter(s => s.trim().length > 0), async (senderName, senderEmail, content) => { const zulipMessage = createMockZulipMessage({ sender_full_name: senderName.trim(), sender_email: senderEmail, content: content.trim(), }); const result = await service.convertMessageFormat(zulipMessage); // 验证消息类型正确 expect(result.t).toBe('chat_render'); // 验证发送者信息存在且非空 expect(result.from).toBeDefined(); expect(result.from.length).toBeGreaterThan(0); // 验证发送者名称正确(应该是senderName或从邮箱提取) expect(result.from).toBe(senderName.trim()); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何sender_full_name为空的消息,应该从邮箱提取用户名 * 验证需求 5.4: 转换消息格式时系统应包含发送者信息 */ it('对于任何sender_full_name为空的消息,应该从邮箱提取用户名', async () => { await fc.assert( fc.asyncProperty( // 生成有效的邮箱用户名部分 fc.string({ minLength: 1, maxLength: 30 }) .filter(s => s.trim().length > 0 && /^[a-zA-Z0-9._-]+$/.test(s)), // 生成有效的域名 fc.constantFrom('example.com', 'test.org', 'mail.net'), // 生成有效的消息内容 fc.string({ minLength: 1, maxLength: 200 }).filter(s => s.trim().length > 0), async (username, domain, content) => { const email = `${username}@${domain}`; const zulipMessage = createMockZulipMessage({ sender_full_name: '', // 空的全名 sender_email: email, content: content.trim(), }); const result = await service.convertMessageFormat(zulipMessage); // 验证从邮箱提取了用户名 expect(result.from).toBe(username); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何消息内容,转换后应该保留核心文本信息 * 验证需求 5.4: 转换消息格式时系统应包含消息内容 */ it('对于任何消息内容,转换后应该保留核心文本信息', async () => { await fc.assert( fc.asyncProperty( // 生成纯文本消息内容(不含Markdown和HTML标记) fc.string({ minLength: 1, maxLength: 150 }) .filter(s => { const trimmed = s.trim(); // 排除Markdown标记和HTML标记 return trimmed.length > 0 && !/[*_`#\[\]<>]/.test(trimmed) && !trimmed.startsWith('>') && !trimmed.startsWith('-') && !trimmed.startsWith('+') && !/^\d+\./.test(trimmed); }), async (content) => { const zulipMessage = createMockZulipMessage({ content: content.trim(), }); const result = await service.convertMessageFormat(zulipMessage); // 验证消息内容存在 expect(result.txt).toBeDefined(); expect(result.txt.length).toBeGreaterThan(0); // 验证核心内容被保留(对于短消息应该完全匹配) if (content.trim().length <= 200) { expect(result.txt).toBe(content.trim()); } } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何超过200字符的消息,应该被截断并添加省略号 * 验证需求 5.4: 转换消息格式时系统应正确处理消息内容 */ it('对于任何超过200字符的消息,应该被截断并添加省略号', async () => { await fc.assert( fc.asyncProperty( // 生成超过200字符的纯字母数字消息内容(避免Markdown/HTML标记影响长度) fc.array(fc.constantFrom(...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 '.split('')), { minLength: 250, maxLength: 500 }) .map(arr => arr.join('')), async (content: string) => { const zulipMessage = createMockZulipMessage({ content: content, }); const result = await service.convertMessageFormat(zulipMessage); // 验证消息被截断 expect(result.txt.length).toBeLessThanOrEqual(200); // 验证添加了省略号 expect(result.txt.endsWith('...')).toBe(true); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何包含Markdown的消息,应该正确移除格式标记 * 验证需求 5.4: 转换消息格式时系统应正确处理消息内容 * 注意: 列表标记(- + *)会被转换为bullet point(•),这是预期行为,不在此测试范围 */ it('对于任何包含Markdown的消息,应该正确移除格式标记', async () => { await fc.assert( fc.asyncProperty( // 生成纯字母数字基础文本(避免特殊字符干扰) fc.array(fc.constantFrom(...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.split('')), { minLength: 1, maxLength: 50 }) .map(arr => arr.join('')), // 选择Markdown格式类型(仅测试inline格式,不测试列表) fc.constantFrom('bold', 'italic', 'code', 'link'), async (text: string, formatType: string) => { if (text.length === 0) return; // 跳过空字符串 let markdownContent: string; switch (formatType) { case 'bold': markdownContent = `**${text}**`; break; case 'italic': // 使用下划线斜体避免与列表标记冲突 markdownContent = `_${text}_`; break; case 'code': markdownContent = `\`${text}\``; break; case 'link': markdownContent = `[${text}](https://example.com)`; break; default: markdownContent = text; } const zulipMessage = createMockZulipMessage({ content: markdownContent, }); const result = await service.convertMessageFormat(zulipMessage); // 验证Markdown标记被移除,只保留文本 expect(result.txt).toBe(text); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何消息,转换结果应该符合游戏协议格式 * 验证需求 5.3: 确定目标玩家时系统应将消息转换为游戏协议格式 */ it('对于任何消息,转换结果应该符合游戏协议格式', async () => { await fc.assert( fc.asyncProperty( // 生成随机的Zulip消息属性 fc.record({ sender_full_name: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), sender_email: fc.emailAddress(), content: fc.string({ minLength: 1, maxLength: 500 }).filter(s => s.trim().length > 0), timestamp: fc.integer({ min: 1000000000, max: 2000000000 }), subject: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), }), async (props) => { const zulipMessage = createMockZulipMessage({ sender_full_name: props.sender_full_name.trim(), sender_email: props.sender_email, content: props.content.trim(), timestamp: props.timestamp, subject: props.subject.trim(), }); const result = await service.convertMessageFormat(zulipMessage); // 验证游戏协议格式 expect(result).toHaveProperty('t', 'chat_render'); expect(result).toHaveProperty('from'); expect(result).toHaveProperty('txt'); expect(result).toHaveProperty('bubble'); // 验证类型正确 expect(typeof result.t).toBe('string'); expect(typeof result.from).toBe('string'); expect(typeof result.txt).toBe('string'); expect(typeof result.bubble).toBe('boolean'); // 验证bubble默认为true expect(result.bubble).toBe(true); } ), { numRuns: 100 } ); }, 60000); }); describe('determineTargetPlayers - 确定目标玩家', () => { it('应该根据Stream名称确定目标地图并获取玩家列表', async () => { const zulipMessage = createMockZulipMessage({ display_recipient: 'Tavern', }); mockConfigManager.getMapIdByStream.mockReturnValue('tavern'); mockSessionManager.getSocketsInMap.mockResolvedValue(['socket-1', 'socket-2']); mockSessionManager.getSession.mockImplementation(async (socketId) => { if (socketId === 'socket-1') { return createMockSession({ socketId: 'socket-1', userId: 'user-1' }); } return createMockSession({ socketId: 'socket-2', userId: 'user-2' }); }); const result = await service.determineTargetPlayers(zulipMessage, 'Tavern', 'sender-user'); expect(mockConfigManager.getMapIdByStream).toHaveBeenCalledWith('Tavern'); expect(mockSessionManager.getSocketsInMap).toHaveBeenCalledWith('tavern'); expect(result).toContain('socket-1'); expect(result).toContain('socket-2'); }); it('应该排除消息发送者', async () => { const zulipMessage = createMockZulipMessage({ display_recipient: 'Tavern', }); mockConfigManager.getMapIdByStream.mockReturnValue('tavern'); mockSessionManager.getSocketsInMap.mockResolvedValue(['socket-1', 'socket-2']); mockSessionManager.getSession.mockImplementation(async (socketId) => { if (socketId === 'socket-1') { return createMockSession({ socketId: 'socket-1', userId: 'sender-user' }); // 发送者 } return createMockSession({ socketId: 'socket-2', userId: 'other-user' }); }); const result = await service.determineTargetPlayers(zulipMessage, 'Tavern', 'sender-user'); // 发送者应该被排除 expect(result).not.toContain('socket-1'); expect(result).toContain('socket-2'); }); it('应该在未找到地图时返回空列表', async () => { const zulipMessage = createMockZulipMessage({ display_recipient: 'UnknownStream', }); mockConfigManager.getMapIdByStream.mockReturnValue(null); const result = await service.determineTargetPlayers(zulipMessage, 'UnknownStream', 'sender-user'); expect(result).toEqual([]); }); }); describe('distributeMessage - 消息分发', () => { it('应该向所有目标玩家发送消息', async () => { const gameMessage: GameMessage = { t: 'chat_render', from: 'TestUser', txt: 'Hello!', bubble: true, }; const targetPlayers = ['socket-1', 'socket-2', 'socket-3']; await service.distributeMessage(gameMessage, targetPlayers); expect(mockDistributor.sendChatRender).toHaveBeenCalledTimes(3); expect(mockDistributor.sendChatRender).toHaveBeenCalledWith('socket-1', 'TestUser', 'Hello!', true); expect(mockDistributor.sendChatRender).toHaveBeenCalledWith('socket-2', 'TestUser', 'Hello!', true); expect(mockDistributor.sendChatRender).toHaveBeenCalledWith('socket-3', 'TestUser', 'Hello!', true); }); it('应该在没有目标玩家时不发送任何消息', async () => { const gameMessage: GameMessage = { t: 'chat_render', from: 'TestUser', txt: 'Hello!', bubble: true, }; await service.distributeMessage(gameMessage, []); expect(mockDistributor.sendChatRender).not.toHaveBeenCalled(); }); }); /** * 属性测试: 消息接收和分发 * * **Feature: zulip-integration, Property 5: 消息接收和分发** * **Validates: Requirements 5.1, 5.2, 5.5** * * 对于任何从Zulip接收的消息,系统应该正确确定目标玩家,转换消息格式, * 并通过WebSocket发送给所有相关的游戏客户端 */ describe('Property 5: 消息接收和分发', () => { /** * 属性: 对于任何有效的Stream消息,应该正确确定目标地图 * 验证需求 5.1: Zulip中有新消息时系统应通过事件队列接收消息通知 * 验证需求 5.2: 接收到消息通知时系统应确定哪些在线玩家应该接收该消息 */ it('对于任何有效的Stream消息,应该正确确定目标地图', async () => { await fc.assert( fc.asyncProperty( // 生成有效的Stream名称 fc.constantFrom('Tavern', 'Novice Village', 'Market', 'General'), // 生成对应的地图ID fc.constantFrom('tavern', 'novice_village', 'market', 'general'), // 生成玩家Socket ID列表 fc.array( fc.string({ minLength: 5, maxLength: 20 }).filter(s => s.trim().length > 0), { minLength: 0, maxLength: 10 } ), async (streamName, mapId, socketIds) => { const zulipMessage = createMockZulipMessage({ display_recipient: streamName, }); // 设置模拟返回值 mockConfigManager.getMapIdByStream.mockReturnValue(mapId); mockSessionManager.getSocketsInMap.mockResolvedValue(socketIds); mockSessionManager.getSession.mockImplementation(async (socketId) => { return createMockSession({ socketId, userId: `user_${socketId}`, currentMap: mapId, }); }); const result = await service.determineTargetPlayers( zulipMessage, streamName, 'different-sender' ); // 验证调用了正确的方法 expect(mockConfigManager.getMapIdByStream).toHaveBeenCalledWith(streamName); if (socketIds.length > 0) { expect(mockSessionManager.getSocketsInMap).toHaveBeenCalledWith(mapId); } // 验证返回的Socket ID数量正确(所有玩家都不是发送者) expect(result.length).toBe(socketIds.length); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何消息分发,发送者应该被排除在接收者之外 * 验证需求 5.2: 接收到消息通知时系统应确定哪些在线玩家应该接收该消息 */ it('对于任何消息分发,发送者应该被排除在接收者之外', async () => { await fc.assert( fc.asyncProperty( // 生成发送者用户ID fc.string({ minLength: 5, maxLength: 20 }).filter(s => s.trim().length > 0), // 生成其他玩家用户ID列表 fc.array( fc.string({ minLength: 5, maxLength: 20 }).filter(s => s.trim().length > 0), { minLength: 1, maxLength: 5 } ), async (senderUserId, otherUserIds) => { const zulipMessage = createMockZulipMessage({ display_recipient: 'Tavern', }); // 创建包含发送者的Socket列表 const allSocketIds = [`socket_${senderUserId}`, ...otherUserIds.map(id => `socket_${id}`)]; mockConfigManager.getMapIdByStream.mockReturnValue('tavern'); mockSessionManager.getSocketsInMap.mockResolvedValue(allSocketIds); mockSessionManager.getSession.mockImplementation(async (socketId) => { const userId = socketId.replace('socket_', ''); return createMockSession({ socketId, userId, }); }); const result = await service.determineTargetPlayers( zulipMessage, 'Tavern', senderUserId ); // 验证发送者被排除 expect(result).not.toContain(`socket_${senderUserId}`); // 验证其他玩家都在结果中 for (const userId of otherUserIds) { expect(result).toContain(`socket_${userId}`); } } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何消息分发,所有目标玩家都应该收到消息 * 验证需求 5.5: 推送消息到游戏客户端时系统应通过WebSocket发送消息 */ it('对于任何消息分发,所有目标玩家都应该收到消息', async () => { await fc.assert( fc.asyncProperty( // 生成发送者名称 fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0), // 生成消息内容 fc.string({ minLength: 1, maxLength: 200 }).filter(s => s.trim().length > 0), // 生成目标玩家Socket ID列表 fc.array( fc.string({ minLength: 5, maxLength: 20 }).filter(s => s.trim().length > 0), { minLength: 1, maxLength: 10 } ), async (from, txt, targetPlayers) => { const gameMessage: GameMessage = { t: 'chat_render', from: from.trim(), txt: txt.trim(), bubble: true, }; // 重置mock mockDistributor.sendChatRender.mockClear(); await service.distributeMessage(gameMessage, targetPlayers); // 验证每个目标玩家都收到了消息 expect(mockDistributor.sendChatRender).toHaveBeenCalledTimes(targetPlayers.length); for (const socketId of targetPlayers) { expect(mockDistributor.sendChatRender).toHaveBeenCalledWith( socketId, from.trim(), txt.trim(), true ); } } ), { numRuns: 100 } ); }, 60000); /** * 属性: 对于任何未知Stream的消息,应该返回空的目标玩家列表 * 验证需求 5.2: 接收到消息通知时系统应确定哪些在线玩家应该接收该消息 */ it('对于任何未知Stream的消息,应该返回空的目标玩家列表', async () => { await fc.assert( fc.asyncProperty( // 生成未知的Stream名称 fc.string({ minLength: 5, maxLength: 50 }) .filter(s => s.trim().length > 0) .map(s => `Unknown_${s}`), async (unknownStream) => { const zulipMessage = createMockZulipMessage({ display_recipient: unknownStream, }); // 模拟未找到对应地图 mockConfigManager.getMapIdByStream.mockReturnValue(null); const result = await service.determineTargetPlayers( zulipMessage, unknownStream, 'sender-user' ); // 验证返回空列表 expect(result).toEqual([]); // 验证没有尝试获取玩家列表 expect(mockSessionManager.getSocketsInMap).not.toHaveBeenCalled(); } ), { numRuns: 100 } ); }, 60000); /** * 属性: 完整的消息处理流程应该正确执行 * 验证需求 5.1, 5.2, 5.5: 完整的消息接收和分发流程 */ it('完整的消息处理流程应该正确执行', async () => { await fc.assert( fc.asyncProperty( // 生成发送者信息 fc.record({ senderName: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0), senderEmail: fc.emailAddress(), senderUserId: fc.string({ minLength: 5, maxLength: 20 }).filter(s => s.trim().length > 0), }), // 生成消息内容 fc.string({ minLength: 1, maxLength: 150 }).filter(s => s.trim().length > 0), // 生成Stream名称 fc.constantFrom('Tavern', 'Novice Village'), // 生成目标玩家数量 fc.integer({ min: 1, max: 5 }), async (sender, content, streamName, playerCount) => { const zulipMessage = createMockZulipMessage({ sender_full_name: sender.senderName.trim(), sender_email: sender.senderEmail, content: content.trim(), display_recipient: streamName, }); // 生成目标玩家 const targetSocketIds = Array.from( { length: playerCount }, (_, i) => `socket_player_${i}` ); const mapId = streamName.toLowerCase().replace(' ', '_'); mockConfigManager.getMapIdByStream.mockReturnValue(mapId); mockSessionManager.getSocketsInMap.mockResolvedValue(targetSocketIds); mockSessionManager.getSession.mockImplementation(async (socketId) => { return createMockSession({ socketId, userId: socketId.replace('socket_', 'user_'), }); }); // 重置mock mockDistributor.sendChatRender.mockClear(); // 执行完整的消息处理 const result = await service.processMessageManually(zulipMessage, sender.senderUserId); // 验证处理成功 expect(result.success).toBe(true); expect(result.targetCount).toBe(playerCount); // 验证消息被分发给所有目标玩家 expect(mockDistributor.sendChatRender).toHaveBeenCalledTimes(playerCount); } ), { numRuns: 100 } ); }, 60000); }); describe('getProcessingStats - 获取处理统计', () => { it('应该返回正确的统计信息', () => { const stats = service.getProcessingStats(); expect(stats).toHaveProperty('isActive'); expect(stats).toHaveProperty('activeQueues'); expect(stats).toHaveProperty('totalQueues'); expect(stats).toHaveProperty('queueIds'); expect(stats).toHaveProperty('processedEvents'); expect(stats).toHaveProperty('processedMessages'); }); }); describe('registerEventQueue / unregisterEventQueue - 队列管理', () => { it('应该正确注册和注销事件队列', async () => { const queueId = 'test-queue-123'; const userId = 'user-456'; // 注册队列 await service.registerEventQueue(queueId, userId, 0); let stats = service.getProcessingStats(); expect(stats.queueIds).toContain(queueId); expect(stats.totalQueues).toBe(1); // 注销队列 await service.unregisterEventQueue(queueId); stats = service.getProcessingStats(); expect(stats.queueIds).not.toContain(queueId); expect(stats.totalQueues).toBe(0); }); }); });