import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react"; interface MockButtonProps extends ButtonHTMLAttributes { children: ReactNode; } interface MockBadgeProps extends HTMLAttributes { children: ReactNode; } const mockApiPost = vi.fn<(endpoint: string, body?: unknown) => Promise<{ message: string }>>(); vi.mock("@/lib/api/client", () => ({ apiPost: (endpoint: string, body?: unknown): Promise<{ message: string }> => mockApiPost(endpoint, body), })); vi.mock("@/components/ui/button", () => ({ Button: ({ children, ...props }: MockButtonProps): React.JSX.Element => ( ), })); vi.mock("@/components/ui/badge", () => ({ Badge: ({ children, ...props }: MockBadgeProps): React.JSX.Element => ( {children} ), })); import { PanelControls } from "./PanelControls"; function renderWithQueryClient(ui: React.JSX.Element): ReturnType { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return render({ui}); } describe("PanelControls", (): void => { beforeEach((): void => { vi.clearAllMocks(); vi.stubGlobal("fetch", vi.fn()); mockApiPost.mockResolvedValue({ message: "ok" }); }); afterEach((): void => { vi.unstubAllGlobals(); }); it("renders action buttons with correct disabled state for active sessions", (): void => { renderWithQueryClient(); expect(screen.getByRole("button", { name: "Pause session" })).toBeEnabled(); expect(screen.getByRole("button", { name: "Resume session" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Gracefully kill session" })).toBeEnabled(); expect(screen.getByRole("button", { name: "Force kill session" })).toBeEnabled(); }); it("disables all action buttons when session is already killed", (): void => { renderWithQueryClient(); expect(screen.getByRole("button", { name: "Pause session" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Resume session" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Gracefully kill session" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Force kill session" })).toBeDisabled(); }); it("pauses a running session and reports the next status", async (): Promise => { const onStatusChange = vi.fn<(status: string) => void>(); const user = userEvent.setup(); renderWithQueryClient( ); await user.click(screen.getByRole("button", { name: "Pause session" })); await waitFor((): void => { expect(mockApiPost).toHaveBeenCalledWith( "/api/mission-control/sessions/session%20with%20space/pause", undefined ); }); expect(onStatusChange).toHaveBeenCalledWith("paused"); }); it("asks for graceful kill confirmation before submitting", async (): Promise => { const onStatusChange = vi.fn<(status: string) => void>(); const user = userEvent.setup(); renderWithQueryClient( ); await user.click(screen.getByRole("button", { name: "Gracefully kill session" })); expect( screen.getByText("Gracefully stop this agent after it finishes the current step?") ).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Confirm" })); await waitFor((): void => { expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-4/kill", { force: false, }); }); expect(onStatusChange).toHaveBeenCalledWith("killed"); }); it("sends force kill after confirmation", async (): Promise => { const onStatusChange = vi.fn<(status: string) => void>(); const user = userEvent.setup(); renderWithQueryClient( ); await user.click(screen.getByRole("button", { name: "Force kill session" })); expect(screen.getByText("This will hard-kill the agent immediately.")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Confirm" })); await waitFor((): void => { expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-5/kill", { force: true, }); }); expect(onStatusChange).toHaveBeenCalledWith("killed"); }); it("shows an error badge when an action fails", async (): Promise => { const user = userEvent.setup(); mockApiPost.mockRejectedValueOnce(new Error("unable to pause")); renderWithQueryClient(); await user.click(screen.getByRole("button", { name: "Pause session" })); await waitFor((): void => { expect(screen.getByText("unable to pause")).toBeInTheDocument(); }); }); });