171 lines
5.2 KiB
TypeScript
171 lines
5.2 KiB
TypeScript
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();
|
|
});
|
|
});
|