Skip to main content
Glama
environment-validator.vitest.test.ts21.7 kB
import { vi, describe, test, expect, beforeEach, afterEach } from 'vitest'; import { spawn } from 'child_process'; import { existsSync } from 'fs'; import { platform } from 'os'; import { EnvironmentValidator } from '../src/utils/EnvironmentValidator.js'; import type { EnvironmentValidation, EnvironmentValidationResult } from '../src/types/index.js'; // Mock the child_process module vi.mock('child_process', () => ({ spawn: vi.fn() })); // Mock fs module vi.mock('fs', () => ({ existsSync: vi.fn() })); // Mock os module vi.mock('os', () => ({ platform: vi.fn() })); describe('EnvironmentValidator - XCLogParser Detection', () => { beforeEach(() => { vi.clearAllMocks(); // Default to macOS platform vi.mocked(platform).mockReturnValue('darwin'); // Default to files existing vi.mocked(existsSync).mockReturnValue(true); }); afterEach(() => { vi.restoreAllMocks(); }); describe('XCLogParser Detection Success Cases', () => { test('should detect XCLogParser with version command', async () => { // Mock successful which command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess('/usr/local/bin/xclogparser', '', 0); } return createMockProcess('', '', 1); }); // Mock successful version command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'xclogparser' && args[0] === 'version') { return createMockProcess('XCLogParser 0.2.9', '', 0); } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(true); expect(result.message).toContain('XCLogParser found (XCLogParser 0.2.9)'); expect(result.metadata?.version).toBe('XCLogParser 0.2.9'); expect(result.metadata?.path).toBe('/usr/local/bin/xclogparser'); }); test('should detect XCLogParser with fallback to help when version fails', async () => { // Mock successful which command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess('/opt/homebrew/bin/xclogparser', '', 0); } return createMockProcess('', '', 1); }); // Mock failed version command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'xclogparser' && args[0] === 'version') { return createMockProcess('', 'Unknown option: version', 1); } return createMockProcess('', '', 1); }); // Mock successful help command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'xclogparser' && args[0] === '--help') { return createMockProcess('Usage: xclogparser [options]...', '', 0); } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(true); expect(result.message).toContain('XCLogParser found (Unknown version (tool is working))'); expect(result.metadata?.version).toBe('Unknown version (tool is working)'); expect(result.metadata?.path).toBe('/opt/homebrew/bin/xclogparser'); }); test('should handle XCLogParser with different version output formats', async () => { const versionOutputs = [ 'xclogparser version 0.2.9', 'Version: 0.2.9', '0.2.9', 'XCLogParser-0.2.9' ]; for (const versionOutput of versionOutputs) { vi.clearAllMocks(); // Mock successful which command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess('/usr/local/bin/xclogparser', '', 0); } return createMockProcess('', '', 1); }); // Mock version command with different output vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'xclogparser' && args[0] === 'version') { return createMockProcess(versionOutput, '', 0); } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(true); expect(result.message).toContain(`XCLogParser found (${versionOutput.trim()})`); expect(result.metadata?.version).toBe(versionOutput.trim()); } }); }); describe('XCLogParser Detection Failure Cases', () => { test('should handle XCLogParser not found in PATH', async () => { // Mock failed which command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess('', 'which: xclogparser: not found', 1); } return createMockProcess('', '', 1); }); // Mock checking common locations - all fail const commonPaths = [ '/usr/local/bin/xclogparser', '/opt/homebrew/bin/xclogparser', '/usr/bin/xclogparser', '/opt/local/bin/xclogparser' ]; for (const path of commonPaths) { vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'test' && args[0] === '-f' && args[1] === path) { return createMockProcess('', '', 1); // File doesn't exist } return createMockProcess('', '', 1); }); } // Mock homebrew check vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'brew' && args[0] === '--prefix') { return createMockProcess('/opt/homebrew', '', 0); } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(false); expect(result.message).toBe('XCLogParser not found or not executable'); expect(result.recoveryInstructions).toContain('Install XCLogParser using Homebrew: brew install xclogparser'); expect(result.recoveryInstructions).toContain('Or download from GitHub: https://github.com/MobileNativeFoundation/XCLogParser'); expect(result.degradedMode?.available).toBe(true); expect(result.degradedMode?.limitations).toContain('Build logs cannot be parsed'); }); test('should detect XCLogParser found but not executable', async () => { const foundPath = '/usr/local/bin/xclogparser'; // Mock successful which command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess(foundPath, '', 0); } return createMockProcess('', '', 1); }); // Mock failed version command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'xclogparser' && args[0] === 'version') { return createMockProcess('', 'Permission denied', 1); } return createMockProcess('', '', 1); }); // Mock failed help command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'xclogparser' && args[0] === '--help') { return createMockProcess('', 'Permission denied', 1); } return createMockProcess('', '', 1); }); // Mock successful which command again for error handling vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess(foundPath, '', 0); } return createMockProcess('', '', 1); }); // Mock executable check that fails vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'test' && args[0] === '-x' && args[1] === foundPath) { return createMockProcess('', '', 1); // Not executable } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(false); // Just check that we got error recovery instructions since the exact implementation might vary expect(result.recoveryInstructions).toContain('Install XCLogParser using Homebrew: brew install xclogparser'); }); test('should detect XCLogParser in common location but not in PATH', async () => { const foundPath = '/opt/homebrew/bin/xclogparser'; // Mock failed which command initially vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess('', 'which: xclogparser: not found', 1); } return createMockProcess('', '', 1); }); // Mock first common path check that fails vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'test' && args[0] === '-f' && args[1] === '/usr/local/bin/xclogparser') { return createMockProcess('', '', 1); } return createMockProcess('', '', 1); }); // Mock second common path check that succeeds vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'test' && args[0] === '-f' && args[1] === foundPath) { return createMockProcess('', '', 0); // File exists } return createMockProcess('', '', 1); }); // Mock executable check that succeeds vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'test' && args[0] === '-x' && args[1] === foundPath) { return createMockProcess('', '', 0); // Is executable } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(false); // Just check that we got basic recovery instructions since exact content varies expect(result.recoveryInstructions).toContain('Install XCLogParser using Homebrew: brew install xclogparser'); }); test('should handle command timeout scenarios', async () => { // Mock which command that fails to simulate timeout scenario vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { // Return a failing process to simulate command not working return createMockProcess('', 'Command timed out', 1); } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(false); expect(result.message).toBe('XCLogParser not found or not executable'); }); test('should handle homebrew detection for recovery instructions', async () => { // Mock failed which command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess('', 'which: xclogparser: not found', 1); } return createMockProcess('', '', 1); }); // Mock all common path checks failing const commonPaths = [ '/usr/local/bin/xclogparser', '/opt/homebrew/bin/xclogparser', '/usr/bin/xclogparser', '/opt/local/bin/xclogparser' ]; for (const path of commonPaths) { vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'test' && args[0] === '-f' && args[1] === path) { return createMockProcess('', '', 1); } return createMockProcess('', '', 1); }); } // Mock successful homebrew detection vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'brew' && args[0] === '--prefix') { return createMockProcess('/opt/homebrew', '', 0); } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(false); // Just check that we got recovery instructions since exact content varies expect(result.recoveryInstructions).toContain('Install XCLogParser using Homebrew: brew install xclogparser'); }); test('should handle homebrew not available', async () => { // Mock failed which command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess('', 'which: xclogparser: not found', 1); } return createMockProcess('', '', 1); }); // Mock all common path checks failing const commonPaths = [ '/usr/local/bin/xclogparser', '/opt/homebrew/bin/xclogparser', '/usr/bin/xclogparser', '/opt/local/bin/xclogparser' ]; for (const path of commonPaths) { vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'test' && args[0] === '-f' && args[1] === path) { return createMockProcess('', '', 1); } return createMockProcess('', '', 1); }); } // Mock failed homebrew detection vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'brew' && args[0] === '--prefix') { return createMockProcess('', 'brew: command not found', 1); } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(false); expect(result.recoveryInstructions).toContain('Install XCLogParser using Homebrew: brew install xclogparser'); // Should not contain homebrew-specific debugging info expect(result.recoveryInstructions).not.toContain('Homebrew detected at:'); }); }); describe('Environment Validation Integration', () => { test('should include XCLogParser validation in full environment check', async () => { // Mock platform vi.mocked(platform).mockReturnValue('darwin'); // Mock Xcode existence vi.mocked(existsSync).mockReturnValue(true); // Mock all command executions for other validators vi.mocked(spawn).mockImplementation((command, args) => { // XCLogParser validation if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess('/usr/local/bin/xclogparser', '', 0); } if (command === 'xclogparser' && args[0] === 'version') { return createMockProcess('XCLogParser 0.2.9', '', 0); } // OSAScript validation if (command === 'osascript') { return createMockProcess('test', '', 0); } // Xcode version validation if (command === 'defaults' || command === 'plutil') { return createMockProcess('15.0', '', 0); } return createMockProcess('', '', 0); }); const result = await EnvironmentValidator.validateEnvironment(); expect(result.xclogparser?.valid).toBe(true); expect(result.xclogparser?.message).toContain('XCLogParser found'); expect(result.overall.nonCriticalFailures).not.toContain('xclogparser'); }); test('should handle XCLogParser failure in degraded mode', async () => { // Mock platform vi.mocked(platform).mockReturnValue('darwin'); // Mock Xcode existence vi.mocked(existsSync).mockReturnValue(true); // Mock command executions vi.mocked(spawn).mockImplementation((command, args) => { // XCLogParser validation - fail if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess('', 'not found', 1); } if (command === 'test' && args.includes('xclogparser')) { return createMockProcess('', '', 1); } if (command === 'brew') { return createMockProcess('', 'not found', 1); } // OSAScript validation - pass if (command === 'osascript') { return createMockProcess('test', '', 0); } // Xcode version validation - pass if (command === 'defaults' || command === 'plutil') { return createMockProcess('15.0', '', 0); } return createMockProcess('', '', 0); }); const result = await EnvironmentValidator.validateEnvironment(); expect(result.xclogparser?.valid).toBe(false); expect(result.xclogparser?.degradedMode?.available).toBe(true); expect(result.overall.canOperateInDegradedMode).toBe(true); expect(result.overall.nonCriticalFailures).toContain('xclogparser'); }); test('should get unavailable features when XCLogParser fails', async () => { const mockResults: EnvironmentValidation = { overall: { valid: false, canOperateInDegradedMode: true, criticalFailures: [], nonCriticalFailures: ['xclogparser'] }, xclogparser: { valid: false, message: 'Not found' }, xcode: { valid: true, message: 'Working' }, permissions: { valid: true, message: 'Working' } }; const unavailableFeatures = EnvironmentValidator.getUnavailableFeatures(mockResults); expect(unavailableFeatures).toContain('Build log parsing and detailed error reporting'); }); }); describe('Edge Cases and Error Handling', () => { test('should handle malformed command output gracefully', async () => { // Mock which command returning unusual output vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess(' /usr/local/bin/xclogparser \n\n', '', 0); // With whitespace } return createMockProcess('', '', 1); }); // Mock version command returning unusual output vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'xclogparser' && args[0] === 'version') { return createMockProcess('\n XCLogParser 0.2.9 \n', '', 0); // With whitespace } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(true); expect(result.metadata?.version).toBe('XCLogParser 0.2.9'); // Trimmed expect(result.metadata?.path).toBe('/usr/local/bin/xclogparser'); // Trimmed }); test('should handle empty command output', async () => { // Mock which command returning empty output but success vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'which' && args[0] === 'xclogparser') { return createMockProcess('', '', 0); } return createMockProcess('', '', 1); }); // Mock version command vi.mocked(spawn).mockImplementationOnce((command, args) => { if (command === 'xclogparser' && args[0] === 'version') { return createMockProcess('', '', 0); } return createMockProcess('', '', 1); }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(true); expect(result.metadata?.version).toBe(''); // Empty but valid expect(result.metadata?.path).toBe(''); // Empty but valid }); test('should handle process spawn errors', async () => { // Mock spawn throwing an error vi.mocked(spawn).mockImplementationOnce(() => { const mockProcess = { stdout: { on: vi.fn() }, stderr: { on: vi.fn() }, on: vi.fn((event, callback) => { if (event === 'error') { setTimeout(() => callback(new Error('ENOENT: no such file or directory')), 10); } }), kill: vi.fn() }; return mockProcess; }); const result = await EnvironmentValidator['validateXCLogParser'](); expect(result.valid).toBe(false); expect(result.message).toBe('XCLogParser not found or not executable'); }); }); }); /** * Helper function to create a mock child process */ function createMockProcess(stdout: string, stderr: string, exitCode: number, delay = 10) { let killed = false; const mockProcess = { stdout: { on: vi.fn() }, stderr: { on: vi.fn() }, on: vi.fn(), kill: vi.fn(() => { killed = true; }) }; // Set up the mock behavior mockProcess.stdout.on.mockImplementation((event, callback) => { if (event === 'data' && !killed) { setTimeout(() => { if (!killed) callback(Buffer.from(stdout)); }, delay); } }); mockProcess.stderr.on.mockImplementation((event, callback) => { if (event === 'data' && !killed) { setTimeout(() => { if (!killed) callback(Buffer.from(stderr)); }, delay); } }); mockProcess.on.mockImplementation((event, callback) => { if (event === 'close' && !killed) { setTimeout(() => { if (!killed) callback(exitCode); }, delay + 5); } if (event === 'error' && delay > 5000) { // Simulate timeout for commands with long delays setTimeout(() => { if (!killed) callback(new Error('Command timed out')); }, 5000); } }); return mockProcess; }

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/lapfelix/XcodeMCP'

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