FN-5710: flush queued first quick chat send after session init
Ensure the first Quick Chat message reliably streams even when a stale sendMessage closure sees no active session. - trigger a microtask flush for queued pre-session sends when a session is already active and no stream is running - preserve existing streaming guards so flush only runs when safe - add regression coverage for stale-closure first-send flow to verify stream start and completion behavior Files changed: .../app/hooks/__tests__/useQuickChat.test.ts | 47 ++++++++++++++++++++++ packages/dashboard/app/hooks/useQuickChat.ts | 12 ++++++ 2 files changed, 59 insertions(+) Fusion-Task-Id: FN-5710 Fusion-Task-Lineage: a4b32867-dedd-40f5-b6a8-3c71b9f7dee1
This commit is contained in:
@@ -102,6 +102,53 @@ describe("useQuickChat", () => {
|
||||
await expect(firstSend).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("flushes queued first send even when sendMessage closure sees stale null session", async () => {
|
||||
const session = makeSession({ id: "session-001", agentId: "agent-001" });
|
||||
mockFetchResumeChatSession.mockResolvedValue({ session });
|
||||
mockFetchChatMessages.mockResolvedValue({ messages: [] });
|
||||
|
||||
const { result } = renderHook(() => useQuickChat("proj-123"));
|
||||
|
||||
const staleSendMessage = result.current.sendMessage;
|
||||
|
||||
await act(async () => {
|
||||
await result.current.switchSession("agent-001");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeSession?.id).toBe("session-001");
|
||||
});
|
||||
|
||||
let onDone: ((data: { messageId: string }) => void) | undefined;
|
||||
mockStreamChatResponse.mockImplementation((_sessionId, _content, handlers) => {
|
||||
onDone = handlers.onDone as typeof onDone;
|
||||
return { close: vi.fn(), isConnected: () => true };
|
||||
});
|
||||
|
||||
let firstSend!: Promise<void>;
|
||||
await act(async () => {
|
||||
firstSend = staleSendMessage("Hello from stale sender");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockStreamChatResponse).toHaveBeenCalledTimes(1);
|
||||
expect(mockStreamChatResponse.mock.calls[0]?.[1]).toBe("Hello from stale sender");
|
||||
expect(result.current.isStreaming).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
onDone?.({ messageId: "msg-001" });
|
||||
});
|
||||
|
||||
await expect(firstSend).resolves.toBeUndefined();
|
||||
await waitFor(() => {
|
||||
expect(result.current.messages.at(-1)).toEqual(expect.objectContaining({
|
||||
role: "assistant",
|
||||
}));
|
||||
expect(result.current.isStreaming).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("sendMessage returns a promise that resolves on stream completion", async () => {
|
||||
const session = makeSession({ id: "session-001", agentId: "agent-001" });
|
||||
mockFetchResumeChatSession.mockResolvedValue({ session });
|
||||
|
||||
@@ -743,6 +743,18 @@ export function useQuickChat(
|
||||
queuedPreSessionCompletionRef.current = { resolve, reject };
|
||||
pendingMessageRef.current = content;
|
||||
setPendingMessage(content);
|
||||
|
||||
// FN-5710: stale sendMessage closures can still observe null
|
||||
// activeSession immediately after session init completed. In that
|
||||
// case, queueing alone can strand the first send because the
|
||||
// activeSession-triggered flush effect has already fired.
|
||||
if (activeSessionRef.current && !isStreamingRef.current && !streamRef.current) {
|
||||
queueMicrotask(() => {
|
||||
if (!isStreamingRef.current && !streamRef.current) {
|
||||
flushPendingMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user