import { afterEach, describe, expect, it, vi } from 'vitest' import { parseSSEBlock, streamSSE } from './sse' afterEach(() => vi.restoreAllMocks()) describe('parseSSEBlock', () => { it('parses Chinese token events with CRLF framing', () => { expect(parseSSEBlock('event: token\r\ndata: {"content":"知识"}')).toEqual({ event: 'token', data: { content: '知识' }, }) }) it('ignores heartbeat comments', () => { expect(parseSSEBlock(': ping')).toBeNull() }) }) describe('streamSSE', () => { it('incrementally decodes arbitrary UTF-8 chunks, CRLF, multiple events, and a tail event', async () => { const bytes = new TextEncoder().encode( 'event: status\r\ndata: {"phase":"researching"}\r\n\r\n' + 'event: token\ndata: {"content":"知识"}\n\n' + 'event: done\ndata: {"message_id":"m1"}', ) const chunks = [bytes.slice(0, 7), bytes.slice(7, 58), bytes.slice(58, 61), bytes.slice(61)] const body = new ReadableStream({ start(controller) { for (const chunk of chunks) controller.enqueue(chunk) controller.close() }, }) const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(body)) const controller = new AbortController() const events: unknown[] = [] await streamSSE('/stream', { query: 'q' }, (event) => events.push(event), controller.signal) expect(events).toEqual([ { event: 'status', data: { phase: 'researching' } }, { event: 'token', data: { content: '知识' } }, { event: 'done', data: { message_id: 'm1' } }, ]) expect(fetchMock).toHaveBeenCalledWith('/stream', expect.objectContaining({ signal: controller.signal })) }) })