Files
fusion/packages/droid-cli/src/__tests__/mcp-config.test.ts
Fusion 0ec5dfa3b1 feat(FN-2978): add droid-cli package with provider registration and model d
This merge introduces a new `packages/droid-cli` package that bridges AI agent execution as a subprocess, including process management, event streaming, tool mapping, MCP configuration, and thinking-mode support. It also adds the `DroidCliProviderCard` dashboard UI component with onboarding flows an

Fusion-Task-Id: FN-2978
2026-05-01 03:47:10 -07:00

311 lines
8.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
// Hoist mock references so they survive vi.mock hoisting
const mocks = vi.hoisted(() => ({
writeFileSync: vi.fn(),
tmpdir: vi.fn(() => "/tmp"),
}));
// Mock node:fs writeFileSync to avoid disk I/O
vi.mock("node:fs", () => ({
writeFileSync: mocks.writeFileSync,
}));
// Mock node:os tmpdir
vi.mock("node:os", () => ({
tmpdir: mocks.tmpdir,
}));
import { getCustomToolDefs, writeMcpConfig, toolsFromContext } from "../mcp-config";
import type { McpToolDef } from "../mcp-config";
describe("getCustomToolDefs", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("filters out all 6 built-in tools and returns only custom tools", () => {
const mockPi = {
getAllTools: vi.fn(() => [
{
name: "read",
description: "Read file",
parameters: { type: "object" },
},
{
name: "write",
description: "Write file",
parameters: { type: "object" },
},
{
name: "edit",
description: "Edit file",
parameters: { type: "object" },
},
{
name: "bash",
description: "Run bash",
parameters: { type: "object" },
},
{ name: "grep", description: "Search", parameters: { type: "object" } },
{
name: "find",
description: "Find files",
parameters: { type: "object" },
},
{
name: "search",
description: "Custom search tool",
parameters: {
type: "object",
properties: { query: { type: "string" } },
},
},
{
name: "deploy",
description: "Deploy app",
parameters: {
type: "object",
properties: { target: { type: "string" } },
},
},
]),
};
const result = getCustomToolDefs(mockPi);
expect(result).toHaveLength(2);
expect(result[0].name).toBe("search");
expect(result[1].name).toBe("deploy");
});
it("returns empty array when all tools are built-in", () => {
const mockPi = {
getAllTools: vi.fn(() => [
{
name: "read",
description: "Read file",
parameters: { type: "object" },
},
{
name: "write",
description: "Write file",
parameters: { type: "object" },
},
{
name: "edit",
description: "Edit file",
parameters: { type: "object" },
},
{
name: "bash",
description: "Run bash",
parameters: { type: "object" },
},
{ name: "grep", description: "Search", parameters: { type: "object" } },
{
name: "find",
description: "Find files",
parameters: { type: "object" },
},
]),
};
const result = getCustomToolDefs(mockPi);
expect(result).toEqual([]);
});
it("includes custom tool with correct name, description, inputSchema from parameters", () => {
const customParams = {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
limit: { type: "number" },
},
required: ["query"],
};
const mockPi = {
getAllTools: vi.fn(() => [
{
name: "custom_search",
description: "Search the codebase",
parameters: customParams,
},
]),
};
const result = getCustomToolDefs(mockPi);
expect(result).toHaveLength(1);
expect(result[0].name).toBe("custom_search");
expect(result[0].description).toBe("Search the codebase");
expect(result[0].inputSchema).toBe(customParams);
});
it("handles pi.getAllTools() returning empty array", () => {
const mockPi = {
getAllTools: vi.fn(() => []),
};
const result = getCustomToolDefs(mockPi);
expect(result).toEqual([]);
});
it("returns empty array when pi.getAllTools() returns undefined", () => {
const mockPi = {
getAllTools: vi.fn(() => undefined),
};
const result = getCustomToolDefs(mockPi);
expect(result).toEqual([]);
});
it("returns empty array when pi.getAllTools() returns null", () => {
const mockPi = {
getAllTools: vi.fn(() => null),
};
const result = getCustomToolDefs(mockPi);
expect(result).toEqual([]);
});
});
describe("toolsFromContext", () => {
it("keeps ls as a custom tool (not filtered as built-in)", () => {
const defs = toolsFromContext([
{ name: "read", description: "builtin", parameters: { type: "object" } },
{ name: "ls", description: "list files", parameters: { type: "object" } },
{
name: "fn_task_list",
description: "list tasks",
parameters: { type: "object", properties: {} },
},
]);
expect(defs.map((d) => d.name)).toEqual(["ls", "fn_task_list"]);
});
});
describe("writeMcpConfig", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.tmpdir.mockReturnValue("/tmp");
});
it("writes schema file to tmpdir with correct content (JSON array of tool defs)", () => {
const toolDefs: McpToolDef[] = [
{
name: "search",
description: "Search",
inputSchema: { type: "object" },
},
];
writeMcpConfig(toolDefs);
// First writeFileSync call is the schema file
const schemaCall = mocks.writeFileSync.mock.calls[0];
expect(schemaCall[0]).toMatch(/droid-cli-mcp-schemas/);
expect(JSON.parse(schemaCall[1])).toEqual(toolDefs);
});
it("writes config file to tmpdir with mcpServers.custom-tools entry", () => {
const toolDefs: McpToolDef[] = [
{
name: "search",
description: "Search",
inputSchema: { type: "object" },
},
];
writeMcpConfig(toolDefs);
// Second writeFileSync call is the config file
const configCall = mocks.writeFileSync.mock.calls[1];
const config = JSON.parse(configCall[1]);
expect(config).toHaveProperty("mcpServers");
expect(config.mcpServers).toHaveProperty("custom-tools");
});
it("config uses 'command': 'node' format (not 'type': 'http')", () => {
const toolDefs: McpToolDef[] = [
{
name: "search",
description: "Search",
inputSchema: { type: "object" },
},
];
writeMcpConfig(toolDefs);
const configCall = mocks.writeFileSync.mock.calls[1];
const config = JSON.parse(configCall[1]);
const server = config.mcpServers["custom-tools"];
expect(server.command).toBe("node");
expect(server).not.toHaveProperty("type");
});
it("config args include path to mcp-schema-server.cjs and schema file path", () => {
const toolDefs: McpToolDef[] = [
{
name: "search",
description: "Search",
inputSchema: { type: "object" },
},
];
writeMcpConfig(toolDefs);
const configCall = mocks.writeFileSync.mock.calls[1];
const config = JSON.parse(configCall[1]);
const server = config.mcpServers["custom-tools"];
expect(server.args).toHaveLength(2);
// First arg should be the server .cjs path (normalize separators for Windows)
expect(server.args[0].replace(/\\/g, "/")).toContain(
"mcp-schema-server.cjs",
);
// Second arg should be the schema file path
expect(server.args[1]).toMatch(/droid-cli-mcp-schemas/);
});
it("returns the config file path", () => {
const toolDefs: McpToolDef[] = [
{
name: "search",
description: "Search",
inputSchema: { type: "object" },
},
];
const result = writeMcpConfig(toolDefs);
expect(result).toMatch(/droid-cli-mcp-config/);
expect(result).toMatch(/\.json$/);
});
it("includes cacheKey in filenames when provided so distinct tool sets do not collide", () => {
const toolDefs: McpToolDef[] = [
{
name: "search",
description: "Search",
inputSchema: { type: "object" },
},
];
const pathA = writeMcpConfig(toolDefs, "aaaaaaaaaaaa");
const pathB = writeMcpConfig(toolDefs, "bbbbbbbbbbbb");
expect(pathA).toContain("aaaaaaaaaaaa");
expect(pathB).toContain("bbbbbbbbbbbb");
expect(pathA).not.toBe(pathB);
const schemaPathA = mocks.writeFileSync.mock.calls[0][0];
const schemaPathB = mocks.writeFileSync.mock.calls[2][0];
expect(schemaPathA).toContain("aaaaaaaaaaaa");
expect(schemaPathB).toContain("bbbbbbbbbbbb");
});
});