fix(FN-3760): add SSE first-event watchdog for legacy chat streams

- Add a first-event watchdog in legacy chat SSE handling to prevent hangs before initial stream data
- Cover watchdog behavior with legacy chat stream API tests
- Add hook-level regression tests for useChat and useQuickChat first-send stream scenarios
- Include a changeset for @runfusion/fusion patch release

Fusion-Task-Id: FN-3760
This commit is contained in:
Fusion
2026-05-08 20:45:45 -07:00
committed by gsxdsm
parent df9f5a95c6
commit b080ea0bc5
5 changed files with 133 additions and 5 deletions

View File

@@ -456,6 +456,46 @@ describe("useChat", () => {
});
});
it("sets isStreaming true during first send and clears on delayed done", async () => {
const session = makeSession({
id: "session-001",
agentId: "agent-001",
title: "Test Session",
});
mockFetchChatSessions.mockResolvedValue({ sessions: [session] });
mockFetchChatMessages.mockResolvedValue({ messages: [] });
const { result } = renderHook(() => useChat(undefined, "project-123"));
await waitFor(() => {
expect(result.current.sessions).toHaveLength(1);
});
act(() => {
result.current.selectSession("session-001");
});
mockStreamChatResponse.mockImplementation((_sessionId, _content, handlers) => {
setTimeout(() => {
handlers.onDone?.({ messageId: "msg-001" });
}, 200);
return { close: vi.fn(), isConnected: () => true };
});
act(() => {
void result.current.sendMessage("Hello");
});
await waitFor(() => {
expect(result.current.isStreaming).toBe(true);
});
await waitFor(() => {
expect(result.current.isStreaming).toBe(false);
});
});
it("uses done payload assistant snapshot when no text chunks were streamed", async () => {
const session = makeSession({ id: "session-001", agentId: "agent-001" });
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [session] });

View File

@@ -78,6 +78,37 @@ describe("useQuickChat", () => {
await expect(sendResult).resolves.toBeUndefined();
});
it("sets isStreaming true during first send and clears on delayed done", async () => {
const session = makeSession({ id: "session-001", agentId: "agent-001" });
mockFetchResumeChatSession.mockResolvedValue({ session });
mockFetchChatMessages.mockResolvedValue({ messages: [] });
const { result } = renderHook(() => useQuickChat("proj-123"));
await act(async () => {
await result.current.switchSession("agent-001");
});
mockStreamChatResponse.mockImplementation((_sessionId, _content, handlers) => {
setTimeout(() => {
handlers.onDone?.({ messageId: "msg-001" });
}, 200);
return { close: vi.fn(), isConnected: () => true };
});
void act(() => {
void result.current.sendMessage("Hello");
});
await waitFor(() => {
expect(result.current.isStreaming).toBe(true);
});
await waitFor(() => {
expect(result.current.isStreaming).toBe(false);
});
});
it("uses done payload assistant snapshot when no text chunks were streamed", async () => {
const session = makeSession({ id: "session-001", agentId: "agent-001" });
mockFetchResumeChatSession.mockResolvedValue({ session });