diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/__tests__/ChatSidebar.test.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/__tests__/ChatSidebar.test.tsx
new file mode 100644
index 0000000000..a62928c4fd
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/__tests__/ChatSidebar.test.tsx
@@ -0,0 +1,350 @@
+import { expect, test, describe, vi, beforeEach, afterEach } from "vitest";
+import {
+ render,
+ screen,
+ waitFor,
+ fireEvent,
+ cleanup,
+} from "@testing-library/react";
+import { ChatSidebar } from "../ChatSidebar";
+import { server } from "@/mocks/mock-server";
+import {
+ getGetV2ListSessionsMockHandler,
+ getDeleteV2DeleteSessionMockHandler204,
+ getDeleteV2DeleteSessionMockHandler422,
+} from "@/app/api/__generated__/endpoints/chat/chat.msw";
+import { SidebarProvider } from "@/components/ui/sidebar";
+import { NuqsTestingAdapter } from "nuqs/adapters/testing";
+import { http, HttpResponse, delay } from "msw";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
+
+// Mock sessions data
+const mockSessions = {
+ sessions: [
+ {
+ id: "session-1",
+ title: "First Chat",
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ },
+ {
+ id: "session-2",
+ title: "Second Chat",
+ created_at: new Date(Date.now() - 86400000).toISOString(),
+ updated_at: new Date(Date.now() - 86400000).toISOString(),
+ },
+ {
+ id: "session-3",
+ title: null,
+ created_at: new Date(Date.now() - 172800000).toISOString(),
+ updated_at: new Date(Date.now() - 172800000).toISOString(),
+ },
+ ],
+ total: 3,
+};
+
+function createTestQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+}
+
+function TestWrapper({
+ children,
+ searchParams = "",
+ onUrlUpdate,
+}: {
+ children: React.ReactNode;
+ searchParams?: string;
+ onUrlUpdate?: (event: { queryString: string }) => void;
+}) {
+ const queryClient = createTestQueryClient();
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+
+function renderChatSidebar(
+ searchParams = "",
+ onUrlUpdate?: (event: { queryString: string }) => void,
+) {
+ return render(
+
+
+ ,
+ );
+}
+
+describe("ChatSidebar", () => {
+ beforeEach(() => {
+ server.use(
+ getGetV2ListSessionsMockHandler(() => mockSessions),
+ getDeleteV2DeleteSessionMockHandler204(),
+ );
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ describe("Sessions List", () => {
+ test("renders session list correctly", async () => {
+ renderChatSidebar();
+
+ // Use getAllByText since component may render multiple times
+ await waitFor(() => {
+ const elements = screen.getAllByText("First Chat");
+ expect(elements.length).toBeGreaterThan(0);
+ });
+
+ expect(screen.getAllByText("Second Chat").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("Untitled chat").length).toBeGreaterThan(0);
+ });
+
+ test("shows empty state when no sessions", async () => {
+ server.use(
+ getGetV2ListSessionsMockHandler(() => ({
+ sessions: [],
+ total: 0,
+ })),
+ );
+
+ renderChatSidebar();
+
+ await waitFor(() => {
+ expect(
+ screen.getAllByText("No conversations yet").length,
+ ).toBeGreaterThan(0);
+ });
+ });
+
+ test("formats dates correctly", async () => {
+ renderChatSidebar();
+
+ await waitFor(() => {
+ expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
+ });
+
+ expect(screen.getAllByText("Today").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("Yesterday").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("2 days ago").length).toBeGreaterThan(0);
+ });
+ });
+
+ describe("Delete Dialog", () => {
+ test("opens delete dialog when trash button is clicked", async () => {
+ renderChatSidebar();
+
+ await waitFor(() => {
+ expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
+ });
+
+ // Find and click the delete button for the first session
+ const deleteButtons = screen.getAllByLabelText("Delete chat");
+ fireEvent.click(deleteButtons[0]);
+
+ // Dialog should appear with confirmation text
+ await waitFor(() => {
+ expect(
+ screen.getByText(/Are you sure you want to delete/),
+ ).toBeDefined();
+ });
+ });
+
+ test("closes dialog when cancel is clicked", async () => {
+ renderChatSidebar();
+
+ await waitFor(() => {
+ expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
+ });
+
+ const deleteButtons = screen.getAllByLabelText("Delete chat");
+ fireEvent.click(deleteButtons[0]);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/Are you sure you want to delete/),
+ ).toBeDefined();
+ });
+
+ const cancelButton = screen.getByRole("button", { name: "Cancel" });
+ fireEvent.click(cancelButton);
+
+ await waitFor(() => {
+ expect(
+ screen.queryByText(/Are you sure you want to delete/),
+ ).toBeNull();
+ });
+ });
+
+ test("calls delete API when delete button is clicked", async () => {
+ const deleteMock = vi.fn();
+ server.use(
+ http.delete(
+ "http://localhost:3000/api/proxy/api/chat/sessions/:sessionId",
+ async ({ params }) => {
+ deleteMock(params.sessionId);
+ return new HttpResponse(null, { status: 204 });
+ },
+ ),
+ );
+
+ renderChatSidebar();
+
+ await waitFor(() => {
+ expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
+ });
+
+ const deleteButtons = screen.getAllByLabelText("Delete chat");
+ fireEvent.click(deleteButtons[0]);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/Are you sure you want to delete/),
+ ).toBeDefined();
+ });
+
+ const deleteButton = screen.getByRole("button", { name: "Delete" });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(deleteMock).toHaveBeenCalledWith("session-1");
+ });
+ });
+
+ test("closes dialog after successful deletion", async () => {
+ renderChatSidebar();
+
+ await waitFor(() => {
+ expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
+ });
+
+ const deleteButtons = screen.getAllByLabelText("Delete chat");
+ fireEvent.click(deleteButtons[0]);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/Are you sure you want to delete/),
+ ).toBeDefined();
+ });
+
+ const deleteButton = screen.getByRole("button", { name: "Delete" });
+ fireEvent.click(deleteButton);
+
+ await waitFor(() => {
+ expect(
+ screen.queryByText(/Are you sure you want to delete/),
+ ).toBeNull();
+ });
+ });
+
+ test("handles deletion error gracefully", async () => {
+ server.use(getDeleteV2DeleteSessionMockHandler422());
+
+ renderChatSidebar();
+
+ await waitFor(() => {
+ expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
+ });
+
+ const deleteButtons = screen.getAllByLabelText("Delete chat");
+ fireEvent.click(deleteButtons[0]);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/Are you sure you want to delete/),
+ ).toBeDefined();
+ });
+
+ const deleteButton = screen.getByRole("button", { name: "Delete" });
+ fireEvent.click(deleteButton);
+
+ // Dialog should close even on error
+ await waitFor(() => {
+ expect(
+ screen.queryByText(/Are you sure you want to delete/),
+ ).toBeNull();
+ });
+ });
+ });
+
+ describe("Session Selection", () => {
+ test("highlights currently selected session", async () => {
+ renderChatSidebar("?sessionId=session-1");
+
+ await waitFor(() => {
+ expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
+ });
+
+ // Find the session with selected styling (text-zinc-600 indicates selected)
+ const selectedSessions = screen.getAllByText("First Chat");
+ const hasSelectedStyle = selectedSessions.some(
+ (el) =>
+ el.className.includes("text-zinc-600") ||
+ el.closest("div")?.className.includes("bg-zinc-100"),
+ );
+ expect(hasSelectedStyle).toBe(true);
+ });
+
+ test("selects session when clicked", async () => {
+ const urlUpdateSpy = vi.fn();
+
+ renderChatSidebar("", urlUpdateSpy);
+
+ await waitFor(() => {
+ expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
+ });
+
+ // Find a session button and click it
+ const sessionElements = screen.getAllByText("First Chat");
+ const sessionButton = sessionElements[0].closest("button");
+ if (sessionButton) {
+ fireEvent.click(sessionButton);
+ }
+
+ await waitFor(() => {
+ expect(urlUpdateSpy).toHaveBeenCalled();
+ const lastCall =
+ urlUpdateSpy.mock.calls[urlUpdateSpy.mock.calls.length - 1];
+ expect(lastCall[0].queryString).toContain("sessionId=session-1");
+ });
+ });
+ });
+
+ describe("New Chat", () => {
+ test("clears session selection when new chat button is clicked", async () => {
+ const urlUpdateSpy = vi.fn();
+
+ renderChatSidebar("?sessionId=session-1", urlUpdateSpy);
+
+ await waitFor(() => {
+ expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
+ });
+
+ const newChatButton = screen.getByRole("button", { name: "New Chat" });
+ fireEvent.click(newChatButton);
+
+ await waitFor(() => {
+ expect(urlUpdateSpy).toHaveBeenCalled();
+ const lastCall =
+ urlUpdateSpy.mock.calls[urlUpdateSpy.mock.calls.length - 1];
+ expect(lastCall[0].queryString).not.toContain("sessionId=session-1");
+ });
+ });
+ });
+});