Files
fusion/packages/dashboard/app/api/__tests__/legacy-chat-stream.test.ts
Fusion 2140148fe2 feat(FN-3609): split TaskDetailModal tests into focused suites
This merge splits the monolithic TaskDetailModal test file into six focused test suites (rendering, attachments/tabs, definition/actions, inline-editing/integrations, models/progress/workflow, responsive/dependencies), adds a changeset for the test isolation baseline, fixes chat SSE optimistic echo

Fusion-Task-Id: FN-3609
2026-05-06 16:05:44 -07:00

124 lines
3.6 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { streamChatResponse } from "../legacy";
function createChunkedStream(chunks: string[]): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
return new ReadableStream<Uint8Array>({
start(controller) {
for (const chunk of chunks) {
controller.enqueue(encoder.encode(chunk));
}
controller.close();
},
});
}
describe("streamChatResponse SSE parser", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("reconstructs text/done events split across arbitrary chunks", async () => {
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
createChunkedStream([
"event: text\n",
"data: \"Hel",
"lo \"\n\n",
"event: text\n",
"data: \"world\"\n\n",
"event: done\n",
"data: {\"messageId\":\"msg-1\"}\n\n",
]),
{ status: 200 },
),
);
const textChunks: string[] = [];
const donePayloads: Array<{ messageId: string; message?: { content: string } }> = [];
streamChatResponse("s-1", "hi", {
onText: (data) => textChunks.push(data),
onDone: (data) => donePayloads.push(data),
onError: vi.fn(),
});
await vi.waitFor(() => {
expect(textChunks.join("")).toBe("Hello world");
expect(donePayloads).toEqual([{ messageId: "msg-1" }]);
});
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("flushes terminal done event when stream ends without final newline", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(createChunkedStream(["event: done\ndata: {\"messageId\":\"msg-tail\"}"]), { status: 200 }),
);
const donePayloads: Array<{ messageId: string }> = [];
streamChatResponse("s-1", "hi", {
onDone: (data) => donePayloads.push(data),
onError: vi.fn(),
});
await vi.waitFor(() => {
expect(donePayloads).toEqual([{ messageId: "msg-tail" }]);
});
});
it("parses done payload assistant snapshots when present", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
createChunkedStream([
"event: done\n",
"data: {\"messageId\":\"msg-1\",\"message\":{\"id\":\"msg-1\",\"sessionId\":\"s-1\",\"role\":\"assistant\",\"content\":\"Final reply\",\"thinkingOutput\":null,\"metadata\":null,\"createdAt\":\"2026-01-01T00:00:00.000Z\"}}\n\n",
]),
{ status: 200 },
),
);
const donePayloads: Array<{ messageId: string; message?: { content: string } }> = [];
streamChatResponse("s-1", "hi", {
onDone: (data) => donePayloads.push(data),
onError: vi.fn(),
});
await vi.waitFor(() => {
expect(donePayloads).toEqual([
{
messageId: "msg-1",
message: {
id: "msg-1",
sessionId: "s-1",
role: "assistant",
content: "Final reply",
thinkingOutput: null,
metadata: null,
createdAt: "2026-01-01T00:00:00.000Z",
},
},
]);
});
});
it("handles done events that have no data payload", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(createChunkedStream(["event: done\n\n"]), { status: 200 }),
);
const donePayloads: Array<{ messageId: string }> = [];
streamChatResponse("s-1", "hi", {
onDone: (data) => donePayloads.push(data),
onError: vi.fn(),
});
await vi.waitFor(() => {
expect(donePayloads).toEqual([{ messageId: "" }]);
});
});
});