inspector/clients/web/src/components/groups/ServerConfigModal/ServerConfigModal.test.tsx
cliffhall 4bad110942 test(web): eliminate timeout flakiness in the web test suite (closes #1596)
Root-cause and deterministically fix the recurring 5s-timeout flakes that
surfaced under v8-instrumented, concurrent load. All fixes are test-only; no
product source changed. No test was skipped or disabled, and the per-file
coverage gate (>=90 on all four dims) still holds.

Common root cause across the modal/form/screen suites: userEvent.setup()
schedules a real setTimeout between keystrokes. Under CPU contention those
yields balloon, so multi-field interactions blew past the 5s per-test
timeout. Fix: userEvent.setup({ delay: null }) removes the per-keystroke
real-timer dependence (typing dispatches synchronously), making the suites
load-independent.

Per file:
- ServerConfigModal / ServerImportConfigModal / ServerImportJsonModal /
  ResourcesScreen / InspectorView: userEvent.setup({ delay: null }).
- PromptArgumentsForm (completions): replaced wall-clock sleeps sized just
  past the 300ms completion debounce (await setTimeout(400)) — which race
  the debounce timer under load — with awaited conditions (findBy/waitFor)
  on the real rendered outcome, plus delay:null. Negative assertions now
  lean on the component's synchronous state/timer teardown instead of a
  timed window, so they are deterministic regardless of machine speed.
- inspectorClient integration "tracks stderr logs": the child's stderr is
  piped out-of-band from the tool's JSON-RPC response, so reading the log
  synchronously after callTool raced the stderr chunk. Wrapped the assertion
  in vi.waitFor so it polls until the line lands.

Verified: web validate + 6x test:coverage (4 sequential + 2 concurrent
under load) + standalone test:integration, all green (229 files / 3237
tests per coverage run; 799 integration tests), zero intermittent failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
2026-07-01 21:11:11 -04:00

451 lines
16 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { waitFor, within } from "@testing-library/react";
import type { MCPServerConfig } from "@inspector/core/mcp/types.js";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { ServerConfigModal } from "./ServerConfigModal";
/** Find the "Clear" button living in the rightSection of `input`'s field. */
function clearButtonFor(input: HTMLElement): HTMLElement {
const root =
input.closest('[class*="mantine-TextInput-root"]') ??
input.closest('[class*="Input-wrapper"]');
return within(root as HTMLElement).getByRole("button", { name: "Clear" });
}
describe("ServerConfigModal", () => {
function base(overrides: Partial<{ existingIds: string[] }> = {}) {
return {
opened: true,
mode: "add" as const,
existingIds: overrides.existingIds ?? [],
onClose: vi.fn(),
onSubmit: vi.fn().mockResolvedValue(undefined),
};
}
it("does not render when opened is false", () => {
renderWithMantine(<ServerConfigModal {...base()} opened={false} />);
expect(screen.queryByText("Add server")).not.toBeInTheDocument();
});
it("renders the add title and stdio fields by default", () => {
renderWithMantine(<ServerConfigModal {...base()} />);
expect(screen.getByText("Add server")).toBeInTheDocument();
expect(screen.getByLabelText(/Server ID/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Command/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Arguments/i)).toBeInTheDocument();
});
it("rejects an id containing illegal characters", async () => {
const user = userEvent.setup({ delay: null });
renderWithMantine(<ServerConfigModal {...base()} />);
await user.type(screen.getByLabelText(/Server ID/i), "bad id!");
expect(
screen.getByText(/letters, numbers, hyphens, and underscores/i),
).toBeInTheDocument();
});
it("flags duplicate ids against existingIds", async () => {
const user = userEvent.setup({ delay: null });
renderWithMantine(
<ServerConfigModal {...base({ existingIds: ["alpha"] })} />,
);
await user.type(screen.getByLabelText(/Server ID/i), "alpha");
expect(screen.getByText(/already exists/i)).toBeInTheDocument();
});
it("calls onSubmit with a valid stdio config", async () => {
const user = userEvent.setup({ delay: null });
const props = base();
renderWithMantine(<ServerConfigModal {...props} />);
await user.type(screen.getByLabelText(/Server ID/i), "alpha");
await user.type(screen.getByLabelText(/Command/i), "node");
await user.type(screen.getByLabelText(/Arguments/i), "x.js\n--port=3000");
await user.click(screen.getByRole("button", { name: /^Add$/ }));
await waitFor(() => expect(props.onSubmit).toHaveBeenCalledOnce());
expect(props.onSubmit).toHaveBeenCalledWith("alpha", {
type: "stdio",
command: "node",
args: ["x.js", "--port=3000"],
});
});
it("requires a command for stdio submission", async () => {
const user = userEvent.setup({ delay: null });
const props = base();
renderWithMantine(<ServerConfigModal {...props} />);
await user.type(screen.getByLabelText(/Server ID/i), "alpha");
await user.click(screen.getByRole("button", { name: /^Add$/ }));
expect(screen.getByRole("alert")).toHaveTextContent(/Command is required/i);
expect(props.onSubmit).not.toHaveBeenCalled();
});
it("rejects malformed env lines", async () => {
const user = userEvent.setup({ delay: null });
const props = base();
renderWithMantine(<ServerConfigModal {...props} />);
await user.type(screen.getByLabelText(/Server ID/i), "alpha");
await user.type(screen.getByLabelText(/Command/i), "node");
await user.type(screen.getByLabelText(/Environment/i), "BAD_LINE");
await user.click(screen.getByRole("button", { name: /^Add$/ }));
expect(screen.getByRole("alert")).toHaveTextContent(/Invalid env/i);
expect(props.onSubmit).not.toHaveBeenCalled();
});
// Mantine Select uses a portal-mounted dropdown that happy-dom doesn't
// open reliably; we exercise the sse rendering branch by seeding the form
// with an sse initialConfig (the same code path the Select onChange takes).
it("renders url (and hides stdio fields) when transport is sse", () => {
renderWithMantine(
<ServerConfigModal
{...base()}
mode="edit"
initialId="remote"
initialConfig={{ type: "sse", url: "https://x.test/sse" }}
/>,
);
expect(screen.getByLabelText(/^URL/)).toBeInTheDocument();
expect(screen.queryByLabelText(/Headers/i)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/^Command/)).not.toBeInTheDocument();
});
it("submits an sse config with just the url (headers move to the settings form)", async () => {
const user = userEvent.setup({ delay: null });
const props = base();
renderWithMantine(
<ServerConfigModal
{...props}
mode="edit"
initialId="remote"
initialConfig={{ type: "sse", url: "" }}
/>,
);
await user.type(screen.getByLabelText(/^URL/), "https://x.test/sse");
await user.click(screen.getByRole("button", { name: /^Save$/ }));
await waitFor(() => expect(props.onSubmit).toHaveBeenCalledOnce());
expect(props.onSubmit).toHaveBeenCalledWith("remote", {
type: "sse",
url: "https://x.test/sse",
});
});
it("submits a streamable-http config", async () => {
const user = userEvent.setup({ delay: null });
const props = base();
renderWithMantine(
<ServerConfigModal
{...props}
mode="edit"
initialId="http-srv"
initialConfig={{ type: "streamable-http", url: "https://x.test/mcp" }}
/>,
);
await user.click(screen.getByRole("button", { name: /^Save$/ }));
await waitFor(() => expect(props.onSubmit).toHaveBeenCalledOnce());
expect(props.onSubmit).toHaveBeenCalledWith("http-srv", {
type: "streamable-http",
url: "https://x.test/mcp",
});
});
it("loads the url from a streamable-http config", () => {
renderWithMantine(
<ServerConfigModal
{...base()}
mode="edit"
initialId="http-srv"
initialConfig={{
type: "streamable-http",
url: "https://x.test/mcp",
}}
/>,
);
expect(screen.getByLabelText(/^URL/)).toHaveValue("https://x.test/mcp");
});
it("rejects an empty URL on sse submission", async () => {
const user = userEvent.setup({ delay: null });
const props = base();
renderWithMantine(
<ServerConfigModal
{...props}
mode="edit"
initialId="remote"
initialConfig={{ type: "sse", url: "" }}
/>,
);
await user.click(screen.getByRole("button", { name: /^Save$/ }));
expect(screen.getByRole("alert")).toHaveTextContent(/URL is required/i);
expect(props.onSubmit).not.toHaveBeenCalled();
});
it("pre-populates fields in edit mode", () => {
const initialConfig: MCPServerConfig = {
type: "stdio",
command: "node",
args: ["server.js"],
env: { DEBUG: "1" },
cwd: "/tmp/here",
};
renderWithMantine(
<ServerConfigModal
{...base()}
mode="edit"
initialId="alpha"
initialConfig={initialConfig}
/>,
);
expect(screen.getByText("Edit server")).toBeInTheDocument();
expect(screen.getByLabelText(/Server ID/i)).toHaveValue("alpha");
expect(screen.getByLabelText(/Command/i)).toHaveValue("node");
expect(screen.getByLabelText(/Arguments/i)).toHaveValue("server.js");
expect(screen.getByLabelText(/Environment/i)).toHaveValue("DEBUG=1");
expect(screen.getByLabelText(/Working directory/i)).toHaveValue(
"/tmp/here",
);
});
it("clears the id field in clone mode (everything else carries over)", () => {
const initialConfig: MCPServerConfig = {
type: "stdio",
command: "node",
};
renderWithMantine(
<ServerConfigModal
{...base()}
mode="clone"
initialId="alpha"
initialConfig={initialConfig}
/>,
);
expect(screen.getByText("Clone server")).toBeInTheDocument();
expect(screen.getByLabelText(/Server ID/i)).toHaveValue("");
expect(screen.getByLabelText(/Command/i)).toHaveValue("node");
});
it("shows the error message and stays open when onSubmit rejects", async () => {
const user = userEvent.setup({ delay: null });
const props = {
...base(),
onSubmit: vi.fn().mockRejectedValue(new Error("server full")),
};
renderWithMantine(<ServerConfigModal {...props} />);
await user.type(screen.getByLabelText(/Server ID/i), "alpha");
await user.type(screen.getByLabelText(/Command/i), "node");
await user.click(screen.getByRole("button", { name: /^Add$/ }));
await waitFor(() => {
expect(screen.getByRole("alert")).toHaveTextContent(/server full/);
});
expect(props.onClose).not.toHaveBeenCalled();
});
it("blocks submit and shows the id error message on bad-id submit click", async () => {
const user = userEvent.setup({ delay: null });
const props = base({ existingIds: ["alpha"] });
renderWithMantine(<ServerConfigModal {...props} />);
await user.type(screen.getByLabelText(/Server ID/i), "alpha");
await user.type(screen.getByLabelText(/Command/i), "node");
await user.click(screen.getByRole("button", { name: /^Add$/ }));
expect(screen.getByRole("alert")).toHaveTextContent(/already exists/i);
expect(props.onSubmit).not.toHaveBeenCalled();
});
it("supports editing the working directory field", async () => {
const user = userEvent.setup({ delay: null });
const props = base();
renderWithMantine(<ServerConfigModal {...props} />);
await user.type(screen.getByLabelText(/Server ID/i), "alpha");
await user.type(screen.getByLabelText(/Command/i), "node");
await user.type(screen.getByLabelText(/Working directory/i), "/tmp/cwd");
await user.click(screen.getByRole("button", { name: /^Add$/ }));
await waitFor(() => expect(props.onSubmit).toHaveBeenCalledOnce());
expect(props.onSubmit).toHaveBeenCalledWith("alpha", {
type: "stdio",
command: "node",
cwd: "/tmp/cwd",
});
});
it("clears the Server ID field via its Clear button", async () => {
const user = userEvent.setup({ delay: null });
renderWithMantine(
<ServerConfigModal
{...base()}
mode="edit"
initialId="alpha"
initialConfig={{ type: "stdio", command: "node" }}
/>,
);
const idInput = screen.getByLabelText(/Server ID/i);
expect(idInput).toHaveValue("alpha");
await user.click(clearButtonFor(idInput));
expect(idInput).toHaveValue("");
});
it("clears the Command field via its Clear button", async () => {
const user = userEvent.setup({ delay: null });
renderWithMantine(
<ServerConfigModal
{...base()}
mode="edit"
initialId="alpha"
initialConfig={{ type: "stdio", command: "node" }}
/>,
);
const cmdInput = screen.getByLabelText(/Command/i);
expect(cmdInput).toHaveValue("node");
await user.click(clearButtonFor(cmdInput));
expect(cmdInput).toHaveValue("");
});
it("clears the URL field via its Clear button", async () => {
const user = userEvent.setup({ delay: null });
renderWithMantine(
<ServerConfigModal
{...base()}
mode="edit"
initialId="remote"
initialConfig={{ type: "sse", url: "https://x.test/sse" }}
/>,
);
const urlInput = screen.getByLabelText(/^URL/);
expect(urlInput).toHaveValue("https://x.test/sse");
await user.click(clearButtonFor(urlInput));
expect(urlInput).toHaveValue("");
});
it("clears the Arguments field via its Clear button", async () => {
const user = userEvent.setup({ delay: null });
renderWithMantine(
<ServerConfigModal
{...base()}
mode="edit"
initialId="alpha"
initialConfig={{
type: "stdio",
command: "node",
args: ["server.js", "--port=3000"],
}}
/>,
);
const argsInput = screen.getByLabelText(/Arguments/i);
expect(argsInput).toHaveValue("server.js\n--port=3000");
await user.click(clearButtonFor(argsInput));
expect(argsInput).toHaveValue("");
});
it("clears the Environment field via its Clear button", async () => {
const user = userEvent.setup({ delay: null });
renderWithMantine(
<ServerConfigModal
{...base()}
mode="edit"
initialId="alpha"
initialConfig={{
type: "stdio",
command: "node",
env: { DEBUG: "1" },
}}
/>,
);
const envInput = screen.getByLabelText(/Environment/i);
expect(envInput).toHaveValue("DEBUG=1");
await user.click(clearButtonFor(envInput));
expect(envInput).toHaveValue("");
});
it("clears the Working directory field via its Clear button", async () => {
const user = userEvent.setup({ delay: null });
renderWithMantine(
<ServerConfigModal
{...base()}
mode="edit"
initialId="alpha"
initialConfig={{
type: "stdio",
command: "node",
cwd: "/tmp/here",
}}
/>,
);
const cwdInput = screen.getByLabelText(/Working directory/i);
expect(cwdInput).toHaveValue("/tmp/here");
await user.click(clearButtonFor(cwdInput));
expect(cwdInput).toHaveValue("");
});
it("switches transport via the Transport select, revealing the URL field", async () => {
const user = userEvent.setup({ delay: null });
renderWithMantine(<ServerConfigModal {...base()} />);
// Starts on stdio — Command is shown, URL is not.
expect(screen.getByLabelText(/^Command/)).toBeInTheDocument();
await user.click(screen.getByRole("textbox", { name: /Transport/i }));
const option = await screen.findByRole("option", {
name: /sse \(Server-Sent Events\)/,
hidden: true,
});
await user.click(option);
// The onChange handler set transport to sse, so the URL field renders
// and the stdio Command field is gone.
expect(await screen.findByLabelText(/^URL/)).toBeInTheDocument();
expect(screen.queryByLabelText(/^Command/)).not.toBeInTheDocument();
});
it("requires a server id before submitting", async () => {
const user = userEvent.setup({ delay: null });
const props = base();
renderWithMantine(<ServerConfigModal {...props} />);
// No id typed at all — handleSubmit short-circuits on the empty id
// before it ever validates the transport config.
await user.type(screen.getByLabelText(/Command/i), "node");
await user.click(screen.getByRole("button", { name: /^Add$/ }));
expect(screen.getByRole("alert")).toHaveTextContent(
/Server id is required/i,
);
expect(props.onSubmit).not.toHaveBeenCalled();
});
it("rejects an env line whose '=' is at the start (no key)", async () => {
const user = userEvent.setup({ delay: null });
const props = base();
renderWithMantine(<ServerConfigModal {...props} />);
await user.type(screen.getByLabelText(/Server ID/i), "alpha");
await user.type(screen.getByLabelText(/Command/i), "node");
// "=value" has the "=" at index 0 (eq === 0), the falsy-index guard.
await user.type(screen.getByLabelText(/Environment/i), "=value");
await user.click(screen.getByRole("button", { name: /^Add$/ }));
expect(screen.getByRole("alert")).toHaveTextContent(/Use KEY=VALUE/i);
expect(props.onSubmit).not.toHaveBeenCalled();
});
it("calls onClose when Cancel is clicked", async () => {
const user = userEvent.setup({ delay: null });
const props = base();
renderWithMantine(<ServerConfigModal {...props} />);
await user.click(screen.getByRole("button", { name: /Cancel/ }));
expect(props.onClose).toHaveBeenCalledOnce();
});
});