UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

299 lines (265 loc) • 10.3 kB
import { describe, expect, it } from "vitest"; import { createSSEEventDecoder, SSEEventDecoder, SSEEventDecoderError, } from "./SSEEventDecoder"; describe("SSEEventDecoder", () => { it("keeps an internally terminated decoder closed for later chunks", () => { const decoder = createSSEEventDecoder( { trailing: "dispatch" }, (event) => event.data === "[DONE]", ); expect( decoder.push('data: before\n\ndata: [DONE]\n\nevent: custom\ndata: {"a"'), ).toEqual([{ data: "before" }, { data: "[DONE]" }]); expect(decoder.push(":1}\n\ndata: 2\n\n")).toEqual([]); expect(decoder.flush()).toBeNull(); }); it("decodes LF terminated events", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data: x\n\n")).toEqual([{ data: "x" }]); }); it("ignores one initial BOM across chunk boundaries and empty pushes", () => { const text = "\uFEFFdata: hello\n\n"; for (let split = 0; split <= text.length; split++) { const decoder = new SSEEventDecoder(); const events = [ ...decoder.push(""), ...decoder.push(text.slice(0, split)), ...decoder.push(""), ...decoder.push(text.slice(split)), ]; expect(events).toEqual([{ data: "hello" }]); } }); it("preserves a later BOM at the start of a data chunk", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data: ")).toEqual([]); expect(decoder.push("\uFEFFhello\n\n")).toEqual([{ data: "\uFEFFhello" }]); }); it("does not strip a second BOM or leading whitespace from field names", () => { for (const prefix of ["\uFEFF\uFEFF", " ", "\n\uFEFF"]) { const decoder = new SSEEventDecoder(); expect(decoder.push(`${prefix}data: ignored\ndata: kept\n\n`)).toEqual([ { data: "kept" }, ]); } }); it("does not strip a BOM from later frames or after flush", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("\uFEFFdata: first\n\n")).toEqual([{ data: "first" }]); expect(decoder.push("\uFEFFdata: ignored\ndata: second\n\n")).toEqual([ { data: "second" }, ]); expect(decoder.flush()).toBeNull(); expect(decoder.push("\uFEFFdata: ignored\ndata: third\n\n")).toEqual([ { data: "third" }, ]); }); it("decodes CRLF terminated events", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data: x\r\n\r\n")).toEqual([{ data: "x" }]); }); it("decodes CR terminated events", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data: x\r\r")).toEqual([{ data: "x" }]); }); it("handles CRLF split across pushes", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data: x\r")).toEqual([]); expect(decoder.push("\n\r\n")).toEqual([{ data: "x" }]); }); it("handles CR terminated lines split across pushes", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data: x\r")).toEqual([]); expect(decoder.push("\r")).toEqual([{ data: "x" }]); }); it("decodes multiple events in one push", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data: first\n\ndata: second\n\n")).toEqual([ { data: "first" }, { data: "second" }, ]); }); it("joins multiple data lines with a newline", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data: first\ndata: second\n\n")).toEqual([ { data: "first\nsecond" }, ]); }); it("preserves data values without a space after the colon", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data:x\n\n")).toEqual([{ data: "x" }]); }); it("skips comment lines", () => { const decoder = new SSEEventDecoder(); expect(decoder.push(": heartbeat\ndata: x\n\n")).toEqual([{ data: "x" }]); }); it("captures event fields and clears them after dispatch", () => { const decoder = new SSEEventDecoder(); expect( decoder.push("event: update\ndata: first\n\ndata: second\n\n"), ).toEqual([{ event: "update", data: "first" }, { data: "second" }]); }); it("persists IDs across frames", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("id: 1\ndata: first\n\ndata: second\n\n")).toEqual([ { data: "first", id: "1" }, { data: "second", id: "1" }, ]); }); it("parses numeric retries and ignores non-numeric retries", () => { const decoder = new SSEEventDecoder(); expect( decoder.push( "retry: 1000\ndata: first\n\nretry: invalid\ndata: second\n\n", ), ).toEqual([ { data: "first", retry: 1000 }, { data: "second", retry: 1000 }, ]); }); it("ignores ids containing NUL and keeps ids containing spaces", () => { const decoder = new SSEEventDecoder(); expect( decoder.push( "id: ok\ndata: a\n\nid: bad\u0000id\ndata: b\n\nid: spaced id\ndata: c\n\n", ), ).toEqual([ { data: "a", id: "ok" }, { data: "b", id: "ok" }, { data: "c", id: "spaced id" }, ]); }); it("ignores non-digit and unsafe-integer retry values", () => { const decoder = new SSEEventDecoder(); expect( decoder.push( "retry: 1000\ndata: a\n\nretry: -1\ndata: b\n\nretry: 1.5\ndata: c\n\nretry: 1e3\ndata: d\n\nretry:\ndata: e\n\n" + `retry: ${"9".repeat(400)}\ndata: f\n\n`, ), ).toEqual([ { data: "a", retry: 1000 }, { data: "b", retry: 1000 }, { data: "c", retry: 1000 }, { data: "d", retry: 1000 }, { data: "e", retry: 1000 }, { data: "f", retry: 1000 }, ]); }); it("does not dispatch blank frames", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("event: update\n\ndata: x\n\n")).toEqual([ { data: "x" }, ]); }); it("preserves a pending LF across empty pushes", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data: x\r")).toEqual([]); expect(decoder.push("")).toEqual([]); expect(decoder.push("\n\r\n")).toEqual([{ data: "x" }]); }); it("drops unterminated trailing data by default", () => { const decoder = new SSEEventDecoder(); expect(decoder.push("data: x")).toEqual([]); expect(decoder.flush()).toBeNull(); }); it("dispatches an unterminated final data line when configured", () => { const decoder = new SSEEventDecoder({ trailing: "dispatch" }); expect(decoder.push("data: x")).toEqual([]); expect(decoder.flush()).toEqual({ data: "x" }); }); it("makes flush idempotent", () => { const decoder = new SSEEventDecoder({ trailing: "dispatch" }); decoder.push("data: x"); expect(decoder.flush()).toEqual({ data: "x" }); expect(decoder.flush()).toBeNull(); }); it("rejects an unterminated line that exceeds the configured limit", () => { const decoder = new SSEEventDecoder({ maxLineLength: 8 }); expect(decoder.push("data: ")).toEqual([]); expect(() => decoder.push("abc")).toThrow( "SSE line exceeds maxLineLength (9 > 8)", ); }); it("rejects a complete line that exceeds the configured limit", () => { const decoder = new SSEEventDecoder({ maxLineLength: 8 }); expect(() => decoder.push("data: abc\n")).toThrow( "SSE line exceeds maxLineLength (9 > 8)", ); }); it("rejects an event whose data lines exceed the configured limit", () => { const decoder = new SSEEventDecoder({ maxEventLength: 2 }); expect(decoder.push("data: a\n")).toEqual([]); expect(() => decoder.push("data: b\n")).toThrow( "SSE event exceeds maxEventLength (3 > 2)", ); }); it("counts parsed data values toward the configured event limit", () => { const decoder = new SSEEventDecoder({ maxEventLength: 3 }); expect(decoder.push("data: abc\n\n")).toEqual([{ data: "abc" }]); }); it("rejects an unfinished data line that exceeds the event limit", () => { const decoder = new SSEEventDecoder({ maxEventLength: 3 }); expect(decoder.push("data: ab")).toEqual([]); expect(() => decoder.push("cd")).toThrow( "SSE event exceeds maxEventLength (4 > 3)", ); }); it("does not dispatch a partial frame after event overflow", () => { const decoder = new SSEEventDecoder({ trailing: "dispatch", maxEventLength: 2, }); expect(decoder.push("data: a\n")).toEqual([]); expect(() => decoder.push("data: b\n")).toThrow( "SSE event exceeds maxEventLength (3 > 2)", ); expect(decoder.flush()).toBeNull(); }); it("validates configured limits", () => { expect(() => new SSEEventDecoder({ maxLineLength: 0 })).toThrowError( SSEEventDecoderError, ); expect(() => new SSEEventDecoder({ maxEventLength: Infinity })).toThrow( "maxEventLength must be a positive safe integer", ); }); it.each(["\n", "\r", "\r\n"])( "decodes fragmented events with %j delimiters", (newline) => { const data = "x".repeat(4 * 1024); const wire = `\uFEFFid: 7${newline}retry: 1000${newline}event: update${newline}data: ${data}${newline}data: end${newline}${newline}data: next${newline}${newline}`; for (const chunkSize of [1, 1024, 4093]) { const decoder = new SSEEventDecoder(); const events = []; for (let i = 0; i < wire.length; i += chunkSize) { events.push(...decoder.push(wire.slice(i, i + chunkSize))); events.push(...decoder.push("")); } expect(events).toEqual([ { id: "7", retry: 1000, event: "update", data: `${data}\nend` }, { id: "7", retry: 1000, data: "next" }, ]); expect(decoder.flush()).toBeNull(); } }, ); it.each(["drop", "dispatch"] as const)( "clears fragmented trailing data with the %s policy", (trailing) => { const decoder = new SSEEventDecoder({ trailing }); const data = "x".repeat(8192); decoder.push("data: "); for (let i = 0; i < data.length; i += 128) { expect(decoder.push(data.slice(i, i + 128))).toEqual([]); } expect(decoder.flush()).toEqual( trailing === "dispatch" ? { data } : null, ); expect(decoder.flush()).toBeNull(); expect(decoder.push("data: fresh\n\n")).toEqual([{ data: "fresh" }]); }, ); });