test(FN-5104): complete Step 5 — add reattach regression coverage

Fusion-Task-Id: FN-5104
Fusion-Task-Lineage: e92a26cb-a930-442a-98be-6ace9f376e73
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 00:53:27 -07:00
committed by gsxdsm
parent 8540a0186d
commit 0e30f85de7
2 changed files with 123 additions and 0 deletions

View File

@@ -1181,6 +1181,46 @@ describe("useChat", () => {
});
});
it("FN-5104 does not reattach after stopStreaming cancels active generation", async () => {
const session = {
...makeSession({ id: "session-001", agentId: "agent-001" }),
isGenerating: true,
inFlightGeneration: {
status: "generating" as const,
streamingText: "partial",
streamingThinking: "thinking",
toolCalls: [],
replayFromEventId: 8,
updatedAt: "2026-04-08T00:00:00.000Z",
},
};
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [session] });
mockFetchChatSession.mockResolvedValue({ session: { ...session, isGenerating: false, inFlightGeneration: null } });
const { result } = renderHook(() => useChat());
await waitFor(() => {
expect(result.current.sessions).toHaveLength(1);
});
act(() => {
result.current.selectSession("session-001");
});
await waitFor(() => {
expect(mockAttachChatStream).toHaveBeenCalledTimes(1);
});
act(() => {
result.current.stopStreaming();
});
await waitFor(() => {
expect(mockAttachChatStream).toHaveBeenCalledTimes(1);
expect(result.current.isStreaming).toBe(false);
});
});
it("fetches session on visible return only when no live stream and swallows reconnect failures", async () => {
const session = {
...makeSession({ id: "session-001", agentId: "agent-001" }),
@@ -2167,6 +2207,51 @@ describe("useChat", () => {
});
});
it("FN-5104 ignores replay checkpoint bumps while attach stream is already active", async () => {
const generating = {
...makeSession({ id: "session-001", agentId: "agent-001", title: "Gen" }),
isGenerating: true,
inFlightGeneration: {
status: "generating" as const,
streamingText: "partial",
streamingThinking: "",
toolCalls: [],
replayFromEventId: 5,
updatedAt: "2026-04-08T00:00:00.000Z",
},
};
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [generating] });
mockFetchChatSession.mockResolvedValue({ session: generating });
const { result } = renderHook(() => useChat("proj-123"));
await waitFor(() => {
expect(result.current.sessions).toHaveLength(1);
});
act(() => {
result.current.selectSession("session-001");
});
await waitFor(() => {
expect(mockAttachChatStream).toHaveBeenCalledTimes(1);
});
const updatedSession = {
...generating,
inFlightGeneration: { ...generating.inFlightGeneration, replayFromEventId: 9 },
};
act(() => {
subscribeHandler["chat:session:updated"]?.({
data: JSON.stringify(updatedSession),
} as MessageEvent);
});
await waitFor(() => {
expect(mockAttachChatStream).toHaveBeenCalledTimes(1);
});
});
it("removes session on chat:session:deleted event", async () => {
mockFetchChatSessions.mockResolvedValueOnce({
sessions: [

View File

@@ -1500,6 +1500,44 @@ describe("useQuickChat", () => {
);
});
it("FN-5104 reattaches once when selectSession refresh reveals generation from stale cache", async () => {
const staleSession = {
...makeSession({ id: "session-001", agentId: "agent-001" }),
isGenerating: false,
inFlightGeneration: null,
};
const generatingSession = {
...staleSession,
isGenerating: true,
inFlightGeneration: {
status: "generating" as const,
streamingText: "partial text",
streamingThinking: "thinking",
toolCalls: [{ toolName: "read", status: "running" as const, isError: false }],
replayFromEventId: 17,
updatedAt: "2026-04-08T00:00:00.000Z",
},
};
mockFetchChatSession.mockResolvedValueOnce({ session: generatingSession });
const { result } = renderHook(() => useQuickChat("proj-123"));
await act(async () => {
await result.current.selectSession(staleSession);
});
await waitFor(() => {
expect(mockAttachChatStream).toHaveBeenCalledTimes(1);
expect(mockAttachChatStream).toHaveBeenCalledWith(
"session-001",
expect.any(Object),
"proj-123",
{ lastEventId: 17 },
);
expect(result.current.streamingText).toBe("partial text");
});
});
it("sets isStreaming=true when initializing a session with isGenerating=true", async () => {
const session = { ...makeSession({ id: "session-001", agentId: "agent-001" }), isGenerating: true };
mockFetchResumeChatSession.mockResolvedValue({ session });