feat(FN-4739): complete Step 6 — intercept room clear slash commands

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:43:49 -07:00
committed by gsxdsm
parent 355aa6487c
commit a5eba7dfdb
2 changed files with 75 additions and 1 deletions

View File

@@ -1678,6 +1678,16 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
return;
}
if (trimmed === "/clear" || trimmed === "/new") {
clearComposerState();
try {
await rooms.clearRoom(rooms.activeRoom.id);
} catch {
addToast("Failed to clear room conversation", "error");
}
return;
}
const previousInput = messageInput;
clearComposerState();
@@ -1694,7 +1704,7 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
}
handleSend();
}, [messageInput, chatRoomsEnabled, chatScope, rooms, clearComposerState, addToast, handleSend]);
}, [messageInput, chatRoomsEnabled, chatScope, rooms, rooms.clearRoom, clearComposerState, addToast, handleSend]);
const handleSkillSelect = useCallback(
(skill: DiscoveredSkill) => {

View File

@@ -90,6 +90,7 @@ const defaultRoomsState: UseChatRoomsResult = {
createRoom: vi.fn(),
deleteRoom: vi.fn(),
sendRoomMessage: vi.fn(),
clearRoom: vi.fn(),
refreshRooms: vi.fn(),
};
@@ -379,6 +380,69 @@ describe("ChatView — rooms (FN-3805..FN-3811 contract)", () => {
});
});
it("intercepts exact /clear in rooms scope and clears active room", async () => {
const clearRoom = vi.fn().mockResolvedValue(undefined);
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
setup({}, { clearRoom, sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, " /clear {enter}");
await waitFor(() => {
expect(clearRoom).toHaveBeenCalledWith("room-a");
});
expect(sendRoomMessage).not.toHaveBeenCalled();
});
it("intercepts exact /new in rooms scope and clears active room", async () => {
const clearRoom = vi.fn().mockResolvedValue(undefined);
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
setup({}, { clearRoom, sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, " /new {enter}");
await waitFor(() => {
expect(clearRoom).toHaveBeenCalledWith("room-a");
});
expect(sendRoomMessage).not.toHaveBeenCalled();
});
it("does not intercept /clear substring commands in rooms scope", async () => {
const clearRoom = vi.fn().mockResolvedValue(undefined);
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
setup({}, { clearRoom, sendRoomMessage, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, "/clear now{enter}");
await waitFor(() => {
expect(sendRoomMessage).toHaveBeenCalledWith("/clear now");
});
expect(clearRoom).not.toHaveBeenCalled();
});
it("toasts error when room clear command fails", async () => {
const addToast = vi.fn();
const clearRoom = vi.fn().mockRejectedValue(new Error("clear failed"));
setup({}, { clearRoom, activeRoom: roomA });
render(<ChatView projectId="proj-123" addToast={addToast} experimentalFeatures={{ chatRooms: true }} />);
const textarea = screen.getByTestId("chat-input") as HTMLTextAreaElement;
await userEvent.type(textarea, "/clear{enter}");
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith("Failed to clear room conversation", "error");
});
});
it("supports delete-room confirm/cancel and rerenders messages from hook state", async () => {
const deleteRoom = vi.fn().mockResolvedValue(undefined);
const rerenderedRooms = {