Files
kandev/apps/web/components/task/chat/reset-context-button.test.tsx
T
2026-09-12 14:58:49 +02:00

213 lines
7.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { TooltipProvider } from "@kandev/ui/tooltip";
const mocks = vi.hoisted(() => ({
request: vi.fn(),
clearContextWindow: vi.fn(),
}));
vi.mock("@/lib/ws/connection", () => ({
getWebSocketClient: () => ({ request: mocks.request }),
}));
vi.mock("@/components/state-provider", () => ({
useAppStore: (
selector: (state: { clearContextWindow: typeof mocks.clearContextWindow }) => unknown,
) => selector({ clearContextWindow: mocks.clearContextWindow }),
}));
import { ResetContextButton } from "./reset-context-button";
const RESET_CONTEXT_BUTTON_TEST_ID = "reset-context-button";
const RESET_CONTEXT_CONFIRM_TEST_ID = "reset-context-confirm";
const RESET_CONTEXT_WARNING =
"This will clear the agent's conversation history and start a fresh context. Your workspace, files, and git state will be preserved.";
function renderResetButton(
presentation: "desktop" | "mobile" = "desktop",
onConfirmationOpenChange?: (open: boolean) => void,
) {
return render(
<TooltipProvider>
<ResetContextButton
sessionId="session-1"
presentation={presentation}
onConfirmationOpenChange={onConfirmationOpenChange}
/>
</TooltipProvider>,
);
}
afterEach(() => {
cleanup();
Object.defineProperty(window, "innerWidth", { configurable: true, value: 1024 });
vi.clearAllMocks();
});
describe("ResetContextButton context-window invalidation", () => {
beforeEach(() => {
mocks.request.mockResolvedValue({ success: true });
});
it("confirms from an anchored popover and clears the session usage cache once", async () => {
renderResetButton();
fireEvent.click(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
fireEvent.click(await screen.findByTestId(RESET_CONTEXT_CONFIRM_TEST_ID));
await waitFor(() => {
expect(mocks.request).toHaveBeenCalledWith(
"session.reset_context",
{ session_id: "session-1" },
30000,
);
expect(mocks.clearContextWindow).toHaveBeenCalledWith("session-1");
});
expect(mocks.request).toHaveBeenCalledTimes(1);
expect(screen.queryByRole("alertdialog")).toBeNull();
});
it("leaves transcript and context untouched when desktop confirmation is cancelled", async () => {
renderResetButton();
fireEvent.click(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
fireEvent.click(await screen.findByRole("button", { name: "Cancel" }));
expect(mocks.request).not.toHaveBeenCalled();
expect(mocks.clearContextWindow).not.toHaveBeenCalled();
expect(screen.queryByTestId("reset-context-confirm-popover")).toBeNull();
});
it("keeps the session usage cache when reset fails", async () => {
mocks.request.mockRejectedValue(new Error("reset failed"));
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
renderResetButton();
fireEvent.click(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
await act(async () => {
fireEvent.click(await screen.findByTestId(RESET_CONTEXT_CONFIRM_TEST_ID));
});
await waitFor(() => expect(mocks.request).toHaveBeenCalled());
expect(mocks.clearContextWindow).not.toHaveBeenCalled();
consoleError.mockRestore();
});
it("keeps reset busy while the confirmed request is pending", async () => {
let resolveRequest!: (value: { success: boolean }) => void;
mocks.request.mockReturnValue(
new Promise<{ success: boolean }>((resolve) => {
resolveRequest = resolve;
}),
);
renderResetButton();
fireEvent.click(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
fireEvent.click(await screen.findByTestId(RESET_CONTEXT_CONFIRM_TEST_ID));
await waitFor(() => expect(mocks.request).toHaveBeenCalledTimes(1));
expect(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID)).toHaveProperty("disabled", true);
expect(mocks.clearContextWindow).not.toHaveBeenCalled();
await act(async () => {
resolveRequest({ success: true });
});
});
});
describe("ResetContextButton mobile presentation", () => {
it("does not leave a phone tooltip floating over a hosted confirmation", () => {
Object.defineProperty(window, "innerWidth", { configurable: true, value: 390 });
renderResetButton("mobile");
act(() => screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID).focus());
expect(screen.queryByRole("tooltip")).toBeNull();
});
it("uses a phone sheet while keeping the reset trigger mounted", async () => {
Object.defineProperty(window, "innerWidth", { configurable: true, value: 390 });
renderResetButton("mobile");
const trigger = screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID);
fireEvent.click(trigger);
const sheet = await screen.findByRole("dialog", { name: "Reset agent context?" });
expect(sheet.getAttribute("data-slot")).toBe("drawer-content");
expect(trigger.isConnected).toBe(true);
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
await waitFor(() => expect(document.activeElement).toBe(trigger));
expect(mocks.request).not.toHaveBeenCalled();
});
beforeEach(() => {
mocks.request.mockResolvedValue({ success: true });
});
it("uses inline touch actions without opening a second overlay", async () => {
renderResetButton("mobile");
fireEvent.click(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
const inlineConfirmation = await screen.findByTestId("reset-context-inline-confirm");
expect(inlineConfirmation).toBeTruthy();
expect(screen.queryByTestId("reset-context-confirm-popover")).toBeNull();
expect(screen.getByTestId(RESET_CONTEXT_CONFIRM_TEST_ID).className).toContain("h-11");
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(mocks.request).not.toHaveBeenCalled();
expect(mocks.clearContextWindow).not.toHaveBeenCalled();
});
it("shows the destructive warning in the inline confirmation", async () => {
renderResetButton("mobile");
fireEvent.click(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
expect(await screen.findByText(RESET_CONTEXT_WARNING)).toBeTruthy();
});
it("returns focus to the reset trigger after cancelling inline", async () => {
renderResetButton("mobile");
fireEvent.click(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
fireEvent.click(await screen.findByRole("button", { name: "Cancel" }));
await waitFor(() =>
expect(document.activeElement).toBe(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID)),
);
});
it("returns focus to the reset trigger after dismissing inline with Escape", async () => {
renderResetButton("mobile");
fireEvent.click(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
fireEvent.keyDown(await screen.findByRole("group"), { key: "Escape" });
await waitFor(() =>
expect(document.activeElement).toBe(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID)),
);
});
it("dispatches reset once after inline confirmation", async () => {
renderResetButton("mobile");
fireEvent.click(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
fireEvent.click(await screen.findByTestId(RESET_CONTEXT_CONFIRM_TEST_ID));
await waitFor(() => {
expect(mocks.request).toHaveBeenCalledTimes(1);
expect(mocks.clearContextWindow).toHaveBeenCalledWith("session-1");
});
expect(document.activeElement).not.toBe(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
});
it("reports one close transition before dispatching the confirmed reset", async () => {
const onConfirmationOpenChange = vi.fn();
renderResetButton("mobile", onConfirmationOpenChange);
fireEvent.click(screen.getByTestId(RESET_CONTEXT_BUTTON_TEST_ID));
fireEvent.click(await screen.findByTestId(RESET_CONTEXT_CONFIRM_TEST_ID));
await waitFor(() => expect(mocks.request).toHaveBeenCalledTimes(1));
expect(onConfirmationOpenChange.mock.calls.map(([open]) => open)).toEqual([true, false]);
});
});