Skip to main content
Glama

MCP Xcode

by Stefan-Nitu
BuildXcodeController.integration.test.ts18.5 kB
import { describe, it, expect, jest, beforeEach } from '@jest/globals'; import { MCPController } from '../../../../presentation/interfaces/MCPController.js'; import { BuildXcodeControllerFactory } from '../../factories/BuildXcodeControllerFactory.js'; import { exec } from 'child_process'; import { existsSync } from 'fs'; import type { NodeExecError, ExecMockCall } from '../../../../shared/tests/types/execTypes.js'; // Mock ONLY external boundaries jest.mock('child_process'); // Mock promisify to return {stdout, stderr} for exec (as node's promisify does) jest.mock('util', () => { const actualUtil = jest.requireActual('util') as typeof import('util'); const { createPromisifiedExec } = require('../../../../shared/tests/mocks/promisifyExec'); return { ...actualUtil, promisify: (fn: Function) => fn?.name === 'exec' ? createPromisifiedExec(fn) : actualUtil.promisify(fn) }; }); jest.mock('fs', () => ({ existsSync: jest.fn<(path: string) => boolean>(), mkdirSync: jest.fn(), readdirSync: jest.fn(() => []), unlinkSync: jest.fn(), writeFileSync: jest.fn(), readFileSync: jest.fn() })); // Mock DependencyChecker to always report dependencies are available in tests jest.mock('../../../../infrastructure/services/DependencyChecker', () => ({ DependencyChecker: jest.fn().mockImplementation(() => ({ check: jest.fn<() => Promise<[]>>().mockResolvedValue([]) // No missing dependencies })) })); const mockExec = exec as jest.MockedFunction<typeof exec>; const mockExistsSync = existsSync as jest.MockedFunction<typeof existsSync>; /** * Integration tests for BuildXcodeController * * Following testing philosophy from TESTING-PHILOSOPHY.md: * - Use REAL components (use case, presenter, adapters) * - Mock ONLY external boundaries (subprocess, filesystem) * - Test actual user-facing behavior, not implementation * - Focus on what users experience when using the tool */ describe('BuildXcodeController Integration', () => { let controller: MCPController; let execCallIndex: number; let execMockResponses: Array<{ stdout: string; stderr: string; error?: NodeExecError }>; // Factory for xcodebuild mock responses const XcodebuildResponse = { success: (output = '** BUILD SUCCEEDED **') => ({ stdout: output, stderr: '' }), buildFailure: (output: string, stderr = 'Build failed') => { const error = new Error(`Command failed: xcodebuild`) as NodeExecError; error.code = 65; // xcodebuild exit code for build failure error.stdout = output; error.stderr = stderr; return { error, stdout: output, stderr }; }, commandNotFound: (command: string, stderr: string) => ({ error: Object.assign(new Error(`Command failed: ${command}`), { code: 127, // Exit code 127 = command not found stdout: '', stderr }), stdout: '', stderr }) }; beforeEach(() => { jest.clearAllMocks(); execCallIndex = 0; execMockResponses = []; // Setup selective exec mock - only mocks xcodebuild commands const actualExec = (jest.requireActual('child_process') as typeof import('child_process')).exec; const { createSelectiveExecMock } = require('../../../../shared/tests/mocks/selectiveExecMock'); const { isXcodebuildCommand } = require('../../../../shared/tests/mocks/xcodebuildHelpers'); mockExec.mockImplementation( createSelectiveExecMock( isXcodebuildCommand, () => execMockResponses[execCallIndex++], actualExec ) ); // Default filesystem mock - project exists mockExistsSync.mockImplementation((path) => { const pathStr = String(path); return pathStr.endsWith('.xcodeproj') || pathStr.endsWith('.xcworkspace'); }); // Create controller with REAL components using factory controller = BuildXcodeControllerFactory.create(); }); describe('successful build scenarios - all platforms', () => { // iOS Platform Tests it('should build iOS app for simulator and return success with app location', async () => { // Arrange const input = { projectPath: '/Users/dev/MyApp/MyApp.xcodeproj', scheme: 'MyApp', destination: 'iOSSimulator' }; execMockResponses = [ XcodebuildResponse.success('** BUILD SUCCEEDED **') ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build succeeded'); expect(result.content[0].text).toContain('App path:'); expect(result.content[0].text).toContain('iOS'); }); it('should build iOS app for device', async () => { // Arrange const input = { projectPath: '/Users/dev/MyApp/MyApp.xcodeproj', scheme: 'MyApp', destination: 'iOSDevice' }; execMockResponses = [ XcodebuildResponse.success() ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build succeeded'); expect(result.content[0].text).toContain('iOS'); // Verify correct destination was used expect(mockExec).toHaveBeenCalledWith( expect.stringContaining('generic/platform=iOS'), expect.any(Object), expect.any(Function) ); }); // macOS Platform Tests it('should build macOS app', async () => { // Arrange const input = { projectPath: '/Users/dev/MacApp/MacApp.xcodeproj', scheme: 'MacApp', destination: 'macOS' }; execMockResponses = [ XcodebuildResponse.success() ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build succeeded'); expect(result.content[0].text).toContain('macOS'); }); // tvOS Platform Tests it('should build tvOS app for simulator', async () => { // Arrange const input = { projectPath: '/Users/dev/TVApp/TVApp.xcodeproj', scheme: 'TVApp', destination: 'tvOSSimulator' }; execMockResponses = [ XcodebuildResponse.success() ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build succeeded'); expect(result.content[0].text).toContain('tvOS'); }); // watchOS Platform Tests it('should build watchOS app for simulator', async () => { // Arrange const input = { projectPath: '/Users/dev/WatchApp/WatchApp.xcodeproj', scheme: 'WatchApp', destination: 'watchOSSimulator' }; execMockResponses = [ XcodebuildResponse.success() ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build succeeded'); expect(result.content[0].text).toContain('watchOS'); }); // visionOS Platform Tests it('should build visionOS app for simulator', async () => { // Arrange const input = { projectPath: '/Users/dev/VisionApp/VisionApp.xcodeproj', scheme: 'VisionApp', destination: 'visionOSSimulator' }; execMockResponses = [ XcodebuildResponse.success() ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build succeeded'); expect(result.content[0].text).toContain('visionOS'); }); it('should use custom derived data path when provided', async () => { // Arrange const customPath = '/Custom/DerivedData'; const input = { projectPath: '/Users/dev/MyApp/MyApp.xcodeproj', scheme: 'MyApp', destination: 'iOSSimulator', derivedDataPath: customPath }; execMockResponses = [ XcodebuildResponse.success() ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build succeeded'); expect(mockExec).toHaveBeenCalledWith( expect.stringContaining(`-derivedDataPath "${customPath}"`), expect.any(Object), expect.any(Function) ); }); }); describe('build output behaviors', () => { it('should include warnings in build output', async () => { // Arrange const input = { projectPath: '/Users/dev/MyApp/MyApp.xcodeproj', scheme: 'MyApp', destination: 'iOSSimulator' }; // Raw xcodebuild output format (before xcbeautify formatting) // Based on real xcodebuild output captured from test_artifacts/TestProjectXCTest const buildOutputWithWarnings = `/Users/dev/MyApp/ViewController.swift:42:10: warning: 'oldMethod()' is deprecated: Use newMethod instead oldMethod() // This will generate a deprecation warning ^ /Users/dev/MyApp/AppDelegate.swift:15:5: warning: variable 'unused' was never used; consider replacing with '_' or removing it let unused = 42 ^ ** BUILD SUCCEEDED **`; execMockResponses = [ XcodebuildResponse.success(buildOutputWithWarnings) ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build succeeded'); expect(result.content[0].text).toContain('Warnings: 2'); }); }); describe('error handling behaviors', () => { it('should return clear error when project not found', async () => { // Arrange const input = { projectPath: '/nonexistent/project.xcodeproj', scheme: 'MyApp', destination: 'iOSSimulator' }; mockExistsSync.mockReturnValue(false); // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Project path does not exist'); expect(result.content[0].text).toContain('/nonexistent/project.xcodeproj'); }); it('should return formatted compilation errors with file locations', async () => { // Arrange const input = { projectPath: '/Users/dev/MyApp/MyApp.xcodeproj', scheme: 'MyApp', destination: 'iOSSimulator' }; // Raw xcodebuild output format (before xcbeautify formatting) // This is what xcodebuild actually outputs for compilation errors const buildOutputWithErrors = `CompileSwift normal x86_64 /Users/dev/MyApp/ViewController.swift (in target 'MyApp' from project 'MyApp') cd /Users/dev/MyApp /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift /Users/dev/MyApp/ViewController.swift:23:15: error: cannot find type 'NonExistentType' in scope let value: NonExistentType = "test" ^~~~~~~~~~~~~~~ CompileSwift normal x86_64 /Users/dev/MyApp/Model.swift (in target 'MyApp' from project 'MyApp') cd /Users/dev/MyApp /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift /Users/dev/MyApp/Model.swift:45:8: error: missing return in closure expected to return 'String' } ^ ** BUILD FAILED ** The following build commands failed: CompileSwift normal x86_64 /Users/dev/MyApp/ViewController.swift (in target 'MyApp' from project 'MyApp') CompileSwift normal x86_64 /Users/dev/MyApp/Model.swift (in target 'MyApp' from project 'MyApp') (2 failures)`; execMockResponses = [ XcodebuildResponse.buildFailure(buildOutputWithErrors, 'The following build commands failed:\n\tCompileSwift') ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build failed'); expect(result.content[0].text).toContain('Errors (2)'); expect(result.content[0].text).toContain('ViewController.swift:23'); expect(result.content[0].text).toContain('cannot find type'); }); it('should return error when scheme not found', async () => { // Arrange const input = { projectPath: '/Users/dev/MyApp/MyApp.xcodeproj', scheme: 'NonExistentScheme', destination: 'iOSSimulator' }; const error = new Error('xcodebuild: error: The project "MyApp" does not contain a scheme named "NonExistentScheme".') as NodeExecError; error.code = 65; error.stdout = ''; error.stderr = ''; execMockResponses = [ { error, stdout: '', stderr: '' } ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build failed'); expect(result.content[0].text).toContain('NonExistentScheme'); }); it('should return helpful error when Xcode not installed', async () => { // Arrange const input = { projectPath: '/Users/dev/MyApp/MyApp.xcodeproj', scheme: 'MyApp', destination: 'iOSSimulator' }; execMockResponses = [ // xcodebuild command fails - shell returns error before xcbeautify runs XcodebuildResponse.commandNotFound( 'xcodebuild', 'xcrun: error: unable to find utility "xcodebuild", not a developer tool or in PATH' ) ]; // Act const result = await controller.execute(input); // Assert // The presenter should show the error message expect(result.content[0].text).toContain('Build failed'); // Should show the actual error from xcrun expect(result.content[0].text).toMatch(/xcrun.*unable to find.*xcodebuild|Errors \(1\)/); }); }); describe('input validation', () => { it('should reject missing projectPath', async () => { // Arrange const input = { scheme: 'MyApp', destination: 'iOSSimulator' } as any; // Act const result = await controller.execute(input); // Assert - validation error returned expect(result.content[0].text).toBe('❌ Project path is required'); }); it('should reject empty projectPath', async () => { // Arrange const input = { projectPath: '', scheme: 'MyApp', destination: 'iOSSimulator' }; // Act const result = await controller.execute(input); // Assert - validation error returned expect(result.content[0].text).toBe('❌ Project path cannot be empty'); }); it('should reject missing scheme', async () => { // Arrange const input = { projectPath: '/Users/dev/MyApp/MyApp.xcodeproj', destination: 'iOSSimulator' } as any; // Act const result = await controller.execute(input); // Assert - validation error returned expect(result.content[0].text).toBe('❌ Scheme is required'); }); it('should reject invalid destination value', async () => { // Arrange const input = { projectPath: '/Users/dev/MyApp/MyApp.xcodeproj', scheme: 'MyApp', destination: 'invalidDestination' }; // Act const result = await controller.execute(input); // Assert - validation error returned expect(result.content[0].text).toBe('❌ Invalid destination. Use format: [platform][Simulator|Device|SimulatorUniversal]'); }); }); describe('edge cases and robustness', () => { it('should handle paths with spaces correctly', async () => { // Arrange const input = { projectPath: '/Users/dev/My iOS App/MyApp.xcodeproj', scheme: 'My App Scheme', destination: 'iOSSimulator', derivedDataPath: '/Users/dev/Derived Data' }; execMockResponses = [ XcodebuildResponse.success() ]; // Act const result = await controller.execute(input); // Assert expect(result.content[0].text).toContain('Build succeeded'); // Verify proper escaping in xcodebuild command // First call is architecture detection, second is xcodebuild const calls = mockExec.mock.calls as ExecMockCall[]; const xcodebuildCall = calls.find((call) => (call[0] as string).includes('xcodebuild')); expect(xcodebuildCall).toBeDefined(); expect(xcodebuildCall?.[0]).toContain('"/Users/dev/My iOS App/MyApp.xcodeproj"'); expect(xcodebuildCall?.[0]).toContain('"My App Scheme"'); expect(xcodebuildCall?.[0]).toContain('"/Users/dev/Derived Data"'); }); }); describe('default values and convenience', () => { it('should use Debug configuration by default', async () => { // Arrange const input = { projectPath: '/Users/dev/MyApp/MyApp.xcodeproj', scheme: 'MyApp', destination: 'iOSSimulator' // No configuration specified }; execMockResponses = [ XcodebuildResponse.success('** BUILD SUCCEEDED **') ]; // Act await controller.execute(input); // Assert const calls = mockExec.mock.calls as ExecMockCall[]; // Find the xcodebuild call (after architecture detection commands) const xcodebuildCall = calls.find((call) => (call[0] as string).includes('xcodebuild')); expect(xcodebuildCall).toBeDefined(); expect(xcodebuildCall?.[0]).toContain('-configuration \"Debug\"'); }); it('should auto-detect workspace vs project', async () => { // Arrange const input = { projectPath: '/Users/dev/MyApp/MyApp.xcworkspace', scheme: 'MyApp', destination: 'iOSSimulator' }; execMockResponses = [ XcodebuildResponse.success('** BUILD SUCCEEDED **') ]; // Act await controller.execute(input); // Assert const calls = mockExec.mock.calls as ExecMockCall[]; const xcodebuildCall = calls.find((call) => (call[0] as string).includes('xcodebuild')); expect(xcodebuildCall).toBeDefined(); expect(xcodebuildCall?.[0]).toContain('-workspace'); expect(xcodebuildCall?.[0]).not.toContain('-project'); }); }); });

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/Stefan-Nitu/mcp-xcode'

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