feat(FN-3432): document chat streaming recovery guarantees in architecture

Added documentation for chat streaming recovery guarantees to the architecture docs.

Fusion-Task-Id: FN-3432
This commit is contained in:
Fusion
2026-05-05 00:06:56 -07:00
committed by gsxdsm
parent 4b10ac0ba6
commit cd1d44abab
5 changed files with 180 additions and 11 deletions

View File

@@ -1485,6 +1485,88 @@ describe("useChat", () => {
});
describe("FN-3336: streaming state recovery on reload", () => {
it("does not re-select and reset active session on subsequent session refreshes", async () => {
const session = { ...makeSession({ id: "session-001", agentId: "agent-001" }), isGenerating: true };
mockGetScopedItem.mockReturnValue("session-001");
mockFetchChatSessions
.mockResolvedValueOnce({ sessions: [session] })
.mockResolvedValueOnce({ sessions: [{ ...session, updatedAt: "2026-04-08T00:05:00.000Z" }] });
mockFetchChatMessages.mockResolvedValue({ messages: [] });
const { result } = renderHook(() => useChat("proj-123"));
await waitFor(() => {
expect(result.current.activeSession?.id).toBe("session-001");
});
expect(mockFetchChatMessages).toHaveBeenCalledTimes(1);
await act(async () => {
await result.current.refreshSessions();
});
await waitFor(() => {
expect(result.current.isStreaming).toBe(true);
});
// A sessions refresh should not auto-reselect/reset the active thread.
expect(mockFetchChatMessages).toHaveBeenCalledTimes(1);
});
it("preserves streaming text/thinking/tool state across sessions refresh", async () => {
const session = makeSession({ id: "session-001", agentId: "agent-001" });
mockFetchChatSessions
.mockResolvedValueOnce({ sessions: [session] })
.mockResolvedValueOnce({ sessions: [{ ...session, updatedAt: "2026-04-08T00:06:00.000Z" }] });
mockFetchChatMessages.mockResolvedValue({ messages: [] });
let textHandler: ((data: string) => void) | undefined;
let thinkingHandler: ((data: string) => void) | undefined;
let toolStartHandler: ((data: { toolName: string; args?: Record<string, unknown> }) => void) | undefined;
mockStreamChatResponse.mockImplementation((_sessionId, _content, handlers) => {
textHandler = handlers.onText;
thinkingHandler = handlers.onThinking;
toolStartHandler = handlers.onToolStart;
return { close: vi.fn(), isConnected: () => true };
});
const { result } = renderHook(() => useChat("proj-123"));
await waitFor(() => {
expect(result.current.sessions).toHaveLength(1);
});
act(() => {
result.current.selectSession("session-001");
});
await act(async () => {
result.current.sendMessage("Hello");
});
await act(async () => {
textHandler?.("Hi");
thinkingHandler?.("plan");
toolStartHandler?.({ toolName: "read", args: { path: "a.ts" } });
});
await waitFor(() => {
expect(result.current.isStreaming).toBe(true);
expect(result.current.streamingText).toBe("Hi");
expect(result.current.streamingThinking).toBe("plan");
expect(result.current.streamingToolCalls).toHaveLength(1);
});
await act(async () => {
await result.current.refreshSessions();
});
expect(result.current.isStreaming).toBe(true);
expect(result.current.streamingText).toBe("Hi");
expect(result.current.streamingThinking).toBe("plan");
expect(result.current.streamingToolCalls).toHaveLength(1);
});
it("sets isStreaming=true when selecting a session with isGenerating=true", async () => {
const session = { ...makeSession({ id: "session-001", agentId: "agent-001" }), isGenerating: true };
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [session] });