sse.spec.ts 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. import { describe, expect, it } from 'vitest'
  2. import { LlmError } from '@deepseek-ai/dsh-llm'
  3. import { DONE, parseSse } from '../src/sse.ts'
  4. /**
  5. * DeepSeek protocol contract only: the [DONE] sentinel and STREAM_CLOSED on
  6. * EOF without it. SSE framing (chunk splits, CRLF, multi-data joins, comments)
  7. * is eventsource-parser's contract, not re-proven here.
  8. */
  9. /** Build an SSE byte stream from string fragments (fragments = network reads). */
  10. function bytes(...fragments: string[]): ReadableStream<Uint8Array<ArrayBuffer>> {
  11. const encoder = new TextEncoder()
  12. return new ReadableStream({
  13. start(controller) {
  14. for (const fragment of fragments) controller.enqueue(encoder.encode(fragment))
  15. controller.close()
  16. },
  17. })
  18. }
  19. async function collect(stream: AsyncIterable<string>): Promise<string[]> {
  20. const out: string[] = []
  21. for await (const item of stream) out.push(item)
  22. return out
  23. }
  24. describe('parseSse', () => {
  25. it('yields event payloads and the DONE sentinel', async () => {
  26. const events = await collect(parseSse(bytes('data: {"a":1}\n\ndata: [DONE]\n\n')))
  27. expect(events).toEqual(['{"a":1}', DONE])
  28. })
  29. it('stops yielding after DONE even when more data follows', async () => {
  30. const events = await collect(parseSse(bytes('data: [DONE]\n\ndata: {"late":1}\n\n')))
  31. expect(events).toEqual([DONE])
  32. })
  33. it('throws STREAM_CLOSED when the stream ends without DONE', async () => {
  34. await expect(collect(parseSse(bytes('data: {"a":1}\n\n')))).rejects.toThrow(LlmError)
  35. await expect(collect(parseSse(bytes('data: {"a":1}\n\n')))).rejects.toThrow(/without \[DONE\]/)
  36. })
  37. it('throws STREAM_CLOSED for an empty stream', async () => {
  38. await expect(collect(parseSse(bytes()))).rejects.toThrow(/without \[DONE\]/)
  39. })
  40. it('throws STREAM_CLOSED for a mid-event close', async () => {
  41. await expect(collect(parseSse(bytes('data: {"a"')))).rejects.toThrow(/without \[DONE\]/)
  42. })
  43. it('treats a final DONE missing its blank-line terminator as truncation', async () => {
  44. // Spec-strict framing: an event dispatches only on its blank-line
  45. // terminator, so an unterminated tail at EOF is STREAM_CLOSED — real
  46. // providers always terminate events, so a missing terminator is truncation.
  47. await expect(collect(parseSse(bytes('data: {"a":1}\n\ndata: [DONE]')))).rejects.toThrow(/without \[DONE\]/)
  48. })
  49. })