/** * TASK-0122 — the frontend SSE parser must round-trip ALL delta types * (delta / tool_call / usage / error / done), skip keepalive comment * lines, and survive partial frames split across chunk boundaries. * * @vitest-environment node (no DOM needed; the config default is jsdom, * which is not installed — every web test opts into node explicitly). */ import { describe, it, expect } from "vitest"; import { parseSseFrames, type AiSseFrame } from "@/lib/ai"; const wire = (frames: AiSseFrame[], keepaliveEvery: number) => frames .map((f, i) => (keepaliveEvery > 0 && i > 0 && i % keepaliveEvery === 0 ? `: keepalive\n\ndata: ${JSON.stringify(f)}\n\n` : `data: ${JSON.stringify(f)}\n\n`)) .join(""); describe("parseSseFrames", () => { it("parses every delta type in the documented contract", () => { const frames: AiSseFrame[] = [ { type: "delta", text: "Hel" }, { type: "delta", text: "lo" }, { type: "tool_call", id: "mock_call_0", name: "createPage", arguments: { title: "X" } }, { type: "usage", tokensIn: 12, tokensOut: 4 }, { type: "error", code: "TIMEOUT", message: "The AI request took too long.", retryable: true, retryAfterSeconds: 5 }, { type: "done", assistantMessageId: "a1", userMessageId: "u1", reason: "ok" } ]; const { frames: out, rest } = parseSseFrames(wire(frames, 2)); expect(rest).toBe(""); expect(out).toEqual(frames); }); it("skips `: keepalive` comment lines", () => { const { frames } = parseSseFrames(": keepalive\n\ndata: {\"type\":\"delta\",\"text\":\"a\"}\n\n: keepalive\n\ndata: {\"type\":\"done\",\"reason\":\"ok\"}\n\n"); expect(frames).toEqual([ { type: "delta", text: "a" }, { type: "done", reason: "ok" } ]); }); it("keeps a partial trailing frame for the next chunk (chunk boundary)", () => { // A real stream splits JSON mid-way between TCP chunks. const full = `data: {"type":"usage","tokensIn":7,"tokensOut":3}\n\n`; const { frames: f1, rest: r1 } = parseSseFrames(full.slice(0, 14)); // mid-JSON expect(f1).toEqual([]); expect(r1).toBe(full.slice(0, 14)); const { frames: f2, rest: r2 } = parseSseFrames(r1 + full.slice(14)); expect(f2).toEqual([{ type: "usage", tokensIn: 7, tokensOut: 3 }]); expect(r2).toBe(""); }); it("handles multiple frames in one chunk and frames split across chunks", () => { const full = `data: {"type":"delta","text":"a"}\n\ndata: {"type":"delta","text":"b"}\n\ndata: {"type":"done","reason":"ok"}\n\n`; // Split at a byte boundary INSIDE the second frame's JSON (mid-stream). const split = full.indexOf("b") + 3; const { frames: f1, rest: r1 } = parseSseFrames(full.slice(0, split)); expect(f1).toEqual([{ type: "delta", text: "a" }]); const { frames: f2, rest: r2 } = parseSseFrames(r1 + full.slice(split)); expect(f2).toEqual([{ type: "delta", text: "b" }, { type: "done", reason: "ok" }]); expect(r2).toBe(""); }); it("skips a malformed data line without dropping the stream", () => { const { frames } = parseSseFrames('data: {not-json}\n\ndata: {"type":"delta","text":"ok"}\n\n'); expect(frames).toEqual([{ type: "delta", text: "ok" }]); }); it("round-trips the exact mock-provider stream (word deltas + sys tail)", () => { // The E2E happy path: mock echoes the question word-by-word. const words = "what is the launch plan".split(" "); const stream = words.map((w) => `data: ${JSON.stringify({ type: "delta", text: w === "what" ? w : ` ${w}` })}\n\n`).join("") + `data: ${JSON.stringify({ type: "usage", tokensIn: 3, tokensOut: 3 })}\n\ndata: ${JSON.stringify({ type: "done", assistantMessageId: "a", userMessageId: "u", reason: "ok" })}\n\n`; const { frames, rest } = parseSseFrames(stream); expect(rest).toBe(""); expect(frames.filter((f) => f.type === "delta")).toHaveLength(words.length); const text = frames.filter((f) => f.type === "delta").map((f) => (f as { text: string }).text).join(""); expect(text).toBe("what is the launch plan"); expect(frames[frames.length - 1]).toEqual({ type: "done", assistantMessageId: "a", userMessageId: "u", reason: "ok" }); }); });