Files
stack/apps/web/src/components/mission-control/GlobalAgentRoster.test.tsx
Jason Woltje f0aa3b5a75
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
test(web): add Mission Control frontend tests for phase 2 gate
2026-03-07 15:25:39 -06:00

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();
});
});