test(web): MS23-P2-009 Mission Control frontend tests
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

This commit is contained in:
2026-03-07 15:26:12 -06:00
parent f0aa3b5a75
commit 7147dc3503
3 changed files with 528 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
import { 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<HTMLButtonElement> {
children: ReactNode;
}
interface MockBadgeProps extends HTMLAttributes<HTMLElement> {
children: ReactNode;
}
interface AuditLogEntry {
id: string;
userId: string;
sessionId: string;
provider: string;
action: string;
content: string | null;
metadata: unknown;
createdAt: string;
}
interface AuditLogResponse {
items: AuditLogEntry[];
total: number;
page: number;
pages: number;
}
const mockApiGet = vi.fn<(endpoint: string) => Promise<AuditLogResponse>>();
vi.mock("@/lib/api/client", () => ({
apiGet: (endpoint: string): Promise<AuditLogResponse> => mockApiGet(endpoint),
}));
vi.mock("@/components/ui/button", () => ({
Button: ({ children, ...props }: MockButtonProps): React.JSX.Element => (
<button {...props}>{children}</button>
),
}));
vi.mock("@/components/ui/badge", () => ({
Badge: ({ children, ...props }: MockBadgeProps): React.JSX.Element => (
<span {...props}>{children}</span>
),
}));
import { AuditLogDrawer } from "./AuditLogDrawer";
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 responseWith(items: AuditLogEntry[], page: number, pages: number): AuditLogResponse {
return {
items,
total: items.length,
page,
pages,
};
}
describe("AuditLogDrawer", (): void => {
beforeEach((): void => {
vi.clearAllMocks();
mockApiGet.mockResolvedValue(responseWith([], 1, 0));
});
it("opens from trigger text and renders empty state", async (): Promise<void> => {
const user = userEvent.setup();
renderWithQueryClient(<AuditLogDrawer trigger="Audit" />);
await user.click(screen.getByRole("button", { name: "Audit" }));
await waitFor((): void => {
expect(screen.getByText("Audit Log")).toBeInTheDocument();
expect(screen.getByText("No audit entries found.")).toBeInTheDocument();
});
});
it("renders audit entries with action, session id, and payload", async (): Promise<void> => {
const user = userEvent.setup();
mockApiGet.mockResolvedValue(
responseWith(
[
{
id: "entry-1",
userId: "operator-1",
sessionId: "1234567890abcdef",
provider: "internal",
action: "inject",
content: "Run diagnostics",
metadata: { payload: { ignored: true } },
createdAt: "2026-03-07T19:00:00.000Z",
},
],
1,
1
)
);
renderWithQueryClient(<AuditLogDrawer trigger="Audit" />);
await user.click(screen.getByRole("button", { name: "Audit" }));
await waitFor((): void => {
expect(screen.getByText("inject")).toBeInTheDocument();
expect(screen.getByText("12345678")).toBeInTheDocument();
expect(screen.getByText("Run diagnostics")).toBeInTheDocument();
});
});
it("supports pagination and metadata payload summary", async (): Promise<void> => {
const user = userEvent.setup();
mockApiGet.mockImplementation((endpoint: string): Promise<AuditLogResponse> => {
const query = endpoint.split("?")[1] ?? "";
const params = new URLSearchParams(query);
const page = Number(params.get("page") ?? "1");
if (page === 1) {
return Promise.resolve({
items: [
{
id: "entry-page-1",
userId: "operator-2",
sessionId: "abcdefgh12345678",
provider: "internal",
action: "pause",
content: "",
metadata: { payload: { reason: "hold" } },
createdAt: "2026-03-07T19:01:00.000Z",
},
],
total: 2,
page: 1,
pages: 2,
});
}
return Promise.resolve({
items: [
{
id: "entry-page-2",
userId: "operator-3",
sessionId: "zzzz111122223333",
provider: "internal",
action: "kill",
content: null,
metadata: { payload: { force: true } },
createdAt: "2026-03-07T19:02:00.000Z",
},
],
total: 2,
page: 2,
pages: 2,
});
});
renderWithQueryClient(<AuditLogDrawer trigger="Audit" />);
await user.click(screen.getByRole("button", { name: "Audit" }));
await waitFor((): void => {
expect(screen.getByText("Page 1 of 2")).toBeInTheDocument();
expect(screen.getByText("reason=hold")).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: "Next" }));
await waitFor((): void => {
expect(screen.getByText("Page 2 of 2")).toBeInTheDocument();
expect(screen.getByText("force=true")).toBeInTheDocument();
});
});
it("includes sessionId filter in query string", async (): Promise<void> => {
const user = userEvent.setup();
renderWithQueryClient(<AuditLogDrawer trigger="Audit" sessionId="session 7" />);
await user.click(screen.getByRole("button", { name: "Audit" }));
await waitFor((): void => {
expect(mockApiGet).toHaveBeenCalled();
});
const firstCall = mockApiGet.mock.calls[0];
const endpoint = firstCall?.[0] ?? "";
expect(endpoint).toContain("sessionId=session+7");
});
});

