Skip to main content
Glama

MCP Xcode

by Stefan-Nitu
hook.test.skip19.7 kB
/** * Tests for the xcode-sync hook script */ import { describe, test, expect, beforeAll, afterAll } from '@jest/globals'; import { execSync } from 'child_process'; import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs'; import { join } from 'path'; describe('Xcode Sync Hook', () => { const testDir = '/tmp/test-xcode-hook'; const projectDir = join(testDir, 'TestApp'); const hookScript = join(process.cwd(), 'scripts', 'xcode-sync.swift'); beforeAll(() => { // Clean up and create test directory if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } mkdirSync(testDir, { recursive: true }); // Create a test Xcode project structure mkdirSync(projectDir, { recursive: true }); mkdirSync(join(projectDir, 'TestApp.xcodeproj'), { recursive: true }); // Create a minimal project.pbxproj file const pbxprojContent = `// !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 56; objects = { /* Begin PBXBuildFile section */ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ F86986901234567890ABCDEF /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; F86986921234567890ABCDEF /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ F86986871234567890ABCDEF = { isa = PBXGroup; children = ( F86986921234567890ABCDEF /* TestApp */, F86986911234567890ABCDEF /* Products */, ); sourceTree = "<group>"; }; F86986911234567890ABCDEF /* Products */ = { isa = PBXGroup; children = ( F86986901234567890ABCDEF /* TestApp.app */, ); name = Products; sourceTree = "<group>"; }; F86986921234567890ABCDEF /* TestApp */ = { isa = PBXGroup; children = ( F86986921234567890ABCDEF /* ContentView.swift */, ); path = TestApp; sourceTree = "<group>"; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F869868F1234567890ABCDEF /* TestApp */ = { isa = PBXNativeTarget; buildConfigurationList = F869869F1234567890ABCDEF /* Build configuration list for PBXNativeTarget "TestApp" */; buildPhases = ( F869868C1234567890ABCDEF /* Sources */, F869868D1234567890ABCDEF /* Frameworks */, F869868E1234567890ABCDEF /* Resources */, ); buildRules = ( ); dependencies = ( ); name = TestApp; productName = TestApp; productReference = F86986901234567890ABCDEF /* TestApp.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F86986881234567890ABCDEF /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1500; }; buildConfigurationList = F869868B1234567890ABCDEF /* Build configuration list for PBXProject "TestApp" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F86986871234567890ABCDEF; productRefGroup = F86986911234567890ABCDEF /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F869868F1234567890ABCDEF /* TestApp */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F869868E1234567890ABCDEF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F869868C1234567890ABCDEF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ }; rootObject = F86986881234567890ABCDEF /* Project object */; }`; writeFileSync( join(projectDir, 'TestApp.xcodeproj', 'project.pbxproj'), pbxprojContent ); // Build the XcodeProjectModifier if it doesn't exist if (!existsSync('/tmp/XcodeProjectModifier/.build/release/XcodeProjectModifier')) { try { mkdirSync('/tmp/XcodeProjectModifier/Sources/XcodeProjectModifier', { recursive: true }); writeFileSync('/tmp/XcodeProjectModifier/Package.swift', ` // swift-tools-version: 5.9 import PackageDescription let package = Package( name: "XcodeProjectModifier", platforms: [.macOS(.v10_15)], dependencies: [ .package(url: "https://github.com/tuist/XcodeProj.git", from: "8.0.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0") ], targets: [ .executableTarget( name: "XcodeProjectModifier", dependencies: [ "XcodeProj", .product(name: "ArgumentParser", package: "swift-argument-parser") ] ) ] )`); // Copy the main.swift from test_artifacts if it exists const mainSwiftPath = join(process.cwd(), 'test_artifacts', 'XcodeProjectModifier', 'main.swift'); if (existsSync(mainSwiftPath)) { const content = readFileSync(mainSwiftPath, 'utf-8'); writeFileSync('/tmp/XcodeProjectModifier/Sources/XcodeProjectModifier/main.swift', content); } else { // Create a minimal stub for testing writeFileSync('/tmp/XcodeProjectModifier/Sources/XcodeProjectModifier/main.swift', ` import Foundation print("Mock XcodeProjectModifier for testing") exit(0) `); } execSync('swift build -c release', { cwd: '/tmp/XcodeProjectModifier', stdio: 'ignore' }); } catch (error) { console.warn('Could not build XcodeProjectModifier:', error); } } }); afterAll(() => { // Clean up test directory if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } }); describe('Hook Data Parsing', () => { test('should parse Write tool data correctly', () => { const hookData = { tool_name: 'Write', tool_input: { file_path: join(projectDir, 'TestApp', 'NewView.swift'), content: 'import SwiftUI\n\nstruct NewView: View {}' }, tool_response: { type: 'create', filePath: join(projectDir, 'TestApp', 'NewView.swift') }, cwd: projectDir }; // Create the file first mkdirSync(join(projectDir, 'TestApp'), { recursive: true }); writeFileSync(join(projectDir, 'TestApp', 'NewView.swift'), hookData.tool_input.content); // Run the hook script const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Detected add operation'); expect(result).toContain('NewView.swift'); }); test('should parse Edit tool data correctly', () => { const hookData = { tool_name: 'Edit', tool_input: { file_path: join(projectDir, 'TestApp', 'ExistingView.swift'), old_string: 'old content', new_string: 'new content' }, tool_response: { type: 'update' }, cwd: projectDir }; // Create the file writeFileSync(join(projectDir, 'TestApp', 'ExistingView.swift'), 'new content'); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Detected update operation for'); expect(result).toContain('ExistingView.swift'); expect(result).toContain('skipping Xcode sync'); }); test('should parse Bash rm command correctly', () => { const hookData = { tool_name: 'Bash', tool_input: { command: `rm ${join(projectDir, 'TestApp', 'DeletedView.swift')}` }, tool_response: { stdout: '', stderr: '' }, cwd: projectDir }; const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Detected remove operation'); expect(result).toContain('DeletedView.swift'); }); test('should parse Bash touch command correctly', () => { const hookData = { tool_name: 'Bash', tool_input: { command: `touch ${join(projectDir, 'TestApp', 'TouchedView.swift')}` }, tool_response: { stdout: '', stderr: '' }, cwd: projectDir }; // Create the file writeFileSync(join(projectDir, 'TestApp', 'TouchedView.swift'), ''); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Detected add operation'); expect(result).toContain('TouchedView.swift'); }); test('should parse Bash echo > command correctly', () => { const hookData = { tool_name: 'Bash', tool_input: { command: `echo 'content' > ${join(projectDir, 'TestApp', 'EchoView.swift')}` }, tool_response: { stdout: '', stderr: '' }, cwd: projectDir }; // Create the file writeFileSync(join(projectDir, 'TestApp', 'EchoView.swift'), 'content'); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Detected add operation'); expect(result).toContain('EchoView.swift'); }); }); describe('File Type Filtering', () => { test('should process Swift files', () => { const hookData = { tool_name: 'Write', tool_input: { file_path: join(projectDir, 'TestApp', 'SwiftFile.swift') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(projectDir, 'TestApp', 'SwiftFile.swift'), ''); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Detected add operation'); }); test('should process Markdown files', () => { const hookData = { tool_name: 'Write', tool_input: { file_path: join(projectDir, 'README.md') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(projectDir, 'README.md'), '# Readme'); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Detected add operation'); expect(result).toContain('README.md'); }); test('should process resource files', () => { const hookData = { tool_name: 'Write', tool_input: { file_path: join(projectDir, 'TestApp', 'config.json') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(projectDir, 'TestApp', 'config.json'), '{}'); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Detected add operation'); expect(result).toContain('config.json'); }); test('should ignore unsupported file types', () => { const hookData = { tool_name: 'Write', tool_input: { file_path: join(projectDir, 'TestApp', 'data.bin') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(projectDir, 'TestApp', 'data.bin'), Buffer.from([0, 1, 2, 3])); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).not.toContain('Detected add operation'); expect(result).toBe(''); // Should exit early }); }); describe('Project Discovery', () => { test('should find Xcode project in parent directories', () => { // Create a nested directory structure const nestedDir = join(projectDir, 'TestApp', 'Views', 'Nested'); mkdirSync(nestedDir, { recursive: true }); const hookData = { tool_name: 'Write', tool_input: { file_path: join(nestedDir, 'DeepView.swift') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(nestedDir, 'DeepView.swift'), ''); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); // Handle macOS /tmp symlink to /private/tmp const expectedPath = join(projectDir, 'TestApp.xcodeproj'); const privatePath = expectedPath.replace('/tmp/', '/private/tmp/'); expect(result.includes(expectedPath) || result.includes(privatePath)).toBe(true); expect(result).toContain('Target: TestApp'); }); test('should exit if no Xcode project found', () => { const noProjectDir = join(testDir, 'NoProject'); mkdirSync(noProjectDir, { recursive: true }); const hookData = { tool_name: 'Write', tool_input: { file_path: join(noProjectDir, 'File.swift') }, tool_response: { type: 'create' }, cwd: noProjectDir }; writeFileSync(join(noProjectDir, 'File.swift'), ''); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('No Xcode project found'); }); }); describe('Sync Opt-out', () => { test('should respect .no-xcode-sync file', () => { // Create opt-out file (testing new name) writeFileSync(join(projectDir, '.no-xcode-sync'), ''); const hookData = { tool_name: 'Write', tool_input: { file_path: join(projectDir, 'TestApp', 'OptedOut.swift') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(projectDir, 'TestApp', 'OptedOut.swift'), ''); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Xcode sync is disabled for this project'); // Clean up rmSync(join(projectDir, '.no-xcode-sync')); }); test('should respect legacy .no-xcode-autoadd file', () => { // Create opt-out file (testing legacy name for backward compatibility) writeFileSync(join(projectDir, '.no-xcode-autoadd'), ''); const hookData = { tool_name: 'Write', tool_input: { file_path: join(projectDir, 'TestApp', 'OptedOut.swift') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(projectDir, 'TestApp', 'OptedOut.swift'), ''); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Xcode sync is disabled for this project'); // Clean up rmSync(join(projectDir, '.no-xcode-autoadd')); }); test('should respect .claude/settings.json xcodeSync setting', () => { // Create .claude/settings.json with sync disabled const claudeDir = join(projectDir, '.claude'); mkdirSync(claudeDir, { recursive: true }); writeFileSync( join(claudeDir, 'settings.json'), JSON.stringify({ xcodeSync: false }, null, 2) ); const hookData = { tool_name: 'Write', tool_input: { file_path: join(projectDir, 'TestApp', 'DisabledBySettings.swift') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(projectDir, 'TestApp', 'DisabledBySettings.swift'), ''); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Xcode sync is disabled for this project'); // Clean up rmSync(claudeDir, { recursive: true }); }); test('should respect legacy .claude/settings.json xcodeAutoadd setting', () => { // Create .claude/settings.json with legacy autoadd disabled const claudeDir = join(projectDir, '.claude'); mkdirSync(claudeDir, { recursive: true }); writeFileSync( join(claudeDir, 'settings.json'), JSON.stringify({ xcodeAutoadd: false }, null, 2) ); const hookData = { tool_name: 'Write', tool_input: { file_path: join(projectDir, 'TestApp', 'DisabledByLegacy.swift') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(projectDir, 'TestApp', 'DisabledByLegacy.swift'), ''); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Xcode sync is disabled for this project'); // Clean up rmSync(claudeDir, { recursive: true }); }); }); describe('Group Path Determination', () => { test('should determine correct group path for files in target directory', () => { const hookData = { tool_name: 'Write', tool_input: { file_path: join(projectDir, 'TestApp', 'TargetFile.swift') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(projectDir, 'TestApp', 'TargetFile.swift'), ''); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Group path: TestApp'); }); test('should determine correct group path for nested directories', () => { const viewsDir = join(projectDir, 'TestApp', 'Views'); mkdirSync(viewsDir, { recursive: true }); const hookData = { tool_name: 'Write', tool_input: { file_path: join(viewsDir, 'NestedView.swift') }, tool_response: { type: 'create' }, cwd: projectDir }; writeFileSync(join(viewsDir, 'NestedView.swift'), ''); const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8', env: { ...process.env } }); expect(result).toContain('Group path: TestApp/Views'); }); }); });

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