import { renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import SettingsService from "#/api/settings-service/settings-service.api";
import { useAddMcpServer } from "#/hooks/mutation/use-add-mcp-server";
import { useDeleteMcpServer } from "#/hooks/mutation/use-delete-mcp-server";
import { useUpdateMcpServer } from "#/hooks/mutation/use-update-mcp-server";
import { useSelectedOrganizationStore } from "#/stores/selected-organization-store";
describe("MCP Server Mutation Hooks", () => {
beforeEach(() => {
vi.restoreAllMocks();
useSelectedOrganizationStore.setState({ organizationId: "test-org-id" });
});
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
{children}
);
};
describe("useAddMcpServer", () => {
it("fetches fresh settings at mutation time", async () => {
const getSettingsSpy = vi
.spyOn(SettingsService, "getSettings")
.mockResolvedValue({
agent_settings: {
mcp_config: {
mcpServers: {
existing: { url: "https://existing.com", transport: "sse" },
},
},
},
} as any);
const saveSettingsSpy = vi
.spyOn(SettingsService, "saveSettings")
.mockResolvedValue(true);
const { result } = renderHook(() => useAddMcpServer(), {
wrapper: createWrapper(),
});
result.current.mutate({
type: "sse",
url: "https://new-server.com",
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(getSettingsSpy).toHaveBeenCalledTimes(1);
expect(saveSettingsSpy).toHaveBeenCalledWith({
agent_settings_diff: {
mcp_config: {
mcpServers: expect.objectContaining({
existing: expect.objectContaining({ url: "https://existing.com" }),
}),
},
},
});
});
it("handles adding server when no existing config", async () => {
vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
agent_settings: {},
} as any);
const saveSettingsSpy = vi
.spyOn(SettingsService, "saveSettings")
.mockResolvedValue(true);
const { result } = renderHook(() => useAddMcpServer(), {
wrapper: createWrapper(),
});
result.current.mutate({
type: "sse",
url: "https://first-server.com",
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(saveSettingsSpy).toHaveBeenCalledWith({
agent_settings_diff: {
mcp_config: {
mcpServers: {
sse: {
url: "https://first-server.com",
transport: "sse",
},
},
},
},
});
});
it("proceeds with empty config when getSettings returns null", async () => {
vi.spyOn(SettingsService, "getSettings").mockResolvedValue(null as any);
const saveSettingsSpy = vi
.spyOn(SettingsService, "saveSettings")
.mockResolvedValue(true);
const { result } = renderHook(() => useAddMcpServer(), {
wrapper: createWrapper(),
});
result.current.mutate({
type: "sse",
url: "https://server.com",
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
// Implementation handles null gracefully and proceeds
expect(saveSettingsSpy).toHaveBeenCalledWith({
agent_settings_diff: {
mcp_config: {
mcpServers: {
sse: {
url: "https://server.com",
transport: "sse",
},
},
},
},
});
});
});
describe("useDeleteMcpServer", () => {
it("deletes the correct server by index", async () => {
vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
agent_settings: {
mcp_config: {
mcpServers: {
server1: { url: "https://server1.com", transport: "sse" },
server2: { url: "https://server2.com", transport: "sse" },
server3: { url: "https://server3.com", transport: "sse" },
},
},
},
} as any);
const saveSettingsSpy = vi
.spyOn(SettingsService, "saveSettings")
.mockResolvedValue(true);
const { result } = renderHook(() => useDeleteMcpServer(), {
wrapper: createWrapper(),
});
// Use hyphen separator as per implementation: serverId.split("-")
result.current.mutate("sse-1");
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
const savedPayload = saveSettingsSpy.mock.calls[0][0] as {
agent_settings_diff: {
mcp_config: { mcpServers: Record } | null;
};
};
const savedConfig = savedPayload.agent_settings_diff.mcp_config;
const serverNames = Object.keys(savedConfig?.mcpServers ?? {});
expect(serverNames).toHaveLength(2);
});
it("handles deleting from empty config", async () => {
vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
agent_settings: {
mcp_config: null,
},
} as any);
const saveSettingsSpy = vi
.spyOn(SettingsService, "saveSettings")
.mockResolvedValue(true);
const { result } = renderHook(() => useDeleteMcpServer(), {
wrapper: createWrapper(),
});
result.current.mutate("sse-0");
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(saveSettingsSpy).toHaveBeenCalled();
});
});
describe("useUpdateMcpServer", () => {
it("updates the correct server URL", async () => {
vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
agent_settings: {
mcp_config: {
mcpServers: {
myserver: { url: "https://old-url.com", transport: "sse" },
},
},
},
} as any);
const saveSettingsSpy = vi
.spyOn(SettingsService, "saveSettings")
.mockResolvedValue(true);
const { result } = renderHook(() => useUpdateMcpServer(), {
wrapper: createWrapper(),
});
// Use hyphen separator as per implementation
result.current.mutate({
serverId: "sse-0",
server: {
type: "sse",
url: "https://new-url.com",
},
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
const savedPayload = saveSettingsSpy.mock.calls[0][0] as {
agent_settings_diff: {
mcp_config: { mcpServers: Record };
};
};
const savedConfig = savedPayload.agent_settings_diff.mcp_config;
const serverUrls = Object.values(savedConfig.mcpServers).map(
(s) => s.url,
);
expect(serverUrls).toContain("https://new-url.com");
});
});
describe("error handling", () => {
it("handles getSettings failure", async () => {
vi.spyOn(SettingsService, "getSettings").mockRejectedValue(
new Error("Network error"),
);
const { result } = renderHook(() => useAddMcpServer(), {
wrapper: createWrapper(),
});
result.current.mutate({ type: "sse", url: "https://server.com" });
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBeDefined();
});
it("handles saveSettings failure", async () => {
vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
agent_settings: { mcp_config: null },
} as any);
vi.spyOn(SettingsService, "saveSettings").mockRejectedValue(
new Error("Save failed"),
);
const { result } = renderHook(() => useAddMcpServer(), {
wrapper: createWrapper(),
});
result.current.mutate({ type: "sse", url: "https://server.com" });
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBeDefined();
});
});
});