import { streamCopilotChat } from "./chatStream"; import { ReadableStream } from "stream/web"; import { TextEncoder, TextDecoder } from "util"; if (!globalThis.ReadableStream) { // @ts-expect-error test polyfill globalThis.ReadableStream = ReadableStream; } if (!globalThis.TextEncoder) { // @ts-expect-error test polyfill globalThis.TextEncoder = TextEncoder; } if (!globalThis.TextDecoder) { // @ts-expect-error test polyfill globalThis.TextDecoder = TextDecoder; } jest.mock("@/lib/apiFetch", () => ({ apiFetch: jest.fn(), })); const { apiFetch } = jest.requireMock("@/lib/apiFetch") as { apiFetch: jest.Mock; }; const makeStream = (chunks: string[]) => new ReadableStream({ start(controller) { const encoder = new TextEncoder(); chunks.forEach((chunk) => controller.enqueue(encoder.encode(chunk))); controller.close(); }, }); describe("streamCopilotChat", () => { beforeEach(() => { apiFetch.mockReset(); }); it("parses token and done events from chunked SSE", async () => { apiFetch.mockResolvedValue({ ok: true, body: makeStream([ 'event: token\ndata: {"conversationId":"c1","content":"he"}\n\n', 'event: token\ndata: {"conversationId":"c1","content":"llo"}\n\n', 'event: done\ndata: {"conversationId":"c1"}\n\n', ]), }); const events: Array<{ type: string; content?: string; conversationId?: string }> = []; await streamCopilotChat({ message: "hi", onEvent: (event) => events.push(event), }); expect(apiFetch).toHaveBeenCalledWith( expect.stringContaining("/api/v1/copilot/chat/stream"), expect.objectContaining({ method: "POST", projectHeaderMode: "include", skipAuthRedirect: true, }), ); expect(events).toEqual([ { type: "token", conversationId: "c1", content: "he" }, { type: "token", conversationId: "c1", content: "llo" }, { type: "done", conversationId: "c1" }, ]); }); it("emits error when response is not ok", async () => { apiFetch.mockResolvedValue({ ok: false, body: null, text: async () => "bad request", }); const events: Array<{ type: string; message?: string; detail?: string }> = []; await streamCopilotChat({ message: "hi", onEvent: (event) => events.push(event), }); expect(events).toEqual([ { type: "error", message: "stream request failed", detail: "bad request" }, ]); }); it("emits re-login message on unauthorized response", async () => { apiFetch.mockResolvedValue({ ok: false, status: 401, body: null, text: async () => "unauthorized", }); const events: Array<{ type: string; message?: string; detail?: string }> = []; await streamCopilotChat({ message: "hi", onEvent: (event) => events.push(event), }); expect(events).toEqual([ { type: "error", message: "Login expired. Please sign in again.", detail: undefined }, ]); }); it("emits network error when fetch throws", async () => { apiFetch.mockRejectedValue(new TypeError("Failed to fetch")); const events: Array<{ type: string; message?: string; detail?: string }> = []; await streamCopilotChat({ message: "hi", onEvent: (event) => events.push(event), }); expect(events).toEqual([ { type: "error", message: "network request failed", detail: "Failed to fetch" }, ]); }); });