204 lines
6.5 KiB
TypeScript
204 lines
6.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
|
|
|
interface MockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
children: ReactNode;
|
|
}
|
|
|
|
interface MockContainerProps extends HTMLAttributes<HTMLElement> {
|
|
children: ReactNode;
|
|
}
|
|
|
|
interface MockSession {
|
|
id: string;
|
|
providerId: string;
|
|
providerType: string;
|
|
status: "active" | "paused" | "killed";
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
const mockApiGet = vi.fn<(endpoint: string) => Promise<MockSession[]>>();
|
|
const mockApiPost = vi.fn<(endpoint: string, body?: unknown) => Promise<{ message: string }>>();
|
|
const mockKillAllDialog = vi.fn<() => React.JSX.Element>();
|
|
|
|
vi.mock("@/lib/api/client", () => ({
|
|
apiGet: (endpoint: string): Promise<MockSession[]> => mockApiGet(endpoint),
|
|
apiPost: (endpoint: string, body?: unknown): Promise<{ message: string }> =>
|
|
mockApiPost(endpoint, body),
|
|
}));
|
|
|
|
vi.mock("@/components/mission-control/KillAllDialog", () => ({
|
|
KillAllDialog: (): React.JSX.Element => mockKillAllDialog(),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/button", () => ({
|
|
Button: ({ children, ...props }: MockButtonProps): React.JSX.Element => (
|
|
<button {...props}>{children}</button>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/badge", () => ({
|
|
Badge: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<span {...props}>{children}</span>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/card", () => ({
|
|
Card: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<section {...props}>{children}</section>
|
|
),
|
|
CardHeader: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<header {...props}>{children}</header>
|
|
),
|
|
CardContent: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<div {...props}>{children}</div>
|
|
),
|
|
CardTitle: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<h2 {...props}>{children}</h2>
|
|
),
|
|
}));
|
|
|
|
import { GlobalAgentRoster } from "./GlobalAgentRoster";
|
|
|
|
function renderWithQueryClient(ui: React.JSX.Element): ReturnType<typeof render> {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
|
|
}
|
|
|
|
function makeSession(overrides: Partial<MockSession>): MockSession {
|
|
return {
|
|
id: "session-12345678",
|
|
providerId: "internal",
|
|
providerType: "internal",
|
|
status: "active",
|
|
createdAt: "2026-03-07T10:00:00.000Z",
|
|
updatedAt: "2026-03-07T10:01:00.000Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function getRowForSessionLabel(label: string): HTMLElement {
|
|
const sessionLabel = screen.getByText(label);
|
|
const row = sessionLabel.closest('[role="button"]');
|
|
|
|
if (!(row instanceof HTMLElement)) {
|
|
throw new Error(`Expected a row element for session label ${label}`);
|
|
}
|
|
|
|
return row;
|
|
}
|
|
|
|
describe("GlobalAgentRoster", (): void => {
|
|
beforeEach((): void => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", vi.fn());
|
|
|
|
mockApiGet.mockResolvedValue([]);
|
|
mockApiPost.mockResolvedValue({ message: "ok" });
|
|
|
|
mockKillAllDialog.mockImplementation(
|
|
(): React.JSX.Element => <div data-testid="kill-all-dialog">kill-all-dialog</div>
|
|
);
|
|
});
|
|
|
|
afterEach((): void => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("renders the empty state when no active sessions are returned", async (): Promise<void> => {
|
|
renderWithQueryClient(<GlobalAgentRoster />);
|
|
|
|
await waitFor((): void => {
|
|
expect(screen.getByText("No active agents")).toBeInTheDocument();
|
|
});
|
|
|
|
expect(screen.queryByTestId("kill-all-dialog")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("groups sessions by provider and shows kill-all control when sessions exist", async (): Promise<void> => {
|
|
mockApiGet.mockResolvedValue([
|
|
makeSession({ id: "alpha123456", providerId: "internal", providerType: "internal" }),
|
|
makeSession({ id: "bravo123456", providerId: "codex", providerType: "openai" }),
|
|
]);
|
|
|
|
renderWithQueryClient(<GlobalAgentRoster />);
|
|
|
|
await waitFor((): void => {
|
|
expect(screen.getByText("internal")).toBeInTheDocument();
|
|
expect(screen.getByText("codex (openai)")).toBeInTheDocument();
|
|
});
|
|
|
|
expect(screen.getByText("alpha123")).toBeInTheDocument();
|
|
expect(screen.getByText("bravo123")).toBeInTheDocument();
|
|
expect(screen.getByTestId("kill-all-dialog")).toBeInTheDocument();
|
|
});
|
|
|
|
it("calls onSelectSession on row click and keyboard activation", async (): Promise<void> => {
|
|
const onSelectSession = vi.fn<(sessionId: string) => void>();
|
|
|
|
mockApiGet.mockResolvedValue([makeSession({ id: "target123456" })]);
|
|
|
|
renderWithQueryClient(<GlobalAgentRoster onSelectSession={onSelectSession} />);
|
|
|
|
await waitFor((): void => {
|
|
expect(screen.getByText("target12")).toBeInTheDocument();
|
|
});
|
|
|
|
const row = getRowForSessionLabel("target12");
|
|
|
|
fireEvent.click(row);
|
|
|
|
fireEvent.keyDown(row, { key: "Enter" });
|
|
|
|
expect(onSelectSession).toHaveBeenCalledTimes(2);
|
|
expect(onSelectSession).toHaveBeenNthCalledWith(1, "target123456");
|
|
expect(onSelectSession).toHaveBeenNthCalledWith(2, "target123456");
|
|
});
|
|
|
|
it("kills a session from the roster", async (): Promise<void> => {
|
|
mockApiGet.mockResolvedValue([makeSession({ id: "killme123456" })]);
|
|
|
|
renderWithQueryClient(<GlobalAgentRoster />);
|
|
|
|
await waitFor((): void => {
|
|
expect(screen.getByRole("button", { name: "Kill session killme12" })).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Kill session killme12" }));
|
|
|
|
await waitFor((): void => {
|
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/killme123456/kill", {
|
|
force: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
it("collapses and reopens provider groups", async (): Promise<void> => {
|
|
mockApiGet.mockResolvedValue([makeSession({ id: "grouped12345" })]);
|
|
|
|
renderWithQueryClient(<GlobalAgentRoster />);
|
|
|
|
await waitFor((): void => {
|
|
expect(screen.getByText("grouped1")).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /internal/i }));
|
|
|
|
expect(screen.queryByText("grouped1")).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /internal/i }));
|
|
|
|
expect(screen.getByText("grouped1")).toBeInTheDocument();
|
|
});
|
|
});
|