feat(FN-4857): complete Step 4 — upload room attachments in useChatRooms
Fusion-Task-Id: FN-4857 Fusion-Task-Lineage: 52e7b1cd-ec8f-4b6c-abfc-08c91e321b9e
This commit is contained in:
committed by
gsxdsm
parent
354dae2d54
commit
a9e76bff81
@@ -13,6 +13,7 @@ vi.mock("../../api", () => ({
|
||||
fetchChatRoomMessages: vi.fn(),
|
||||
deleteChatRoom: vi.fn(),
|
||||
postChatRoomMessage: vi.fn(),
|
||||
uploadChatRoomAttachment: vi.fn(),
|
||||
clearChatRoomMessages: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -32,6 +33,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 mockUploadChatRoomAttachment = vi.mocked(apiModule.uploadChatRoomAttachment);
|
||||
const mockClearChatRoomMessages = vi.mocked(apiModule.clearChatRoomMessages);
|
||||
const mockSubscribeSse = vi.mocked(sseBusModule.subscribeSse);
|
||||
|
||||
@@ -86,6 +88,16 @@ 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") });
|
||||
mockUploadChatRoomAttachment.mockResolvedValue({
|
||||
attachment: {
|
||||
id: "att-uploaded",
|
||||
filename: "upload.png",
|
||||
originalName: "upload.png",
|
||||
mimeType: "image/png",
|
||||
size: 4,
|
||||
createdAt: "2026-05-09T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
mockClearChatRoomMessages.mockResolvedValue({ success: true, deletedCount: 1 });
|
||||
});
|
||||
|
||||
@@ -288,6 +300,53 @@ describe("useChatRooms", () => {
|
||||
expect(result.current.messages.map((message) => message.id)).toEqual(["msg-user", "msg-assistant"]);
|
||||
});
|
||||
|
||||
it("uploads files before posting room message", 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: [] });
|
||||
act(() => result.current.selectRoom("room-1"));
|
||||
await waitFor(() => expect(result.current.activeRoom?.id).toBe("room-1"));
|
||||
|
||||
const file = new File(["png"], "upload.png", { type: "image/png" });
|
||||
mockFetchChatRoomMessages.mockResolvedValueOnce({ messages: [roomMessage("msg-user", "room-1", "hello")] });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendRoomMessage("hello", { files: [file] });
|
||||
});
|
||||
|
||||
expect(mockUploadChatRoomAttachment).toHaveBeenCalledWith("room-1", file, "proj-1");
|
||||
expect(mockPostChatRoomMessage).toHaveBeenCalledWith("room-1", {
|
||||
content: "hello",
|
||||
attachments: [expect.objectContaining({ id: "att-uploaded", filename: "upload.png" })],
|
||||
}, "proj-1");
|
||||
});
|
||||
|
||||
it("throws on upload failure and does not post", 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: [] });
|
||||
act(() => result.current.selectRoom("room-1"));
|
||||
await waitFor(() => expect(result.current.activeRoom?.id).toBe("room-1"));
|
||||
|
||||
const file = new File(["x"], "bad.txt", { type: "text/plain" });
|
||||
mockUploadChatRoomAttachment.mockRejectedValueOnce(new Error("Upload failed"));
|
||||
mockFetchChatRoomMessages.mockResolvedValueOnce({ messages: [] });
|
||||
|
||||
await act(async () => {
|
||||
await expect(result.current.sendRoomMessage("hello", { files: [file] })).rejects.toThrow("Failed to upload attachment: bad.txt");
|
||||
});
|
||||
|
||||
expect(mockPostChatRoomMessage).not.toHaveBeenCalledWith("room-1", expect.objectContaining({ content: "hello" }), "proj-1");
|
||||
});
|
||||
|
||||
it("rolls back optimistic temp message when post fails and transcript refresh fails", async () => {
|
||||
const active = room("room-1", "one", "2026-05-09T01:00:00.000Z");
|
||||
mockFetchChatRooms.mockResolvedValueOnce({ rooms: [active] });
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
fetchChatRoomMessages,
|
||||
fetchChatRooms,
|
||||
postChatRoomMessage,
|
||||
uploadChatRoomAttachment,
|
||||
} from "../api";
|
||||
import { subscribeSse } from "../sse-bus";
|
||||
import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||
@@ -26,7 +27,7 @@ export interface UseChatRoomsResult {
|
||||
selectRoom: (roomId: string | null) => void;
|
||||
createRoom: (input: { name: string; memberAgentIds: string[] }) => Promise<ChatRoom>;
|
||||
deleteRoom: (roomId: string) => Promise<void>;
|
||||
sendRoomMessage: (content: string, opts?: { attachments?: ChatAttachment[] }) => Promise<void>;
|
||||
sendRoomMessage: (content: string, opts?: { attachments?: ChatAttachment[]; files?: File[] }) => Promise<void>;
|
||||
clearRoom: (roomId: string) => Promise<void>;
|
||||
refreshRooms: () => Promise<void>;
|
||||
}
|
||||
@@ -51,7 +52,7 @@ function parseSsePayload<T>(event: MessageEvent): T | null {
|
||||
}
|
||||
}
|
||||
|
||||
function createOptimisticRoomMessage(roomId: string, content: string): ChatRoomMessage {
|
||||
function createOptimisticRoomMessage(roomId: string, content: string, attachments?: ChatAttachment[]): ChatRoomMessage {
|
||||
return {
|
||||
id: `temp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
roomId,
|
||||
@@ -61,6 +62,7 @@ function createOptimisticRoomMessage(roomId: string, content: string): ChatRoomM
|
||||
metadata: null,
|
||||
senderAgentId: null,
|
||||
mentions: [],
|
||||
...(attachments?.length ? { attachments } : {}),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -203,22 +205,45 @@ export function useChatRooms(
|
||||
}
|
||||
}, [activeRoomCacheKey, projectId]);
|
||||
|
||||
const sendRoomMessage = useCallback(async (content: string, opts?: { attachments?: ChatAttachment[] }) => {
|
||||
const sendRoomMessage = useCallback(async (content: string, opts?: { attachments?: ChatAttachment[]; files?: File[] }) => {
|
||||
const activeRoomSnapshot = activeRoomRef.current;
|
||||
const roomId = activeRoomSnapshot?.id;
|
||||
if (!roomId) {
|
||||
throw new Error("Select a room before sending a message");
|
||||
}
|
||||
|
||||
const optimisticMessage = createOptimisticRoomMessage(roomId, content);
|
||||
const placeholderAttachments = opts?.files?.map((file) => ({
|
||||
id: `upload-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
filename: file.name,
|
||||
originalName: file.name,
|
||||
mimeType: file.type || "application/octet-stream",
|
||||
size: file.size,
|
||||
createdAt: new Date().toISOString(),
|
||||
} satisfies ChatAttachment));
|
||||
|
||||
const optimisticMessage = createOptimisticRoomMessage(roomId, content, placeholderAttachments?.length ? placeholderAttachments : opts?.attachments);
|
||||
if (activeRoomRef.current?.id === roomId) {
|
||||
setMessages((previous) => [...previous, optimisticMessage]);
|
||||
}
|
||||
|
||||
try {
|
||||
const uploadedAttachments: ChatAttachment[] = [];
|
||||
if (opts?.files?.length) {
|
||||
for (const file of opts.files) {
|
||||
try {
|
||||
const uploaded = await uploadChatRoomAttachment(roomId, file, projectId);
|
||||
uploadedAttachments.push(uploaded.attachment);
|
||||
} catch {
|
||||
throw new Error(`Failed to upload attachment: ${file.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mergedAttachments = [...(opts?.attachments ?? []), ...uploadedAttachments];
|
||||
|
||||
const postResult = await postChatRoomMessage(roomId, {
|
||||
content,
|
||||
...(opts?.attachments ? { attachments: opts.attachments } : {}),
|
||||
...(mergedAttachments.length ? { attachments: mergedAttachments } : {}),
|
||||
}, projectId);
|
||||
|
||||
if (postResult.message?.createdAt && activeRoomSnapshot) {
|
||||
|
||||
Reference in New Issue
Block a user