Files
whale-town-end/src/business/zulip/services/zulip_event_processor.service.spec.ts
moyin bb796a2469 refactor:项目架构重构和命名规范化
- 统一文件命名为snake_case格式(kebab-case  snake_case)
- 重构zulip模块为zulip_core,明确Core层职责
- 重构user-mgmt模块为user_mgmt,统一命名规范
- 调整模块依赖关系,优化架构分层
- 删除过时的文件和目录结构
- 更新相关文档和配置文件

本次重构涉及大量文件重命名和模块重组,
旨在建立更清晰的项目架构和统一的命名规范。
2026-01-08 00:14:14 +08:00

830 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<AppLoggerService>;
let mockSessionManager: jest.Mocked<SessionManagerService>;
let mockConfigManager: jest.Mocked<IZulipConfigService>;
let mockClientPool: jest.Mocked<IZulipClientPoolService>;
let mockDistributor: jest.Mocked<MessageDistributor>;
// 创建模拟Zulip消息
const createMockZulipMessage = (overrides: Partial<ZulipMessage> = {}): 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> = {}): 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>(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);
});
});
});