Files
TJWaterFrontend_Refine/src/lib/chatStream.test.ts
T

124 lines
3.4 KiB
TypeScript

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<Uint8Array>({
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" },
]);
});
});