logancyang_obsidian-copilot/src/agentMode/session/mcpResolver.test.ts
Zero Liu c2ac27edef
feat(agent-mode): introduce Agent Mode feature
Squashed from 8 commits on zero/acp-test:
- feat(agent-mode): introduce Agent Mode feature (squashed)
- Fix eslint
- hide add context button
- chore: ban parent-relative imports and rewrite existing ones to @/ aliases
- wip(agent-mode): skills management + slash command revamp
- fix(agent-mode): initialize SkillManager before preload probes
- fix(build): align svgr jsxRuntime with tsconfig classic jsx
- fix(test): stub ItemView/WorkspaceLeaf in ChatSingleMessage obsidian mock
2026-05-20 19:05:41 -07:00

174 lines
5.5 KiB
TypeScript

import { logWarn } from "@/logger";
import {
resolveMcpServers,
sanitizeStoredMcpServers,
toMcpServerSpec,
type StoredMcpServer,
} from "./mcpResolver";
jest.mock("@/logger", () => ({
logInfo: jest.fn(),
logWarn: jest.fn(),
logError: jest.fn(),
}));
function fakeProc(supports: { http?: boolean; sse?: boolean }) {
return {
supportsMcpTransport: (transport: "http" | "sse") => {
if (transport === "http") return supports.http === true;
if (transport === "sse") return supports.sse === true;
return false;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
}
describe("toMcpServerSpec", () => {
it("maps a stdio entry without a `type` field (per ACP schema)", () => {
const stored: StoredMcpServer = {
id: "1",
enabled: true,
name: "fs",
transport: "stdio",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem"],
env: [{ name: "FOO", value: "bar" }],
};
expect(toMcpServerSpec(stored)).toEqual({
name: "fs",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem"],
env: [{ name: "FOO", value: "bar" }],
});
});
it("maps an http entry with the `type: http` discriminant", () => {
const stored: StoredMcpServer = {
id: "2",
enabled: true,
name: "remote",
transport: "http",
url: "https://example.com/mcp",
headers: [{ name: "Authorization", value: "Bearer x" }],
};
expect(toMcpServerSpec(stored)).toEqual({
type: "http",
name: "remote",
url: "https://example.com/mcp",
headers: [{ name: "Authorization", value: "Bearer x" }],
});
});
it("returns null for stdio missing a command", () => {
expect(toMcpServerSpec({ id: "x", enabled: true, name: "n", transport: "stdio" })).toBeNull();
});
it("returns null for http missing a url", () => {
expect(toMcpServerSpec({ id: "x", enabled: true, name: "n", transport: "http" })).toBeNull();
});
it("returns null for empty name", () => {
expect(
toMcpServerSpec({ id: "x", enabled: true, name: " ", transport: "stdio", command: "ls" })
).toBeNull();
});
});
describe("sanitizeStoredMcpServers", () => {
it("returns [] for non-array input (legacy unknown[] placeholder)", () => {
expect(sanitizeStoredMcpServers(undefined)).toEqual([]);
expect(sanitizeStoredMcpServers(null)).toEqual([]);
expect(sanitizeStoredMcpServers({})).toEqual([]);
});
it("drops items with an unknown transport", () => {
expect(sanitizeStoredMcpServers([{ name: "x", transport: "websocket" }])).toEqual([]);
});
it("backfills a fresh id when missing", () => {
const out = sanitizeStoredMcpServers([{ name: "x", transport: "stdio", command: "ls" }]);
expect(out).toHaveLength(1);
expect(out[0].id).toMatch(/.+/);
expect(out[0].enabled).toBe(true);
});
it("treats enabled=false as disabled, anything else as enabled", () => {
const out = sanitizeStoredMcpServers([
{ id: "a", name: "x", transport: "stdio", enabled: false, command: "ls" },
{ id: "b", name: "y", transport: "stdio", command: "ls" },
]);
expect(out.map((s) => [s.id, s.enabled])).toEqual([
["a", false],
["b", true],
]);
});
});
describe("resolveMcpServers", () => {
beforeEach(() => jest.clearAllMocks());
it("returns [] when the raw value is missing", () => {
expect(resolveMcpServers(fakeProc({}), undefined)).toEqual([]);
expect(resolveMcpServers(fakeProc({}), null)).toEqual([]);
});
it("skips disabled entries silently", () => {
const out = resolveMcpServers(fakeProc({}), [
{ id: "a", name: "x", transport: "stdio", enabled: false, command: "ls" },
]);
expect(out).toEqual([]);
expect(logWarn).not.toHaveBeenCalled();
});
it("filters http servers when the agent does not advertise mcp/http", () => {
const out = resolveMcpServers(fakeProc({ http: false }), [
{
id: "a",
enabled: true,
name: "remote",
transport: "http",
url: "https://example.com",
headers: [],
},
]);
expect(out).toEqual([]);
expect(logWarn).toHaveBeenCalledTimes(1);
});
it("passes http servers when the agent advertises mcp/http", () => {
const out = resolveMcpServers(fakeProc({ http: true }), [
{
id: "a",
enabled: true,
name: "remote",
transport: "http",
url: "https://example.com",
headers: [],
},
]);
expect(out).toHaveLength(1);
expect(out[0]).toMatchObject({ type: "http", url: "https://example.com" });
});
it("drops malformed entries (no command) and warns about the missing field", () => {
const out = resolveMcpServers(fakeProc({}), [
{ id: "a", enabled: true, name: "x", transport: "stdio" },
]);
expect(out).toEqual([]);
expect(logWarn).toHaveBeenCalledWith(
expect.stringContaining('skipping MCP server "x": missing required field "command"')
);
});
it("warns about a missing field even when the agent lacks the matching capability", () => {
// Validation must run before the capability gate so the user gets a
// precise diagnostic instead of a misleading "transport not supported".
const out = resolveMcpServers(fakeProc({ http: false }), [
{ id: "a", enabled: true, name: "broken", transport: "http" },
]);
expect(out).toEqual([]);
expect(logWarn).toHaveBeenCalledWith(
expect.stringContaining('skipping MCP server "broken": missing required field "url"')
);
});
});