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:
committed by
gsxdsm
parent
207b62a31d
commit
355aa6487c
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user