Skip to main content
Glama

XC-MCP: XCode CLI wrapper

by conorluddy
cache-management.test.ts12.2 kB
import { jest } from '@jest/globals'; import { getCacheStatsTool, setCacheConfigTool, clearCacheTool, getCacheConfigTool, } from '../../../src/tools/cache/cache-management.js'; import { simulatorCache } from '../../../src/state/simulator-cache.js'; import { projectCache } from '../../../src/state/project-cache.js'; import { responseCache } from '../../../src/utils/response-cache.js'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; // Mock the cache modules jest.mock('../../../src/state/simulator-cache.js', () => ({ simulatorCache: { getCacheStats: jest.fn(), setCacheMaxAge: jest.fn(), clearCache: jest.fn(), getCacheMaxAge: jest.fn(), }, })); jest.mock('../../../src/state/project-cache.js', () => ({ projectCache: { getCacheStats: jest.fn(), setCacheMaxAge: jest.fn(), clearCache: jest.fn(), getCacheMaxAge: jest.fn(), }, })); jest.mock('../../../src/utils/response-cache.js', () => ({ responseCache: { getStats: jest.fn(), clear: jest.fn(), }, })); const mockSimulatorCache = simulatorCache as jest.Mocked<typeof simulatorCache>; const mockProjectCache = projectCache as jest.Mocked<typeof projectCache>; const mockResponseCache = responseCache as jest.Mocked<typeof responseCache>; describe('Cache Management Tools', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('getCacheStatsTool', () => { it('should return combined cache statistics', async () => { const mockSimStats = { isCached: true, lastUpdated: new Date(), cacheMaxAgeMs: 3600000, cacheMaxAgeHuman: '1h', deviceCount: 5, recentlyUsedCount: 3, isExpired: false, timeUntilExpiry: '30m', }; const mockProjectStats = { projectCount: 3, buildHistoryCount: 15, dependencyCount: 8, cacheMaxAgeMs: 3600000, cacheMaxAgeHuman: '1h', }; const mockResponseStats = { totalEntries: 10, byTool: { 'xcodebuild-build': 5 } }; mockSimulatorCache.getCacheStats.mockReturnValue(mockSimStats); mockProjectCache.getCacheStats.mockReturnValue(mockProjectStats); mockResponseCache.getStats.mockReturnValue(mockResponseStats); const result = await getCacheStatsTool({}); expect(result.content).toHaveLength(1); expect(result.content[0].type).toBe('text'); const response = JSON.parse(result.content[0].text); expect(response.simulator).toMatchObject({ ...mockSimStats, lastUpdated: mockSimStats.lastUpdated.toISOString(), }); expect(response.project).toEqual(mockProjectStats); expect(response.response).toEqual(mockResponseStats); expect(response.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); }); it('should handle errors from cache stats', async () => { mockSimulatorCache.getCacheStats.mockImplementation(() => { throw new Error('Simulator cache error'); }); await expect(getCacheStatsTool({})).rejects.toThrow(McpError); await expect(getCacheStatsTool({})).rejects.toThrow( 'Failed to get cache stats: Simulator cache error' ); }); }); describe('setCacheConfigTool', () => { it('should set cache config with maxAgeMs', async () => { const args = { cacheType: 'simulator', maxAgeMs: 5000 }; const result = await setCacheConfigTool(args); expect(mockSimulatorCache.setCacheMaxAge).toHaveBeenCalledWith(5000); expect(result.content[0].type).toBe('text'); const response = JSON.parse(result.content[0].text); expect(response.message).toBe('Cache configuration updated'); expect(response.results.simulator).toBe('Set to 5s'); }); it('should set cache config with maxAgeMinutes', async () => { const args = { cacheType: 'project', maxAgeMinutes: 10 }; const result = await setCacheConfigTool(args); expect(mockProjectCache.setCacheMaxAge).toHaveBeenCalledWith(600000); // 10 * 60 * 1000 expect(result.content[0].type).toBe('text'); const response = JSON.parse(result.content[0].text); expect(response.results.project).toBe('Set to 10m 0s'); }); it('should set cache config with maxAgeHours', async () => { const args = { cacheType: 'all', maxAgeHours: 2 }; const result = await setCacheConfigTool(args); const expectedMaxAge = 2 * 60 * 60 * 1000; // 7200000ms expect(mockSimulatorCache.setCacheMaxAge).toHaveBeenCalledWith(expectedMaxAge); expect(mockProjectCache.setCacheMaxAge).toHaveBeenCalledWith(expectedMaxAge); const response = JSON.parse(result.content[0].text); expect(response.results.simulator).toBe('Set to 2h 0m'); expect(response.results.project).toBe('Set to 2h 0m'); expect(response.results.response).toBe('Response cache config is fixed at 30 minutes'); }); it('should reject invalid cache type', async () => { const args = { cacheType: 'invalid', maxAgeMs: 5000 }; await expect(setCacheConfigTool(args)).rejects.toThrow(McpError); await expect(setCacheConfigTool(args)).rejects.toThrow( 'cacheType must be one of: simulator, project, response, all' ); }); it('should reject when no age parameter is provided', async () => { const args = { cacheType: 'simulator' }; await expect(setCacheConfigTool(args)).rejects.toThrow(McpError); await expect(setCacheConfigTool(args)).rejects.toThrow( 'Must specify one of: maxAgeMs, maxAgeMinutes, or maxAgeHours' ); }); it('should reject age less than 1000ms', async () => { const args = { cacheType: 'simulator', maxAgeMs: 500 }; await expect(setCacheConfigTool(args)).rejects.toThrow(McpError); await expect(setCacheConfigTool(args)).rejects.toThrow( 'Cache max age must be at least 1000ms (1 second)' ); }); it('should handle unexpected errors', async () => { const args = { cacheType: 'simulator', maxAgeMs: 5000 }; mockSimulatorCache.setCacheMaxAge.mockImplementation(() => { throw new Error('Unexpected error'); }); await expect(setCacheConfigTool(args)).rejects.toThrow(McpError); await expect(setCacheConfigTool(args)).rejects.toThrow( 'Failed to set cache config: Unexpected error' ); }); }); describe('clearCacheTool', () => { it('should clear simulator cache', async () => { const args = { cacheType: 'simulator' }; const result = await clearCacheTool(args); expect(mockSimulatorCache.clearCache).toHaveBeenCalled(); expect(mockProjectCache.clearCache).not.toHaveBeenCalled(); expect(mockResponseCache.clear).not.toHaveBeenCalled(); const response = JSON.parse(result.content[0].text); expect(response.message).toBe('Cache cleared successfully'); expect(response.results.simulator).toBe('Cleared successfully'); expect(response.results.project).toBeUndefined(); }); it('should clear project cache', async () => { const args = { cacheType: 'project' }; const result = await clearCacheTool(args); expect(mockProjectCache.clearCache).toHaveBeenCalled(); expect(mockSimulatorCache.clearCache).not.toHaveBeenCalled(); expect(mockResponseCache.clear).not.toHaveBeenCalled(); const response = JSON.parse(result.content[0].text); expect(response.results.project).toBe('Cleared successfully'); }); it('should clear response cache', async () => { const args = { cacheType: 'response' }; const result = await clearCacheTool(args); expect(mockResponseCache.clear).toHaveBeenCalled(); expect(mockSimulatorCache.clearCache).not.toHaveBeenCalled(); expect(mockProjectCache.clearCache).not.toHaveBeenCalled(); const response = JSON.parse(result.content[0].text); expect(response.results.response).toBe('Cleared successfully'); }); it('should clear all caches', async () => { const args = { cacheType: 'all' }; const result = await clearCacheTool(args); expect(mockSimulatorCache.clearCache).toHaveBeenCalled(); expect(mockProjectCache.clearCache).toHaveBeenCalled(); expect(mockResponseCache.clear).toHaveBeenCalled(); const response = JSON.parse(result.content[0].text); expect(response.results.simulator).toBe('Cleared successfully'); expect(response.results.project).toBe('Cleared successfully'); expect(response.results.response).toBe('Cleared successfully'); }); it('should reject invalid cache type', async () => { const args = { cacheType: 'invalid' }; await expect(clearCacheTool(args)).rejects.toThrow(McpError); await expect(clearCacheTool(args)).rejects.toThrow( 'cacheType must be one of: simulator, project, response, all' ); }); it('should handle unexpected errors', async () => { const args = { cacheType: 'simulator' }; mockSimulatorCache.clearCache.mockImplementation(() => { throw new Error('Clear failed'); }); await expect(clearCacheTool(args)).rejects.toThrow(McpError); await expect(clearCacheTool(args)).rejects.toThrow('Failed to clear cache: Clear failed'); }); }); describe('getCacheConfigTool', () => { beforeEach(() => { mockSimulatorCache.getCacheMaxAge.mockReturnValue(3600000); // 1 hour mockProjectCache.getCacheMaxAge.mockReturnValue(7200000); // 2 hours }); it('should get config for all caches by default', async () => { const result = await getCacheConfigTool({}); expect(mockSimulatorCache.getCacheMaxAge).toHaveBeenCalled(); expect(mockProjectCache.getCacheMaxAge).toHaveBeenCalled(); const response = JSON.parse(result.content[0].text); expect(response.cacheConfiguration.simulator).toEqual({ maxAgeMs: 3600000, maxAgeHuman: '1h 0m', }); expect(response.cacheConfiguration.project).toEqual({ maxAgeMs: 7200000, maxAgeHuman: '2h 0m', }); expect(response.cacheConfiguration.response).toEqual({ maxAgeMs: 1800000, // 30 minutes maxAgeHuman: '30m', note: 'Response cache duration is currently fixed', }); }); it('should get config for specific cache type', async () => { const args = { cacheType: 'simulator' }; const result = await getCacheConfigTool(args); expect(mockSimulatorCache.getCacheMaxAge).toHaveBeenCalled(); expect(mockProjectCache.getCacheMaxAge).not.toHaveBeenCalled(); const response = JSON.parse(result.content[0].text); expect(response.cacheConfiguration.simulator).toBeDefined(); expect(response.cacheConfiguration.project).toBeUndefined(); expect(response.cacheConfiguration.response).toBeUndefined(); }); it('should format durations correctly', async () => { // Test various durations const testCases = [ { ms: 1000, expected: '1s' }, { ms: 65000, expected: '1m 5s' }, { ms: 3665000, expected: '1h 1m' }, { ms: 90061000, expected: '1d 1h' }, ]; for (const testCase of testCases) { mockSimulatorCache.getCacheMaxAge.mockReturnValue(testCase.ms); const result = await getCacheConfigTool({ cacheType: 'simulator' }); const response = JSON.parse(result.content[0].text); expect(response.cacheConfiguration.simulator.maxAgeHuman).toBe(testCase.expected); } }); it('should reject invalid cache type', async () => { const args = { cacheType: 'invalid' }; await expect(getCacheConfigTool(args)).rejects.toThrow(McpError); await expect(getCacheConfigTool(args)).rejects.toThrow( 'cacheType must be one of: simulator, project, response, all' ); }); it('should handle unexpected errors', async () => { mockSimulatorCache.getCacheMaxAge.mockImplementation(() => { throw new Error('Config error'); }); await expect(getCacheConfigTool({})).rejects.toThrow(McpError); await expect(getCacheConfigTool({})).rejects.toThrow( 'Failed to get cache config: Config error' ); }); }); });

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/conorluddy/xc-mcp'

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