Skip to main content
Glama
config-extensions.test.ts11.5 kB
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as configLoader from '../utils/config-loader.js'; import axios from 'axios'; // Mock axios vi.mock('axios'); // Create temp directory helper function createTempDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'blah-test-')); } // Test configs const baseConfig = { name: "base-config", version: "1.0.0", description: "Base config for testing extends", tools: [ { name: "tool1", description: "Tool 1 from base config", inputSchema: {} }, { name: "tool2", description: "Tool 2 from base config", inputSchema: {} } ], env: { BASE_VAR: "base_value" } }; const extensionConfig1 = { name: "extension-config-1", version: "1.0.0", description: "Extension config 1", tools: [ { name: "ext1_tool1", description: "Tool 1 from extension 1", inputSchema: {} }, { name: "ext1_tool2", description: "Tool 2 from extension 1", inputSchema: {} }, { name: "tool1", // This should be overridden by base config description: "Duplicate tool from extension 1", inputSchema: {} } ], env: { EXT1_VAR: "ext1_value", SHARED_VAR: "ext1_value" } }; const extensionConfig2 = { name: "extension-config-2", version: "1.0.0", description: "Extension config 2", tools: [ { name: "ext2_tool1", description: "Tool 1 from extension 2", inputSchema: {} }, { name: "tool2", // This should be overridden by base config description: "Duplicate tool from extension 2", inputSchema: {} } ], env: { EXT2_VAR: "ext2_value", SHARED_VAR: "ext2_value" // This should be overridden by base config } }; // Config with circular reference const circularConfig1 = { name: "circular-config-1", version: "1.0.0", description: "Circular reference config 1", extends: { circular2: "./circular2.json" }, tools: [ { name: "circular_tool1", description: "Tool from circular config 1", inputSchema: {} } ] }; const circularConfig2 = { name: "circular-config-2", version: "1.0.0", description: "Circular reference config 2", extends: { circular1: "./circular1.json" }, tools: [ { name: "circular_tool2", description: "Tool from circular config 2", inputSchema: {} } ] }; // Create a simple test for direct testing describe('Direct Config Extensions Processing', () => { it('should directly test processConfigExtensions', async () => { // Create a test config with extends property const testConfig = { name: "test-config", version: "1.0.0", extends: { ext1: "./ext1.json" }, tools: [ { name: "base_tool", description: "Base tool", inputSchema: {} } ] }; // Create a temp dir and test files const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'blah-test-direct-')); const basePath = path.join(tempDir, 'base.json'); const extPath = path.join(tempDir, 'ext1.json'); // Create the extension file const extConfig = { name: "ext-config", version: "1.0.0", tools: [ { name: "ext_tool", description: "Extension tool", inputSchema: {} } ] }; // Write the files fs.writeFileSync(basePath, JSON.stringify(testConfig)); fs.writeFileSync(extPath, JSON.stringify(extConfig)); try { // Call processConfigExtensions directly const result = await configLoader.processConfigExtensions(testConfig, basePath, new Set()); // Check that it was called (using our marker) expect((configLoader.processConfigExtensions as any).called).toBe(true); // Check merged tools expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe('base_tool'); expect(result.tools[1].name).toBe('ext_tool'); expect(result.tools[1].fromExtension).toBe('ext1'); // Check extends property was removed expect(result.extends).toBeUndefined(); } finally { // Cleanup fs.rmSync(tempDir, { recursive: true, force: true }); } }); }); // Since we've verified the direct implementation works, we can focus on testing // edge cases and the full integration in these tests describe('Config Extensions Integration', () => { it('should handle circular references gracefully', async () => { // Create a temp dir const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'blah-test-circular-')); const config1Path = path.join(tempDir, 'circular1.json'); const config2Path = path.join(tempDir, 'circular2.json'); // Write circular configs fs.writeFileSync(config1Path, JSON.stringify(circularConfig1)); fs.writeFileSync(config2Path, JSON.stringify(circularConfig2)); try { // Process config1 directly to avoid mocking getConfig const config = await configLoader.processConfigExtensions(circularConfig1, config1Path, new Set()); // Should have the tool from the first config expect(config.tools.length).toBeGreaterThanOrEqual(1); expect(config.tools.some(t => t.name === 'circular_tool1')).toBe(true); } finally { // Cleanup fs.rmSync(tempDir, { recursive: true, force: true }); } }); it('should handle remote extensions using axios', async () => { // Mock axios to return extension config vi.mocked(axios.get).mockResolvedValueOnce({ data: extensionConfig1, status: 200 }); // Create a config with remote extends const remoteConfig = { ...baseConfig, extends: { remote: 'https://example.com/ext1.json' } }; // Create temp dir and file const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'blah-test-remote-')); const configPath = path.join(tempDir, 'remote-config.json'); fs.writeFileSync(configPath, JSON.stringify(remoteConfig)); try { // Process the config directly const result = await configLoader.processConfigExtensions(remoteConfig, configPath, new Set()); // Check that axios was called expect(axios.get).toHaveBeenCalledWith('https://example.com/ext1.json', expect.anything()); // Check merged tools (2 base + 2 from remote, one duplicate) expect(result.tools.length).toBeGreaterThan(2); // Tools from extension should have fromExtension property const extTools = result.tools.filter((t: any) => t.fromExtension === 'remote'); expect(extTools.length).toBeGreaterThan(0); } finally { // Cleanup fs.rmSync(tempDir, { recursive: true, force: true }); } }); it('should handle failed remote extensions gracefully', async () => { // Mock axios to fail vi.mocked(axios.get).mockRejectedValueOnce(new Error('Network error')); // Create a config with remote extends const remoteConfig = { ...baseConfig, extends: { remote: 'https://example.com/failing.json' } }; // Create temp dir and file const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'blah-test-fail-')); const configPath = path.join(tempDir, 'remote-config.json'); fs.writeFileSync(configPath, JSON.stringify(remoteConfig)); try { // Process the config directly const result = await configLoader.processConfigExtensions(remoteConfig, configPath, new Set()); // Check that axios was called expect(axios.get).toHaveBeenCalledWith('https://example.com/failing.json', expect.anything()); // Should still have at least the base tools expect(result.tools.length).toBeGreaterThanOrEqual(2); expect(result.tools.some(t => t.name === 'tool1')).toBe(true); expect(result.tools.some(t => t.name === 'tool2')).toBe(true); } finally { // Cleanup fs.rmSync(tempDir, { recursive: true, force: true }); } }); it('should handle missing extension files gracefully', async () => { // Create a config with nonexistent file const badConfig = { ...baseConfig, extends: { missing: './nonexistent.json' } }; // Create temp dir and file const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'blah-test-missing-')); const configPath = path.join(tempDir, 'bad-config.json'); fs.writeFileSync(configPath, JSON.stringify(badConfig)); try { // Process the config directly const result = await configLoader.processConfigExtensions(badConfig, configPath, new Set()); // Should still have at least the base tools expect(result.tools.length).toBeGreaterThanOrEqual(2); expect(result.tools.some(t => t.name === 'tool1')).toBe(true); expect(result.tools.some(t => t.name === 'tool2')).toBe(true); } finally { // Cleanup fs.rmSync(tempDir, { recursive: true, force: true }); } }); it('should properly merge extended configs', async () => { // Create a temp dir const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'blah-test-merge-')); const basePath = path.join(tempDir, 'base.json'); const ext1Path = path.join(tempDir, 'ext1.json'); const ext2Path = path.join(tempDir, 'ext2.json'); // Create config with multiple extensions const mergeConfig = { ...baseConfig, extends: { ext1: './ext1.json', ext2: './ext2.json' } }; // Write files fs.writeFileSync(basePath, JSON.stringify(mergeConfig)); fs.writeFileSync(ext1Path, JSON.stringify(extensionConfig1)); fs.writeFileSync(ext2Path, JSON.stringify(extensionConfig2)); try { // Process the config directly const result = await configLoader.processConfigExtensions(mergeConfig, basePath, new Set()); // Check that tools were merged correctly (2 base + 2 from ext1 + 1 from ext2) // We expect 5 tools after merging (2 base + 2 unique from ext1 + 1 unique from ext2) expect(result.tools).toHaveLength(5); // Check for tools from both extensions // Log the tools for debugging console.log("Tools with fromExtension attributes:", result.tools.map((t: any) => ({ name: t.name, fromExtension: t.fromExtension })) ); // Check for ext1 tools (should have "ext1_tool1" and "ext1_tool2") const ext1Tools = result.tools.filter((t: any) => t.name === 'ext1_tool1' || t.name === 'ext1_tool2' ); // Check for ext2 tools (should have "ext2_tool1") const ext2Tools = result.tools.filter((t: any) => t.name === 'ext2_tool1' ); expect(ext1Tools.length).toBeGreaterThan(0); expect(ext2Tools.length).toBeGreaterThan(0); // Check that environment variables were merged expect(result.env.BASE_VAR).toBe('base_value'); expect(result.env.EXT1_VAR).toBe('ext1_value'); expect(result.env.EXT2_VAR).toBe('ext2_value'); // Check that base env vars override extension ones expect(result.env.SHARED_VAR).toBe('ext1_value'); // First extension takes precedence } finally { // Cleanup fs.rmSync(tempDir, { recursive: true, force: true }); } }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/thomasdavis/blah'

If you have feedback or need assistance with the MCP directory API, please join our Discord server