mcpb/test/config.test.ts
Bryan Thompson cb052a20e5
fix: merge platform_overrides env with base env instead of replacing
platform_overrides.*.env was replacing the entire mcp_config.env object
instead of merging, causing base env vars (including ${user_config.*}
references) to be silently dropped when platform overrides defined any
env vars.

Fixes #217

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 08:23:18 -05:00

552 lines
14 KiB
TypeScript

import {
getMcpConfigForManifest,
hasRequiredConfigMissing,
replaceVariables,
} from "../src/shared/config";
import type { Logger, McpbManifestAny } from "../src/types";
describe("replaceVariables", () => {
it("should replace variables in strings", () => {
const result = replaceVariables("Hello ${name}!", { name: "World" });
expect(result).toBe("Hello World!");
});
it("should replace multiple variables in strings", () => {
const result = replaceVariables("${greeting} ${name}!", {
greeting: "Hello",
name: "World",
});
expect(result).toBe("Hello World!");
});
it("should warn and not replace arrays in string context", () => {
const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
const result = replaceVariables("Hello ${names}!", {
names: ["Alice", "Bob"],
});
expect(result).toBe("Hello ${names}!");
expect(consoleWarnSpy).toHaveBeenCalled();
consoleWarnSpy.mockRestore();
});
it("should replace variables in objects", () => {
const result = replaceVariables(
{ message: "Hello ${name}!" },
{ name: "World" },
);
expect(result).toEqual({ message: "Hello World!" });
});
it("should handle user config variables in arrays with string expansion", () => {
const result = replaceVariables(["${user_config.path}"], {
"user_config.path": "/some/path",
});
expect(result).toEqual(["/some/path"]);
});
it("should handle user config variables in arrays with array expansion", () => {
const result = replaceVariables(["${user_config.paths}"], {
"user_config.paths": ["/path1", "/path2"],
});
expect(result).toEqual(["/path1", "/path2"]);
});
it("should recursively process arrays", () => {
const result = replaceVariables(["Hello ${name}!", "Goodbye ${name}!"], {
name: "World",
});
expect(result).toEqual(["Hello World!", "Goodbye World!"]);
});
it("should handle mixed arrays with variable expansion", () => {
const result = replaceVariables(["start", "${user_config.items}", "end"], {
"user_config.items": ["item1", "item2"],
});
expect(result).toEqual(["start", "item1", "item2", "end"]);
});
it("should return primitive values unchanged", () => {
expect(replaceVariables(42, {})).toBe(42);
expect(replaceVariables(true, {})).toBe(true);
expect(replaceVariables(null, {})).toBe(null);
});
it("should handle undefined variables by keeping them as-is", () => {
const result = replaceVariables("Hello ${unknown}!", {});
expect(result).toBe("Hello ${unknown}!");
});
});
describe("getMcpConfigForManifest", () => {
const mockLogger: Logger = {
log: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
};
const mockSystemDirs = {
home: "/home/user",
data: "/data",
};
const baseManifest: McpbManifestAny = {
manifest_version: "0.2",
name: "test-extension",
version: "1.0.0",
description: "Test extension",
author: { name: "Test Author" },
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js", "${__dirname}"],
},
},
compatibility: { claude_desktop: "0.1.0" },
};
beforeEach(() => {
jest.clearAllMocks();
});
it("should return undefined when manifest has no mcp_config", async () => {
const manifest = {
...baseManifest,
server: undefined,
} as unknown as McpbManifestAny;
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: {},
pathSeparator: "/",
logger: mockLogger,
});
expect(result).toBeUndefined();
});
it("should return undefined when required config is missing", async () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
apiKey: {
type: "string",
title: "API Key",
description: "API Key",
required: true,
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: {},
pathSeparator: "/",
logger: mockLogger,
});
expect(result).toBeUndefined();
expect(mockLogger.warn).toHaveBeenCalled();
});
it("should replace variables in basic config", async () => {
const result = await getMcpConfigForManifest({
manifest: baseManifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: {},
pathSeparator: "/",
logger: mockLogger,
});
expect(result).toEqual({
command: "node",
args: ["server.js", "/ext/path"],
});
});
it("should apply platform overrides", async () => {
const manifest: McpbManifestAny = {
...baseManifest,
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "default-command",
args: ["default"],
platform_overrides: {
[process.platform]: {
command: "platform-command",
args: ["platform-specific"],
},
},
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: {},
pathSeparator: "/",
logger: mockLogger,
});
expect(result?.command).toBe("platform-command");
expect(result?.args).toEqual(["platform-specific"]);
});
it("should handle user config variable replacement with defaults", async () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
port: {
type: "number",
title: "Port Number",
description: "Port number",
default: 8080,
},
},
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js", "--port=${user_config.port}"],
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: {},
pathSeparator: "/",
logger: mockLogger,
});
expect(result?.args).toEqual(["server.js", "--port=8080"]);
});
it("should handle user config variable replacement with user values", async () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
paths: {
type: "string",
title: "File Paths",
description: "File paths",
default: ["/default"],
multiple: true,
},
},
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js", "${user_config.paths}"],
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: { paths: ["/user/path1", "/user/path2"] },
pathSeparator: "/",
logger: mockLogger,
});
expect(result?.args).toEqual(["server.js", "/user/path1", "/user/path2"]);
});
it("should merge platform override env with base env instead of replacing", async () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
data_directory: {
type: "directory",
title: "Data Directory",
description: "Where data lives",
required: true,
},
},
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js"],
env: {
DATA_DIR: "${user_config.data_directory}",
EXISTING_VAR: "keep-me",
},
platform_overrides: {
[process.platform]: {
env: {
PATH: "/usr/local/bin:${HOME}/.local/bin",
},
},
},
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: { ...mockSystemDirs, HOME: "/home/user" },
userConfig: { data_directory: "/custom/data" },
pathSeparator: "/",
logger: mockLogger,
});
// Base env vars should be preserved after platform override merge
expect(result?.env?.DATA_DIR).toBe("/custom/data");
expect(result?.env?.EXISTING_VAR).toBe("keep-me");
// Platform override env should also be present
expect(result?.env?.PATH).toBe("/usr/local/bin:/home/user/.local/bin");
});
it("should convert boolean user config values to strings", async () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
verbose: {
type: "boolean",
title: "Verbose Mode",
description: "Verbose output",
default: false,
},
},
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js", "--verbose=${user_config.verbose}"],
},
},
};
const result = await getMcpConfigForManifest({
manifest,
extensionPath: "/ext/path",
systemDirs: mockSystemDirs,
userConfig: { verbose: true },
pathSeparator: "/",
logger: mockLogger,
});
expect(result?.args).toEqual(["server.js", "--verbose=true"]);
});
});
describe("hasRequiredConfigMissing", () => {
const baseManifest: McpbManifestAny = {
manifest_version: "0.2",
name: "test-extension",
version: "1.0.0",
description: "Test extension",
author: { name: "Test Author" },
server: {
type: "node",
entry_point: "server.js",
mcp_config: {
command: "node",
args: ["server.js"],
},
},
};
it("should return false when manifest has no user_config", () => {
const result = hasRequiredConfigMissing({
manifest: baseManifest,
userConfig: {},
});
expect(result).toBe(false);
});
it("should return false when no config fields are required", () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
port: {
type: "number",
title: "Port",
description: "Port number",
required: false,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: {},
});
expect(result).toBe(false);
});
it("should return false when required config is provided", () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
apiKey: {
type: "string",
title: "API Key",
description: "API Key",
required: true,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: { apiKey: "test-key" },
});
expect(result).toBe(false);
});
it("should return true when required config is undefined", () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
apiKey: {
type: "string",
title: "API Key",
description: "API Key",
required: true,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: {},
});
expect(result).toBe(true);
});
it("should return true when required config is empty string", () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
apiKey: {
type: "string",
title: "API Key",
description: "API Key",
required: true,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: { apiKey: "" },
});
expect(result).toBe(true);
});
it("should return true when required config is array with invalid values", () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
paths: {
type: "string",
title: "Paths",
description: "File paths",
required: true,
multiple: true,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: { paths: [""] },
});
expect(result).toBe(true);
});
it("should return true when required config is empty array", () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
paths: {
type: "string",
title: "Paths",
description: "File paths",
required: true,
multiple: true,
},
},
};
const result = hasRequiredConfigMissing({
manifest,
userConfig: { paths: [] },
});
expect(result).toBe(true);
});
it("should return false when no user config is provided but nothing is required", () => {
const result = hasRequiredConfigMissing({
manifest: baseManifest,
// userConfig not provided
});
expect(result).toBe(false);
});
it("should handle multiple required config fields", () => {
const manifest: McpbManifestAny = {
...baseManifest,
user_config: {
apiKey: {
type: "string",
title: "API Key",
description: "API Key",
required: true,
},
database: {
type: "string",
title: "Database",
description: "Database connection",
required: true,
},
port: {
type: "number",
title: "Port",
description: "Port number",
required: false,
},
},
};
const resultMissingBoth = hasRequiredConfigMissing({
manifest,
userConfig: {},
});
expect(resultMissingBoth).toBe(true);
const resultMissingOne = hasRequiredConfigMissing({
manifest,
userConfig: { apiKey: "test-key" },
});
expect(resultMissingOne).toBe(true);
const resultHasBoth = hasRequiredConfigMissing({
manifest,
userConfig: { apiKey: "test-key", database: "test-db" },
});
expect(resultHasBoth).toBe(false);
});
});