/** * SSE (Server-Sent Events) Handler 测试 * * 测试 SSE 事件格式、订阅者管理、各类事件发送等功能 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { Hono } from 'hono'; // Mock dependencies before imports const mockExists = vi.fn(); const mockSessionManager = { exists: mockExists, }; vi.mock('../../src/session/manager.js', () => ({ getSessionManager: vi.fn(() => mockSessionManager), })); // Import after mocking import { emitEvent, broadcastEvent, emitStatusEvent, emitLogEvent, emitProgressEvent, emitFileChangeEvent, getSSEStats, handleSSE, } from '../../src/sse.js'; describe('SSE Handler', () => { beforeEach(() => { vi.clearAllMocks(); mockExists.mockReturnValue(true); }); describe('emitEvent - 发送事件', () => { it('无订阅者时不抛出错误', () => { expect(() => { emitEvent('non-existent-session', { event: 'test', data: { timestamp: Date.now(), payload: null }, }); }).not.toThrow(); }); }); describe('broadcastEvent - 广播事件', () => { it('无订阅者时不抛出错误', () => { expect(() => { broadcastEvent({ event: 'broadcast-test', data: { timestamp: Date.now(), payload: { message: 'hello' } }, }); }).not.toThrow(); }); }); describe('emitStatusEvent - 发送状态事件', () => { it('调用时不抛出错误', () => { expect(() => { emitStatusEvent('session-1', 'processing', { step: 1, total: 5 }); }).not.toThrow(); }); it('只传状态不传详情时不抛出错误', () => { expect(() => { emitStatusEvent('session-1', 'idle'); }).not.toThrow(); }); }); describe('emitLogEvent - 发送日志事件', () => { it('发送 info 级别日志', () => { expect(() => { emitLogEvent('session-1', 'info', 'Processing started'); }).not.toThrow(); }); it('发送 warn 级别日志', () => { expect(() => { emitLogEvent('session-1', 'warn', 'Rate limit approaching'); }).not.toThrow(); }); it('发送 error 级别日志', () => { expect(() => { emitLogEvent('session-1', 'error', 'Connection failed'); }).not.toThrow(); }); }); describe('emitProgressEvent - 发送进度事件', () => { it('发送进度(带消息)', () => { expect(() => { emitProgressEvent('session-1', 50, 'Half way done'); }).not.toThrow(); }); it('发送进度(不带消息)', () => { expect(() => { emitProgressEvent('session-1', 100); }).not.toThrow(); }); it('发送 0% 进度', () => { expect(() => { emitProgressEvent('session-1', 0, 'Starting...'); }).not.toThrow(); }); }); describe('emitFileChangeEvent - 发送文件变更事件', () => { it('发送文件创建事件', () => { expect(() => { emitFileChangeEvent('session-1', 'created', '/path/to/new/file.ts'); }).not.toThrow(); }); it('发送文件修改事件', () => { expect(() => { emitFileChangeEvent('session-1', 'modified', '/path/to/existing/file.ts'); }).not.toThrow(); }); it('发送文件删除事件', () => { expect(() => { emitFileChangeEvent('session-1', 'deleted', '/path/to/removed/file.ts'); }).not.toThrow(); }); }); describe('getSSEStats - 获取统计信息', () => { it('无订阅者时返回 0', () => { const stats = getSSEStats(); // 由于没有实际连接,应该至少有这些属性 expect(stats).toHaveProperty('sessions'); expect(stats).toHaveProperty('subscribers'); expect(typeof stats.sessions).toBe('number'); expect(typeof stats.subscribers).toBe('number'); }); it('初始状态统计为 0', () => { const stats = getSSEStats(); expect(stats.sessions).toBe(0); expect(stats.subscribers).toBe(0); }); }); describe('事件格式化', () => { it('emitEvent 格式化带 event 字段的事件', () => { // 无订阅者时调用不会抛错,但内部会格式化 emitEvent('test-session', { event: 'custom_event', data: { timestamp: 12345, payload: { key: 'value' } }, }); // 测试不抛错即为通过 }); it('emitEvent 格式化不带 event 字段的事件', () => { emitEvent('test-session', { data: { timestamp: 12345, payload: null }, }); }); it('复杂 payload 数据正确序列化', () => { expect(() => { emitStatusEvent('session-1', 'processing', { nested: { deep: { value: [1, 2, 3] } }, array: ['a', 'b', 'c'], number: 42, boolean: true, null: null, }); }).not.toThrow(); }); }); describe('handleSSE - SSE 路由处理', () => { it('会话不存在时返回 404', async () => { mockExists.mockReturnValue(false); const app = new Hono(); app.get('/api/sessions/:id/events', handleSSE); const res = await app.request('/api/sessions/non-existent/events'); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); expect(json.error).toBe('Session not found'); }); it('会话存在时返回 SSE 流响应', async () => { mockExists.mockReturnValue(true); const app = new Hono(); app.get('/api/sessions/:id/events', handleSSE); const res = await app.request('/api/sessions/valid-session/events'); // SSE 流应该返回 200 状态码 expect(res.status).toBe(200); // Content-Type 应该是 text/event-stream expect(res.headers.get('content-type')).toContain('text/event-stream'); }); }); });