View File

@@ -0,0 +1,170 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type {
ButtonHTMLAttributes,
InputHTMLAttributes,
LabelHTMLAttributes,
ReactNode,
} from "react";
interface MockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
}
interface MockInputProps extends InputHTMLAttributes<HTMLInputElement> {
children?: ReactNode;
}
interface MockLabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
children: ReactNode;
}
interface MockSession {
id: string;
providerId: string;
providerType: string;
status: "active" | "paused";
createdAt: Date;
updatedAt: Date;
}
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 => (
<button {...props}>{children}</button>
),
}));
vi.mock("@/components/ui/input", () => ({
Input: ({ ...props }: MockInputProps): React.JSX.Element => <input {...props} />,
}));
vi.mock("@/components/ui/label", () => ({
Label: ({ children, ...props }: MockLabelProps): React.JSX.Element => (
<label {...props}>{children}</label>
),
}));
import { KillAllDialog } from "./KillAllDialog";
function makeSession(overrides: Partial<MockSession>): MockSession {
return {
id: "session-1",
providerId: "internal",
providerType: "internal",
status: "active",
createdAt: new Date("2026-03-07T10:00:00.000Z"),
updatedAt: new Date("2026-03-07T10:01:00.000Z"),
...overrides,
};
}
describe("KillAllDialog", (): void => {
beforeEach((): void => {
vi.clearAllMocks();
mockApiPost.mockResolvedValue({ message: "killed" });
});
it("renders trigger button and requires exact confirmation text", async (): Promise<void> => {
const user = userEvent.setup();
render(<KillAllDialog sessions={[makeSession({})]} />);
await user.click(screen.getByRole("button", { name: "Kill All" }));
const confirmInput = screen.getByLabelText("Type KILL ALL to confirm");
const confirmButton = screen.getByRole("button", { name: "Kill All Agents" });
expect(confirmButton).toBeDisabled();
await user.type(confirmInput, "kill all");
expect(confirmButton).toBeDisabled();
await user.clear(confirmInput);
await user.type(confirmInput, "KILL ALL");
expect(confirmButton).toBeEnabled();
});
it("kills only internal sessions by default and invokes completion callback", async (): Promise<void> => {
const onComplete = vi.fn<() => void>();
const user = userEvent.setup();
render(
<KillAllDialog
sessions={[
makeSession({ id: "internal-1", providerType: "internal" }),
makeSession({ id: "external-1", providerType: "external" }),
]}
onComplete={onComplete}
/>
);
await user.click(screen.getByRole("button", { name: "Kill All" }));
await user.type(screen.getByLabelText("Type KILL ALL to confirm"), "KILL ALL");
await user.click(screen.getByRole("button", { name: "Kill All Agents" }));
await waitFor((): void => {
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/internal-1/kill", {
force: true,
});
});
expect(mockApiPost).not.toHaveBeenCalledWith("/api/mission-control/sessions/external-1/kill", {
force: true,
});
expect(onComplete).toHaveBeenCalledTimes(1);
});
it("kills all providers when all scope is selected", async (): Promise<void> => {
const user = userEvent.setup();
render(
<KillAllDialog
sessions={[
makeSession({ id: "internal-2", providerType: "internal" }),
makeSession({ id: "external-2", providerType: "external" }),
]}
/>
);
await user.click(screen.getByRole("button", { name: "Kill All" }));
await user.click(screen.getByRole("radio", { name: /All providers \(2\)/ }));
await user.type(screen.getByLabelText("Type KILL ALL to confirm"), "KILL ALL");
await user.click(screen.getByRole("button", { name: "Kill All Agents" }));
await waitFor((): void => {
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/internal-2/kill", {
force: true,
});
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/external-2/kill", {
force: true,
});
});
});
it("shows empty-scope warning when internal sessions are unavailable", async (): Promise<void> => {
const user = userEvent.setup();
render(
<KillAllDialog
sessions={[
makeSession({ id: "external-only", providerId: "ext", providerType: "external" }),
]}
/>
);
await user.click(screen.getByRole("button", { name: "Kill All" }));
await user.type(screen.getByLabelText("Type KILL ALL to confirm"), "KILL ALL");
expect(screen.getByText("No sessions in the selected scope.")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Kill All Agents" })).toBeDisabled();
});
});

View File

