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 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); }); });