test(FN-4854): cover chat-room cache hydration and invalidation

Fusion-Task-Id: FN-4854
Fusion-Task-Lineage: 969ec04a-efe3-4dee-b183-2d4f2de6b388
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 00:35:36 -07:00
committed by gsxdsm
parent 4307d71c3a
commit 4caee9be30
3 changed files with 60 additions and 6 deletions

View File

@@ -47,6 +47,12 @@ export function DashboardLoader({ stage }: DashboardLoaderProps) {
clearCache(SWR_CACHE_KEYS.TASKS_PREFIX);
clearCache(SWR_CACHE_KEYS.PROJECTS);
clearCache(SWR_CACHE_KEYS.CURRENT_PROJECT_ID);
clearCache(SWR_CACHE_KEYS.AGENTS);
clearCache(SWR_CACHE_KEYS.AGENT_STATS);
clearCache(SWR_CACHE_KEYS.DOCUMENTS_PREFIX);
clearCache(SWR_CACHE_KEYS.TODO_LISTS_PREFIX);
clearCache(SWR_CACHE_KEYS.CHAT_ROOMS);
clearCache(SWR_CACHE_KEYS.ACTIVE_CHAT_ROOM_ID);
}
return versionUpdated;
});

View File

@@ -33,6 +33,7 @@ describe("DashboardLoader", () => {
beforeEach(() => {
mockConsumeVersionUpdateFlag.mockReset();
mockConsumeVersionUpdateFlag.mockReturnValue(false);
mockClearCache.mockReset();
});
it("renders projects stage with active first step and pending remaining steps", () => {
@@ -75,6 +76,15 @@ describe("DashboardLoader", () => {
expect(screen.getByText("Updating to a new frontend version...")).toBeInTheDocument();
expect(screen.getByRole("status", { name: "Updating Fusion dashboard" })).toBeInTheDocument();
expect(mockClearCache).toHaveBeenCalledWith(SWR_CACHE_KEYS.TASKS_PREFIX);
expect(mockClearCache).toHaveBeenCalledWith(SWR_CACHE_KEYS.PROJECTS);
expect(mockClearCache).toHaveBeenCalledWith(SWR_CACHE_KEYS.CURRENT_PROJECT_ID);
expect(mockClearCache).toHaveBeenCalledWith(SWR_CACHE_KEYS.AGENTS);
expect(mockClearCache).toHaveBeenCalledWith(SWR_CACHE_KEYS.AGENT_STATS);
expect(mockClearCache).toHaveBeenCalledWith(SWR_CACHE_KEYS.DOCUMENTS_PREFIX);
expect(mockClearCache).toHaveBeenCalledWith(SWR_CACHE_KEYS.TODO_LISTS_PREFIX);
expect(mockClearCache).toHaveBeenCalledWith(SWR_CACHE_KEYS.CHAT_ROOMS);
expect(mockClearCache).toHaveBeenCalledWith(SWR_CACHE_KEYS.ACTIVE_CHAT_ROOM_ID);
});
it("keeps stage visuals stable across all stages", () => {

View File

@@ -4,6 +4,7 @@ import type { ChatRoom, ChatRoomMember, ChatRoomMessage } from "@fusion/core";
import { useChatRooms } from "../useChatRooms";
import * as apiModule from "../../api";
import * as sseBusModule from "../../sse-bus";
import { SWR_CACHE_KEYS } from "../../utils/swrCache";
vi.mock("../../api", () => ({
fetchChatRooms: vi.fn(),
@@ -78,6 +79,7 @@ describe("useChatRooms", () => {
capturedEvents = sub.events ?? {};
return unsubscribe;
});
window.localStorage.clear();
mockFetchChatRooms.mockResolvedValue({ rooms: [] });
mockFetchChatRoomMembers.mockResolvedValue({ members: [] });
mockFetchChatRoomMessages.mockResolvedValue({ messages: [] });
@@ -87,8 +89,25 @@ describe("useChatRooms", () => {
mockClearChatRoomMessages.mockResolvedValue({ success: true, deletedCount: 1 });
});
it("loads rooms on mount", async () => {
mockFetchChatRooms.mockResolvedValueOnce({ rooms: [room("room-1", "one", "2026-05-09T01:00:00.000Z")] });
it("hydrates cached rooms and active room synchronously", async () => {
const cachedRooms = [room("room-1", "one", "2026-05-09T01:00:00.000Z")];
window.localStorage.setItem(`${SWR_CACHE_KEYS.CHAT_ROOMS}:proj-1`, JSON.stringify(cachedRooms));
window.localStorage.setItem(`${SWR_CACHE_KEYS.ACTIVE_CHAT_ROOM_ID}:proj-1`, JSON.stringify("room-1"));
mockFetchChatRooms.mockImplementationOnce(
() =>
new Promise(() => {
// keep pending; assert fast-path hydration
}),
);
const { result } = renderHook(() => useChatRooms("proj-1"));
expect(result.current.rooms).toEqual(cachedRooms);
expect(result.current.activeRoom?.id).toBe("room-1");
expect(result.current.roomsLoading).toBe(false);
});
it("loads rooms on mount", async () => { mockFetchChatRooms.mockResolvedValueOnce({ rooms: [room("room-1", "one", "2026-05-09T01:00:00.000Z")] });
const { result } = renderHook(() => useChatRooms("proj-1"));
await waitFor(() => expect(result.current.roomsLoading).toBe(false));
@@ -96,8 +115,18 @@ describe("useChatRooms", () => {
expect(mockFetchChatRooms).toHaveBeenCalledWith({}, "proj-1");
});
it("createRoom persists and loads active room members/messages", async () => {
const { result } = renderHook(() => useChatRooms("proj-1"));
it("writes rooms list to cache after successful refresh", async () => {
const rooms = [room("room-1", "one", "2026-05-09T01:00:00.000Z")];
mockFetchChatRooms.mockResolvedValueOnce({ rooms });
renderHook(() => useChatRooms("proj-1"));
await waitFor(() => {
expect(JSON.parse(window.localStorage.getItem(`${SWR_CACHE_KEYS.CHAT_ROOMS}:proj-1`) ?? "[]")).toEqual(rooms);
});
});
it("createRoom persists and loads active room members/messages", async () => { const { result } = renderHook(() => useChatRooms("proj-1"));
mockFetchChatRoomMembers.mockResolvedValueOnce({ members: [roomMember("room-new", "agent-1")] });
mockFetchChatRoomMessages.mockResolvedValueOnce({ messages: [roomMessage("msg-1", "room-new", "hello")] });
@@ -144,8 +173,17 @@ describe("useChatRooms", () => {
await waitFor(() => expect(result.current.messages[0]?.id).toBe("msg-2"));
});
it("handles room message SSE for active and inactive rooms", async () => {
const older = room("room-1", "one", "2026-05-09T01:00:00.000Z");
it("keeps messages out of cache payload", async () => {
const rooms = [room("room-1", "one", "2026-05-09T01:00:00.000Z")];
mockFetchChatRooms.mockResolvedValueOnce({ rooms });
const { result } = renderHook(() => useChatRooms("proj-1"));
await waitFor(() => expect(result.current.roomsLoading).toBe(false));
const cached = JSON.parse(window.localStorage.getItem(`${SWR_CACHE_KEYS.CHAT_ROOMS}:proj-1`) ?? "[]") as Array<Record<string, unknown>>;
expect(cached[0]).not.toHaveProperty("messages");
});
it("handles room message SSE for active and inactive rooms", async () => { const older = room("room-1", "one", "2026-05-09T01:00:00.000Z");
const newer = room("room-2", "two", "2026-05-09T02:00:00.000Z");
mockFetchChatRooms.mockResolvedValueOnce({ rooms: [older, newer] });
const { result } = renderHook(() => useChatRooms("proj-1"));