Files
cc-switch/tests/hooks/useMcpValidation.test.tsx
Jason bfc27349b3 feat(mcp): add SSE (Server-Sent Events) transport type support
Add comprehensive support for SSE transport type to MCP server configuration,
enabling real-time streaming connections alongside existing stdio and http types.

Backend Changes:
- Add SSE type validation in mcp.rs validate_server_spec()
- Extend Codex TOML import/export to handle SSE servers
- Update claude_mcp.rs legacy API for backward compatibility
- Unify http/sse handling in json_server_to_toml_table()

Frontend Changes:
- Extend McpServerSpec type definition to include "sse"
- Add SSE radio button to configuration wizard UI
- Update wizard form logic to handle SSE url and headers
- Add SSE validation in McpFormModal submission

Validation & Error Handling:
- Add SSE support in useMcpValidation hook (TOML/JSON)
- Extend tomlUtils normalizeServerConfig for SSE parsing
- Update Zod schemas (common.ts, mcp.ts) with SSE enum
- Add SSE error message mapping in errorUtils

Internationalization:
- Add "typeSse" translations (zh: "sse", en: "sse")

Tests:
- Add SSE validation test cases in useMcpValidation.test.tsx

SSE Configuration Format:
{
  "type": "sse",
  "url": "https://api.example.com/sse",
  "headers": { "Authorization": "Bearer token" }
}
2025-11-16 16:15:17 +08:00

159 lines
5.3 KiB
TypeScript

import { renderHook } from "@testing-library/react";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { useMcpValidation } from "@/components/mcp/useMcpValidation";
const validateTomlMock = vi.hoisted(() => vi.fn());
const tomlToMcpServerMock = vi.hoisted(() => vi.fn());
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock("@/utils/tomlUtils", () => ({
validateToml: (...args: unknown[]) => validateTomlMock(...args),
tomlToMcpServer: (...args: unknown[]) => tomlToMcpServerMock(...args),
}));
describe("useMcpValidation", () => {
beforeEach(() => {
validateTomlMock.mockReset();
tomlToMcpServerMock.mockReset();
validateTomlMock.mockReturnValue("");
});
const getHookResult = () => renderHook(() => useMcpValidation()).result.current;
describe("validateJson", () => {
it("returns empty string for blank text", () => {
const { validateJson } = getHookResult();
expect(validateJson(" ")).toBe("");
});
it("returns error key when JSON parsing fails", () => {
const { validateJson } = getHookResult();
expect(validateJson("{ invalid")).toBe("mcp.error.jsonInvalid");
});
it("returns error key when parsed value is not an object", () => {
const { validateJson } = getHookResult();
expect(validateJson('"string"')).toBe("mcp.error.jsonInvalid");
expect(validateJson("[]")).toBe("mcp.error.jsonInvalid");
});
it("accepts valid object payload", () => {
const { validateJson } = getHookResult();
expect(validateJson('{"id":"demo"}')).toBe("");
});
});
describe("formatTomlError", () => {
it("maps mustBeObject and parseError to i18n key", () => {
const { formatTomlError } = getHookResult();
expect(formatTomlError("mustBeObject")).toBe("mcp.error.tomlInvalid");
expect(formatTomlError("parseError")).toBe("mcp.error.tomlInvalid");
});
it("appends error message when details provided", () => {
const { formatTomlError } = getHookResult();
expect(formatTomlError("unknown")).toBe("mcp.error.tomlInvalid: unknown");
});
});
describe("validateTomlConfig", () => {
it("propagates errors returned by validateToml", () => {
validateTomlMock.mockReturnValue("parse-error-detail");
const { validateTomlConfig } = getHookResult();
expect(validateTomlConfig("foo")).toBe("mcp.error.tomlInvalid: parse-error-detail");
expect(tomlToMcpServerMock).not.toHaveBeenCalled();
});
it("returns command required when stdio server missing command", () => {
tomlToMcpServerMock.mockReturnValue({
type: "stdio",
command: " ",
});
const { validateTomlConfig } = getHookResult();
expect(validateTomlConfig("foo")).toBe("mcp.error.commandRequired");
});
it("returns url required when http server missing url", () => {
tomlToMcpServerMock.mockReturnValue({
type: "http",
url: "",
});
const { validateTomlConfig } = getHookResult();
expect(validateTomlConfig("foo")).toBe("mcp.wizard.urlRequired");
});
it("returns url required when sse server missing url", () => {
tomlToMcpServerMock.mockReturnValue({
type: "sse",
url: "",
});
const { validateTomlConfig } = getHookResult();
expect(validateTomlConfig("foo")).toBe("mcp.wizard.urlRequired");
});
it("surface tomlToMcpServer errors via formatter", () => {
tomlToMcpServerMock.mockImplementation(() => {
throw new Error("normalize failed");
});
const { validateTomlConfig } = getHookResult();
expect(validateTomlConfig("foo")).toBe("mcp.error.tomlInvalid: normalize failed");
});
it("returns empty string when validation passes", () => {
tomlToMcpServerMock.mockReturnValue({
type: "stdio",
command: "run.sh",
});
const { validateTomlConfig } = getHookResult();
expect(validateTomlConfig("foo")).toBe("");
});
});
describe("validateJsonConfig", () => {
it("returns error when JSON invalid", () => {
const { validateJsonConfig } = getHookResult();
expect(validateJsonConfig("invalid")).toBe("mcp.error.jsonInvalid");
});
it("rejects arrays of servers", () => {
const { validateJsonConfig } = getHookResult();
expect(validateJsonConfig('{"mcpServers": {}}')).toBe(
"mcp.error.singleServerObjectRequired",
);
});
it("requires command for stdio type", () => {
const { validateJsonConfig } = getHookResult();
expect(validateJsonConfig('{"type":"stdio"}')).toBe("mcp.error.commandRequired");
});
it("requires url for http type", () => {
const { validateJsonConfig } = getHookResult();
expect(validateJsonConfig('{"type":"http","url":""}')).toBe("mcp.wizard.urlRequired");
});
it("requires url for sse type", () => {
const { validateJsonConfig } = getHookResult();
expect(validateJsonConfig('{"type":"sse","url":""}')).toBe("mcp.wizard.urlRequired");
});
it("returns empty string when json config valid", () => {
const { validateJsonConfig } = getHookResult();
expect(
validateJsonConfig(
JSON.stringify({
type: "stdio",
command: "node",
args: ["index.js"],
}),
),
).toBe("");
});
});
});