48 lines
1.7 KiB
TypeScript
48 lines
1.7 KiB
TypeScript
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<Uint8Array>({
|
|
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 }))
|
|
})
|
|
})
|