mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-13 10:55:59 +00:00
GET /api/mcp/config returns 403 for non-admin users, but the previous
client returned the error body as MCPConfig, causing MCPServerList to
crash with 'Cannot convert undefined or null to object' on
Object.entries(config.mcp_servers).
- api.ts: introduce MCPConfigRequestError; loadMCPConfig and
updateMCPConfig now throw it (carrying status + isAdminRequired)
instead of letting non-2xx bodies leak through as parsed config
- tool-settings-page.tsx: render a friendly 'admin privileges required'
empty state when the React Query error is an admin-required
MCPConfigRequestError; keep MCPServerList resilient with
Object.entries(servers ?? {}) and an empty-state for no servers
- i18n: add settings.tools.adminRequired and settings.tools.empty in
en-US, zh-CN and the Translations type
- tests: cover 403 / 5xx / instanceof / detail-fallback for both
loadMCPConfig and updateMCPConfig in tests/unit/core/mcp/api.test.ts
Refs: #3527
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/core/api/fetcher", () => ({
|
||||
fetch: vi.fn(),
|
||||
}));
|
||||
|
||||
import { fetch } from "@/core/api/fetcher";
|
||||
import { MCPConfigRequestError, loadMCPConfig } from "@/core/mcp/api";
|
||||
|
||||
const mockedFetch = vi.mocked(fetch);
|
||||
|
||||
function makeClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retryDelay: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("useMCPConfig retry policy", () => {
|
||||
beforeEach(() => {
|
||||
mockedFetch.mockReset();
|
||||
});
|
||||
|
||||
it("does not retry when loadMCPConfig throws MCPConfigRequestError (403)", async () => {
|
||||
mockedFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
json: async () => ({ detail: "Forbidden" }),
|
||||
} as Response);
|
||||
|
||||
const client = makeClient();
|
||||
await expect(
|
||||
client.fetchQuery({
|
||||
queryKey: ["mcpConfig"],
|
||||
queryFn: () => loadMCPConfig(),
|
||||
retry: (count, error) =>
|
||||
!(error instanceof MCPConfigRequestError) && count < 3,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(MCPConfigRequestError);
|
||||
|
||||
expect(mockedFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("retries up to 3 times on generic errors", async () => {
|
||||
mockedFetch.mockRejectedValue(new Error("network down"));
|
||||
|
||||
const client = makeClient();
|
||||
await expect(
|
||||
client.fetchQuery({
|
||||
queryKey: ["mcpConfig"],
|
||||
queryFn: () => loadMCPConfig(),
|
||||
retry: (count, error) =>
|
||||
!(error instanceof MCPConfigRequestError) && count < 3,
|
||||
}),
|
||||
).rejects.toThrow("network down");
|
||||
|
||||
// initial + 3 retries = 4 calls
|
||||
expect(mockedFetch).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("does not retry on MCPConfigRequestError 5xx either (deterministic typed error)", async () => {
|
||||
mockedFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: async () => ({ detail: "Boom" }),
|
||||
} as Response);
|
||||
|
||||
const client = makeClient();
|
||||
await expect(
|
||||
client.fetchQuery({
|
||||
queryKey: ["mcpConfig"],
|
||||
queryFn: () => loadMCPConfig(),
|
||||
retry: (count, error) =>
|
||||
!(error instanceof MCPConfigRequestError) && count < 3,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(MCPConfigRequestError);
|
||||
|
||||
expect(mockedFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user