@@ -0,0 +1,153 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { MAX_PANEL_COUNT, MissionControlPanel, type PanelConfig } from "./MissionControlPanel";
interface MockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
}
interface MockOrchestratorPanelProps {
sessionId?: string;
onClose?: () => void;
closeDisabled?: boolean;
onExpand?: () => void;
expanded?: boolean;
}
const mockOrchestratorPanel = vi.fn<(props: MockOrchestratorPanelProps) => React.JSX.Element>();
vi.mock("@/components/mission-control/OrchestratorPanel", () => ({
OrchestratorPanel: (props: MockOrchestratorPanelProps): React.JSX.Element =>
mockOrchestratorPanel(props),
}));
vi.mock("@/components/ui/button", () => ({
Button: ({ children, ...props }: MockButtonProps): React.JSX.Element => (
<button {...props}>{children}</button>
),
}));
function buildPanels(count: number): PanelConfig[] {
return Array.from({ length: count }, (_, index) => ({
sessionId: `session-${String(index + 1)}`,
}));
}
describe("MissionControlPanel", (): void => {
beforeEach((): void => {
vi.clearAllMocks();
mockOrchestratorPanel.mockImplementation(
({ sessionId, closeDisabled, expanded }: MockOrchestratorPanelProps): React.JSX.Element => (
<div
data-testid="orchestrator-panel"
data-session-id={sessionId ?? ""}
data-close-disabled={String(closeDisabled ?? false)}
data-expanded={String(expanded ?? false)}
/>
)
);
});
it("renders the panel grid and default heading", (): void => {
render(
<MissionControlPanel
panels={[{}]}
onAddPanel={vi.fn<() => void>()}
onRemovePanel={vi.fn<(index: number) => void>()}
onExpandPanel={vi.fn<(index: number) => void>()}
/>
);
expect(screen.getByRole("heading", { name: "Panels" })).toBeInTheDocument();
expect(screen.getAllByTestId("orchestrator-panel")).toHaveLength(1);
});
it("calls onAddPanel when the add button is clicked", (): void => {
const onAddPanel = vi.fn<() => void>();
render(
<MissionControlPanel
panels={[{}]}
onAddPanel={onAddPanel}
onRemovePanel={vi.fn<(index: number) => void>()}
onExpandPanel={vi.fn<(index: number) => void>()}
/>
);
fireEvent.click(screen.getByRole("button", { name: "Add panel" }));
expect(onAddPanel).toHaveBeenCalledTimes(1);
});
it("disables add panel at the configured maximum", (): void => {
render(
<MissionControlPanel
panels={buildPanels(MAX_PANEL_COUNT)}
onAddPanel={vi.fn<() => void>()}
onRemovePanel={vi.fn<(index: number) => void>()}
onExpandPanel={vi.fn<(index: number) => void>()}
/>
);
const addButton = screen.getByRole("button", { name: "Add panel" });
expect(addButton).toBeDisabled();
expect(addButton).toHaveAttribute("title", "Maximum of 6 panels");
});
it("passes closeDisabled=false when more than one panel exists", (): void => {
render(
<MissionControlPanel
panels={buildPanels(2)}
onAddPanel={vi.fn<() => void>()}
onRemovePanel={vi.fn<(index: number) => void>()}
onExpandPanel={vi.fn<(index: number) => void>()}
/>
);
const renderedPanels = screen.getAllByTestId("orchestrator-panel");
expect(renderedPanels).toHaveLength(2);
for (const panel of renderedPanels) {
expect(panel).toHaveAttribute("data-close-disabled", "false");
}
});
it("renders only the expanded panel in focused mode", (): void => {
render(
<MissionControlPanel
panels={[{ sessionId: "session-1" }, { sessionId: "session-2", expanded: true }]}
onAddPanel={vi.fn<() => void>()}
onRemovePanel={vi.fn<(index: number) => void>()}
onExpandPanel={vi.fn<(index: number) => void>()}
/>
);
const renderedPanels = screen.getAllByTestId("orchestrator-panel");
expect(renderedPanels).toHaveLength(1);
expect(renderedPanels[0]).toHaveAttribute("data-session-id", "session-2");
expect(renderedPanels[0]).toHaveAttribute("data-expanded", "true");
});
it("handles Escape key by toggling expanded panel", async (): Promise<void> => {
const onExpandPanel = vi.fn<(index: number) => void>();
render(
<MissionControlPanel
panels={[{ sessionId: "session-1", expanded: true }, { sessionId: "session-2" }]}
onAddPanel={vi.fn<() => void>()}
onRemovePanel={vi.fn<(index: number) => void>()}
onExpandPanel={onExpandPanel}
/>
);
fireEvent.keyDown(window, { key: "Escape" });
await waitFor((): void => {
expect(onExpandPanel).toHaveBeenCalledWith(0);
});
});
});