feat(FN-4739): complete Step 5 — add useChatRooms clearRoom support

Fusion-Task-Id: FN-4739
Fusion-Task-Lineage: fc120dfa-d1b2-4065-9595-d0bc6304d2af
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 09:40:36 -07:00
committed by gsxdsm
parent 207b62a31d
commit 355aa6487c
2 changed files with 108 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ vi.mock("../../api", () => ({
fetchChatRoomMessages: vi.fn(),
deleteChatRoom: vi.fn(),
postChatRoomMessage: vi.fn(),
clearChatRoomMessages: vi.fn(),
}));
vi.mock("../../sse-bus", () => ({
@@ -30,6 +31,7 @@ const mockFetchChatRoomMembers = vi.mocked(apiModule.fetchChatRoomMembers);
const mockFetchChatRoomMessages = vi.mocked(apiModule.fetchChatRoomMessages);
const mockDeleteChatRoom = vi.mocked(apiModule.deleteChatRoom);
const mockPostChatRoomMessage = vi.mocked(apiModule.postChatRoomMessage);
const mockClearChatRoomMessages = vi.mocked(apiModule.clearChatRoomMessages);
const mockSubscribeSse = vi.mocked(sseBusModule.subscribeSse);
function room(id: string, name: string, updatedAt: string): ChatRoom {
@@ -82,6 +84,7 @@ describe("useChatRooms", () => {
mockCreateChatRoom.mockResolvedValue({ room: room("room-new", "new", "2026-05-09T01:00:00.000Z") });
mockDeleteChatRoom.mockResolvedValue({ success: true });
mockPostChatRoomMessage.mockResolvedValue({ message: roomMessage("msg-posted", "room-new", "posted") });
mockClearChatRoomMessages.mockResolvedValue({ success: true, deletedCount: 1 });
});
it("loads rooms on mount", async () => {
@@ -312,6 +315,82 @@ describe("useChatRooms", () => {
});
});
it("clearRoom empties messages for active room", async () => {
const active = room("room-1", "one", "2026-05-09T01:00:00.000Z");
mockFetchChatRooms.mockResolvedValueOnce({ rooms: [active] });
const { result } = renderHook(() => useChatRooms("proj-1"));
await waitFor(() => expect(result.current.rooms.length).toBe(1));
mockFetchChatRoomMembers.mockResolvedValueOnce({ members: [] });
mockFetchChatRoomMessages.mockResolvedValueOnce({ messages: [roomMessage("msg-1", "room-1", "hello")] });
act(() => result.current.selectRoom("room-1"));
await waitFor(() => expect(result.current.messages).toHaveLength(1));
await act(async () => {
await result.current.clearRoom("room-1");
});
expect(mockClearChatRoomMessages).toHaveBeenCalledWith("room-1", "proj-1");
expect(result.current.messages).toEqual([]);
});
it("clearRoom does not mutate messages when another room is active", async () => {
const first = room("room-1", "one", "2026-05-09T01:00:00.000Z");
const second = room("room-2", "two", "2026-05-09T02:00:00.000Z");
mockFetchChatRooms.mockResolvedValueOnce({ rooms: [first, second] });
const { result } = renderHook(() => useChatRooms("proj-1"));
await waitFor(() => expect(result.current.rooms.length).toBe(2));
mockFetchChatRoomMembers.mockResolvedValueOnce({ members: [] });
mockFetchChatRoomMessages.mockResolvedValueOnce({ messages: [roomMessage("msg-2", "room-2", "hello")] });
act(() => result.current.selectRoom("room-2"));
await waitFor(() => expect(result.current.messages).toHaveLength(1));
await act(async () => {
await result.current.clearRoom("room-1");
});
expect(result.current.messages).toHaveLength(1);
});
it("SSE clear event empties messages for active room", async () => {
const active = room("room-1", "one", "2026-05-09T01:00:00.000Z");
mockFetchChatRooms.mockResolvedValueOnce({ rooms: [active] });
const { result } = renderHook(() => useChatRooms("proj-1"));
await waitFor(() => expect(result.current.rooms.length).toBe(1));
mockFetchChatRoomMembers.mockResolvedValueOnce({ members: [] });
mockFetchChatRoomMessages.mockResolvedValueOnce({ messages: [roomMessage("msg-1", "room-1", "hello")] });
act(() => result.current.selectRoom("room-1"));
await waitFor(() => expect(result.current.messages).toHaveLength(1));
act(() => {
capturedEvents["chat:room:messages:cleared"]?.({ data: JSON.stringify({ roomId: "room-1", deletedCount: 3 }) } as MessageEvent);
});
expect(result.current.messages).toEqual([]);
});
it("clearRoom rethrows API failure and preserves messages", async () => {
const active = room("room-1", "one", "2026-05-09T01:00:00.000Z");
mockFetchChatRooms.mockResolvedValueOnce({ rooms: [active] });
const { result } = renderHook(() => useChatRooms("proj-1"));
await waitFor(() => expect(result.current.rooms.length).toBe(1));
mockFetchChatRoomMembers.mockResolvedValueOnce({ members: [] });
mockFetchChatRoomMessages.mockResolvedValueOnce({ messages: [roomMessage("msg-1", "room-1", "hello")] });
act(() => result.current.selectRoom("room-1"));
await waitFor(() => expect(result.current.messages).toHaveLength(1));
mockClearChatRoomMessages.mockRejectedValueOnce(new Error("clear failed"));
await act(async () => {
await expect(result.current.clearRoom("room-1")).rejects.toThrow("clear failed");
});
expect(result.current.messages).toHaveLength(1);
});
it("tears down sse subscription on unmount", async () => {
const { unmount } = renderHook(() => useChatRooms("proj-1"));
unmount();

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { ChatAttachment, ChatRoom, ChatRoomMember, ChatRoomMessage } from "@fusion/core";
import {
clearChatRoomMessages,
createChatRoom,
deleteChatRoom,
fetchChatRoomMembers,
@@ -25,6 +26,7 @@ export interface UseChatRoomsResult {
createRoom: (input: { name: string; memberAgentIds: string[] }) => Promise<ChatRoom>;
deleteRoom: (roomId: string) => Promise<void>;
sendRoomMessage: (content: string, opts?: { attachments?: ChatAttachment[] }) => Promise<void>;
clearRoom: (roomId: string) => Promise<void>;
refreshRooms: () => Promise<void>;
}
@@ -228,6 +230,17 @@ export function useChatRooms(
}
}, [projectId]);
const clearRoom = useCallback(async (roomId: string) => {
if (!roomId || !roomsRef.current.some((room) => room.id === roomId)) {
return;
}
await clearChatRoomMessages(roomId, projectId);
if (activeRoomRef.current?.id === roomId) {
setMessages([]);
}
}, [projectId]);
useEffect(() => {
void refreshRooms();
}, [refreshRooms]);
@@ -329,6 +342,21 @@ export function useChatRooms(
if (!payload?.id) return;
setMessages((previous) => previous.filter((message) => message.id !== payload.id));
},
"chat:room:messages:cleared": (event) => {
if (projectContextVersionRef.current !== contextVersionAtStart) return;
const payload = parseSsePayload<{ roomId: string; deletedCount: number }>(event);
if (!payload?.roomId) return;
if (activeRoomRef.current?.id === payload.roomId) {
setMessages([]);
}
setRooms((previous) => {
const room = previous.find((candidate) => candidate.id === payload.roomId);
if (!room) return previous;
return upsertRoom(previous, { ...room, updatedAt: new Date().toISOString() });
});
},
},
});
}, [projectId, refreshRooms]);
@@ -355,6 +383,7 @@ export function useChatRooms(
createRoom: createRoomLocal,
deleteRoom: deleteRoomLocal,
sendRoomMessage,
clearRoom,
refreshRooms,
};
}