| 1 | import assert from "node:assert/strict"; |
| 2 | import { describe, it } from "node:test"; |
| 3 | import { SseParser, parseFrame, type RuntimeEvent } from "../sse"; |
| 4 | |
| 5 | function eventsOf(chunks: string[]): RuntimeEvent[] { |
| 6 | const parser = new SseParser(); |
| 7 | return chunks.flatMap((chunk) => parser.push(chunk)); |
| 8 | } |
| 9 | |
| 10 | describe("SseParser", () => { |
| 11 | it("parses a single frame with the runtime envelope", () => { |
| 12 | const events = eventsOf([ |
| 13 | 'data: {"seq":42,"previous_seq":38,"event":"item.delta","thread_id":"t1","turn_id":"turn_1","item_id":"i1","timestamp":"2026-02-11T20:18:49.123Z","payload":{"delta":"partial output","kind":"agent_message"}}\n\n', |
| 14 | ]); |
| 15 | assert.equal(events.length, 1); |
| 16 | assert.equal(events[0].seq, 42); |
| 17 | assert.equal(events[0].previousSeq, 38); |
| 18 | assert.equal(events[0].event, "item.delta"); |
| 19 | assert.equal(events[0].threadId, "t1"); |
| 20 | assert.equal(events[0].turnId, "turn_1"); |
| 21 | assert.equal(events[0].itemId, "i1"); |
| 22 | assert.deepEqual(events[0].payload, { delta: "partial output", kind: "agent_message" }); |
| 23 | }); |
| 24 | |
| 25 | it("parses several frames in one chunk", () => { |
| 26 | const chunk = |
| 27 | 'data: {"seq":1,"event":"turn.started"}\n\n' + |
| 28 | 'data: {"seq":2,"event":"item.started"}\n\n' + |
| 29 | 'data: {"seq":3,"event":"turn.completed"}\n\n'; |
| 30 | const events = eventsOf([chunk]); |
| 31 | assert.deepEqual( |
| 32 | events.map((event) => event.seq), |
| 33 | [1, 2, 3], |
| 34 | ); |
| 35 | }); |
| 36 | |
| 37 | it("reassembles a frame split across chunks", () => { |
| 38 | const events = eventsOf([ |
| 39 | 'data: {"seq":7,', |
| 40 | '"event":"item.delta"}', |
| 41 | "\n\n", |
| 42 | ]); |
| 43 | assert.equal(events.length, 1); |
| 44 | assert.equal(events[0].seq, 7); |
| 45 | assert.equal(events[0].event, "item.delta"); |
| 46 | }); |
| 47 | |
| 48 | it("handles CRLF frame boundaries", () => { |
| 49 | const events = eventsOf(['data: {"seq":9,"event":"turn.started"}\r\n\r\n']); |
| 50 | assert.equal(events.length, 1); |
| 51 | assert.equal(events[0].seq, 9); |
| 52 | }); |
| 53 | |
| 54 | it("ignores comments and heartbeats", () => { |
| 55 | const events = eventsOf([": heartbeat\n\n", ": keep-alive\n\n"]); |
| 56 | assert.equal(events.length, 0); |
| 57 | }); |
| 58 | |
| 59 | it("ignores frames without valid JSON", () => { |
| 60 | const events = eventsOf(["data: not-json\n\n"]); |
| 61 | assert.equal(events.length, 0); |
| 62 | }); |
| 63 | |
| 64 | it("joins multi-line data fields", () => { |
| 65 | // Two `data:` lines join with \n; the joined payload is valid JSON here. |
| 66 | const events = eventsOf(['data: {"seq":1,\ndata: "event":"x"}\n\n']); |
| 67 | assert.equal(events.length, 1); |
| 68 | assert.equal(events[0].seq, 1); |
| 69 | assert.equal(events[0].event, "x"); |
| 70 | const good = parseFrame('data: {"seq":1,\ndata: "x":1}\n\n'); |
| 71 | assert.equal(good, undefined); // parses, but has no event name |
| 72 | }); |
| 73 | |
| 74 | it("drops events without seq or event name", () => { |
| 75 | assert.equal(eventsOf(['data: {"event":"no-seq"}\n\n']).length, 0); |
| 76 | assert.equal(eventsOf(['data: {"seq":1}\n\n']).length, 0); |
| 77 | }); |
| 78 | }); |
| 79 |