#
tokens: 49138/50000 21/195 files (page 3/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 of 5. Use http://codebase.md/stefan-nitu/mcp-xcode-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .claude
│   └── settings.local.json
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .vscode
│   └── settings.json
├── CLAUDE.md
├── CONTRIBUTING.md
├── docs
│   ├── ARCHITECTURE.md
│   ├── ERROR-HANDLING.md
│   └── TESTING-PHILOSOPHY.md
├── examples
│   └── screenshot-demo.js
├── jest.config.cjs
├── jest.e2e.config.cjs
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── xcode-sync.swift
├── src
│   ├── application
│   │   └── ports
│   │       ├── ArtifactPorts.ts
│   │       ├── BuildPorts.ts
│   │       ├── CommandPorts.ts
│   │       ├── ConfigPorts.ts
│   │       ├── LoggingPorts.ts
│   │       ├── MappingPorts.ts
│   │       ├── OutputFormatterPorts.ts
│   │       ├── OutputParserPorts.ts
│   │       └── SimulatorPorts.ts
│   ├── cli.ts
│   ├── config.ts
│   ├── domain
│   │   ├── errors
│   │   │   └── DomainError.ts
│   │   ├── services
│   │   │   └── PlatformDetector.ts
│   │   ├── shared
│   │   │   └── Result.ts
│   │   └── tests
│   │       └── unit
│   │           └── PlatformDetector.unit.test.ts
│   ├── features
│   │   ├── app-management
│   │   │   ├── controllers
│   │   │   │   └── InstallAppController.ts
│   │   │   ├── domain
│   │   │   │   ├── InstallRequest.ts
│   │   │   │   └── InstallResult.ts
│   │   │   ├── factories
│   │   │   │   └── InstallAppControllerFactory.ts
│   │   │   ├── index.ts
│   │   │   ├── infrastructure
│   │   │   │   └── AppInstallerAdapter.ts
│   │   │   ├── tests
│   │   │   │   ├── e2e
│   │   │   │   │   ├── InstallAppController.e2e.test.ts
│   │   │   │   │   └── InstallAppMCP.e2e.test.ts
│   │   │   │   ├── integration
│   │   │   │   │   └── InstallAppController.integration.test.ts
│   │   │   │   └── unit
│   │   │   │       ├── AppInstallerAdapter.unit.test.ts
│   │   │   │       ├── InstallAppController.unit.test.ts
│   │   │   │       ├── InstallAppUseCase.unit.test.ts
│   │   │   │       ├── InstallRequest.unit.test.ts
│   │   │   │       └── InstallResult.unit.test.ts
│   │   │   └── use-cases
│   │   │       └── InstallAppUseCase.ts
│   │   ├── build
│   │   │   ├── controllers
│   │   │   │   └── BuildXcodeController.ts
│   │   │   ├── domain
│   │   │   │   ├── BuildDestination.ts
│   │   │   │   ├── BuildIssue.ts
│   │   │   │   ├── BuildRequest.ts
│   │   │   │   ├── BuildResult.ts
│   │   │   │   └── PlatformInfo.ts
│   │   │   ├── factories
│   │   │   │   └── BuildXcodeControllerFactory.ts
│   │   │   ├── index.ts
│   │   │   ├── infrastructure
│   │   │   │   ├── BuildArtifactLocatorAdapter.ts
│   │   │   │   ├── BuildDestinationMapperAdapter.ts
│   │   │   │   ├── XcbeautifyFormatterAdapter.ts
│   │   │   │   ├── XcbeautifyOutputParserAdapter.ts
│   │   │   │   └── XcodeBuildCommandAdapter.ts
│   │   │   ├── tests
│   │   │   │   ├── e2e
│   │   │   │   │   ├── BuildXcodeController.e2e.test.ts
│   │   │   │   │   └── BuildXcodeMCP.e2e.test.ts
│   │   │   │   ├── integration
│   │   │   │   │   └── BuildXcodeController.integration.test.ts
│   │   │   │   └── unit
│   │   │   │       ├── BuildArtifactLocatorAdapter.unit.test.ts
│   │   │   │       ├── BuildDestinationMapperAdapter.unit.test.ts
│   │   │   │       ├── BuildIssue.unit.test.ts
│   │   │   │       ├── BuildProjectUseCase.unit.test.ts
│   │   │   │       ├── BuildRequest.unit.test.ts
│   │   │   │       ├── BuildResult.unit.test.ts
│   │   │   │       ├── BuildXcodeController.unit.test.ts
│   │   │   │       ├── BuildXcodePresenter.unit.test.ts
│   │   │   │       ├── PlatformInfo.unit.test.ts
│   │   │   │       ├── XcbeautifyFormatterAdapter.unit.test.ts
│   │   │   │       ├── XcbeautifyOutputParserAdapter.unit.test.ts
│   │   │   │       └── XcodeBuildCommandAdapter.unit.test.ts
│   │   │   └── use-cases
│   │   │       └── BuildProjectUseCase.ts
│   │   └── simulator
│   │       ├── controllers
│   │       │   ├── BootSimulatorController.ts
│   │       │   ├── ListSimulatorsController.ts
│   │       │   └── ShutdownSimulatorController.ts
│   │       ├── domain
│   │       │   ├── BootRequest.ts
│   │       │   ├── BootResult.ts
│   │       │   ├── ListSimulatorsRequest.ts
│   │       │   ├── ListSimulatorsResult.ts
│   │       │   ├── ShutdownRequest.ts
│   │       │   ├── ShutdownResult.ts
│   │       │   └── SimulatorState.ts
│   │       ├── factories
│   │       │   ├── BootSimulatorControllerFactory.ts
│   │       │   ├── ListSimulatorsControllerFactory.ts
│   │       │   └── ShutdownSimulatorControllerFactory.ts
│   │       ├── index.ts
│   │       ├── infrastructure
│   │       │   ├── SimulatorControlAdapter.ts
│   │       │   └── SimulatorLocatorAdapter.ts
│   │       ├── tests
│   │       │   ├── e2e
│   │       │   │   ├── BootSimulatorController.e2e.test.ts
│   │       │   │   ├── BootSimulatorMCP.e2e.test.ts
│   │       │   │   ├── ListSimulatorsController.e2e.test.ts
│   │       │   │   ├── ListSimulatorsMCP.e2e.test.ts
│   │       │   │   ├── ShutdownSimulatorController.e2e.test.ts
│   │       │   │   └── ShutdownSimulatorMCP.e2e.test.ts
│   │       │   ├── integration
│   │       │   │   ├── BootSimulatorController.integration.test.ts
│   │       │   │   ├── ListSimulatorsController.integration.test.ts
│   │       │   │   └── ShutdownSimulatorController.integration.test.ts
│   │       │   └── unit
│   │       │       ├── BootRequest.unit.test.ts
│   │       │       ├── BootResult.unit.test.ts
│   │       │       ├── BootSimulatorController.unit.test.ts
│   │       │       ├── BootSimulatorUseCase.unit.test.ts
│   │       │       ├── ListSimulatorsController.unit.test.ts
│   │       │       ├── ListSimulatorsUseCase.unit.test.ts
│   │       │       ├── ShutdownRequest.unit.test.ts
│   │       │       ├── ShutdownResult.unit.test.ts
│   │       │       ├── ShutdownSimulatorUseCase.unit.test.ts
│   │       │       ├── SimulatorControlAdapter.unit.test.ts
│   │       │       └── SimulatorLocatorAdapter.unit.test.ts
│   │       └── use-cases
│   │           ├── BootSimulatorUseCase.ts
│   │           ├── ListSimulatorsUseCase.ts
│   │           └── ShutdownSimulatorUseCase.ts
│   ├── index.ts
│   ├── infrastructure
│   │   ├── repositories
│   │   │   └── DeviceRepository.ts
│   │   ├── services
│   │   │   └── DependencyChecker.ts
│   │   └── tests
│   │       └── unit
│   │           ├── DependencyChecker.unit.test.ts
│   │           └── DeviceRepository.unit.test.ts
│   ├── logger.ts
│   ├── presentation
│   │   ├── decorators
│   │   │   └── DependencyCheckingDecorator.ts
│   │   ├── formatters
│   │   │   ├── ErrorFormatter.ts
│   │   │   └── strategies
│   │   │       ├── BuildIssuesStrategy.ts
│   │   │       ├── DefaultErrorStrategy.ts
│   │   │       ├── ErrorFormattingStrategy.ts
│   │   │       └── OutputFormatterErrorStrategy.ts
│   │   ├── interfaces
│   │   │   ├── IDependencyChecker.ts
│   │   │   ├── MCPController.ts
│   │   │   └── MCPResponse.ts
│   │   ├── presenters
│   │   │   └── BuildXcodePresenter.ts
│   │   └── tests
│   │       └── unit
│   │           ├── BuildIssuesStrategy.unit.test.ts
│   │           ├── DefaultErrorStrategy.unit.test.ts
│   │           ├── DependencyCheckingDecorator.unit.test.ts
│   │           └── ErrorFormatter.unit.test.ts
│   ├── shared
│   │   ├── domain
│   │   │   ├── AppPath.ts
│   │   │   ├── DeviceId.ts
│   │   │   ├── Platform.ts
│   │   │   └── ProjectPath.ts
│   │   ├── index.ts
│   │   ├── infrastructure
│   │   │   ├── ConfigProviderAdapter.ts
│   │   │   └── ShellCommandExecutorAdapter.ts
│   │   └── tests
│   │       ├── mocks
│   │       │   ├── promisifyExec.ts
│   │       │   ├── selectiveExecMock.ts
│   │       │   └── xcodebuildHelpers.ts
│   │       ├── skipped
│   │       │   ├── cli.e2e.test.skip
│   │       │   ├── hook-e2e.test.skip
│   │       │   ├── hook-path.e2e.test.skip
│   │       │   └── hook.test.skip
│   │       ├── types
│   │       │   └── execTypes.ts
│   │       ├── unit
│   │       │   ├── AppPath.unit.test.ts
│   │       │   ├── ConfigProviderAdapter.unit.test.ts
│   │       │   ├── ProjectPath.unit.test.ts
│   │       │   └── ShellCommandExecutorAdapter.unit.test.ts
│   │       └── utils
│   │           ├── gitResetTestArtifacts.ts
│   │           ├── mockHelpers.ts
│   │           ├── TestEnvironmentCleaner.ts
│   │           ├── TestErrorInjector.ts
│   │           ├── testHelpers.ts
│   │           ├── TestProjectManager.ts
│   │           └── TestSimulatorManager.ts
│   ├── types.ts
│   ├── utils
│   │   ├── devices
│   │   │   ├── Devices.ts
│   │   │   ├── SimulatorApps.ts
│   │   │   ├── SimulatorBoot.ts
│   │   │   ├── SimulatorDevice.ts
│   │   │   ├── SimulatorInfo.ts
│   │   │   ├── SimulatorReset.ts
│   │   │   └── SimulatorUI.ts
│   │   ├── errors
│   │   │   ├── index.ts
│   │   │   └── xcbeautify-parser.ts
│   │   ├── index.ts
│   │   ├── LogManager.ts
│   │   ├── LogManagerInstance.ts
│   │   └── projects
│   │       ├── SwiftBuild.ts
│   │       ├── SwiftPackage.ts
│   │       ├── SwiftPackageInfo.ts
│   │       ├── Xcode.ts
│   │       ├── XcodeArchive.ts
│   │       ├── XcodeBuild.ts
│   │       ├── XcodeErrors.ts
│   │       ├── XcodeInfo.ts
│   │       └── XcodeProject.ts
│   └── utils.ts
├── test_artifacts
│   ├── Test.xcworkspace
│   │   ├── contents.xcworkspacedata
│   │   └── xcuserdata
│   │       └── stefan.xcuserdatad
│   │           └── UserInterfaceState.xcuserstate
│   ├── TestProjectSwiftTesting
│   │   ├── TestProjectSwiftTesting
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── Item.swift
│   │   │   ├── TestProjectSwiftTesting.entitlements
│   │   │   └── TestProjectSwiftTestingApp.swift
│   │   ├── TestProjectSwiftTesting.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   ├── project.xcworkspace
│   │   │   │   ├── contents.xcworkspacedata
│   │   │   │   └── xcuserdata
│   │   │   │       └── stefan.xcuserdatad
│   │   │   │           └── UserInterfaceState.xcuserstate
│   │   │   └── xcuserdata
│   │   │       └── stefan.xcuserdatad
│   │   │           └── xcschemes
│   │   │               └── xcschememanagement.plist
│   │   ├── TestProjectSwiftTestingTests
│   │   │   └── TestProjectSwiftTestingTests.swift
│   │   └── TestProjectSwiftTestingUITests
│   │       ├── TestProjectSwiftTestingUITests.swift
│   │       └── TestProjectSwiftTestingUITestsLaunchTests.swift
│   ├── TestProjectWatchOS
│   │   ├── TestProjectWatchOS
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   └── TestProjectWatchOSApp.swift
│   │   ├── TestProjectWatchOS Watch App
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   └── TestProjectWatchOSApp.swift
│   │   ├── TestProjectWatchOS Watch AppTests
│   │   │   └── TestProjectWatchOS_Watch_AppTests.swift
│   │   ├── TestProjectWatchOS Watch AppUITests
│   │   │   ├── TestProjectWatchOS_Watch_AppUITests.swift
│   │   │   └── TestProjectWatchOS_Watch_AppUITestsLaunchTests.swift
│   │   ├── TestProjectWatchOS.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── project.xcworkspace
│   │   │       └── contents.xcworkspacedata
│   │   ├── TestProjectWatchOSTests
│   │   │   └── TestProjectWatchOSTests.swift
│   │   └── TestProjectWatchOSUITests
│   │       ├── TestProjectWatchOSUITests.swift
│   │       └── TestProjectWatchOSUITestsLaunchTests.swift
│   ├── TestProjectXCTest
│   │   ├── TestProjectXCTest
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── Item.swift
│   │   │   ├── TestProjectXCTest.entitlements
│   │   │   └── TestProjectXCTestApp.swift
│   │   ├── TestProjectXCTest.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   ├── project.xcworkspace
│   │   │   │   ├── contents.xcworkspacedata
│   │   │   │   └── xcuserdata
│   │   │   │       └── stefan.xcuserdatad
│   │   │   │           └── UserInterfaceState.xcuserstate
│   │   │   └── xcuserdata
│   │   │       └── stefan.xcuserdatad
│   │   │           └── xcschemes
│   │   │               └── xcschememanagement.plist
│   │   ├── TestProjectXCTestTests
│   │   │   └── TestProjectXCTestTests.swift
│   │   └── TestProjectXCTestUITests
│   │       ├── TestProjectXCTestUITests.swift
│   │       └── TestProjectXCTestUITestsLaunchTests.swift
│   ├── TestSwiftPackageSwiftTesting
│   │   ├── .gitignore
│   │   ├── Package.swift
│   │   ├── Sources
│   │   │   ├── TestSwiftPackageSwiftTesting
│   │   │   │   └── TestSwiftPackageSwiftTesting.swift
│   │   │   └── TestSwiftPackageSwiftTestingExecutable
│   │   │       └── main.swift
│   │   └── Tests
│   │       └── TestSwiftPackageSwiftTestingTests
│   │           └── TestSwiftPackageSwiftTestingTests.swift
│   └── TestSwiftPackageXCTest
│       ├── .gitignore
│       ├── Package.swift
│       ├── Sources
│       │   ├── TestSwiftPackageXCTest
│       │   │   └── TestSwiftPackageXCTest.swift
│       │   └── TestSwiftPackageXCTestExecutable
│       │       └── main.swift
│       └── Tests
│           └── TestSwiftPackageXCTestTests
│               └── TestSwiftPackageXCTestTests.swift
├── tsconfig.json
└── XcodeProjectModifier
    ├── Package.resolved
    ├── Package.swift
    └── Sources
        └── XcodeProjectModifier
            └── main.swift
```

# Files

--------------------------------------------------------------------------------
/src/utils/devices/SimulatorDevice.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { SimulatorBoot } from './SimulatorBoot.js';
  2 | import { SimulatorApps } from './SimulatorApps.js';
  3 | import { SimulatorUI } from './SimulatorUI.js';
  4 | import { SimulatorInfo } from './SimulatorInfo.js';
  5 | import { SimulatorReset } from './SimulatorReset.js';
  6 | import { createModuleLogger } from '../../logger.js';
  7 | 
  8 | const logger = createModuleLogger('SimulatorDevice');
  9 | 
 10 | /**
 11 |  * Represents a specific simulator device instance.
 12 |  * Provides a complete interface for simulator operations while
 13 |  * delegating to specialized components internally.
 14 |  */
 15 | export class SimulatorDevice {
 16 |   private boot: SimulatorBoot;
 17 |   private apps: SimulatorApps;
 18 |   private ui: SimulatorUI;
 19 |   private info: SimulatorInfo;
 20 |   private reset: SimulatorReset;
 21 | 
 22 |   constructor(
 23 |     public readonly id: string,
 24 |     public readonly name: string,
 25 |     public readonly platform: string,
 26 |     public readonly runtime: string,
 27 |     components?: {
 28 |       boot?: SimulatorBoot;
 29 |       apps?: SimulatorApps;
 30 |       ui?: SimulatorUI;
 31 |       info?: SimulatorInfo;
 32 |       reset?: SimulatorReset;
 33 |     }
 34 |   ) {
 35 |     this.boot = components?.boot || new SimulatorBoot();
 36 |     this.apps = components?.apps || new SimulatorApps();
 37 |     this.ui = components?.ui || new SimulatorUI();
 38 |     this.info = components?.info || new SimulatorInfo();
 39 |     this.reset = components?.reset || new SimulatorReset();
 40 |   }
 41 | 
 42 |   /**
 43 |    * Boot this simulator device
 44 |    */
 45 |   async bootDevice(): Promise<void> {
 46 |     logger.debug({ deviceId: this.id, name: this.name }, 'Booting device');
 47 |     await this.boot.boot(this.id);
 48 |   }
 49 | 
 50 |   /**
 51 |    * Shutdown this simulator device
 52 |    */
 53 |   async shutdown(): Promise<void> {
 54 |     logger.debug({ deviceId: this.id, name: this.name }, 'Shutting down device');
 55 |     await this.boot.shutdown(this.id);
 56 |   }
 57 | 
 58 |   /**
 59 |    * Install an app on this device
 60 |    */
 61 |   async install(appPath: string): Promise<void> {
 62 |     logger.debug({ deviceId: this.id, appPath }, 'Installing app on device');
 63 |     await this.apps.install(appPath, this.id);
 64 |   }
 65 | 
 66 |   /**
 67 |    * Uninstall an app from this device
 68 |    */
 69 |   async uninstall(bundleId: string): Promise<void> {
 70 |     logger.debug({ deviceId: this.id, bundleId }, 'Uninstalling app from device');
 71 |     await this.apps.uninstall(bundleId, this.id);
 72 |   }
 73 | 
 74 |   /**
 75 |    * Launch an app on this device
 76 |    */
 77 |   async launch(bundleId: string): Promise<string> {
 78 |     logger.debug({ deviceId: this.id, bundleId }, 'Launching app on device');
 79 |     return await this.apps.launch(bundleId, this.id);
 80 |   }
 81 | 
 82 |   /**
 83 |    * Get bundle ID from an app path
 84 |    */
 85 |   async getBundleId(appPath: string): Promise<string> {
 86 |     return await this.apps.getBundleId(appPath);
 87 |   }
 88 | 
 89 |   /**
 90 |    * Take a screenshot of this device
 91 |    */
 92 |   async screenshot(outputPath: string): Promise<void> {
 93 |     logger.debug({ deviceId: this.id, outputPath }, 'Taking screenshot');
 94 |     await this.ui.screenshot(outputPath, this.id);
 95 |   }
 96 | 
 97 |   /**
 98 |    * Get screenshot data as base64
 99 |    */
100 |   async screenshotData(): Promise<{ base64: string; mimeType: string }> {
101 |     logger.debug({ deviceId: this.id }, 'Getting screenshot data');
102 |     return await this.ui.screenshotData(this.id);
103 |   }
104 | 
105 |   /**
106 |    * Set appearance mode (light/dark)
107 |    */
108 |   async setAppearance(appearance: 'light' | 'dark'): Promise<void> {
109 |     logger.debug({ deviceId: this.id, appearance }, 'Setting appearance');
110 |     await this.ui.setAppearance(appearance, this.id);
111 |   }
112 | 
113 |   /**
114 |    * Open the Simulator app UI
115 |    */
116 |   async open(): Promise<void> {
117 |     await this.ui.open();
118 |   }
119 | 
120 |   /**
121 |    * Get device logs
122 |    */
123 |   async logs(predicate?: string, last?: string): Promise<string> {
124 |     logger.debug({ deviceId: this.id, predicate, last }, 'Getting device logs');
125 |     return await this.info.logs(this.id, predicate, last);
126 |   }
127 | 
128 |   /**
129 |    * Get current device state
130 |    */
131 |   async getState(): Promise<string> {
132 |     return await this.info.getDeviceState(this.id);
133 |   }
134 | 
135 |   /**
136 |    * Check if device is available
137 |    */
138 |   async checkAvailability(): Promise<boolean> {
139 |     return await this.info.isAvailable(this.id);
140 |   }
141 | 
142 |   /**
143 |    * Reset this device to clean state
144 |    */
145 |   async resetDevice(): Promise<void> {
146 |     logger.debug({ deviceId: this.id, name: this.name }, 'Resetting device');
147 |     await this.reset.reset(this.id);
148 |   }
149 | 
150 |   /**
151 |    * Check if device is currently booted
152 |    * Checks actual current state, not cached value
153 |    */
154 |   async isBooted(): Promise<boolean> {
155 |     const currentState = await this.getState();
156 |     return currentState === 'Booted';
157 |   }
158 | 
159 |   /**
160 |    * Ensure this device is booted, boot if necessary
161 |    */
162 |   async ensureBooted(): Promise<void> {
163 |     // Check if device is available before trying to boot
164 |     const available = await this.checkAvailability();
165 |     if (!available) {
166 |       throw new Error(
167 |         `Device "${this.name}" (${this.id}) is not available. ` +
168 |         `The runtime "${this.runtime}" may be missing or corrupted. ` +
169 |         `Try downloading the runtime in Xcode or use a different simulator.`
170 |       );
171 |     }
172 |     
173 |     // Use the async isBooted() method to check actual state
174 |     if (!(await this.isBooted())) {
175 |       await this.bootDevice();
176 |     } else {
177 |       logger.debug({ deviceId: this.id, name: this.name }, 'Device already booted');
178 |     }
179 |   }
180 | }
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * MCP Xcode Server
  5 |  * Provides tools for building, running, and testing Apple platform projects
  6 |  */
  7 | 
  8 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  9 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
 10 | import {
 11 |   CallToolRequestSchema,
 12 |   ListToolsRequestSchema,
 13 | } from '@modelcontextprotocol/sdk/types.js';
 14 | import { logger, logToolExecution, logError } from './logger.js';
 15 | 
 16 | // Import all tool classes
 17 | // import {
 18 | //   ListSimulatorsTool,
 19 | //   BootSimulatorTool,
 20 | //   ShutdownSimulatorTool,
 21 | //   ViewSimulatorScreenTool,
 22 | //   BuildSwiftPackageTool,
 23 | //   RunSwiftPackageTool,
 24 | //   RunXcodeTool,
 25 | //   TestXcodeTool,
 26 | //   TestSwiftPackageTool,
 27 | //   CleanBuildTool,
 28 | //   ArchiveProjectTool,
 29 | //   ExportIPATool,
 30 | //   ListSchemesTool,
 31 | //   GetBuildSettingsTool,
 32 | //   GetProjectInfoTool,
 33 | //   ListTargetsTool,
 34 | //   InstallAppTool,
 35 | //   UninstallAppTool,
 36 | //   GetDeviceLogsTool,
 37 | //   ManageDependenciesTool
 38 | // } from './tools/index.js';
 39 | 
 40 | // Import factories for Clean Architecture controllers
 41 | import {
 42 |   BootSimulatorControllerFactory,
 43 |   ShutdownSimulatorControllerFactory,
 44 |   ListSimulatorsControllerFactory
 45 | } from './features/simulator/index.js';
 46 | import { BuildXcodeControllerFactory } from './features/build/index.js';
 47 | import { InstallAppControllerFactory } from './features/app-management/index.js';
 48 | 
 49 | type Tool = {
 50 |   execute(args: any): Promise<any>;
 51 |   getToolDefinition(): any;
 52 | };
 53 | 
 54 | class XcodeServer {
 55 |   private server: Server;
 56 |   private tools: Map<string, Tool>;
 57 | 
 58 |   constructor() {
 59 |     this.server = new Server(
 60 |       {
 61 |         name: 'mcp-xcode-server',
 62 |         version: '2.4.0',
 63 |       },
 64 |       {
 65 |         capabilities: {
 66 |           tools: {},
 67 |         },
 68 |       }
 69 |     );
 70 | 
 71 |     // Initialize all tools
 72 |     this.tools = new Map<string, Tool>();
 73 |     this.registerTools();
 74 |     this.setupHandlers();
 75 |   }
 76 | 
 77 |   private registerTools() {
 78 |     // Create instances of all tools
 79 |     const toolInstances = [
 80 |       // Simulator management
 81 |       ListSimulatorsControllerFactory.create(),
 82 |       BootSimulatorControllerFactory.create(),
 83 |       ShutdownSimulatorControllerFactory.create(),
 84 |       // new ViewSimulatorScreenTool(),
 85 |       // Build and test
 86 |       // new BuildSwiftPackageTool(),
 87 |       // new RunSwiftPackageTool(),
 88 |       BuildXcodeControllerFactory.create(),
 89 |       InstallAppControllerFactory.create(),
 90 |       // new RunXcodeTool(),
 91 |       // new TestXcodeTool(),
 92 |       // new TestSwiftPackageTool(),
 93 |       // new CleanBuildTool(),
 94 |       // Archive and export
 95 |       // new ArchiveProjectTool(),
 96 |       // new ExportIPATool(),
 97 |       // Project info and schemes
 98 |       // new ListSchemesTool(),
 99 |       // new GetBuildSettingsTool(),
100 |       // new GetProjectInfoTool(),
101 |       // new ListTargetsTool(),
102 |       // App management
103 |       // new InstallAppTool(),
104 |       // new UninstallAppTool(),
105 |       // Device logs
106 |       // new GetDeviceLogsTool(),
107 |       // Advanced project management
108 |       // new ManageDependenciesTool()
109 |     ];
110 | 
111 |     // Register each tool by its name
112 |     for (const tool of toolInstances) {
113 |       const definition = tool.getToolDefinition();
114 |       this.tools.set(definition.name, tool);
115 |     }
116 | 
117 |     logger.info({ toolCount: this.tools.size }, 'Tools registered');
118 |   }
119 | 
120 |   private setupHandlers() {
121 |     // Handle listing all available tools
122 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => {
123 |       const tools = Array.from(this.tools.values()).map(tool => tool.getToolDefinition());
124 |       return { tools };
125 |     });
126 | 
127 |     // Handle tool execution
128 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
129 |       const { name, arguments: args } = request.params;
130 |       const startTime = Date.now();
131 | 
132 |       logger.debug({ tool: name, args }, 'Tool request received');
133 | 
134 |       try {
135 |         const tool = this.tools.get(name);
136 |         if (!tool) {
137 |           throw new Error(`Unknown tool: ${name}`);
138 |         }
139 | 
140 |         const result = await tool.execute(args);
141 |         
142 |         // Log successful execution
143 |         logToolExecution(name, args, Date.now() - startTime);
144 |         return result;
145 |       } catch (error: any) {
146 |         
147 |         logError(error as Error, { tool: name, args });
148 |         return {
149 |           content: [
150 |             {
151 |               type: 'text',
152 |               text: `Error: ${error instanceof Error ? error.message : String(error)}`
153 |             }
154 |           ]
155 |         };
156 |       }
157 |     });
158 |   }
159 | 
160 |   async run() {
161 |     const transport = new StdioServerTransport();
162 |     await this.server.connect(transport);
163 |     logger.info({ transport: 'stdio' }, 'MCP Xcode server started');
164 |   }
165 | }
166 | 
167 | const server = new XcodeServer();
168 | 
169 | // Handle graceful shutdown
170 | process.on('SIGTERM', async () => {
171 |   logger.info('Received SIGTERM, shutting down gracefully');
172 |   // Give logger time to flush
173 |   await new Promise(resolve => setTimeout(resolve, 100));
174 |   process.exit(0);
175 | });
176 | 
177 | process.on('SIGINT', async () => {
178 |   logger.info('Received SIGINT, shutting down gracefully');
179 |   // Give logger time to flush
180 |   await new Promise(resolve => setTimeout(resolve, 100));
181 |   process.exit(0);
182 | });
183 | 
184 | server.run().catch((error) => {
185 |   logger.fatal({ error }, 'Failed to start MCP server');
186 |   process.exit(1);
187 | });
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/e2e/ListSimulatorsMCP.e2e.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * E2E Test for List Simulators through MCP Protocol
  3 |  *
  4 |  * Tests critical user journey: Listing simulators through MCP
  5 |  * Following testing philosophy: E2E tests for critical paths only (10%)
  6 |  *
  7 |  * NO MOCKS - Uses real MCP server, real simulators
  8 |  */
  9 | 
 10 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 11 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 12 | import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
 13 | import { createAndConnectClient, cleanupClientAndTransport } from '../../../../shared/tests/utils/testHelpers.js';
 14 | 
 15 | describe('List Simulators MCP E2E', () => {
 16 |   let client: Client;
 17 |   let transport: StdioClientTransport;
 18 | 
 19 |   beforeAll(async () => {
 20 |     // Build the server
 21 |     const { execSync } = await import('child_process');
 22 |     execSync('npm run build', { stdio: 'inherit' });
 23 |   });
 24 | 
 25 |   beforeEach(async () => {
 26 |     ({ client, transport } = await createAndConnectClient());
 27 |   });
 28 | 
 29 |   afterEach(async () => {
 30 |     await cleanupClientAndTransport(client, transport);
 31 |   });
 32 | 
 33 |   it('should list simulators through MCP', async () => {
 34 |     // This tests the critical user journey:
 35 |     // User connects via MCP → calls list_simulators → receives result
 36 | 
 37 |     const result = await client.request(
 38 |       {
 39 |         method: 'tools/call',
 40 |         params: {
 41 |           name: 'list_simulators',
 42 |           arguments: {}
 43 |         }
 44 |       },
 45 |       CallToolResultSchema,
 46 |       { timeout: 30000 }
 47 |     );
 48 | 
 49 |     expect(result).toBeDefined();
 50 |     expect(result.content).toBeInstanceOf(Array);
 51 | 
 52 |     const textContent = result.content.find((c: any) => c.type === 'text') as { type: string; text: string } | undefined;
 53 |     expect(textContent).toBeDefined();
 54 | 
 55 |     const text = textContent?.text || '';
 56 |     if (text.includes('No simulators found')) {
 57 |       expect(text).toBe('🔍 No simulators found');
 58 |     } else {
 59 |       expect(text).toMatch(/Found \d+ simulator/);
 60 |     }
 61 |   });
 62 | 
 63 |   it('should filter simulators by platform through MCP', async () => {
 64 |     const result = await client.request(
 65 |       {
 66 |         method: 'tools/call',
 67 |         params: {
 68 |           name: 'list_simulators',
 69 |           arguments: {
 70 |             platform: 'iOS'
 71 |           }
 72 |         }
 73 |       },
 74 |       CallToolResultSchema,
 75 |       { timeout: 30000 }
 76 |     );
 77 | 
 78 |     expect(result).toBeDefined();
 79 |     expect(result.content).toBeInstanceOf(Array);
 80 | 
 81 |     const textContent = result.content.find((c: any) => c.type === 'text') as { type: string; text: string } | undefined;
 82 |     const text = textContent?.text || '';
 83 | 
 84 |     // Should find iOS simulators
 85 |     expect(text).toMatch(/Found \d+ simulator/);
 86 | 
 87 |     const lines = text.split('\n');
 88 |     const deviceLines = lines.filter((line: string) =>
 89 |       line.includes('(') && line.includes(')') && line.includes('-')
 90 |     );
 91 | 
 92 |     expect(deviceLines.length).toBeGreaterThan(0);
 93 |     for (const line of deviceLines) {
 94 |       // All devices should show iOS runtime since we filtered by iOS platform
 95 |       expect(line).toContain(' - iOS ');
 96 |       // Should not contain other platform devices
 97 |       expect(line).not.toMatch(/Apple TV|Apple Watch/);
 98 |     }
 99 |   });
100 | 
101 |   it('should filter simulators by state through MCP', async () => {
102 |     const result = await client.request(
103 |       {
104 |         method: 'tools/call',
105 |         params: {
106 |           name: 'list_simulators',
107 |           arguments: {
108 |             state: 'Shutdown'
109 |           }
110 |         }
111 |       },
112 |       CallToolResultSchema,
113 |       { timeout: 30000 }
114 |     );
115 | 
116 |     expect(result).toBeDefined();
117 |     expect(result.content).toBeInstanceOf(Array);
118 | 
119 |     const textContent = result.content.find((c: any) => c.type === 'text') as { type: string; text: string } | undefined;
120 |     const text = textContent?.text || '';
121 | 
122 |     // Should find simulators in shutdown state
123 |     expect(text).toMatch(/Found \d+ simulator/);
124 | 
125 |     const lines = text.split('\n');
126 |     const deviceLines = lines.filter(line =>
127 |       line.includes('(') && line.includes(')') && line.includes('-')
128 |     );
129 | 
130 |     expect(deviceLines.length).toBeGreaterThan(0);
131 |     for (const line of deviceLines) {
132 |       expect(line).toContain('Shutdown');
133 |     }
134 |   });
135 | 
136 |   it('should handle combined filters through MCP', async () => {
137 |     const result = await client.request(
138 |       {
139 |         method: 'tools/call',
140 |         params: {
141 |           name: 'list_simulators',
142 |           arguments: {
143 |             platform: 'iOS',
144 |             state: 'Booted'
145 |           }
146 |         }
147 |       },
148 |       CallToolResultSchema,
149 |       { timeout: 30000 }
150 |     );
151 | 
152 |     expect(result).toBeDefined();
153 |     expect(result.content).toBeInstanceOf(Array);
154 | 
155 |     const textContent = result.content.find((c: any) => c.type === 'text') as { type: string; text: string } | undefined;
156 |     const text = textContent?.text || '';
157 | 
158 |     // The combined filter might not find any booted iOS simulators
159 |     // but the test should still assert the behavior
160 |     if (text.includes('No simulators found')) {
161 |       expect(text).toBe('🔍 No simulators found');
162 |     } else {
163 |       expect(text).toMatch(/Found \d+ simulator/);
164 | 
165 |       const lines = text.split('\n');
166 |       const deviceLines = lines.filter(line =>
167 |         line.includes('(') && line.includes(')') && line.includes('-')
168 |       );
169 | 
170 |       for (const line of deviceLines) {
171 |         expect(line).toContain(' - iOS ');
172 |         expect(line).toContain('Booted');
173 |       }
174 |     }
175 |   });
176 | });
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/e2e/ShutdownSimulatorMCP.e2e.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * E2E Test for Shutdown Simulator through MCP Protocol
  3 |  * 
  4 |  * Tests critical user journey: Shutting down a simulator through MCP
  5 |  * Following testing philosophy: E2E tests for critical paths only (10%)
  6 |  * 
  7 |  * Focus: MCP protocol interaction, not simulator shutdown logic
  8 |  * The controller tests already verify shutdown works with real simulators
  9 |  * This test verifies the MCP transport/serialization/protocol works
 10 |  * 
 11 |  * NO MOCKS - Uses real MCP server, real simulators
 12 |  */
 13 | 
 14 | import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
 15 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 16 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
 17 | import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
 18 | import { createAndConnectClient, cleanupClientAndTransport } from '../../../../shared/tests/utils/testHelpers.js';
 19 | import { TestSimulatorManager } from '../../../../shared/tests/utils/TestSimulatorManager.js';
 20 | import { exec } from 'child_process';
 21 | import { promisify } from 'util';
 22 | 
 23 | const execAsync = promisify(exec);
 24 | 
 25 | describe('Shutdown Simulator MCP E2E', () => {
 26 |   let client: Client;
 27 |   let transport: StdioClientTransport;
 28 |   let testSimManager: TestSimulatorManager;
 29 |   
 30 |   beforeAll(async () => {
 31 |     // Build the server
 32 |     const { execSync } = await import('child_process');
 33 |     execSync('npm run build', { stdio: 'inherit' });
 34 | 
 35 |     // Set up test simulator
 36 |     testSimManager = new TestSimulatorManager();
 37 |     await testSimManager.getOrCreateSimulator('TestSimulator-ShutdownMCP');
 38 | 
 39 |     // Connect to MCP server
 40 |     ({ client, transport } = await createAndConnectClient());
 41 |   });
 42 |   
 43 |   afterAll(async () => {
 44 |     // Cleanup test simulator
 45 |     await testSimManager.cleanup();
 46 | 
 47 |     // Cleanup MCP connection
 48 |     await cleanupClientAndTransport(client, transport);
 49 |   });
 50 | 
 51 |   describe('shutdown simulator through MCP', () => {
 52 |     it('should shutdown simulator via MCP protocol', async () => {
 53 |       // Arrange - Boot the simulator first
 54 |       await testSimManager.bootAndWait(30);
 55 | 
 56 |       // Act - Call tool through MCP
 57 |       const result = await client.callTool({
 58 |         name: 'shutdown_simulator',
 59 |         arguments: {
 60 |           deviceId: testSimManager.getSimulatorName()
 61 |         }
 62 |       });
 63 |       
 64 |       // Assert - Verify MCP response
 65 |       const parsed = CallToolResultSchema.parse(result);
 66 |       expect(parsed.content[0].type).toBe('text');
 67 |       expect(parsed.content[0].text).toBe(`✅ Successfully shutdown simulator: ${testSimManager.getSimulatorName()} (${testSimManager.getSimulatorId()})`);
 68 |       
 69 |       // Verify simulator is actually shutdown
 70 |       const listResult = await execAsync('xcrun simctl list devices --json');
 71 |       const devices = JSON.parse(listResult.stdout);
 72 |       let found = false;
 73 |       for (const runtime of Object.values(devices.devices) as any[]) {
 74 |         const device = runtime.find((d: any) => d.udid === testSimManager.getSimulatorId());
 75 |         if (device) {
 76 |           expect(device.state).toBe('Shutdown');
 77 |           found = true;
 78 |           break;
 79 |         }
 80 |       }
 81 |       expect(found).toBe(true);
 82 |     });
 83 |     
 84 |     it('should handle already shutdown simulator via MCP', async () => {
 85 |       // Arrange - ensure simulator is shutdown
 86 |       await testSimManager.shutdownAndWait();
 87 |       
 88 |       // Act - Call tool through MCP
 89 |       const result = await client.callTool({
 90 |         name: 'shutdown_simulator',
 91 |         arguments: {
 92 |           deviceId: testSimManager.getSimulatorId()
 93 |         }
 94 |       });
 95 |       
 96 |       // Assert
 97 |       const parsed = CallToolResultSchema.parse(result);
 98 |       expect(parsed.content[0].text).toBe(`✅ Simulator already shutdown: ${testSimManager.getSimulatorName()} (${testSimManager.getSimulatorId()})`);
 99 |     });
100 |     
101 |     it('should shutdown simulator by UUID via MCP', async () => {
102 |       // Arrange - Boot the simulator first
103 |       await testSimManager.bootAndWait(30);
104 | 
105 |       // Act - Call tool with UUID
106 |       const result = await client.callTool({
107 |         name: 'shutdown_simulator',
108 |         arguments: {
109 |           deviceId: testSimManager.getSimulatorId()
110 |         }
111 |       });
112 |       
113 |       // Assert
114 |       const parsed = CallToolResultSchema.parse(result);
115 |       expect(parsed.content[0].text).toBe(`✅ Successfully shutdown simulator: ${testSimManager.getSimulatorName()} (${testSimManager.getSimulatorId()})`);
116 |       
117 |       // Verify simulator is actually shutdown
118 |       const listResult = await execAsync('xcrun simctl list devices --json');
119 |       const devices = JSON.parse(listResult.stdout);
120 |       for (const runtime of Object.values(devices.devices) as any[]) {
121 |         const device = runtime.find((d: any) => d.udid === testSimManager.getSimulatorId());
122 |         if (device) {
123 |           expect(device.state).toBe('Shutdown');
124 |           break;
125 |         }
126 |       }
127 |     });
128 |   });
129 | 
130 |   describe('error handling through MCP', () => {
131 |     it('should return error for non-existent simulator', async () => {
132 |       // Act
133 |       const result = await client.callTool({
134 |         name: 'shutdown_simulator',
135 |         arguments: {
136 |           deviceId: 'NonExistentSimulator-MCP'
137 |         }
138 |       });
139 |       
140 |       // Assert
141 |       const parsed = CallToolResultSchema.parse(result);
142 |       expect(parsed.content[0].text).toBe('❌ Simulator not found: NonExistentSimulator-MCP');
143 |     });
144 |   });
145 | });
```

--------------------------------------------------------------------------------
/src/shared/tests/utils/testHelpers.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Client } from '@modelcontextprotocol/sdk/client/index';
  2 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio';
  3 | import { exec } from 'child_process';
  4 | import { promisify } from 'util';
  5 | 
  6 | const execAsync = promisify(exec);
  7 | 
  8 | /**
  9 |  * Cleanup MCP client and transport connections
 10 |  */
 11 | export async function cleanupClientAndTransport(
 12 |   client: Client | null | undefined,
 13 |   transport: StdioClientTransport | null | undefined
 14 | ): Promise<void> {
 15 |   if (client) {
 16 |     await client.close();
 17 |   }
 18 |   
 19 |   if (transport) {
 20 |     const transportProcess = (transport as any)._process;
 21 |     await transport.close();
 22 |     
 23 |     if (transportProcess) {
 24 |       if (transportProcess.stdin && !transportProcess.stdin.destroyed) {
 25 |         transportProcess.stdin.end();
 26 |         transportProcess.stdin.destroy();
 27 |       }
 28 |       if (transportProcess.stdout && !transportProcess.stdout.destroyed) {
 29 |         transportProcess.stdout.destroy();
 30 |       }
 31 |       if (transportProcess.stderr && !transportProcess.stderr.destroyed) {
 32 |         transportProcess.stderr.destroy();
 33 |       }
 34 |       transportProcess.unref();
 35 |       if (!transportProcess.killed) {
 36 |         transportProcess.kill('SIGTERM');
 37 |         await new Promise(resolve => {
 38 |           const timeout = setTimeout(resolve, 100);
 39 |           transportProcess.once('exit', () => {
 40 |             clearTimeout(timeout);
 41 |             resolve(undefined);
 42 |           });
 43 |         });
 44 |       }
 45 |     }
 46 |   }
 47 | }
 48 | 
 49 | /**
 50 |  * Create and connect a new MCP client and transport
 51 |  */
 52 | export async function createAndConnectClient(): Promise<{
 53 |   client: Client;
 54 |   transport: StdioClientTransport;
 55 | }> {
 56 |   const transport = new StdioClientTransport({
 57 |     command: 'node',
 58 |     args: ['dist/index.js'],
 59 |     cwd: process.cwd(),
 60 |   });
 61 |   
 62 |   const client = new Client({
 63 |     name: 'test-client',
 64 |     version: '1.0.0',
 65 |   }, {
 66 |     capabilities: {}
 67 |   });
 68 |   
 69 |   await client.connect(transport);
 70 |   
 71 |   return { client, transport };
 72 | }
 73 | 
 74 | /**
 75 |  * Wait for a simulator to reach the Booted state
 76 |  * @param simulatorId The simulator UUID to wait for
 77 |  * @param maxSeconds Maximum seconds to wait (default 30)
 78 |  * @returns Promise that resolves when booted or rejects on timeout
 79 |  */
 80 | export async function waitForSimulatorBoot(
 81 |   simulatorId: string,
 82 |   maxSeconds: number = 30
 83 | ): Promise<void> {
 84 |   for (let i = 0; i < maxSeconds; i++) {
 85 |     const listResult = await execAsync('xcrun simctl list devices --json');
 86 |     const devices = JSON.parse(listResult.stdout);
 87 | 
 88 |     for (const runtime of Object.values(devices.devices) as any[]) {
 89 |       const device = runtime.find((d: any) => d.udid === simulatorId);
 90 |       if (device && device.state === 'Booted') {
 91 |         return; // Successfully booted
 92 |       }
 93 |     }
 94 | 
 95 |     // Wait 1 second before trying again
 96 |     await new Promise(resolve => setTimeout(resolve, 1000));
 97 |   }
 98 | 
 99 |   throw new Error(`Failed to boot simulator ${simulatorId} after ${maxSeconds} seconds`);
100 | }
101 | 
102 | /**
103 |  * Boot a simulator and wait for it to be ready
104 |  * @param simulatorId The simulator UUID to boot
105 |  * @param maxSeconds Maximum seconds to wait (default 30)
106 |  */
107 | export async function bootAndWaitForSimulator(
108 |   simulatorId: string,
109 |   maxSeconds: number = 30
110 | ): Promise<void> {
111 |   try {
112 |     await execAsync(`xcrun simctl boot "${simulatorId}"`);
113 |   } catch {
114 |     // Ignore if already booted
115 |   }
116 | 
117 |   await waitForSimulatorBoot(simulatorId, maxSeconds);
118 | }
119 | 
120 | /**
121 |  * Wait for a simulator to reach the Shutdown state
122 |  * @param simulatorId The simulator UUID to wait for
123 |  * @param maxSeconds Maximum seconds to wait (default 30)
124 |  * @returns Promise that resolves when shutdown or rejects on timeout
125 |  */
126 | export async function waitForSimulatorShutdown(
127 |   simulatorId: string,
128 |   maxSeconds: number = 30
129 | ): Promise<void> {
130 |   for (let i = 0; i < maxSeconds; i++) {
131 |     const listResult = await execAsync('xcrun simctl list devices --json');
132 |     const devices = JSON.parse(listResult.stdout);
133 | 
134 |     for (const runtime of Object.values(devices.devices) as any[]) {
135 |       const device = runtime.find((d: any) => d.udid === simulatorId);
136 |       if (device && device.state === 'Shutdown') {
137 |         return; // Successfully shutdown
138 |       }
139 |     }
140 | 
141 |     // Wait 1 second before trying again
142 |     await new Promise(resolve => setTimeout(resolve, 1000));
143 |   }
144 | 
145 |   throw new Error(`Failed to shutdown simulator ${simulatorId} after ${maxSeconds} seconds`);
146 | }
147 | 
148 | /**
149 |  * Shutdown a simulator and wait for it to be shutdown
150 |  * @param simulatorId The simulator UUID to shutdown
151 |  * @param maxSeconds Maximum seconds to wait (default 30)
152 |  */
153 | export async function shutdownAndWaitForSimulator(
154 |   simulatorId: string,
155 |   maxSeconds: number = 30
156 | ): Promise<void> {
157 |   try {
158 |     await execAsync(`xcrun simctl shutdown "${simulatorId}"`);
159 |   } catch {
160 |     // Ignore if already shutdown
161 |   }
162 | 
163 |   await waitForSimulatorShutdown(simulatorId, maxSeconds);
164 | }
165 | 
166 | /**
167 |  * Cleanup a test simulator by shutting it down and deleting it
168 |  * @param simulatorId The simulator UUID to cleanup
169 |  */
170 | export async function cleanupTestSimulator(simulatorId: string | undefined): Promise<void> {
171 |   if (!simulatorId) return;
172 | 
173 |   try {
174 |     await execAsync(`xcrun simctl shutdown "${simulatorId}"`);
175 |   } catch {
176 |     // Ignore shutdown errors - simulator might already be shutdown
177 |   }
178 | 
179 |   try {
180 |     await execAsync(`xcrun simctl delete "${simulatorId}"`);
181 |   } catch {
182 |     // Ignore delete errors - simulator might already be deleted
183 |   }
184 | }
185 | 
186 | 
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/e2e/ShutdownSimulatorController.e2e.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * E2E Test for ShutdownSimulatorController
  3 |  * 
  4 |  * Tests the controller with REAL simulators and REAL system commands
  5 |  * Following testing philosophy: E2E tests for critical paths only (10%)
  6 |  * 
  7 |  * NO MOCKS - Uses real xcrun simctl commands with actual simulators
  8 |  */
  9 | 
 10 | import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
 11 | import { MCPController } from '../../../../presentation/interfaces/MCPController.js';
 12 | import { ShutdownSimulatorControllerFactory } from '../../factories/ShutdownSimulatorControllerFactory.js';
 13 | import { exec } from 'child_process';
 14 | import { promisify } from 'util';
 15 | import { TestSimulatorManager } from '../../../../shared/tests/utils/TestSimulatorManager.js';
 16 | 
 17 | const execAsync = promisify(exec);
 18 | 
 19 | describe('ShutdownSimulatorController E2E', () => {
 20 |   let controller: MCPController;
 21 |   let testSimManager: TestSimulatorManager;
 22 |   
 23 |   beforeAll(async () => {
 24 |     // Create controller with REAL components
 25 |     controller = ShutdownSimulatorControllerFactory.create();
 26 | 
 27 |     // Set up test simulator
 28 |     testSimManager = new TestSimulatorManager();
 29 |     await testSimManager.getOrCreateSimulator('TestSimulator-Shutdown');
 30 |   });
 31 |   
 32 |   beforeEach(async () => {
 33 |     // Boot simulator before each test (to ensure we can shut it down)
 34 |     await testSimManager.bootAndWait(30);
 35 |   });
 36 |   
 37 |   afterAll(async () => {
 38 |     // Cleanup test simulator
 39 |     await testSimManager.cleanup();
 40 |   });
 41 | 
 42 |   describe('shutdown real simulators', () => {
 43 |     it('should shutdown a booted simulator', async () => {
 44 |       // Act
 45 |       const result = await controller.execute({
 46 |         deviceId: testSimManager.getSimulatorName()
 47 |       });
 48 |       
 49 |       // Assert
 50 |       expect(result.content[0].text).toBe(`✅ Successfully shutdown simulator: ${testSimManager.getSimulatorName()} (${testSimManager.getSimulatorId()})`);
 51 |       
 52 |       // Verify simulator is actually shutdown
 53 |       const listResult = await execAsync('xcrun simctl list devices --json');
 54 |       const devices = JSON.parse(listResult.stdout);
 55 |       let found = false;
 56 |       for (const runtime of Object.values(devices.devices) as any[]) {
 57 |         const device = runtime.find((d: any) => d.udid === testSimManager.getSimulatorId());
 58 |         if (device) {
 59 |           expect(device.state).toBe('Shutdown');
 60 |           found = true;
 61 |           break;
 62 |         }
 63 |       }
 64 |       expect(found).toBe(true);
 65 |     });
 66 | 
 67 |     it('should handle already shutdown simulator', async () => {
 68 |       // Arrange - shutdown simulator first
 69 |       await testSimManager.shutdownAndWait(5);
 70 |       await new Promise(resolve => setTimeout(resolve, 1000));
 71 |       
 72 |       // Act
 73 |       const result = await controller.execute({
 74 |         deviceId: testSimManager.getSimulatorName()
 75 |       });
 76 |       
 77 |       // Assert
 78 |       expect(result.content[0].text).toBe(`✅ Simulator already shutdown: ${testSimManager.getSimulatorName()} (${testSimManager.getSimulatorId()})`);
 79 |     });
 80 | 
 81 |     it('should shutdown simulator by UUID', async () => {
 82 |       // Act
 83 |       const result = await controller.execute({
 84 |         deviceId: testSimManager.getSimulatorId()
 85 |       });
 86 |       
 87 |       // Assert
 88 |       expect(result.content[0].text).toBe(`✅ Successfully shutdown simulator: ${testSimManager.getSimulatorName()} (${testSimManager.getSimulatorId()})`);
 89 |       
 90 |       // Verify simulator is actually shutdown
 91 |       const listResult = await execAsync('xcrun simctl list devices --json');
 92 |       const devices = JSON.parse(listResult.stdout);
 93 |       let found = false;
 94 |       for (const runtime of Object.values(devices.devices) as any[]) {
 95 |         const device = runtime.find((d: any) => d.udid === testSimManager.getSimulatorId());
 96 |         if (device) {
 97 |           expect(device.state).toBe('Shutdown');
 98 |           found = true;
 99 |           break;
100 |         }
101 |       }
102 |       expect(found).toBe(true);
103 |     });
104 |   });
105 | 
106 |   describe('error handling', () => {
107 |     it('should handle non-existent simulator', async () => {
108 |       // Act
109 |       const result = await controller.execute({
110 |         deviceId: 'NonExistent-Simulator-That-Does-Not-Exist'
111 |       });
112 |       
113 |       // Assert
114 |       expect(result.content[0].text).toBe('❌ Simulator not found: NonExistent-Simulator-That-Does-Not-Exist');
115 |     });
116 |   });
117 | 
118 |   describe('complex scenarios', () => {
119 |     it('should shutdown simulator that was booting', async () => {
120 |       // Arrange - boot and immediately try to shutdown
121 |       const bootPromise = testSimManager.bootAndWait(30);
122 |       
123 |       // Act - shutdown while booting
124 |       const result = await controller.execute({
125 |         deviceId: testSimManager.getSimulatorName()
126 |       });
127 |       
128 |       // Assert
129 |       expect(result.content[0].text).toContain('✅');
130 |       expect(result.content[0].text).toContain(testSimManager.getSimulatorName());
131 |       
132 |       // Wait for operations to complete
133 |       try {
134 |         await bootPromise;
135 |       } catch {
136 |         // Boot might fail if shutdown interrupted it, that's OK
137 |       }
138 |       
139 |       // Verify final state is shutdown
140 |       const listResult = await execAsync('xcrun simctl list devices --json');
141 |       const devices = JSON.parse(listResult.stdout);
142 |       for (const runtime of Object.values(devices.devices) as any[]) {
143 |         const device = runtime.find((d: any) => d.udid === testSimManager.getSimulatorId());
144 |         if (device) {
145 |           expect(device.state).toBe('Shutdown');
146 |           break;
147 |         }
148 |       }
149 |     });
150 |   });
151 | });
```

--------------------------------------------------------------------------------
/src/utils/projects/SwiftPackageInfo.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execAsync } from '../../utils.js';
  2 | import { createModuleLogger } from '../../logger.js';
  3 | 
  4 | const logger = createModuleLogger('SwiftPackageInfo');
  5 | 
  6 | export interface Dependency {
  7 |   name: string;
  8 |   url: string;
  9 |   version?: string;
 10 |   branch?: string;
 11 |   revision?: string;
 12 | }
 13 | 
 14 | export interface Product {
 15 |   name: string;
 16 |   type: 'executable' | 'library';
 17 |   targets: string[];
 18 | }
 19 | 
 20 | /**
 21 |  * Queries information about Swift packages
 22 |  */
 23 | export class SwiftPackageInfo {
 24 |   /**
 25 |    * Get list of products in a package
 26 |    */
 27 |   async getProducts(packagePath: string): Promise<Product[]> {
 28 |     const command = `swift package --package-path "${packagePath}" describe --type json`;
 29 |     
 30 |     logger.debug({ command }, 'Describe package command');
 31 |     
 32 |     try {
 33 |       const { stdout } = await execAsync(command);
 34 |       const packageInfo = JSON.parse(stdout);
 35 |       
 36 |       const products: Product[] = packageInfo.products?.map((p: any) => ({
 37 |         name: p.name,
 38 |         type: p.type?.executable ? 'executable' : 'library',
 39 |         targets: p.targets || []
 40 |       })) || [];
 41 |       
 42 |       logger.debug({ packagePath, products }, 'Found products');
 43 |       return products;
 44 |     } catch (error: any) {
 45 |       logger.error({ error: error.message, packagePath }, 'Failed to get products');
 46 |       throw new Error(`Failed to get products: ${error.message}`);
 47 |     }
 48 |   }
 49 |   
 50 |   /**
 51 |    * Get list of targets in a package
 52 |    */
 53 |   async getTargets(packagePath: string): Promise<string[]> {
 54 |     const command = `swift package --package-path "${packagePath}" describe --type json`;
 55 |     
 56 |     logger.debug({ command }, 'Describe package command');
 57 |     
 58 |     try {
 59 |       const { stdout } = await execAsync(command);
 60 |       const packageInfo = JSON.parse(stdout);
 61 |       
 62 |       const targets = packageInfo.targets?.map((t: any) => t.name) || [];
 63 |       
 64 |       logger.debug({ packagePath, targets }, 'Found targets');
 65 |       return targets;
 66 |     } catch (error: any) {
 67 |       logger.error({ error: error.message, packagePath }, 'Failed to get targets');
 68 |       throw new Error(`Failed to get targets: ${error.message}`);
 69 |     }
 70 |   }
 71 |   
 72 |   /**
 73 |    * Get list of dependencies
 74 |    */
 75 |   async getDependencies(packagePath: string): Promise<Dependency[]> {
 76 |     const command = `swift package --package-path "${packagePath}" show-dependencies --format json`;
 77 |     
 78 |     logger.debug({ command }, 'Show dependencies command');
 79 |     
 80 |     try {
 81 |       const { stdout } = await execAsync(command);
 82 |       const depTree = JSON.parse(stdout);
 83 |       
 84 |       // Extract direct dependencies from the tree
 85 |       const dependencies: Dependency[] = depTree.dependencies?.map((d: any) => ({
 86 |         name: d.name,
 87 |         url: d.url,
 88 |         version: d.version,
 89 |         branch: d.branch,
 90 |         revision: d.revision
 91 |       })) || [];
 92 |       
 93 |       logger.debug({ packagePath, dependencies }, 'Found dependencies');
 94 |       return dependencies;
 95 |     } catch (error: any) {
 96 |       logger.error({ error: error.message, packagePath }, 'Failed to get dependencies');
 97 |       throw new Error(`Failed to get dependencies: ${error.message}`);
 98 |     }
 99 |   }
100 |   
101 |   /**
102 |    * Add a dependency to the package
103 |    */
104 |   async addDependency(
105 |     packagePath: string,
106 |     url: string,
107 |     options: {
108 |       version?: string;
109 |       branch?: string;
110 |       exact?: boolean;
111 |       from?: string;
112 |       upToNextMajor?: string;
113 |     } = {}
114 |   ): Promise<void> {
115 |     let command = `swift package --package-path "${packagePath}" add-dependency "${url}"`;
116 |     
117 |     if (options.branch) {
118 |       command += ` --branch "${options.branch}"`;
119 |     } else if (options.exact) {
120 |       command += ` --exact "${options.version}"`;
121 |     } else if (options.from) {
122 |       command += ` --from "${options.from}"`;
123 |     } else if (options.upToNextMajor) {
124 |       command += ` --up-to-next-major-from "${options.upToNextMajor}"`;
125 |     }
126 |     
127 |     logger.debug({ command }, 'Add dependency command');
128 |     
129 |     try {
130 |       await execAsync(command);
131 |       logger.info({ packagePath, url }, 'Dependency added');
132 |     } catch (error: any) {
133 |       logger.error({ error: error.message, packagePath, url }, 'Failed to add dependency');
134 |       throw new Error(`Failed to add dependency: ${error.message}`);
135 |     }
136 |   }
137 |   
138 |   /**
139 |    * Remove a dependency from the package
140 |    */
141 |   async removeDependency(packagePath: string, name: string): Promise<void> {
142 |     const command = `swift package --package-path "${packagePath}" remove-dependency "${name}"`;
143 |     
144 |     logger.debug({ command }, 'Remove dependency command');
145 |     
146 |     try {
147 |       await execAsync(command);
148 |       logger.info({ packagePath, name }, 'Dependency removed');
149 |     } catch (error: any) {
150 |       logger.error({ error: error.message, packagePath, name }, 'Failed to remove dependency');
151 |       throw new Error(`Failed to remove dependency: ${error.message}`);
152 |     }
153 |   }
154 |   
155 |   /**
156 |    * Update package dependencies
157 |    */
158 |   async updateDependencies(packagePath: string): Promise<void> {
159 |     const command = `swift package --package-path "${packagePath}" update`;
160 |     
161 |     logger.debug({ command }, 'Update dependencies command');
162 |     
163 |     try {
164 |       await execAsync(command);
165 |       logger.info({ packagePath }, 'Dependencies updated');
166 |     } catch (error: any) {
167 |       logger.error({ error: error.message, packagePath }, 'Failed to update dependencies');
168 |       throw new Error(`Failed to update dependencies: ${error.message}`);
169 |     }
170 |   }
171 |   
172 |   /**
173 |    * Resolve package dependencies
174 |    */
175 |   async resolveDependencies(packagePath: string): Promise<void> {
176 |     const command = `swift package --package-path "${packagePath}" resolve`;
177 |     
178 |     logger.debug({ command }, 'Resolve dependencies command');
179 |     
180 |     try {
181 |       await execAsync(command);
182 |       logger.info({ packagePath }, 'Dependencies resolved');
183 |     } catch (error: any) {
184 |       logger.error({ error: error.message, packagePath }, 'Failed to resolve dependencies');
185 |       throw new Error(`Failed to resolve dependencies: ${error.message}`);
186 |     }
187 |   }
188 | }
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/unit/ListSimulatorsController.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest } from '@jest/globals';
  2 | import { ListSimulatorsController } from '../../controllers/ListSimulatorsController.js';
  3 | import { ListSimulatorsUseCase } from '../../use-cases/ListSimulatorsUseCase.js';
  4 | import { ListSimulatorsRequest } from '../../domain/ListSimulatorsRequest.js';
  5 | import { ListSimulatorsResult, SimulatorInfo } from '../../domain/ListSimulatorsResult.js';
  6 | import { SimulatorState } from '../../domain/SimulatorState.js';
  7 | 
  8 | describe('ListSimulatorsController', () => {
  9 |   function createSUT() {
 10 |     const mockExecute = jest.fn<(request: ListSimulatorsRequest) => Promise<ListSimulatorsResult>>();
 11 |     const mockUseCase: Partial<ListSimulatorsUseCase> = {
 12 |       execute: mockExecute
 13 |     };
 14 |     const sut = new ListSimulatorsController(mockUseCase as ListSimulatorsUseCase);
 15 |     return { sut, mockExecute };
 16 |   }
 17 | 
 18 |   describe('MCP tool interface', () => {
 19 |     it('should define correct tool metadata', () => {
 20 |       // Arrange
 21 |       const { sut } = createSUT();
 22 | 
 23 |       // Act
 24 |       const definition = sut.getToolDefinition();
 25 | 
 26 |       // Assert
 27 |       expect(definition.name).toBe('list_simulators');
 28 |       expect(definition.description).toBe('List available iOS simulators');
 29 |       expect(definition.inputSchema).toBeDefined();
 30 |     });
 31 | 
 32 |     it('should define correct input schema with optional filters', () => {
 33 |       // Arrange
 34 |       const { sut } = createSUT();
 35 | 
 36 |       // Act
 37 |       const schema = sut.inputSchema;
 38 | 
 39 |       // Assert
 40 |       expect(schema.type).toBe('object');
 41 |       expect(schema.properties.platform).toBeDefined();
 42 |       expect(schema.properties.platform.type).toBe('string');
 43 |       expect(schema.properties.platform.enum).toEqual(['iOS', 'tvOS', 'watchOS', 'visionOS']);
 44 |       expect(schema.properties.state).toBeDefined();
 45 |       expect(schema.properties.state.type).toBe('string');
 46 |       expect(schema.properties.state.enum).toEqual(['Booted', 'Shutdown']);
 47 |       expect(schema.properties.name).toBeDefined();
 48 |       expect(schema.properties.name.type).toBe('string');
 49 |       expect(schema.properties.name.description).toBe('Filter by device name (partial match, case-insensitive)');
 50 |       expect(schema.required).toEqual([]);
 51 |     });
 52 |   });
 53 | 
 54 |   describe('execute', () => {
 55 |     it('should list all simulators without filters', async () => {
 56 |       // Arrange
 57 |       const { sut, mockExecute } = createSUT();
 58 |       const simulators: SimulatorInfo[] = [
 59 |         {
 60 |           udid: 'ABC123',
 61 |           name: 'iPhone 15',
 62 |           state: SimulatorState.Booted,
 63 |           platform: 'iOS',
 64 |           runtime: 'iOS 17.0'
 65 |         },
 66 |         {
 67 |           udid: 'DEF456',
 68 |           name: 'iPad Pro',
 69 |           state: SimulatorState.Shutdown,
 70 |           platform: 'iOS',
 71 |           runtime: 'iOS 17.0'
 72 |         }
 73 |       ];
 74 |       const mockResult = ListSimulatorsResult.success(simulators);
 75 |       mockExecute.mockResolvedValue(mockResult);
 76 | 
 77 |       // Act
 78 |       const result = await sut.execute({});
 79 | 
 80 |       // Assert
 81 |       expect(result.content[0].text).toContain('Found 2 simulators');
 82 |       expect(result.content[0].text).toContain('iPhone 15 (ABC123) - Booted');
 83 |       expect(result.content[0].text).toContain('iPad Pro (DEF456) - Shutdown');
 84 |     });
 85 | 
 86 |     it('should filter by platform', async () => {
 87 |       // Arrange
 88 |       const { sut, mockExecute } = createSUT();
 89 |       const simulators: SimulatorInfo[] = [
 90 |         {
 91 |           udid: 'ABC123',
 92 |           name: 'iPhone 15',
 93 |           state: SimulatorState.Booted,
 94 |           platform: 'iOS',
 95 |           runtime: 'iOS 17.0'
 96 |         }
 97 |       ];
 98 |       const mockResult = ListSimulatorsResult.success(simulators);
 99 |       mockExecute.mockResolvedValue(mockResult);
100 | 
101 |       // Act
102 |       const result = await sut.execute({ platform: 'iOS' });
103 | 
104 |       // Assert
105 |       expect(result.content[0].text).toContain('Found 1 simulator');
106 |     });
107 | 
108 |     it('should filter by state', async () => {
109 |       // Arrange
110 |       const { sut, mockExecute } = createSUT();
111 |       const simulators: SimulatorInfo[] = [
112 |         {
113 |           udid: 'ABC123',
114 |           name: 'iPhone 15',
115 |           state: SimulatorState.Booted,
116 |           platform: 'iOS',
117 |           runtime: 'iOS 17.0'
118 |         }
119 |       ];
120 |       const mockResult = ListSimulatorsResult.success(simulators);
121 |       mockExecute.mockResolvedValue(mockResult);
122 | 
123 |       // Act
124 |       const result = await sut.execute({ state: 'Booted' });
125 | 
126 |       // Assert
127 |       expect(result.content[0].text).toContain('✅');
128 |     });
129 | 
130 |     it('should handle no simulators found', async () => {
131 |       // Arrange
132 |       const { sut, mockExecute } = createSUT();
133 |       const mockResult = ListSimulatorsResult.success([]);
134 |       mockExecute.mockResolvedValue(mockResult);
135 | 
136 |       // Act
137 |       const result = await sut.execute({});
138 | 
139 |       // Assert
140 |       expect(result.content[0].text).toBe('🔍 No simulators found');
141 |     });
142 | 
143 |     it('should handle errors gracefully', async () => {
144 |       // Arrange
145 |       const { sut, mockExecute } = createSUT();
146 |       const mockResult = ListSimulatorsResult.failed(new Error('Failed to list devices'));
147 |       mockExecute.mockResolvedValue(mockResult);
148 | 
149 |       // Act
150 |       const result = await sut.execute({});
151 | 
152 |       // Assert
153 |       expect(result.content[0].text).toContain('❌');
154 |       expect(result.content[0].text).toContain('Failed to list devices');
155 |     });
156 | 
157 |     it('should format simulators with runtime info', async () => {
158 |       // Arrange
159 |       const { sut, mockExecute } = createSUT();
160 |       const simulators: SimulatorInfo[] = [
161 |         {
162 |           udid: 'ABC123',
163 |           name: 'iPhone 15 Pro Max',
164 |           state: SimulatorState.Booted,
165 |           platform: 'iOS',
166 |           runtime: 'iOS 17.2'
167 |         }
168 |       ];
169 |       const mockResult = ListSimulatorsResult.success(simulators);
170 |       mockExecute.mockResolvedValue(mockResult);
171 | 
172 |       // Act
173 |       const result = await sut.execute({});
174 | 
175 |       // Assert
176 |       expect(result.content[0].text).toContain('iOS 17.2');
177 |     });
178 | 
179 |     it('should return validation error for invalid input', async () => {
180 |       // Arrange
181 |       const { sut } = createSUT();
182 | 
183 |       // Act
184 |       const result = await sut.execute({
185 |         platform: 'invalid'
186 |       });
187 | 
188 |       // Assert
189 |       expect(result.content[0].text).toBe('❌ Invalid platform: invalid. Valid values are: iOS, macOS, tvOS, watchOS, visionOS');
190 |     });
191 | 
192 |   });
193 | });
```

--------------------------------------------------------------------------------
/src/shared/tests/unit/ConfigProviderAdapter.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ConfigProviderAdapter } from '../../infrastructure/ConfigProviderAdapter.js';
  2 | import { IConfigProvider } from '../../../application/ports/ConfigPorts.js';
  3 | import { homedir } from 'os';
  4 | import path from 'path';
  5 | 
  6 | describe('ConfigProvider', () => {
  7 |   // Save original env
  8 |   const originalEnv = process.env;
  9 |   
 10 |   beforeEach(() => {
 11 |     // Reset env before each test
 12 |     process.env = { ...originalEnv };
 13 |   });
 14 |   
 15 |   afterAll(() => {
 16 |     // Restore original env
 17 |     process.env = originalEnv;
 18 |   });
 19 |   
 20 |   // Factory method for creating the SUT
 21 |   function createSUT(): IConfigProvider {
 22 |     return new ConfigProviderAdapter();
 23 |   }
 24 |   
 25 |   describe('getDerivedDataPath', () => {
 26 |     it('should use default path when no env var is set', () => {
 27 |       // Arrange
 28 |       delete process.env.MCP_XCODE_DERIVED_DATA_PATH;
 29 |       const sut = createSUT();
 30 |       const expectedPath = path.join(homedir(), 'Library', 'Developer', 'Xcode', 'DerivedData', 'MCP-Xcode');
 31 |       
 32 |       // Act
 33 |       const result = sut.getDerivedDataPath();
 34 |       
 35 |       // Assert
 36 |       expect(result).toBe(expectedPath);
 37 |     });
 38 |     
 39 |     it('should use env var when set', () => {
 40 |       // Arrange
 41 |       process.env.MCP_XCODE_DERIVED_DATA_PATH = '/custom/path';
 42 |       const sut = createSUT();
 43 |       
 44 |       // Act
 45 |       const result = sut.getDerivedDataPath();
 46 |       
 47 |       // Assert
 48 |       expect(result).toBe('/custom/path');
 49 |     });
 50 |     
 51 |     it('should return project-specific path when project path is provided', () => {
 52 |       // Arrange
 53 |       process.env.MCP_XCODE_DERIVED_DATA_PATH = '/base/path';
 54 |       const sut = createSUT();
 55 |       
 56 |       // Act
 57 |       const result = sut.getDerivedDataPath('/Users/dev/MyApp.xcodeproj');
 58 |       
 59 |       // Assert
 60 |       expect(result).toBe('/base/path/MyApp');
 61 |     });
 62 |     
 63 |     it('should handle workspace paths correctly', () => {
 64 |       // Arrange
 65 |       process.env.MCP_XCODE_DERIVED_DATA_PATH = '/base/path';
 66 |       const sut = createSUT();
 67 |       
 68 |       // Act
 69 |       const result = sut.getDerivedDataPath('/Users/dev/MyWorkspace.xcworkspace');
 70 |       
 71 |       // Assert
 72 |       expect(result).toBe('/base/path/MyWorkspace');
 73 |     });
 74 |     
 75 |     it('should handle paths with spaces', () => {
 76 |       // Arrange
 77 |       process.env.MCP_XCODE_DERIVED_DATA_PATH = '/base/path';
 78 |       const sut = createSUT();
 79 |       
 80 |       // Act
 81 |       const result = sut.getDerivedDataPath('/Users/dev/My App.xcodeproj');
 82 |       
 83 |       // Assert
 84 |       expect(result).toBe('/base/path/My App');
 85 |     });
 86 |   });
 87 |   
 88 |   describe('getBuildTimeout', () => {
 89 |     it('should return default timeout when no env var is set', () => {
 90 |       // Arrange
 91 |       delete process.env.MCP_XCODE_BUILD_TIMEOUT;
 92 |       const sut = createSUT();
 93 |       
 94 |       // Act
 95 |       const result = sut.getBuildTimeout();
 96 |       
 97 |       // Assert
 98 |       expect(result).toBe(600000); // 10 minutes
 99 |     });
100 |     
101 |     it('should use env var when set', () => {
102 |       // Arrange
103 |       process.env.MCP_XCODE_BUILD_TIMEOUT = '300000';
104 |       const sut = createSUT();
105 |       
106 |       // Act
107 |       const result = sut.getBuildTimeout();
108 |       
109 |       // Assert
110 |       expect(result).toBe(300000);
111 |     });
112 |     
113 |     it('should handle invalid timeout value', () => {
114 |       // Arrange
115 |       process.env.MCP_XCODE_BUILD_TIMEOUT = 'invalid';
116 |       const sut = createSUT();
117 |       
118 |       // Act
119 |       const result = sut.getBuildTimeout();
120 |       
121 |       // Assert
122 |       expect(result).toBeNaN(); // parseInt returns NaN for invalid strings
123 |     });
124 |   });
125 |   
126 |   describe('isXcbeautifyEnabled', () => {
127 |     it('should return true by default', () => {
128 |       // Arrange
129 |       delete process.env.MCP_XCODE_XCBEAUTIFY_ENABLED;
130 |       const sut = createSUT();
131 |       
132 |       // Act
133 |       const result = sut.isXcbeautifyEnabled();
134 |       
135 |       // Assert
136 |       expect(result).toBe(true);
137 |     });
138 |     
139 |     it('should return true when env var is "true"', () => {
140 |       // Arrange
141 |       process.env.MCP_XCODE_XCBEAUTIFY_ENABLED = 'true';
142 |       const sut = createSUT();
143 |       
144 |       // Act
145 |       const result = sut.isXcbeautifyEnabled();
146 |       
147 |       // Assert
148 |       expect(result).toBe(true);
149 |     });
150 |     
151 |     it('should return false when env var is "false"', () => {
152 |       // Arrange
153 |       process.env.MCP_XCODE_XCBEAUTIFY_ENABLED = 'false';
154 |       const sut = createSUT();
155 |       
156 |       // Act
157 |       const result = sut.isXcbeautifyEnabled();
158 |       
159 |       // Assert
160 |       expect(result).toBe(false);
161 |     });
162 |     
163 |     it('should handle case insensitive true value', () => {
164 |       // Arrange
165 |       process.env.MCP_XCODE_XCBEAUTIFY_ENABLED = 'TRUE';
166 |       const sut = createSUT();
167 |       
168 |       // Act
169 |       const result = sut.isXcbeautifyEnabled();
170 |       
171 |       // Assert
172 |       expect(result).toBe(true);
173 |     });
174 |     
175 |     it('should return false for any non-true value', () => {
176 |       // Arrange
177 |       process.env.MCP_XCODE_XCBEAUTIFY_ENABLED = 'yes';
178 |       const sut = createSUT();
179 |       
180 |       // Act
181 |       const result = sut.isXcbeautifyEnabled();
182 |       
183 |       // Assert
184 |       expect(result).toBe(false);
185 |     });
186 |   });
187 |   
188 |   describe('getCustomBuildSettings', () => {
189 |     it('should return empty object by default', () => {
190 |       // Arrange
191 |       delete process.env.MCP_XCODE_CUSTOM_BUILD_SETTINGS;
192 |       const sut = createSUT();
193 |       
194 |       // Act
195 |       const result = sut.getCustomBuildSettings();
196 |       
197 |       // Assert
198 |       expect(result).toEqual({});
199 |     });
200 |     
201 |     it('should parse valid JSON from env var', () => {
202 |       // Arrange
203 |       const settings = { 'SWIFT_VERSION': '5.9', 'DEBUG': 'true' };
204 |       process.env.MCP_XCODE_CUSTOM_BUILD_SETTINGS = JSON.stringify(settings);
205 |       const sut = createSUT();
206 |       
207 |       // Act
208 |       const result = sut.getCustomBuildSettings();
209 |       
210 |       // Assert
211 |       expect(result).toEqual(settings);
212 |     });
213 |     
214 |     it('should return empty object for invalid JSON', () => {
215 |       // Arrange
216 |       process.env.MCP_XCODE_CUSTOM_BUILD_SETTINGS = 'not valid json';
217 |       const sut = createSUT();
218 |       
219 |       // Act
220 |       const result = sut.getCustomBuildSettings();
221 |       
222 |       // Assert
223 |       expect(result).toEqual({});
224 |     });
225 |     
226 |     it('should handle empty JSON object', () => {
227 |       // Arrange
228 |       process.env.MCP_XCODE_CUSTOM_BUILD_SETTINGS = '{}';
229 |       const sut = createSUT();
230 |       
231 |       // Act
232 |       const result = sut.getCustomBuildSettings();
233 |       
234 |       // Assert
235 |       expect(result).toEqual({});
236 |     });
237 |   });
238 | });
```

--------------------------------------------------------------------------------
/src/shared/tests/utils/TestSimulatorManager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { exec } from 'child_process';
  2 | import { promisify } from 'util';
  3 | 
  4 | const execAsync = promisify(exec);
  5 | 
  6 | /**
  7 |  * Manages test simulator lifecycle for E2E tests
  8 |  * Handles creation, booting, shutdown, and cleanup of test simulators
  9 |  */
 10 | export class TestSimulatorManager {
 11 |   private simulatorId?: string;
 12 |   private simulatorName?: string;
 13 | 
 14 |   /**
 15 |    * Create or reuse a test simulator
 16 |    * @param namePrefix Prefix for the simulator name (e.g., "TestSimulator-Boot")
 17 |    * @param deviceType Device type (defaults to iPhone 15)
 18 |    * @returns The simulator ID
 19 |    */
 20 |   async getOrCreateSimulator(
 21 |     namePrefix: string,
 22 |     deviceType: string = 'iPhone 15'
 23 |   ): Promise<string> {
 24 |     // Check if simulator already exists
 25 |     const devicesResult = await execAsync('xcrun simctl list devices --json');
 26 |     const devices = JSON.parse(devicesResult.stdout);
 27 | 
 28 |     // Look for existing test simulator
 29 |     for (const runtime of Object.values(devices.devices) as any[]) {
 30 |       const existingSim = runtime.find((d: any) => d.name.includes(namePrefix));
 31 |       if (existingSim) {
 32 |         this.simulatorId = existingSim.udid;
 33 |         this.simulatorName = existingSim.name;
 34 |         return existingSim.udid;
 35 |       }
 36 |     }
 37 | 
 38 |     // Create new simulator if none exists
 39 |     const runtimesResult = await execAsync('xcrun simctl list runtimes --json');
 40 |     const runtimes = JSON.parse(runtimesResult.stdout);
 41 |     const iosRuntime = runtimes.runtimes.find((r: { platform: string }) => r.platform === 'iOS');
 42 | 
 43 |     if (!iosRuntime) {
 44 |       throw new Error('No iOS runtime found. Please install an iOS simulator runtime.');
 45 |     }
 46 | 
 47 |     const createResult = await execAsync(
 48 |       `xcrun simctl create "${namePrefix}" "${deviceType}" "${iosRuntime.identifier}"`
 49 |     );
 50 |     this.simulatorId = createResult.stdout.trim();
 51 |     this.simulatorName = namePrefix;
 52 | 
 53 |     return this.simulatorId;
 54 |   }
 55 | 
 56 |   /**
 57 |    * Boot the simulator and wait for it to be ready
 58 |    * @param maxSeconds Maximum seconds to wait (default 30)
 59 |    */
 60 |   async bootAndWait(maxSeconds: number = 30): Promise<void> {
 61 |     if (!this.simulatorId) {
 62 |       throw new Error('No simulator to boot. Call getOrCreateSimulator first.');
 63 |     }
 64 | 
 65 |     try {
 66 |       await execAsync(`xcrun simctl boot "${this.simulatorId}"`);
 67 |     } catch {
 68 |       // Ignore if already booted
 69 |     }
 70 | 
 71 |     await this.waitForBoot(maxSeconds);
 72 |   }
 73 | 
 74 |   /**
 75 |    * Shutdown the simulator and wait for completion
 76 |    * @param maxSeconds Maximum seconds to wait (default 30)
 77 |    */
 78 |   async shutdownAndWait(maxSeconds: number = 30): Promise<void> {
 79 |     if (!this.simulatorId) return;
 80 | 
 81 |     try {
 82 |       await execAsync(`xcrun simctl shutdown "${this.simulatorId}"`);
 83 |     } catch {
 84 |       // Ignore if already shutdown
 85 |     }
 86 | 
 87 |     await this.waitForShutdown(maxSeconds);
 88 |   }
 89 | 
 90 |   /**
 91 |    * Cleanup the test simulator (shutdown and delete)
 92 |    */
 93 |   async cleanup(): Promise<void> {
 94 |     if (!this.simulatorId) return;
 95 | 
 96 |     try {
 97 |       await execAsync(`xcrun simctl shutdown "${this.simulatorId}"`);
 98 |     } catch {
 99 |       // Ignore shutdown errors
100 |     }
101 | 
102 |     try {
103 |       await execAsync(`xcrun simctl delete "${this.simulatorId}"`);
104 |     } catch {
105 |       // Ignore delete errors
106 |     }
107 | 
108 |     this.simulatorId = undefined;
109 |     this.simulatorName = undefined;
110 |   }
111 | 
112 |   /**
113 |    * Get the current simulator ID
114 |    */
115 |   getSimulatorId(): string | undefined {
116 |     return this.simulatorId;
117 |   }
118 | 
119 |   /**
120 |    * Get the current simulator name
121 |    */
122 |   getSimulatorName(): string | undefined {
123 |     return this.simulatorName;
124 |   }
125 | 
126 |   /**
127 |    * Check if the simulator is booted
128 |    */
129 |   async isBooted(): Promise<boolean> {
130 |     if (!this.simulatorId) return false;
131 | 
132 |     const listResult = await execAsync('xcrun simctl list devices --json');
133 |     const devices = JSON.parse(listResult.stdout);
134 | 
135 |     for (const runtime of Object.values(devices.devices) as any[]) {
136 |       const device = runtime.find((d: any) => d.udid === this.simulatorId);
137 |       if (device) {
138 |         return device.state === 'Booted';
139 |       }
140 |     }
141 |     return false;
142 |   }
143 | 
144 |   /**
145 |    * Check if the simulator is shutdown
146 |    */
147 |   async isShutdown(): Promise<boolean> {
148 |     if (!this.simulatorId) return true;
149 | 
150 |     const listResult = await execAsync('xcrun simctl list devices --json');
151 |     const devices = JSON.parse(listResult.stdout);
152 | 
153 |     for (const runtime of Object.values(devices.devices) as any[]) {
154 |       const device = runtime.find((d: any) => d.udid === this.simulatorId);
155 |       if (device) {
156 |         return device.state === 'Shutdown';
157 |       }
158 |     }
159 |     return true;
160 |   }
161 | 
162 |   private async waitForBoot(maxSeconds: number): Promise<void> {
163 |     for (let i = 0; i < maxSeconds; i++) {
164 |       const listResult = await execAsync('xcrun simctl list devices --json');
165 |       const devices = JSON.parse(listResult.stdout);
166 | 
167 |       for (const runtime of Object.values(devices.devices) as any[]) {
168 |         const device = runtime.find((d: any) => d.udid === this.simulatorId);
169 |         if (device && device.state === 'Booted') {
170 |           // Wait a bit more for services to be ready
171 |           await new Promise(resolve => setTimeout(resolve, 2000));
172 |           return;
173 |         }
174 |       }
175 | 
176 |       await new Promise(resolve => setTimeout(resolve, 1000));
177 |     }
178 | 
179 |     throw new Error(`Failed to boot simulator ${this.simulatorId} after ${maxSeconds} seconds`);
180 |   }
181 | 
182 |   private async waitForShutdown(maxSeconds: number): Promise<void> {
183 |     for (let i = 0; i < maxSeconds; i++) {
184 |       const listResult = await execAsync('xcrun simctl list devices --json');
185 |       const devices = JSON.parse(listResult.stdout);
186 | 
187 |       for (const runtime of Object.values(devices.devices) as any[]) {
188 |         const device = runtime.find((d: any) => d.udid === this.simulatorId);
189 |         if (device && device.state === 'Shutdown') {
190 |           return;
191 |         }
192 |       }
193 | 
194 |       await new Promise(resolve => setTimeout(resolve, 1000));
195 |     }
196 | 
197 |     throw new Error(`Failed to shutdown simulator ${this.simulatorId} after ${maxSeconds} seconds`);
198 |   }
199 | 
200 |   /**
201 |    * Shutdown all other booted simulators except this one
202 |    */
203 |   async shutdownOtherSimulators(): Promise<void> {
204 |     const devicesResult = await execAsync('xcrun simctl list devices --json');
205 |     const devices = JSON.parse(devicesResult.stdout);
206 | 
207 |     for (const runtime of Object.values(devices.devices) as any[][]) {
208 |       for (const device of runtime) {
209 |         if (device.state === 'Booted' && device.udid !== this.simulatorId) {
210 |           try {
211 |             await execAsync(`xcrun simctl shutdown "${device.udid}"`);
212 |           } catch {
213 |             // Ignore errors
214 |           }
215 |         }
216 |       }
217 |     }
218 |   }
219 | }
```

--------------------------------------------------------------------------------
/src/shared/tests/unit/ProjectPath.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
  2 | import { ProjectPath } from '../../domain/ProjectPath.js';
  3 | import { existsSync } from 'fs';
  4 | 
  5 | // Mock fs module
  6 | jest.mock('fs', () => ({
  7 |   existsSync: jest.fn<(path: string) => boolean>()
  8 | }));
  9 | const mockExistsSync = existsSync as jest.MockedFunction<typeof existsSync>;
 10 | 
 11 | describe('ProjectPath', () => {
 12 |   // Reset mocks between tests for isolation
 13 |   beforeEach(() => {
 14 |     jest.clearAllMocks();
 15 |   });
 16 |   
 17 |   afterEach(() => {
 18 |     jest.restoreAllMocks();
 19 |   });
 20 |   
 21 |   describe('when creating a project path', () => {
 22 |     it('should accept valid .xcodeproj path that exists', () => {
 23 |       // Setup - all visible in test
 24 |       const projectPath = '/Users/dev/MyApp.xcodeproj';
 25 |       mockExistsSync.mockReturnValue(true);
 26 |       
 27 |       // Act
 28 |       const result = ProjectPath.create(projectPath);
 29 |       
 30 |       // Assert
 31 |       expect(result.toString()).toBe(projectPath);
 32 |       expect(mockExistsSync).toHaveBeenCalledWith(projectPath);
 33 |     });
 34 |     
 35 |     it('should accept valid .xcworkspace path that exists', () => {
 36 |       const workspacePath = '/Users/dev/MyApp.xcworkspace';
 37 |       mockExistsSync.mockReturnValue(true);
 38 |       
 39 |       const result = ProjectPath.create(workspacePath);
 40 |       
 41 |       expect(result.toString()).toBe(workspacePath);
 42 |       expect(mockExistsSync).toHaveBeenCalledWith(workspacePath);
 43 |     });
 44 |     
 45 |     it('should accept paths with spaces', () => {
 46 |       const pathWithSpaces = '/Users/dev/My Cool App.xcodeproj';
 47 |       mockExistsSync.mockReturnValue(true);
 48 |       
 49 |       const result = ProjectPath.create(pathWithSpaces);
 50 |       
 51 |       expect(result.toString()).toBe(pathWithSpaces);
 52 |     });
 53 |     
 54 |     it('should accept paths with special characters', () => {
 55 |       const specialPath = '/Users/dev/App-2024_v1.0.xcworkspace';
 56 |       mockExistsSync.mockReturnValue(true);
 57 |       
 58 |       const result = ProjectPath.create(specialPath);
 59 |       
 60 |       expect(result.toString()).toBe(specialPath);
 61 |     });
 62 |   });
 63 |   
 64 |   describe('when validating input', () => {
 65 |     it('should reject empty path', () => {
 66 |       expect(() => ProjectPath.create('')).toThrow('Project path cannot be empty');
 67 |       expect(mockExistsSync).not.toHaveBeenCalled();
 68 |     });
 69 |     
 70 |     it('should reject null path', () => {
 71 |       expect(() => ProjectPath.create(null as any))
 72 |         .toThrow('Project path is required');
 73 |       expect(mockExistsSync).not.toHaveBeenCalled();
 74 |     });
 75 |     
 76 |     it('should reject undefined path', () => {
 77 |       expect(() => ProjectPath.create(undefined as any))
 78 |         .toThrow('Project path is required');
 79 |       expect(mockExistsSync).not.toHaveBeenCalled();
 80 |     });
 81 |     
 82 |     it('should reject non-existent path', () => {
 83 |       const nonExistentPath = '/Users/dev/DoesNotExist.xcodeproj';
 84 |       mockExistsSync.mockReturnValue(false);
 85 |       
 86 |       expect(() => ProjectPath.create(nonExistentPath))
 87 |         .toThrow(`Project path does not exist: ${nonExistentPath}`);
 88 |       expect(mockExistsSync).toHaveBeenCalledWith(nonExistentPath);
 89 |     });
 90 |     
 91 |     it('should reject non-Xcode project files', () => {
 92 |       mockExistsSync.mockReturnValue(true);
 93 |       
 94 |       const invalidFiles = [
 95 |         '/Users/dev/MyApp.swift',
 96 |         '/Users/dev/MyApp.txt',
 97 |         '/Users/dev/MyApp.app',
 98 |         '/Users/dev/MyApp.framework',
 99 |         '/Users/dev/MyApp',  // No extension
100 |         '/Users/dev/MyApp.xcode',  // Wrong extension
101 |       ];
102 |       
103 |       invalidFiles.forEach(file => {
104 |         expect(() => ProjectPath.create(file))
105 |           .toThrow('Project path must be an .xcodeproj or .xcworkspace file');
106 |       });
107 |     });
108 |     
109 |     it('should reject directories without proper extension', () => {
110 |       const directory = '/Users/dev/MyProject';
111 |       mockExistsSync.mockReturnValue(true);
112 |       
113 |       expect(() => ProjectPath.create(directory))
114 |         .toThrow('Project path must be an .xcodeproj or .xcworkspace file');
115 |     });
116 |   });
117 |   
118 |   describe('when getting project name', () => {
119 |     it('should extract name from .xcodeproj path', () => {
120 |       const projectPath = '/Users/dev/MyAwesomeApp.xcodeproj';
121 |       mockExistsSync.mockReturnValue(true);
122 |       
123 |       const result = ProjectPath.create(projectPath);
124 |       
125 |       expect(result.name).toBe('MyAwesomeApp');
126 |     });
127 |     
128 |     it('should extract name from .xcworkspace path', () => {
129 |       const workspacePath = '/Users/dev/CoolWorkspace.xcworkspace';
130 |       mockExistsSync.mockReturnValue(true);
131 |       
132 |       const result = ProjectPath.create(workspacePath);
133 |       
134 |       expect(result.name).toBe('CoolWorkspace');
135 |     });
136 |     
137 |     it('should handle names with dots', () => {
138 |       const pathWithDots = '/Users/dev/App.v2.0.xcodeproj';
139 |       mockExistsSync.mockReturnValue(true);
140 |       
141 |       const result = ProjectPath.create(pathWithDots);
142 |       
143 |       expect(result.name).toBe('App.v2.0');
144 |     });
145 |     
146 |     it('should handle names with spaces', () => {
147 |       const pathWithSpaces = '/Users/dev/My Cool App.xcworkspace';
148 |       mockExistsSync.mockReturnValue(true);
149 |       
150 |       const result = ProjectPath.create(pathWithSpaces);
151 |       
152 |       expect(result.name).toBe('My Cool App');
153 |     });
154 |   });
155 |   
156 |   describe('when checking project type', () => {
157 |     it('should identify workspace files', () => {
158 |       const workspacePath = '/Users/dev/MyApp.xcworkspace';
159 |       mockExistsSync.mockReturnValue(true);
160 |       
161 |       const result = ProjectPath.create(workspacePath);
162 |       
163 |       expect(result.isWorkspace).toBe(true);
164 |     });
165 |     
166 |     it('should identify non-workspace files as projects', () => {
167 |       const projectPath = '/Users/dev/MyApp.xcodeproj';
168 |       mockExistsSync.mockReturnValue(true);
169 |       
170 |       const result = ProjectPath.create(projectPath);
171 |       
172 |       expect(result.isWorkspace).toBe(false);
173 |     });
174 |   });
175 |   
176 |   describe('when converting to string', () => {
177 |     it('should return the original path', () => {
178 |       const originalPath = '/Users/dev/path/to/MyApp.xcodeproj';
179 |       mockExistsSync.mockReturnValue(true);
180 |       
181 |       const result = ProjectPath.create(originalPath);
182 |       
183 |       expect(result.toString()).toBe(originalPath);
184 |       expect(`${result}`).toBe(originalPath); // Implicit string conversion
185 |     });
186 |   });
187 |   
188 |   describe('when path validation is called multiple times', () => {
189 |     it('should check existence only once during creation', () => {
190 |       const projectPath = '/Users/dev/MyApp.xcodeproj';
191 |       mockExistsSync.mockReturnValue(true);
192 |       
193 |       const result = ProjectPath.create(projectPath);
194 |       
195 |       // Access properties multiple times
196 |       result.name;
197 |       result.isWorkspace;
198 |       result.toString();
199 |       
200 |       // Should only check existence once during creation
201 |       expect(mockExistsSync).toHaveBeenCalledTimes(1);
202 |     });
203 |   });
204 | });
```

--------------------------------------------------------------------------------
/src/shared/tests/unit/ShellCommandExecutorAdapter.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  2 | import { ExecOptions } from 'child_process';
  3 | import { ShellCommandExecutorAdapter } from '../../infrastructure/ShellCommandExecutorAdapter.js';
  4 | 
  5 | /**
  6 |  * Unit tests for ShellCommandExecutor
  7 |  * 
  8 |  * Following testing philosophy:
  9 |  * - Test behavior, not implementation
 10 |  * - Use dependency injection for clean testing
 11 |  */
 12 | describe('ShellCommandExecutor', () => {
 13 |   beforeEach(() => {
 14 |     jest.clearAllMocks();
 15 |   });
 16 |   
 17 |   // Factory method for creating the SUT with mocked exec function
 18 |   function createSUT() {
 19 |     const mockExecAsync = jest.fn<(command: string, options: ExecOptions) => Promise<{ stdout: string; stderr: string }>>();
 20 |     const sut = new ShellCommandExecutorAdapter(mockExecAsync);
 21 |     return { sut, mockExecAsync };
 22 |   }
 23 |   
 24 |   describe('execute', () => {
 25 |     describe('when executing successful commands', () => {
 26 |       it('should return stdout and stderr with exitCode 0', async () => {
 27 |         // Arrange
 28 |         const { sut, mockExecAsync } = createSUT();
 29 |         const command = 'echo "hello world"';
 30 |         mockExecAsync.mockResolvedValue({ 
 31 |           stdout: 'hello world\n', 
 32 |           stderr: '' 
 33 |         });
 34 |         
 35 |         // Act
 36 |         const result = await sut.execute(command);
 37 |         
 38 |         // Assert
 39 |         expect(result).toEqual({
 40 |           stdout: 'hello world\n',
 41 |           stderr: '',
 42 |           exitCode: 0
 43 |         });
 44 |         expect(mockExecAsync).toHaveBeenCalledWith(command, expect.objectContaining({
 45 |           maxBuffer: 50 * 1024 * 1024,
 46 |           timeout: 300000,
 47 |           shell: '/bin/bash'
 48 |         }));
 49 |       });
 50 |       
 51 |       it('should handle large output', async () => {
 52 |         // Arrange
 53 |         const { sut, mockExecAsync } = createSUT();
 54 |         const command = 'cat large_file.txt';
 55 |         const largeOutput = 'x'.repeat(10 * 1024 * 1024); // 10MB
 56 |         mockExecAsync.mockResolvedValue({ 
 57 |           stdout: largeOutput, 
 58 |           stderr: '' 
 59 |         });
 60 |         
 61 |         // Act
 62 |         const result = await sut.execute(command);
 63 |         
 64 |         // Assert
 65 |         expect(result.stdout).toHaveLength(10 * 1024 * 1024);
 66 |         expect(result.exitCode).toBe(0);
 67 |       });
 68 |       
 69 |       it('should handle both stdout and stderr', async () => {
 70 |         // Arrange
 71 |         const { sut, mockExecAsync } = createSUT();
 72 |         const command = 'some-command';
 73 |         mockExecAsync.mockResolvedValue({ 
 74 |           stdout: 'standard output', 
 75 |           stderr: 'warning: something happened' 
 76 |         });
 77 |         
 78 |         // Act
 79 |         const result = await sut.execute(command);
 80 |         
 81 |         // Assert
 82 |         expect(result.stdout).toBe('standard output');
 83 |         expect(result.stderr).toBe('warning: something happened');
 84 |         expect(result.exitCode).toBe(0);
 85 |       });
 86 |     });
 87 |     
 88 |     describe('when executing failing commands', () => {
 89 |       it('should return output with non-zero exit code', async () => {
 90 |         // Arrange
 91 |         const { sut, mockExecAsync } = createSUT();
 92 |         const command = 'false';
 93 |         const error: any = new Error('Command failed');
 94 |         error.code = 1;
 95 |         error.stdout = '';
 96 |         error.stderr = 'command failed';
 97 |         mockExecAsync.mockRejectedValue(error);
 98 |         
 99 |         // Act
100 |         const result = await sut.execute(command);
101 |         
102 |         // Assert
103 |         expect(result).toEqual({
104 |           stdout: '',
105 |           stderr: 'command failed',
106 |           exitCode: 1
107 |         });
108 |       });
109 |       
110 |       it('should capture output even on failure', async () => {
111 |         // Arrange
112 |         const { sut, mockExecAsync } = createSUT();
113 |         const command = 'build-command';
114 |         const error: any = new Error('Build failed');
115 |         error.code = 65;
116 |         error.stdout = 'Compiling...\nError at line 42';
117 |         error.stderr = 'error: undefined symbol';
118 |         mockExecAsync.mockRejectedValue(error);
119 |         
120 |         // Act
121 |         const result = await sut.execute(command);
122 |         
123 |         // Assert
124 |         expect(result.stdout).toContain('Compiling');
125 |         expect(result.stderr).toContain('undefined symbol');
126 |         expect(result.exitCode).toBe(65);
127 |       });
128 |       
129 |       it('should handle missing error code', async () => {
130 |         // Arrange
131 |         const { sut, mockExecAsync } = createSUT();
132 |         const command = 'unknown-command';
133 |         const error: any = new Error('Command not found');
134 |         // No error.code set
135 |         error.stdout = '';
136 |         error.stderr = 'command not found';
137 |         mockExecAsync.mockRejectedValue(error);
138 |         
139 |         // Act
140 |         const result = await sut.execute(command);
141 |         
142 |         // Assert
143 |         expect(result.exitCode).toBe(1); // Default to 1 when no code
144 |       });
145 |     });
146 |     
147 |     describe('with custom options', () => {
148 |       it('should pass maxBuffer option', async () => {
149 |         // Arrange
150 |         const { sut, mockExecAsync } = createSUT();
151 |         const command = 'echo test';
152 |         const options = { maxBuffer: 100 * 1024 * 1024 }; // 100MB
153 |         mockExecAsync.mockResolvedValue({ stdout: 'test', stderr: '' });
154 |         
155 |         // Act
156 |         await sut.execute(command, options);
157 |         
158 |         // Assert
159 |         expect(mockExecAsync).toHaveBeenCalledWith(command, expect.objectContaining({
160 |           maxBuffer: 100 * 1024 * 1024
161 |         }));
162 |       });
163 |       
164 |       it('should pass timeout option', async () => {
165 |         // Arrange
166 |         const { sut, mockExecAsync } = createSUT();
167 |         const command = 'long-running-command';
168 |         const options = { timeout: 600000 }; // 10 minutes
169 |         mockExecAsync.mockResolvedValue({ stdout: 'done', stderr: '' });
170 |         
171 |         // Act
172 |         await sut.execute(command, options);
173 |         
174 |         // Assert
175 |         expect(mockExecAsync).toHaveBeenCalledWith(command, expect.objectContaining({
176 |           timeout: 600000
177 |         }));
178 |       });
179 |       
180 |       it('should pass shell option', async () => {
181 |         // Arrange
182 |         const { sut, mockExecAsync } = createSUT();
183 |         const command = 'echo $SHELL';
184 |         const options = { shell: '/bin/zsh' };
185 |         mockExecAsync.mockResolvedValue({ stdout: '/bin/zsh', stderr: '' });
186 |         
187 |         // Act
188 |         await sut.execute(command, options);
189 |         
190 |         // Assert
191 |         expect(mockExecAsync).toHaveBeenCalledWith(command, expect.objectContaining({
192 |           shell: '/bin/zsh'
193 |         }));
194 |       });
195 |       
196 |       it('should use default options when not provided', async () => {
197 |         // Arrange
198 |         const { sut, mockExecAsync } = createSUT();
199 |         const command = 'echo test';
200 |         mockExecAsync.mockResolvedValue({ stdout: 'test', stderr: '' });
201 |         
202 |         // Act
203 |         await sut.execute(command);
204 |         
205 |         // Assert
206 |         expect(mockExecAsync).toHaveBeenCalledWith(command, {
207 |           maxBuffer: 50 * 1024 * 1024,
208 |           timeout: 300000,
209 |           shell: '/bin/bash'
210 |         });
211 |       });
212 |     });
213 |   });
214 | });
```

--------------------------------------------------------------------------------
/src/utils/devices/SimulatorBoot.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { execAsync } from '../../utils.js';
  2 | import { createModuleLogger } from '../../logger.js';
  3 | 
  4 | const logger = createModuleLogger('SimulatorBoot');
  5 | 
  6 | /**
  7 |  * Simple utility to boot simulators for Xcode builds
  8 |  * Extracted shared logic from BuildXcodeTool and RunXcodeTool
  9 |  */
 10 | export class SimulatorBoot {
 11 |   /**
 12 |    * Boot a simulator
 13 |    */
 14 |   async boot(deviceId: string): Promise<void> {
 15 |     try {
 16 |       await execAsync(`xcrun simctl boot "${deviceId}"`);
 17 |       logger.debug({ deviceId }, 'Simulator booted successfully');
 18 |     } catch (error: any) {
 19 |       // Check if already booted
 20 |       if (!error.message?.includes('Unable to boot device in current state: Booted')) {
 21 |         logger.error({ error: error.message, deviceId }, 'Failed to boot simulator');
 22 |         throw new Error(`Failed to boot simulator: ${error.message}`);
 23 |       }
 24 |       logger.debug({ deviceId }, 'Simulator already booted');
 25 |     }
 26 |   }
 27 | 
 28 |   /**
 29 |    * Shutdown a simulator
 30 |    */
 31 |   async shutdown(deviceId: string): Promise<void> {
 32 |     try {
 33 |       await execAsync(`xcrun simctl shutdown "${deviceId}"`);
 34 |       logger.debug({ deviceId }, 'Simulator shutdown successfully');
 35 |     } catch (error: any) {
 36 |       logger.error({ error: error.message, deviceId }, 'Failed to shutdown simulator');
 37 |       throw new Error(`Failed to shutdown simulator: ${error.message}`);
 38 |     }
 39 |   }
 40 | 
 41 |   /**
 42 |    * Opens the Simulator app GUI (skipped during tests)
 43 |    */
 44 |   async openSimulatorApp(): Promise<void> {
 45 |     // Skip opening GUI during tests
 46 |     if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID) {
 47 |       logger.debug('Skipping Simulator GUI in test environment');
 48 |       return;
 49 |     }
 50 |     
 51 |     try {
 52 |       await execAsync('open -g -a Simulator');
 53 |       logger.debug('Opened Simulator app');
 54 |     } catch (error: any) {
 55 |       logger.warn({ error: error.message }, 'Failed to open Simulator app');
 56 |     }
 57 |   }
 58 | 
 59 |   /**
 60 |    * Ensures a simulator is booted for the given platform and device
 61 |    * Returns the booted device ID (UDID)
 62 |    */
 63 |   async ensureBooted(platform: string, deviceId?: string): Promise<string> {
 64 |     if (!deviceId) {
 65 |       // No specific device requested, use first available
 66 |       return this.bootFirstAvailable(platform);
 67 |     }
 68 | 
 69 |     // Check if the device exists and get its state
 70 |     try {
 71 |       const { stdout } = await execAsync('xcrun simctl list devices --json');
 72 |       const data = JSON.parse(stdout);
 73 |       
 74 |       // Collect all matching devices first, then pick the best one
 75 |       const matchingDevices: any[] = [];
 76 |       
 77 |       for (const deviceList of Object.values(data.devices)) {
 78 |         for (const device of deviceList as any[]) {
 79 |           if (device.udid === deviceId || device.name === deviceId) {
 80 |             matchingDevices.push(device);
 81 |           }
 82 |         }
 83 |       }
 84 |       
 85 |       if (matchingDevices.length === 0) {
 86 |         throw new Error(`Device '${deviceId}' not found`);
 87 |       }
 88 |       
 89 |       // Sort devices: prefer available ones, then already booted ones
 90 |       matchingDevices.sort((a, b) => {
 91 |         // First priority: available devices
 92 |         if (a.isAvailable && !b.isAvailable) return -1;
 93 |         if (!a.isAvailable && b.isAvailable) return 1;
 94 |         // Second priority: booted devices
 95 |         if (a.state === 'Booted' && b.state !== 'Booted') return -1;
 96 |         if (a.state !== 'Booted' && b.state === 'Booted') return 1;
 97 |         return 0;
 98 |       });
 99 |       
100 |       // Use the best matching device
101 |       const device = matchingDevices[0];
102 |       
103 |       if (!device.isAvailable) {
104 |         // All matching devices are unavailable
105 |         const availableErrors = matchingDevices
106 |           .map(d => `${d.name} (${d.udid}): ${d.availabilityError || 'unavailable'}`)
107 |           .join(', ');
108 |         throw new Error(`All devices named '${deviceId}' are unavailable: ${availableErrors}`);
109 |       }
110 |       
111 |       if (device.state === 'Booted') {
112 |         logger.debug({ deviceId: device.udid, name: device.name }, 'Device already booted');
113 |         // Still open the Simulator app to make it visible
114 |         await this.openSimulatorApp();
115 |         return device.udid;
116 |       }
117 |       
118 |       // Boot the device
119 |       logger.info({ deviceId: device.udid, name: device.name }, 'Booting simulator');
120 |       try {
121 |         await execAsync(`xcrun simctl boot "${device.udid}"`);
122 |       } catch (error: any) {
123 |         // Device might already be booted
124 |         if (!error.message?.includes('Unable to boot device in current state: Booted')) {
125 |           throw error;
126 |         }
127 |       }
128 |       
129 |       // Open the Simulator app to show the GUI
130 |       await this.openSimulatorApp();
131 |       
132 |       return device.udid;
133 |     } catch (error: any) {
134 |       logger.error({ error: error.message, deviceId }, 'Failed to boot simulator');
135 |       throw new Error(`Failed to boot simulator: ${error.message}`);
136 |     }
137 |   }
138 | 
139 |   /**
140 |    * Boots the first available simulator for a platform
141 |    */
142 |   private async bootFirstAvailable(platform: string): Promise<string> {
143 |     try {
144 |       const { stdout } = await execAsync('xcrun simctl list devices --json');
145 |       const data = JSON.parse(stdout);
146 |       
147 |       // Find first available device for the platform
148 |       for (const [runtime, deviceList] of Object.entries(data.devices)) {
149 |         const runtimeLower = runtime.toLowerCase();
150 |         const platformLower = platform.toLowerCase();
151 |         
152 |         // Handle visionOS which is internally called xrOS
153 |         const isVisionOS = platformLower === 'visionos' && runtimeLower.includes('xros');
154 |         const isOtherPlatform = platformLower !== 'visionos' && runtimeLower.includes(platformLower);
155 |         
156 |         if (!isVisionOS && !isOtherPlatform) {
157 |           continue;
158 |         }
159 |         
160 |         for (const device of deviceList as any[]) {
161 |           if (!device.isAvailable) continue;
162 |           
163 |           // Check if already booted
164 |           if (device.state === 'Booted') {
165 |             logger.debug({ deviceId: device.udid, name: device.name }, 'Using already booted device');
166 |             // Still open the Simulator app to make it visible
167 |             await this.openSimulatorApp();
168 |             return device.udid;
169 |           }
170 |           
171 |           // Boot this device
172 |           logger.info({ deviceId: device.udid, name: device.name }, 'Booting first available simulator');
173 |           try {
174 |             await execAsync(`xcrun simctl boot ${device.udid}`);
175 |           } catch (error: any) {
176 |             // Device might already be booted
177 |             if (!error.message?.includes('Unable to boot device in current state: Booted')) {
178 |               throw error;
179 |             }
180 |           }
181 |           
182 |           // Open the Simulator app to show the GUI
183 |           await this.openSimulatorApp();
184 |           
185 |           return device.udid;
186 |         }
187 |       }
188 |       
189 |       throw new Error(`No available simulators found for platform ${platform}`);
190 |     } catch (error: any) {
191 |       logger.error({ error: error.message, platform }, 'Failed to boot simulator');
192 |       throw new Error(`Failed to boot simulator: ${error.message}`);
193 |     }
194 |   }
195 | }
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/unit/BootSimulatorUseCase.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  2 | import { BootSimulatorUseCase } from '../../use-cases/BootSimulatorUseCase.js';
  3 | import { BootRequest } from '../../domain/BootRequest.js';
  4 | import { DeviceId } from '../../../../shared/domain/DeviceId.js';
  5 | import { BootResult, BootOutcome, SimulatorNotFoundError, BootCommandFailedError, SimulatorBusyError } from '../../domain/BootResult.js';
  6 | import { SimulatorState } from '../../domain/SimulatorState.js';
  7 | import { ISimulatorLocator, ISimulatorControl, SimulatorInfo } from '../../../../application/ports/SimulatorPorts.js';
  8 | 
  9 | describe('BootSimulatorUseCase', () => {
 10 |   let useCase: BootSimulatorUseCase;
 11 |   let mockLocator: jest.Mocked<ISimulatorLocator>;
 12 |   let mockControl: jest.Mocked<ISimulatorControl>;
 13 | 
 14 |   beforeEach(() => {
 15 |     jest.clearAllMocks();
 16 |     
 17 |     mockLocator = {
 18 |       findSimulator: jest.fn<ISimulatorLocator['findSimulator']>(),
 19 |       findBootedSimulator: jest.fn<ISimulatorLocator['findBootedSimulator']>()
 20 |     };
 21 |     
 22 |     mockControl = {
 23 |       boot: jest.fn<ISimulatorControl['boot']>(),
 24 |       shutdown: jest.fn<ISimulatorControl['shutdown']>()
 25 |     };
 26 |     
 27 |     useCase = new BootSimulatorUseCase(mockLocator, mockControl);
 28 |   });
 29 | 
 30 |   describe('execute', () => {
 31 |     it('should boot a shutdown simulator', async () => {
 32 |       // Arrange
 33 |       const request = BootRequest.create(DeviceId.create('iPhone-15'));
 34 |       const simulatorInfo: SimulatorInfo = {
 35 |         id: 'ABC123',
 36 |         name: 'iPhone 15',
 37 |         state: SimulatorState.Shutdown,
 38 |         platform: 'iOS',
 39 |         runtime: 'iOS-17.0'
 40 |       };
 41 |       
 42 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
 43 |       mockControl.boot.mockResolvedValue(undefined);
 44 | 
 45 |       // Act
 46 |       const result = await useCase.execute(request);
 47 | 
 48 |       // Assert
 49 |       expect(mockLocator.findSimulator).toHaveBeenCalledWith('iPhone-15');
 50 |       expect(mockControl.boot).toHaveBeenCalledWith('ABC123');
 51 |       expect(result.outcome).toBe(BootOutcome.Booted);
 52 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
 53 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
 54 |       expect(result.diagnostics.platform).toBe('iOS');
 55 |       expect(result.diagnostics.runtime).toBe('iOS-17.0');
 56 |     });
 57 | 
 58 |     it('should handle already booted simulator', async () => {
 59 |       // Arrange
 60 |       const request = BootRequest.create(DeviceId.create('iPhone-15'));
 61 |       const simulatorInfo: SimulatorInfo = {
 62 |         id: 'ABC123',
 63 |         name: 'iPhone 15',
 64 |         state: SimulatorState.Booted,
 65 |         platform: 'iOS',
 66 |         runtime: 'iOS-17.0'
 67 |       };
 68 |       
 69 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
 70 | 
 71 |       // Act
 72 |       const result = await useCase.execute(request);
 73 | 
 74 |       // Assert
 75 |       expect(mockControl.boot).not.toHaveBeenCalled();
 76 |       expect(result.outcome).toBe(BootOutcome.AlreadyBooted);
 77 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
 78 |     });
 79 | 
 80 |     it('should return failure when simulator not found', async () => {
 81 |       // Arrange
 82 |       const request = BootRequest.create(DeviceId.create('non-existent'));
 83 |       mockLocator.findSimulator.mockResolvedValue(null);
 84 | 
 85 |       // Act
 86 |       const result = await useCase.execute(request);
 87 |       
 88 |       // Assert - Test behavior: simulator not found error
 89 |       expect(mockControl.boot).not.toHaveBeenCalled();
 90 |       expect(result.outcome).toBe(BootOutcome.Failed);
 91 |       expect(result.diagnostics.error).toBeInstanceOf(SimulatorNotFoundError);
 92 |       expect((result.diagnostics.error as SimulatorNotFoundError).deviceId).toBe('non-existent');
 93 |     });
 94 | 
 95 |     it('should return failure on boot error', async () => {
 96 |       // Arrange
 97 |       const request = BootRequest.create(DeviceId.create('iPhone-15'));
 98 |       const simulatorInfo: SimulatorInfo = {
 99 |         id: 'ABC123',
100 |         name: 'iPhone 15',
101 |         state: SimulatorState.Shutdown,
102 |         platform: 'iOS',
103 |         runtime: 'iOS-17.0'
104 |       };
105 |       
106 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
107 |       mockControl.boot.mockRejectedValue(new Error('Boot failed'));
108 | 
109 |       // Act
110 |       const result = await useCase.execute(request);
111 | 
112 |       // Assert - Test behavior: boot command failed error
113 |       expect(result.outcome).toBe(BootOutcome.Failed);
114 |       expect(result.diagnostics.error).toBeInstanceOf(BootCommandFailedError);
115 |       expect((result.diagnostics.error as BootCommandFailedError).stderr).toBe('Boot failed');
116 |     });
117 | 
118 |     it('should boot simulator found by UUID', async () => {
119 |       // Arrange
120 |       const uuid = '838C707D-5703-4AEE-AF43-4798E0BA1B05';
121 |       const request = BootRequest.create(DeviceId.create(uuid));
122 |       const simulatorInfo: SimulatorInfo = {
123 |         id: uuid,
124 |         name: 'iPhone 15',
125 |         state: SimulatorState.Shutdown,
126 |         platform: 'iOS',
127 |         runtime: 'iOS-17.0'
128 |       };
129 |       
130 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
131 |       mockControl.boot.mockResolvedValue(undefined);
132 | 
133 |       // Act
134 |       const result = await useCase.execute(request);
135 | 
136 |       // Assert
137 |       expect(mockLocator.findSimulator).toHaveBeenCalledWith(uuid);
138 |       expect(mockControl.boot).toHaveBeenCalledWith(uuid);
139 |       expect(result.outcome).toBe(BootOutcome.Booted);
140 |       expect(result.diagnostics.simulatorId).toBe(uuid);
141 |     });
142 | 
143 |     it('should handle simulator in Booting state as already booted', async () => {
144 |       // Arrange
145 |       const request = BootRequest.create(DeviceId.create('iPhone-15'));
146 |       const simulatorInfo: SimulatorInfo = {
147 |         id: 'ABC123',
148 |         name: 'iPhone 15',
149 |         state: SimulatorState.Booting,
150 |         platform: 'iOS',
151 |         runtime: 'iOS-17.0'
152 |       };
153 |       
154 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
155 | 
156 |       // Act
157 |       const result = await useCase.execute(request);
158 | 
159 |       // Assert
160 |       expect(mockControl.boot).not.toHaveBeenCalled();
161 |       expect(result.outcome).toBe(BootOutcome.AlreadyBooted);
162 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
163 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
164 |     });
165 | 
166 |     it('should return failure when simulator is ShuttingDown', async () => {
167 |       // Arrange
168 |       const request = BootRequest.create(DeviceId.create('iPhone-15'));
169 |       const simulatorInfo: SimulatorInfo = {
170 |         id: 'ABC123',
171 |         name: 'iPhone 15',
172 |         state: SimulatorState.ShuttingDown,
173 |         platform: 'iOS',
174 |         runtime: 'iOS-17.0'
175 |       };
176 |       
177 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
178 | 
179 |       // Act
180 |       const result = await useCase.execute(request);
181 | 
182 |       // Assert
183 |       expect(mockControl.boot).not.toHaveBeenCalled();
184 |       expect(result.outcome).toBe(BootOutcome.Failed);
185 |       expect(result.diagnostics.error).toBeInstanceOf(SimulatorBusyError);
186 |       expect((result.diagnostics.error as SimulatorBusyError).currentState).toBe(SimulatorState.ShuttingDown);
187 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
188 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
189 |     });
190 | 
191 |   });
192 | });
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/e2e/ListSimulatorsController.e2e.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * E2E Test for ListSimulatorsController
  3 |  *
  4 |  * Tests CRITICAL USER PATH with REAL simulators:
  5 |  * - Can the controller actually list real simulators?
  6 |  * - Does filtering work with real simulator data?
  7 |  * - Does error handling work with real failures?
  8 |  *
  9 |  * NO MOCKS - uses real simulators
 10 |  * This is an E2E test (10% of test suite) for critical user journeys
 11 |  *
 12 |  * NOTE: This test requires Xcode and iOS simulators to be installed
 13 |  */
 14 | 
 15 | import { describe, it, expect, beforeAll, beforeEach } from '@jest/globals';
 16 | import { MCPController } from '../../../../presentation/interfaces/MCPController.js';
 17 | import { ListSimulatorsControllerFactory } from '../../factories/ListSimulatorsControllerFactory.js';
 18 | import { exec } from 'child_process';
 19 | import { promisify } from 'util';
 20 | 
 21 | const execAsync = promisify(exec);
 22 | 
 23 | describe('ListSimulatorsController E2E', () => {
 24 |   let controller: MCPController;
 25 | 
 26 |   beforeAll(() => {
 27 |     controller = ListSimulatorsControllerFactory.create();
 28 |   });
 29 | 
 30 |   describe('list real simulators', () => {
 31 |     it('should list all available simulators', async () => {
 32 |       // Arrange, Act, Assert
 33 |       const result = await controller.execute({});
 34 | 
 35 |       expect(result).toMatchObject({
 36 |         content: expect.arrayContaining([
 37 |           expect.objectContaining({
 38 |             type: 'text',
 39 |             text: expect.any(String)
 40 |           })
 41 |         ])
 42 |       });
 43 | 
 44 |       const text = result.content[0].text;
 45 |       expect(text).toMatch(/Found \d+ simulator/);
 46 | 
 47 |       // Verify actual device lines exist
 48 |       const deviceLines = text.split('\n').filter((line: string) =>
 49 |         line.includes('(') && line.includes(')') && line.includes('-')
 50 |       );
 51 |       expect(deviceLines.length).toBeGreaterThan(0);
 52 |     });
 53 | 
 54 |     it('should filter by iOS platform', async () => {
 55 |       // Arrange, Act, Assert
 56 |       const result = await controller.execute({ platform: 'iOS' });
 57 | 
 58 |       const text = result.content[0].text;
 59 |       expect(text).toMatch(/Found \d+ simulator/);
 60 | 
 61 |       const deviceLines = text.split('\n').filter((line: string) =>
 62 |         line.includes('(') && line.includes(')') && line.includes('-')
 63 |       );
 64 | 
 65 |       expect(deviceLines.length).toBeGreaterThan(0);
 66 |       for (const line of deviceLines) {
 67 |         // All devices should show iOS runtime since we filtered by iOS platform
 68 |         expect(line).toContain(' - iOS ');
 69 |         // Should not contain other platform devices
 70 |         expect(line).not.toMatch(/Apple TV|Apple Watch/);
 71 |       }
 72 |     });
 73 | 
 74 |     it('should filter by booted state', async () => {
 75 |       // First, check if there are any booted simulators
 76 |       const checkResult = await execAsync('xcrun simctl list devices booted --json');
 77 |       const bootedDevices = JSON.parse(checkResult.stdout);
 78 |       const hasBootedDevices = Object.values(bootedDevices.devices).some(
 79 |         (devices: any) => devices.length > 0
 80 |       );
 81 | 
 82 |       // Act
 83 |       const result = await controller.execute({ state: 'Booted' });
 84 | 
 85 |       // Assert
 86 |       const text = result.content[0].text;
 87 | 
 88 |       if (hasBootedDevices) {
 89 |         expect(text).toMatch(/Found \d+ simulator/);
 90 |         const lines = text.split('\n');
 91 |         const deviceLines = lines.filter((line: string) =>
 92 |           line.includes('(') && line.includes(')') && line.includes('-')
 93 |         );
 94 | 
 95 |         for (const line of deviceLines) {
 96 |           expect(line).toContain('Booted');
 97 |         }
 98 |       } else {
 99 |         expect(text).toBe('🔍 No simulators found');
100 |       }
101 |     });
102 | 
103 |     it('should show runtime information', async () => {
104 |       // Arrange, Act, Assert
105 |       const result = await controller.execute({});
106 | 
107 |       const text = result.content[0].text;
108 |       expect(text).toMatch(/Found \d+ simulator/);
109 | 
110 |       const deviceLines = text.split('\n').filter((line: string) =>
111 |         line.includes('(') && line.includes(')') && line.includes('-')
112 |       );
113 | 
114 |       expect(deviceLines.length).toBeGreaterThan(0);
115 |       for (const line of deviceLines) {
116 |         expect(line).toMatch(/iOS \d+\.\d+|tvOS \d+\.\d+|watchOS \d+\.\d+|visionOS \d+\.\d+/);
117 |       }
118 |     });
119 | 
120 |     it('should filter by tvOS platform', async () => {
121 |       // Arrange, Act, Assert
122 |       const result = await controller.execute({ platform: 'tvOS' });
123 | 
124 |       const text = result.content[0].text;
125 | 
126 |       // tvOS simulators might not exist in all environments
127 |       if (text.includes('No simulators found')) {
128 |         expect(text).toBe('🔍 No simulators found');
129 |       } else {
130 |         expect(text).toMatch(/Found \d+ simulator/);
131 |         const deviceLines = text.split('\n').filter((line: string) =>
132 |           line.includes('(') && line.includes(')') && line.includes('-')
133 |         );
134 | 
135 |         for (const line of deviceLines) {
136 |           expect(line).toContain('Apple TV');
137 |           expect(line).toContain(' - tvOS ');
138 |         }
139 |       }
140 |     });
141 | 
142 |     it('should handle combined filters', async () => {
143 |       // Arrange, Act, Assert
144 |       const result = await controller.execute({
145 |         platform: 'iOS',
146 |         state: 'Shutdown'
147 |       });
148 | 
149 |       const text = result.content[0].text;
150 |       expect(text).toMatch(/Found \d+ simulator/);
151 | 
152 |       const deviceLines = text.split('\n').filter((line: string) =>
153 |         line.includes('(') && line.includes(')') && line.includes('-')
154 |       );
155 | 
156 |       expect(deviceLines.length).toBeGreaterThan(0);
157 |       for (const line of deviceLines) {
158 |         expect(line).toContain(' - iOS ');
159 |         expect(line).toContain('Shutdown');
160 |       }
161 |     });
162 |   });
163 | 
164 |   describe('error handling', () => {
165 |     it('should return error for invalid platform', async () => {
166 |       // Arrange, Act, Assert
167 |       const result = await controller.execute({
168 |         platform: 'Android'
169 |       });
170 | 
171 |       expect(result.content[0].text).toBe('❌ Invalid platform: Android. Valid values are: iOS, macOS, tvOS, watchOS, visionOS');
172 |     });
173 | 
174 |     it('should return error for invalid state', async () => {
175 |       // Arrange, Act, Assert
176 |       const result = await controller.execute({
177 |         state: 'Running'
178 |       });
179 | 
180 |       expect(result.content[0].text).toBe('❌ Invalid simulator state: Running. Valid values are: Booted, Booting, Shutdown, Shutting Down');
181 |     });
182 | 
183 |     it('should return error for invalid input types', async () => {
184 |       // Arrange, Act, Assert
185 |       const result1 = await controller.execute({
186 |         platform: 123
187 |       });
188 | 
189 |       expect(result1.content[0].text).toBe('❌ Platform must be a string (one of: iOS, macOS, tvOS, watchOS, visionOS), got number');
190 | 
191 |       const result2 = await controller.execute({
192 |         state: true
193 |       });
194 | 
195 |       expect(result2.content[0].text).toBe('❌ Simulator state must be a string (one of: Booted, Booting, Shutdown, Shutting Down), got boolean');
196 |     });
197 |   });
198 | 
199 |   describe('output formatting', () => {
200 |     it('should format simulator list properly', async () => {
201 |       // Arrange, Act, Assert
202 |       const result = await controller.execute({});
203 | 
204 |       const text = result.content[0].text;
205 |       expect(text).toMatch(/^✅ Found \d+ simulator/);
206 | 
207 |       const lines = text.split('\n');
208 |       expect(lines[0]).toMatch(/^✅ Found \d+ simulator/);
209 |       expect(lines[1]).toBe('');
210 | 
211 |       const deviceLines = lines.filter((line: string) =>
212 |         line.includes('(') && line.includes(')') && line.includes('-')
213 |       );
214 | 
215 |       expect(deviceLines.length).toBeGreaterThan(0);
216 |       for (const line of deviceLines) {
217 |         expect(line).toMatch(/^• .+ \([A-F0-9-]+\) - (Booted|Booting|Shutdown|Shutting Down|Unknown) - (iOS|tvOS|watchOS|visionOS|macOS|Unknown) \d+\.\d+$/);
218 |       }
219 |     });
220 | 
221 |     it('should use warning emoji for no results', async () => {
222 |       // Act - filter that likely returns no results
223 |       const result = await controller.execute({
224 |         platform: 'visionOS',
225 |         state: 'Booted'
226 |       });
227 | 
228 |       // Assert
229 |       const text = result.content[0].text;
230 | 
231 |       if (text.includes('No simulators found')) {
232 |         expect(text).toBe('🔍 No simulators found');
233 |       }
234 |     });
235 |   });
236 | });
```

--------------------------------------------------------------------------------
/src/utils/errors/xcbeautify-parser.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Unified parser for xcbeautify output
  3 |  * 
  4 |  * This replaces all the complex parsers and handlers since all our output
  5 |  * goes through xcbeautify which already formats it nicely.
  6 |  * 
  7 |  * xcbeautify output format:
  8 |  * - ❌ for errors
  9 |  * - ⚠️ for warnings
 10 |  * - ✔ for test passes
 11 |  * - ✖ for test failures
 12 |  */
 13 | 
 14 | import { createModuleLogger } from '../../logger.js';
 15 | 
 16 | const logger = createModuleLogger('XcbeautifyParser');
 17 | 
 18 | export interface Issue {
 19 |   type: 'error' | 'warning';
 20 |   file?: string;
 21 |   line?: number;
 22 |   column?: number;
 23 |   message: string;
 24 |   rawLine: string;
 25 | }
 26 | 
 27 | export interface Test {
 28 |   name: string;
 29 |   passed: boolean;
 30 |   duration?: number;
 31 |   failureReason?: string;
 32 | }
 33 | 
 34 | export interface XcbeautifyOutput {
 35 |   errors: Issue[];
 36 |   warnings: Issue[];
 37 |   tests: Test[];
 38 |   buildSucceeded: boolean;
 39 |   testsPassed: boolean;
 40 |   totalTests: number;
 41 |   failedTests: number;
 42 | }
 43 | 
 44 | 
 45 | /**
 46 |  * Parse a line with error or warning from xcbeautify
 47 |  */
 48 | function parseErrorLine(line: string, isError: boolean): Issue {
 49 |   // Remove the emoji prefix (❌ or ⚠️) and any color codes
 50 |   const cleanLine = line
 51 |     .replace(/^[❌⚠]\s*/, '')  // Character class with individual emojis
 52 |     .replace(/^️\s*/, '')       // Remove any lingering emoji variation selectors
 53 |     .replace(/\x1b\[[0-9;]*m/g, ''); // Remove ANSI color codes
 54 |   
 55 |   // Try to extract file:line:column information
 56 |   // Format: /path/to/file.swift:10:15: error message
 57 |   const fileMatch = cleanLine.match(/^([^:]+):(\d+):(\d+):\s*(.*)$/);
 58 |   
 59 |   if (fileMatch) {
 60 |     const [, file, lineStr, columnStr, message] = fileMatch;
 61 |     
 62 |     return {
 63 |       type: isError ? 'error' : 'warning',
 64 |       file,
 65 |       line: parseInt(lineStr, 10),
 66 |       column: parseInt(columnStr, 10),
 67 |       message,
 68 |       rawLine: line
 69 |     };
 70 |   }
 71 |   
 72 |   // No file information
 73 |   return {
 74 |     type: isError ? 'error' : 'warning',
 75 |     message: cleanLine,
 76 |     rawLine: line
 77 |   };
 78 | }
 79 | 
 80 | /**
 81 |  * Parse test results from xcbeautify output
 82 |  */
 83 | function parseTestLine(line: string): Test | null {
 84 |   // Remove color codes
 85 |   const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, '');
 86 |   
 87 |   // Test passed: ✔ testName (0.123 seconds)
 88 |   const passMatch = cleanLine.match(/✔\s+(\w+)\s*\(([0-9.]+)\s+seconds?\)/);
 89 |   if (passMatch) {
 90 |     return {
 91 |       name: passMatch[1],
 92 |       passed: true,
 93 |       duration: parseFloat(passMatch[2])
 94 |     };
 95 |   }
 96 |   
 97 |   // Test failed: ✖ testName, failure reason
 98 |   const failMatch = cleanLine.match(/✖\s+(\w+)(?:,\s*(.*))?/);
 99 |   if (failMatch) {
100 |     return {
101 |       name: failMatch[1],
102 |       passed: false,
103 |       failureReason: failMatch[2] || 'Test failed'
104 |     };
105 |   }
106 |   
107 |   // XCTest format: Test Case '-[ClassName testName]' passed/failed (X.XXX seconds)
108 |   const xcTestMatch = cleanLine.match(/Test Case\s+'-\[(\w+)\s+(\w+)\]'\s+(passed|failed)\s*\(([0-9.]+)\s+seconds\)/);
109 |   if (xcTestMatch) {
110 |     return {
111 |       name: `${xcTestMatch[1]}.${xcTestMatch[2]}`,
112 |       passed: xcTestMatch[3] === 'passed',
113 |       duration: parseFloat(xcTestMatch[4])
114 |     };
115 |   }
116 |   
117 |   return null;
118 | }
119 | 
120 | /**
121 |  * Main parser for xcbeautify output
122 |  */
123 | export function parseXcbeautifyOutput(output: string): XcbeautifyOutput {
124 |   const lines = output.split('\n');
125 |   
126 |   // Use Maps to deduplicate errors/warnings (for multi-architecture builds)
127 |   const errorMap = new Map<string, Issue>();
128 |   const warningMap = new Map<string, Issue>();
129 |   
130 |   let buildSucceeded = true;
131 |   let testsPassed = true;
132 |   let totalTests = 0;
133 |   let failedTests = 0;
134 |   const tests: Test[] = [];
135 |   
136 |   for (const line of lines) {
137 |     if (!line.trim()) continue;
138 |     
139 |     // Skip xcbeautify header
140 |     if (line.includes('xcbeautify') || line.startsWith('---') || line.startsWith('Version:')) {
141 |       continue;
142 |     }
143 |     
144 |     // Parse errors (❌)
145 |     if (line.includes('❌')) {
146 |       const error = parseErrorLine(line, true);
147 |       const key = `${error.file}:${error.line}:${error.column}:${error.message}`;
148 |       errorMap.set(key, error);
149 |       buildSucceeded = false;
150 |     }
151 |     // Parse warnings (⚠️)
152 |     else if (line.includes('⚠️')) {
153 |       const warning = parseErrorLine(line, false);
154 |       const key = `${warning.file}:${warning.line}:${warning.column}:${warning.message}`;
155 |       warningMap.set(key, warning);
156 |     }
157 |     // Parse test results (✔ or ✖)
158 |     else if (line.includes('✔') || line.includes('✖')) {
159 |       const test = parseTestLine(line);
160 |       if (test) {
161 |         tests.push(test);
162 |         totalTests++;
163 |         if (!test.passed) {
164 |           failedTests++;
165 |           testsPassed = false;
166 |         }
167 |       }
168 |     }
169 |     // Check for build/test failure indicators
170 |     else if (line.includes('** BUILD FAILED **') || line.includes('BUILD FAILED')) {
171 |       buildSucceeded = false;
172 |     }
173 |     else if (line.includes('** TEST FAILED **') || line.includes('TEST FAILED')) {
174 |       testsPassed = false;
175 |     }
176 |     // Parse test summary: "Executed X tests, with Y failures"
177 |     else if (line.includes('Executed') && line.includes('test')) {
178 |       const summaryMatch = line.match(/Executed\s+(\d+)\s+tests?,\s+with\s+(\d+)\s+failures?/);
179 |       if (summaryMatch) {
180 |         totalTests = parseInt(summaryMatch[1], 10);
181 |         failedTests = parseInt(summaryMatch[2], 10);
182 |         testsPassed = failedTests === 0;
183 |       }
184 |     }
185 |   }
186 |   
187 |   // Convert Maps to arrays
188 |   const result: XcbeautifyOutput = {
189 |     errors: Array.from(errorMap.values()),
190 |     warnings: Array.from(warningMap.values()),
191 |     tests,
192 |     buildSucceeded,
193 |     testsPassed,
194 |     totalTests,
195 |     failedTests
196 |   };
197 |   
198 |   // Log summary
199 |   logger.debug({
200 |     errors: result.errors.length,
201 |     warnings: result.warnings.length,
202 |     tests: result.tests.length,
203 |     buildSucceeded: result.buildSucceeded,
204 |     testsPassed: result.testsPassed
205 |   }, 'Parsed xcbeautify output');
206 |   
207 |   return result;
208 | }
209 | 
210 | /**
211 |  * Format parsed output for display
212 |  */
213 | export function formatParsedOutput(parsed: XcbeautifyOutput): string {
214 |   const lines: string[] = [];
215 |   
216 |   // Build status
217 |   if (!parsed.buildSucceeded) {
218 |     lines.push(`❌ Build failed with ${parsed.errors.length} error${parsed.errors.length !== 1 ? 's' : ''}`);
219 |   } else if (parsed.errors.length === 0 && parsed.warnings.length === 0) {
220 |     lines.push('✅ Build succeeded');
221 |   } else {
222 |     lines.push(`⚠️ Build succeeded with ${parsed.warnings.length} warning${parsed.warnings.length !== 1 ? 's' : ''}`);
223 |   }
224 |   
225 |   // Errors
226 |   if (parsed.errors.length > 0) {
227 |     lines.push('\nErrors:');
228 |     for (const error of parsed.errors.slice(0, 10)) { // Show first 10
229 |       if (error.file) {
230 |         lines.push(`  ❌ ${error.file}:${error.line}:${error.column} - ${error.message}`);
231 |       } else {
232 |         lines.push(`  ❌ ${error.message}`);
233 |       }
234 |     }
235 |     if (parsed.errors.length > 10) {
236 |       lines.push(`  ... and ${parsed.errors.length - 10} more errors`);
237 |     }
238 |   }
239 |   
240 |   // Warnings (only show if no errors)
241 |   if (parsed.errors.length === 0 && parsed.warnings.length > 0) {
242 |     lines.push('\nWarnings:');
243 |     for (const warning of parsed.warnings.slice(0, 5)) { // Show first 5
244 |       if (warning.file) {
245 |         lines.push(`  ⚠️ ${warning.file}:${warning.line}:${warning.column} - ${warning.message}`);
246 |       } else {
247 |         lines.push(`  ⚠️ ${warning.message}`);
248 |       }
249 |     }
250 |     if (parsed.warnings.length > 5) {
251 |       lines.push(`  ... and ${parsed.warnings.length - 5} more warnings`);
252 |     }
253 |   }
254 |   
255 |   // Test results
256 |   if (parsed.tests.length > 0) {
257 |     lines.push('\nTest Results:');
258 |     if (parsed.testsPassed) {
259 |       lines.push(`  ✅ All ${parsed.totalTests} tests passed`);
260 |     } else {
261 |       lines.push(`  ❌ ${parsed.failedTests} of ${parsed.totalTests} tests failed`);
262 |       
263 |       // Show failed tests
264 |       const failedTests = parsed.tests.filter(t => !t.passed);
265 |       for (const test of failedTests.slice(0, 5)) {
266 |         lines.push(`    ✖ ${test.name}: ${test.failureReason || 'Failed'}`);
267 |       }
268 |       if (failedTests.length > 5) {
269 |         lines.push(`    ... and ${failedTests.length - 5} more failures`);
270 |       }
271 |     }
272 |   }
273 |   
274 |   return lines.join('\n');
275 | }
```

--------------------------------------------------------------------------------
/docs/ERROR-HANDLING.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Error Handling and Presentation Patterns
  2 | 
  3 | ## Overview
  4 | 
  5 | This document describes the error handling patterns and presentation conventions used across the MCP Xcode Server codebase. Following these patterns ensures consistent user experience and maintainable error handling.
  6 | 
  7 | ## Core Principles
  8 | 
  9 | ### 1. Separation of Concerns
 10 | - **Domain Layer**: Creates typed error objects with data only (no formatting)
 11 | - **Use Cases**: Return domain errors without formatting messages
 12 | - **Presentation Layer**: Formats errors for user display with consistent styling
 13 | 
 14 | ### 2. Typed Domain Errors
 15 | Each domain has its own error types that extend a base error class:
 16 | 
 17 | ```typescript
 18 | // Base class for domain-specific errors
 19 | export abstract class BootError extends Error {}
 20 | 
 21 | // Specific error types with relevant data
 22 | export class SimulatorNotFoundError extends BootError {
 23 |   constructor(public readonly deviceId: string) {
 24 |     super(deviceId); // Just store the data, no formatting
 25 |     this.name = 'SimulatorNotFoundError';
 26 |   }
 27 | }
 28 | 
 29 | export class BootCommandFailedError extends BootError {
 30 |   constructor(public readonly stderr: string) {
 31 |     super(stderr); // Just store the stderr output
 32 |     this.name = 'BootCommandFailedError';
 33 |   }
 34 | }
 35 | ```
 36 | 
 37 | **Important**: Domain errors should NEVER contain user-facing messages. They should only contain data. The presentation layer is responsible for formatting messages based on the error type and data.
 38 | 
 39 | ### 3. Error Type Checking
 40 | Use `instanceof` to check error types in the presentation layer:
 41 | 
 42 | ```typescript
 43 | if (error instanceof SimulatorNotFoundError) {
 44 |   return `❌ Simulator not found: ${error.deviceId}`;
 45 | }
 46 | 
 47 | if (error instanceof BootCommandFailedError) {
 48 |   return `❌ ${ErrorFormatter.format(error)}`;
 49 | }
 50 | ```
 51 | 
 52 | ## Presentation Patterns
 53 | 
 54 | ### Visual Indicators (Emojis)
 55 | 
 56 | All tools use consistent emoji prefixes for different outcomes:
 57 | 
 58 | - **✅ Success**: Successful operations
 59 | - **❌ Error**: Failed operations  
 60 | - **⚠️ Warning**: Operations with warnings
 61 | - **📁 Info**: Additional information (like log paths)
 62 | 
 63 | Examples:
 64 | ```
 65 | ✅ Successfully booted simulator: iPhone 15 (ABC123)
 66 | ✅ Build succeeded: MyApp
 67 | 
 68 | ❌ Simulator not found: iPhone-16
 69 | ❌ Build failed
 70 | 
 71 | ⚠️ Warnings (3):
 72 |   • Deprecated API usage
 73 |   • Unused variable 'x'
 74 |   
 75 | 📁 Full logs saved to: /path/to/logs
 76 | ```
 77 | 
 78 | ### Error Message Format
 79 | 
 80 | 1. **Simple Errors**: Direct message with emoji
 81 |    ```
 82 |    ❌ Simulator not found: iPhone-16
 83 |    ❌ Unable to boot device
 84 |    ```
 85 | 
 86 | 2. **Complex Errors** (builds, tests): Structured format
 87 |    ```
 88 |    ❌ Build failed: MyApp
 89 |    Platform: iOS
 90 |    Configuration: Debug
 91 |    
 92 |    ❌ Errors (3):
 93 |      • /path/file.swift:10: Cannot find type 'Foo'
 94 |      • /path/file.swift:20: Missing return statement
 95 |    ```
 96 | 
 97 | ### ErrorFormatter Usage
 98 | 
 99 | The `ErrorFormatter` class provides consistent error formatting across all tools:
100 | 
101 | ```typescript
102 | import { ErrorFormatter } from '../formatters/ErrorFormatter.js';
103 | 
104 | // In controller or presenter
105 | const message = ErrorFormatter.format(error);
106 | return `❌ ${message}`;
107 | ```
108 | 
109 | The ErrorFormatter:
110 | - Formats domain validation errors
111 | - Formats build issues
112 | - Cleans up common error prefixes
113 | - Provides fallback for unknown errors
114 | 
115 | ## Implementation Guidelines
116 | 
117 | ### Controllers
118 | 
119 | Controllers should format results consistently:
120 | 
121 | ```typescript
122 | private formatResult(result: DomainResult): string {
123 |   switch (result.outcome) {
124 |     case Outcome.Success:
125 |       return `✅ Successfully completed: ${result.name}`;
126 |       
127 |     case Outcome.Failed:
128 |       if (result.error instanceof SpecificError) {
129 |         return `❌ Specific error: ${result.error.details}`;
130 |       }
131 |       return `❌ ${ErrorFormatter.format(result.error)}`;
132 |   }
133 | }
134 | ```
135 | 
136 | ### Use Cases
137 | 
138 | Use cases should NOT format error messages:
139 | 
140 | ```typescript
141 | // ❌ BAD: Formatting in use case
142 | return Result.failed(
143 |   `Simulator not found: ${deviceId}` // Don't format here!
144 | );
145 | 
146 | // ✅ GOOD: Return typed error
147 | return Result.failed(
148 |   new SimulatorNotFoundError(deviceId) // Just the error object
149 | );
150 | ```
151 | 
152 | ### Presenters
153 | 
154 | For complex formatting (like build results), use a dedicated presenter:
155 | 
156 | ```typescript
157 | export class BuildXcodePresenter {
158 |   presentError(error: Error): MCPResponse {
159 |     const message = ErrorFormatter.format(error);
160 |     return {
161 |       content: [{
162 |         type: 'text',
163 |         text: `❌ ${message}`
164 |       }]
165 |     };
166 |   }
167 | }
168 | ```
169 | 
170 | ## Testing Error Handling
171 | 
172 | ### Unit Tests
173 | 
174 | Test that controllers format errors correctly:
175 | 
176 | ```typescript
177 | it('should handle boot failure', async () => {
178 |   // Arrange
179 |   const error = new BootCommandFailedError('Device is locked');
180 |   const result = BootResult.failed('123', 'iPhone', error);
181 |   
182 |   // Act
183 |   const response = controller.execute({ deviceId: 'iPhone' });
184 |   
185 |   // Assert - Check for emoji and message
186 |   expect(response.text).toBe('❌ Device is locked');
187 | });
188 | ```
189 | 
190 | ### Integration Tests
191 | 
192 | Test behavior, not specific formatting:
193 | 
194 | ```typescript
195 | it('should handle simulator not found', async () => {
196 |   // Act
197 |   const result = await controller.execute({ deviceId: 'NonExistent' });
198 |   
199 |   // Assert - Test behavior: error message shown
200 |   expect(result.content[0].text).toContain('❌');
201 |   expect(result.content[0].text).toContain('not found');
202 | });
203 | ```
204 | 
205 | ## Common Error Scenarios
206 | 
207 | ### 1. Resource Not Found
208 | ```typescript
209 | export class ResourceNotFoundError extends DomainError {
210 |   constructor(public readonly resourceId: string) {
211 |     super(resourceId);
212 |   }
213 | }
214 | 
215 | // Presentation
216 | `❌ Resource not found: ${error.resourceId}`
217 | ```
218 | 
219 | ### 2. Command Execution Failed
220 | ```typescript
221 | export class CommandFailedError extends DomainError {
222 |   constructor(public readonly stderr: string, public readonly exitCode?: number) {
223 |     super(stderr);
224 |   }
225 | }
226 | 
227 | // Presentation
228 | `❌ Command failed: ${error.stderr}`
229 | ```
230 | 
231 | ### 3. Resource Busy/State Conflicts
232 | ```typescript
233 | export class SimulatorBusyError extends DomainError {
234 |   constructor(public readonly currentState: string) {
235 |     super(currentState); // Just store the state, no message
236 |   }
237 | }
238 | 
239 | // Presentation layer formats the message
240 | `❌ Cannot boot simulator: currently ${error.currentState.toLowerCase()}`
241 | ```
242 | 
243 | ### 4. Validation Failed
244 | Domain value objects handle their own validation with consistent error types:
245 | 
246 | ```typescript
247 | // Domain value objects validate themselves
248 | export class AppPath {
249 |   static create(path: unknown): AppPath {
250 |     // Check for missing field
251 |     if (path === undefined || path === null) {
252 |       throw new AppPath.RequiredError(); // "App path is required"
253 |     }
254 | 
255 |     // Check type
256 |     if (typeof path !== 'string') {
257 |       throw new AppPath.InvalidTypeError(path); // "App path must be a string"
258 |     }
259 | 
260 |     // Check empty
261 |     if (path.trim() === '') {
262 |       throw new AppPath.EmptyError(path); // "App path cannot be empty"
263 |     }
264 | 
265 |     // Check format
266 |     if (!path.endsWith('.app')) {
267 |       throw new AppPath.InvalidFormatError(path); // "App path must end with .app"
268 |     }
269 | 
270 |     return new AppPath(path);
271 |   }
272 | }
273 | ```
274 | 
275 | #### Validation Error Hierarchy
276 | Each value object follows this consistent validation order:
277 | 1. **Required**: `undefined`/`null` → "X is required"
278 | 2. **Type**: Wrong type → "X must be a {type}"
279 | 3. **Empty**: Empty string → "X cannot be empty"
280 | 4. **Format**: Invalid format → Specific format message
281 | 
282 | ```typescript
283 | // Consistent error base classes
284 | export abstract class DomainRequiredError extends DomainError {
285 |   constructor(fieldDisplayName: string) {
286 |     super(`${fieldDisplayName} is required`);
287 |   }
288 | }
289 | 
290 | export abstract class DomainEmptyError extends DomainError {
291 |   constructor(fieldDisplayName: string) {
292 |     super(`${fieldDisplayName} cannot be empty`);
293 |   }
294 | }
295 | 
296 | export abstract class DomainInvalidTypeError extends DomainError {
297 |   constructor(fieldDisplayName: string, expectedType: string) {
298 |     super(`${fieldDisplayName} must be a ${expectedType}`);
299 |   }
300 | }
301 | ```
302 | 
303 | ## Benefits
304 | 
305 | 1. **Consistency**: Users see consistent error formatting across all tools
306 | 2. **Maintainability**: Error formatting logic is centralized
307 | 3. **Testability**: Domain logic doesn't depend on presentation
308 | 4. **Flexibility**: Easy to change formatting without touching business logic
309 | 5. **Type Safety**: TypeScript ensures error types are handled correctly
```

--------------------------------------------------------------------------------
/src/shared/tests/utils/TestErrorInjector.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { readFileSync, writeFileSync, existsSync } from 'fs';
  2 | import { join } from 'path';
  3 | import { createModuleLogger } from '../../../logger';
  4 | import { gitResetTestArtifacts, gitResetFile } from './gitResetTestArtifacts';
  5 | 
  6 | const logger = createModuleLogger('TestErrorInjector');
  7 | 
  8 | /**
  9 |  * Helper class to inject specific error conditions into test projects
 10 |  * for testing error handling and display
 11 |  */
 12 | export class TestErrorInjector {
 13 |   private originalFiles: Map<string, string> = new Map();
 14 | 
 15 |   /**
 16 |    * Inject a compile error into a Swift file
 17 |    */
 18 |   injectCompileError(filePath: string, errorType: 'type-mismatch' | 'syntax' | 'missing-member' = 'type-mismatch') {
 19 |     this.backupFile(filePath);
 20 |     
 21 |     let content = readFileSync(filePath, 'utf8');
 22 |     
 23 |     switch (errorType) {
 24 |       case 'type-mismatch':
 25 |         // Add a type mismatch error
 26 |         content = content.replace(
 27 |           'let newItem = Item(timestamp: Date())',
 28 |           'let x: String = 123  // Type mismatch error\n        let newItem = Item(timestamp: Date())'
 29 |         );
 30 |         break;
 31 |       
 32 |       case 'syntax':
 33 |         // Add a syntax error
 34 |         content = content.replace(
 35 |           'import SwiftUI',
 36 |           'import SwiftUI\nlet incomplete = // Syntax error'
 37 |         );
 38 |         break;
 39 |       
 40 |       case 'missing-member':
 41 |         // Reference a non-existent property
 42 |         content = content.replace(
 43 |           'modelContext.insert(newItem)',
 44 |           'modelContext.insert(newItem)\n        let _ = newItem.nonExistentProperty // Missing member error'
 45 |         );
 46 |         break;
 47 |     }
 48 |     
 49 |     writeFileSync(filePath, content);
 50 |     logger.debug({ filePath, errorType }, 'Injected compile error');
 51 |   }
 52 | 
 53 |   /**
 54 |    * Inject multiple compile errors into a file
 55 |    */
 56 |   injectMultipleCompileErrors(filePath: string) {
 57 |     if (!existsSync(filePath)) {
 58 |       throw new Error(`File not found: ${filePath}`);
 59 |     }
 60 |     
 61 |     this.backupFile(filePath);
 62 |     
 63 |     let content = readFileSync(filePath, 'utf8');
 64 |     
 65 |     // Add multiple different types of errors
 66 |     content = content.replace(
 67 |       'let newItem = Item(timestamp: Date())',
 68 |       `let x: String = 123  // Type mismatch error 1
 69 |         let y: Int = "hello"  // Type mismatch error 2
 70 |         let z = nonExistentFunction()  // Undefined function error
 71 |         let newItem = Item(timestamp: Date())`
 72 |     );
 73 |     
 74 |     writeFileSync(filePath, content);
 75 |     logger.debug({ filePath }, 'Injected multiple compile errors');
 76 |   }
 77 | 
 78 |   /**
 79 |    * Remove code signing from a project to trigger signing errors
 80 |    */
 81 |   injectCodeSigningError(projectPath: string) {
 82 |     const pbxprojPath = join(projectPath, 'project.pbxproj');
 83 |     if (!existsSync(pbxprojPath)) {
 84 |       throw new Error(`Project file not found: ${pbxprojPath}`);
 85 |     }
 86 |     
 87 |     this.backupFile(pbxprojPath);
 88 |     
 89 |     let content = readFileSync(pbxprojPath, 'utf8');
 90 |     
 91 |     // Change code signing settings to trigger errors
 92 |     content = content.replace(
 93 |       /CODE_SIGN_STYLE = Automatic;/g,
 94 |       'CODE_SIGN_STYLE = Manual;'
 95 |     );
 96 |     content = content.replace(
 97 |       /DEVELOPMENT_TEAM = [A-Z0-9]+;/g,
 98 |       'DEVELOPMENT_TEAM = "";'
 99 |     );
100 |     
101 |     writeFileSync(pbxprojPath, content);
102 |     logger.debug({ projectPath }, 'Injected code signing error');
103 |   }
104 | 
105 |   /**
106 |    * Inject a provisioning profile error by requiring a non-existent profile
107 |    */
108 |   injectProvisioningError(projectPath: string) {
109 |     const pbxprojPath = join(projectPath, 'project.pbxproj');
110 |     if (!existsSync(pbxprojPath)) {
111 |       throw new Error(`Project file not found: ${pbxprojPath}`);
112 |     }
113 |     
114 |     this.backupFile(pbxprojPath);
115 |     
116 |     let content = readFileSync(pbxprojPath, 'utf8');
117 |     
118 |     // Add a non-existent provisioning profile requirement
119 |     content = content.replace(
120 |       /PRODUCT_BUNDLE_IDENTIFIER = /g,
121 |       'PROVISIONING_PROFILE_SPECIFIER = "NonExistent Profile";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = '
122 |     );
123 |     
124 |     writeFileSync(pbxprojPath, content);
125 |     logger.debug({ projectPath }, 'Injected provisioning profile error');
126 |   }
127 | 
128 |   /**
129 |    * Inject a missing dependency error into a Swift package
130 |    */
131 |   injectMissingDependency(packagePath: string) {
132 |     const packageSwiftPath = join(packagePath, 'Package.swift');
133 |     if (!existsSync(packageSwiftPath)) {
134 |       throw new Error(`Package.swift not found: ${packageSwiftPath}`);
135 |     }
136 |     
137 |     this.backupFile(packageSwiftPath);
138 |     
139 |     let content = readFileSync(packageSwiftPath, 'utf8');
140 |     
141 |     // In Swift Package Manager, the Package initializer arguments must be in order:
142 |     // name, platforms?, products, dependencies?, targets
143 |     // We need to insert dependencies after products but before targets
144 |     
145 |     // Find the end of products array and beginning of targets
146 |     // Use [\s\S]*? for non-greedy multiline matching
147 |     const regex = /(products:\s*\[[\s\S]*?\])(,\s*)(targets:)/;
148 |     const match = content.match(regex);
149 |     
150 |     if (match) {
151 |       // Insert dependencies between products and targets
152 |       // Use a non-existent package to trigger dependency resolution error
153 |       content = content.replace(
154 |         regex,
155 |         `$1,\n    dependencies: [\n        .package(url: "https://github.com/nonexistent-org/nonexistent-package.git", from: "1.0.0")\n    ]$2$3`
156 |       );
157 |     }
158 |     
159 |     // Now add the dependency to a target so it tries to use it
160 |     // Find the first target that doesn't have dependencies yet
161 |     const targetRegex = /\.target\(\s*name:\s*"([^"]+)"\s*\)/;
162 |     const targetMatch = content.match(targetRegex);
163 |     
164 |     if (targetMatch) {
165 |       const targetName = targetMatch[1];
166 |       // Replace the target to add dependencies
167 |       content = content.replace(
168 |         targetMatch[0],
169 |         `.target(\n            name: "${targetName}",\n            dependencies: [\n                .product(name: "NonExistentPackage", package: "nonexistent-package")\n            ])`
170 |       );
171 |       
172 |       // Also add import to a source file to trigger the error at compile time
173 |       const sourcePath = join(packagePath, 'Sources', targetName);
174 |       if (existsSync(sourcePath)) {
175 |         const sourceFiles = require('fs').readdirSync(sourcePath, { recursive: true })
176 |           .filter((f: string) => f.endsWith('.swift'));
177 |         
178 |         if (sourceFiles.length > 0) {
179 |           const sourceFile = join(sourcePath, sourceFiles[0]);
180 |           this.backupFile(sourceFile);
181 |           let sourceContent = readFileSync(sourceFile, 'utf8');
182 |           sourceContent = 'import NonExistentPackage\n' + sourceContent;
183 |           writeFileSync(sourceFile, sourceContent);
184 |         }
185 |       }
186 |     }
187 |     
188 |     writeFileSync(packageSwiftPath, content);
189 |     logger.debug({ packagePath }, 'Injected missing dependency error');
190 |   }
191 | 
192 |   /**
193 |    * Inject a platform compatibility error
194 |    */
195 |   injectPlatformError(projectPath: string) {
196 |     const pbxprojPath = join(projectPath, 'project.pbxproj');
197 |     if (!existsSync(pbxprojPath)) {
198 |       throw new Error(`Project file not found: ${pbxprojPath}`);
199 |     }
200 |     
201 |     this.backupFile(pbxprojPath);
202 |     
203 |     let content = readFileSync(pbxprojPath, 'utf8');
204 |     
205 |     // Set incompatible deployment targets
206 |     content = content.replace(
207 |       /IPHONEOS_DEPLOYMENT_TARGET = \d+\.\d+;/g,
208 |       'IPHONEOS_DEPLOYMENT_TARGET = 99.0;' // Impossible iOS version
209 |     );
210 |     
211 |     writeFileSync(pbxprojPath, content);
212 |     logger.debug({ projectPath }, 'Injected platform compatibility error');
213 |   }
214 | 
215 |   /**
216 |    * Backup a file before modifying it
217 |    */
218 |   private backupFile(filePath: string) {
219 |     if (!this.originalFiles.has(filePath)) {
220 |       const content = readFileSync(filePath, 'utf8');
221 |       this.originalFiles.set(filePath, content);
222 |       logger.debug({ filePath }, 'Backed up original file');
223 |     }
224 |   }
225 | 
226 |   /**
227 |    * Restore all modified files to their original state
228 |    */
229 |   restoreAll() {
230 |     // Use git to reset all test_artifacts
231 |     gitResetTestArtifacts();
232 |     // Clear our tracking
233 |     this.originalFiles.clear();
234 |   }
235 | 
236 |   /**
237 |    * Restore a specific file
238 |    */
239 |   restoreFile(filePath: string) {
240 |     // Use git to reset the specific file
241 |     gitResetFile(filePath);
242 |     // Remove from our tracking
243 |     this.originalFiles.delete(filePath);
244 |   }
245 | }
```

--------------------------------------------------------------------------------
/src/presentation/tests/unit/DefaultErrorStrategy.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { DefaultErrorStrategy } from '../../formatters/strategies/DefaultErrorStrategy.js';
  2 | 
  3 | describe('DefaultErrorStrategy', () => {
  4 |   function createSUT(): DefaultErrorStrategy {
  5 |     return new DefaultErrorStrategy();
  6 |   }
  7 | 
  8 |   describe('canFormat', () => {
  9 |     it('should always return true as the fallback strategy', () => {
 10 |       const sut = createSUT();
 11 |       
 12 |       expect(sut.canFormat(new Error('Any error'))).toBe(true);
 13 |       expect(sut.canFormat({ message: 'Plain object' })).toBe(true);
 14 |       expect(sut.canFormat('String error')).toBe(true);
 15 |       expect(sut.canFormat(123)).toBe(true);
 16 |       expect(sut.canFormat(null)).toBe(true);
 17 |       expect(sut.canFormat(undefined)).toBe(true);
 18 |       expect(sut.canFormat({})).toBe(true);
 19 |       expect(sut.canFormat([])).toBe(true);
 20 |     });
 21 |   });
 22 | 
 23 |   describe('format', () => {
 24 |     describe('when formatting errors with messages', () => {
 25 |       it('should return plain message without modification', () => {
 26 |         const sut = createSUT();
 27 |         const error = new Error('Simple error message');
 28 |         
 29 |         const result = sut.format(error);
 30 |         
 31 |         expect(result).toBe('Simple error message');
 32 |       });
 33 | 
 34 |       it('should preserve message with special characters', () => {
 35 |         const sut = createSUT();
 36 |         const error = { message: 'Error: with @#$% special chars!' };
 37 |         
 38 |         const result = sut.format(error);
 39 |         
 40 |         expect(result).toBe('with @#$% special chars!');
 41 |       });
 42 | 
 43 |       it('should handle multiline messages', () => {
 44 |         const sut = createSUT();
 45 |         const error = new Error('First line\nSecond line\nThird line');
 46 |         
 47 |         const result = sut.format(error);
 48 |         
 49 |         expect(result).toBe('First line\nSecond line\nThird line');
 50 |       });
 51 |     });
 52 | 
 53 |     describe('when cleaning common prefixes', () => {
 54 |       it('should remove "Error:" prefix (case insensitive)', () => {
 55 |         const sut = createSUT();
 56 |         
 57 |         expect(sut.format(new Error('Error: Something went wrong'))).toBe('Something went wrong');
 58 |         expect(sut.format(new Error('error: lowercase prefix'))).toBe('lowercase prefix');
 59 |         expect(sut.format(new Error('ERROR: uppercase prefix'))).toBe('uppercase prefix');
 60 |         expect(sut.format(new Error('ErRoR: mixed case'))).toBe('mixed case');
 61 |       });
 62 | 
 63 |       it('should remove "Invalid arguments:" prefix (case insensitive)', () => {
 64 |         const sut = createSUT();
 65 |         
 66 |         expect(sut.format(new Error('Invalid arguments: Missing required field'))).toBe('Missing required field');
 67 |         expect(sut.format(new Error('invalid arguments: lowercase'))).toBe('lowercase');
 68 |         expect(sut.format(new Error('INVALID ARGUMENTS: uppercase'))).toBe('uppercase');
 69 |       });
 70 | 
 71 |       it('should remove "Validation failed:" prefix (case insensitive)', () => {
 72 |         const sut = createSUT();
 73 |         
 74 |         expect(sut.format(new Error('Validation failed: Bad input'))).toBe('Bad input');
 75 |         expect(sut.format(new Error('validation failed: lowercase'))).toBe('lowercase');
 76 |         expect(sut.format(new Error('VALIDATION FAILED: uppercase'))).toBe('uppercase');
 77 |       });
 78 | 
 79 |       it('should handle multiple spaces after prefix', () => {
 80 |         const sut = createSUT();
 81 |         
 82 |         expect(sut.format(new Error('Error:     Multiple spaces'))).toBe('Multiple spaces');
 83 |         expect(sut.format(new Error('Invalid arguments:   Extra spaces'))).toBe('Extra spaces');
 84 |       });
 85 | 
 86 |       it('should only remove prefix at start of message', () => {
 87 |         const sut = createSUT();
 88 |         const error = new Error('Something Error: in the middle');
 89 |         
 90 |         const result = sut.format(error);
 91 |         
 92 |         expect(result).toBe('Something Error: in the middle');
 93 |       });
 94 | 
 95 |       it('should handle messages that are only the prefix', () => {
 96 |         const sut = createSUT();
 97 |         
 98 |         expect(sut.format(new Error('Error:'))).toBe('');
 99 |         expect(sut.format(new Error('Error: '))).toBe('');
100 |         expect(sut.format(new Error('Invalid arguments:'))).toBe('');
101 |         expect(sut.format(new Error('Validation failed:'))).toBe('');
102 |       });
103 | 
104 |       it('should clean all matching prefixes', () => {
105 |         const sut = createSUT();
106 |         const error = new Error('Error: Invalid arguments: Something');
107 |         
108 |         const result = sut.format(error);
109 |         
110 |         // Both "Error:" and "Invalid arguments:" are cleaned
111 |         expect(result).toBe('Something');
112 |       });
113 |     });
114 | 
115 |     describe('when handling errors without messages', () => {
116 |       it('should return default message for error without message property', () => {
117 |         const sut = createSUT();
118 |         const error = {};
119 |         
120 |         const result = sut.format(error);
121 |         
122 |         expect(result).toBe('An error occurred');
123 |       });
124 | 
125 |       it('should return default message for null error', () => {
126 |         const sut = createSUT();
127 |         
128 |         const result = sut.format(null);
129 |         
130 |         expect(result).toBe('An error occurred');
131 |       });
132 | 
133 |       it('should return default message for undefined error', () => {
134 |         const sut = createSUT();
135 |         
136 |         const result = sut.format(undefined);
137 |         
138 |         expect(result).toBe('An error occurred');
139 |       });
140 | 
141 |       it('should return default message for non-object errors', () => {
142 |         const sut = createSUT();
143 |         
144 |         expect(sut.format('string error')).toBe('An error occurred');
145 |         expect(sut.format(123)).toBe('An error occurred');
146 |         expect(sut.format(true)).toBe('An error occurred');
147 |         expect(sut.format([])).toBe('An error occurred');
148 |       });
149 | 
150 |       it('should handle error with null message', () => {
151 |         const sut = createSUT();
152 |         const error = { message: null };
153 |         
154 |         const result = sut.format(error);
155 |         
156 |         expect(result).toBe('An error occurred');
157 |       });
158 | 
159 |       it('should handle error with undefined message', () => {
160 |         const sut = createSUT();
161 |         const error = { message: undefined };
162 |         
163 |         const result = sut.format(error);
164 |         
165 |         expect(result).toBe('An error occurred');
166 |       });
167 | 
168 |       it('should handle error with empty string message', () => {
169 |         const sut = createSUT();
170 |         const error = { message: '' };
171 |         
172 |         const result = sut.format(error);
173 |         
174 |         expect(result).toBe('An error occurred');
175 |       });
176 | 
177 |       it('should handle error with whitespace-only message', () => {
178 |         const sut = createSUT();
179 |         const error = { message: '   ' };
180 |         
181 |         const result = sut.format(error);
182 |         
183 |         expect(result).toBe('   '); // Preserves whitespace as it's truthy
184 |       });
185 |     });
186 | 
187 |     describe('when handling edge cases', () => {
188 |       it('should handle very long messages', () => {
189 |         const sut = createSUT();
190 |         const longMessage = 'A'.repeat(10000);
191 |         const error = new Error(longMessage);
192 |         
193 |         const result = sut.format(error);
194 |         
195 |         expect(result).toBe(longMessage);
196 |       });
197 | 
198 |       it('should preserve unicode and emoji', () => {
199 |         const sut = createSUT();
200 |         const error = new Error('Error: Failed to process 你好 🚫');
201 |         
202 |         const result = sut.format(error);
203 |         
204 |         expect(result).toBe('Failed to process 你好 🚫');
205 |       });
206 | 
207 |       it('should handle messages with only special characters', () => {
208 |         const sut = createSUT();
209 |         const error = new Error('@#$%^&*()');
210 |         
211 |         const result = sut.format(error);
212 |         
213 |         expect(result).toBe('@#$%^&*()');
214 |       });
215 | 
216 |       it('should handle error-like objects with toString', () => {
217 |         const sut = createSUT();
218 |         const error = {
219 |           message: 'Custom error',
220 |           toString: () => 'ToString output'
221 |         };
222 |         
223 |         const result = sut.format(error);
224 |         
225 |         expect(result).toBe('Custom error'); // Prefers message over toString
226 |       });
227 | 
228 |       it('should not modify messages without known prefixes', () => {
229 |         const sut = createSUT();
230 |         const messages = [
231 |           'Unknown prefix: Something',
232 |           'Warning: Something else',
233 |           'Notice: Important info',
234 |           'Failed: Operation incomplete'
235 |         ];
236 |         
237 |         messages.forEach(msg => {
238 |           const error = new Error(msg);
239 |           expect(sut.format(error)).toBe(msg);
240 |         });
241 |       });
242 |     });
243 |   });
244 | });
```

--------------------------------------------------------------------------------
/src/utils/projects/Xcode.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { XcodeProject } from './XcodeProject.js';
  2 | import { SwiftPackage } from './SwiftPackage.js';
  3 | import { XcodeError, XcodeErrorType } from './XcodeErrors.js';
  4 | import { createModuleLogger } from '../../logger.js';
  5 | import { existsSync } from 'fs';
  6 | import { readdir } from 'fs/promises';
  7 | import path from 'path';
  8 | 
  9 | const logger = createModuleLogger('Xcode');
 10 | 
 11 | /**
 12 |  * Xcode project discovery and management.
 13 |  * Provides methods to find and open Xcode projects and Swift packages.
 14 |  */
 15 | export class Xcode {
 16 |   /**
 17 |    * Open a project at the specified path.
 18 |    * Automatically detects whether it's an Xcode project or Swift package.
 19 |    * @param projectPath Path to the project
 20 |    * @param expectedType Optional type to expect ('xcode' | 'swift-package' | 'auto')
 21 |    */
 22 |   async open(projectPath: string, expectedType: 'xcode' | 'swift-package' | 'auto' = 'auto'): Promise<XcodeProject | SwiftPackage> {
 23 |     // If expecting Swift package specifically, only look for Package.swift
 24 |     if (expectedType === 'swift-package') {
 25 |       // Check if it's a Package.swift file directly
 26 |       if (projectPath.endsWith('Package.swift')) {
 27 |         if (!existsSync(projectPath)) {
 28 |           throw new Error(`No Package.swift found at: ${projectPath}`);
 29 |         }
 30 |         const packageDir = path.dirname(projectPath);
 31 |         logger.debug({ packageDir }, 'Opening Swift package from Package.swift');
 32 |         return new SwiftPackage(packageDir);
 33 |       }
 34 |       
 35 |       // Check if directory contains Package.swift
 36 |       if (existsSync(projectPath)) {
 37 |         const packageSwiftPath = path.join(projectPath, 'Package.swift');
 38 |         if (existsSync(packageSwiftPath)) {
 39 |           logger.debug({ projectPath }, 'Found Package.swift in directory');
 40 |           return new SwiftPackage(projectPath);
 41 |         }
 42 |       }
 43 |       
 44 |       throw new Error(`No Package.swift found at: ${projectPath}`);
 45 |     }
 46 |     
 47 |     // If expecting Xcode project specifically, only look for .xcodeproj/.xcworkspace
 48 |     if (expectedType === 'xcode') {
 49 |       // Check if it's an Xcode project or workspace
 50 |       if (projectPath.endsWith('.xcodeproj') || projectPath.endsWith('.xcworkspace')) {
 51 |         if (!existsSync(projectPath)) {
 52 |           throw new Error(`No Xcode project found at: ${projectPath}`);
 53 |         }
 54 |         const type = projectPath.endsWith('.xcworkspace') ? 'workspace' : 'project';
 55 |         logger.debug({ projectPath, type }, 'Opening Xcode project');
 56 |         return new XcodeProject(projectPath, type);
 57 |       }
 58 |       
 59 |       // Check directory for Xcode projects
 60 |       if (existsSync(projectPath)) {
 61 |         const files = await readdir(projectPath);
 62 |         
 63 |         const workspace = files.find(f => f.endsWith('.xcworkspace'));
 64 |         if (workspace) {
 65 |           const workspacePath = path.join(projectPath, workspace);
 66 |           logger.debug({ workspacePath }, 'Found workspace in directory');
 67 |           return new XcodeProject(workspacePath, 'workspace');
 68 |         }
 69 |         
 70 |         const xcodeproj = files.find(f => f.endsWith('.xcodeproj'));
 71 |         if (xcodeproj) {
 72 |           const xcodeprojPath = path.join(projectPath, xcodeproj);
 73 |           logger.debug({ xcodeprojPath }, 'Found Xcode project in directory');
 74 |           return new XcodeProject(xcodeprojPath, 'project');
 75 |         }
 76 |       }
 77 |       
 78 |       throw new Error(`No Xcode project found at: ${projectPath}`);
 79 |     }
 80 |     
 81 |     // Auto mode - original behavior
 82 |     // Check if it's an Xcode project or workspace
 83 |     if (projectPath.endsWith('.xcodeproj') || projectPath.endsWith('.xcworkspace')) {
 84 |       if (!existsSync(projectPath)) {
 85 |         throw new Error(`Xcode project not found at: ${projectPath}`);
 86 |       }
 87 |       const type = projectPath.endsWith('.xcworkspace') ? 'workspace' : 'project';
 88 |       logger.debug({ projectPath, type }, 'Opening Xcode project');
 89 |       return new XcodeProject(projectPath, type);
 90 |     }
 91 |     
 92 |     // Check if it's a Swift package (directory containing Package.swift)
 93 |     const packagePath = path.join(projectPath, 'Package.swift');
 94 |     if (existsSync(packagePath)) {
 95 |       logger.debug({ projectPath }, 'Opening Swift package');
 96 |       return new SwiftPackage(projectPath);
 97 |     }
 98 |     
 99 |     // If it's a Package.swift file directly
100 |     if (projectPath.endsWith('Package.swift') && existsSync(projectPath)) {
101 |       const packageDir = path.dirname(projectPath);
102 |       logger.debug({ packageDir }, 'Opening Swift package from Package.swift');
103 |       return new SwiftPackage(packageDir);
104 |     }
105 |     
106 |     // Try to auto-detect in the directory
107 |     if (existsSync(projectPath)) {
108 |       // Look for .xcworkspace first (higher priority)
109 |       const files = await readdir(projectPath);
110 |       
111 |       const workspace = files.find(f => f.endsWith('.xcworkspace'));
112 |       if (workspace) {
113 |         const workspacePath = path.join(projectPath, workspace);
114 |         logger.debug({ workspacePath }, 'Found workspace in directory');
115 |         return new XcodeProject(workspacePath, 'workspace');
116 |       }
117 |       
118 |       const xcodeproj = files.find(f => f.endsWith('.xcodeproj'));
119 |       if (xcodeproj) {
120 |         const xcodeprojPath = path.join(projectPath, xcodeproj);
121 |         logger.debug({ xcodeprojPath }, 'Found Xcode project in directory');
122 |         return new XcodeProject(xcodeprojPath, 'project');
123 |       }
124 |       
125 |       // Check for Package.swift
126 |       if (files.includes('Package.swift')) {
127 |         logger.debug({ projectPath }, 'Found Package.swift in directory');
128 |         return new SwiftPackage(projectPath);
129 |       }
130 |     }
131 |     
132 |     throw new XcodeError(XcodeErrorType.ProjectNotFound, projectPath);
133 |   }
134 |   
135 |   /**
136 |    * Find all Xcode projects in a directory
137 |    */
138 |   async findProjects(directory: string): Promise<XcodeProject[]> {
139 |     const projects: XcodeProject[] = [];
140 |     
141 |     try {
142 |       const files = await readdir(directory, { withFileTypes: true });
143 |       
144 |       for (const file of files) {
145 |         const fullPath = path.join(directory, file.name);
146 |         
147 |         if (file.name.endsWith('.xcworkspace')) {
148 |           projects.push(new XcodeProject(fullPath, 'workspace'));
149 |         } else if (file.name.endsWith('.xcodeproj')) {
150 |           // Only add if there's no workspace (workspace takes precedence)
151 |           const workspaceName = file.name.replace('.xcodeproj', '.xcworkspace');
152 |           const hasWorkspace = files.some(f => f.name === workspaceName);
153 |           if (!hasWorkspace) {
154 |             projects.push(new XcodeProject(fullPath, 'project'));
155 |           }
156 |         } else if (file.isDirectory() && !file.name.startsWith('.')) {
157 |           // Recursively search subdirectories
158 |           const subProjects = await this.findProjects(fullPath);
159 |           projects.push(...subProjects);
160 |         }
161 |       }
162 |     } catch (error: any) {
163 |       logger.error({ error: error.message, directory }, 'Failed to find projects');
164 |       throw new Error(`Failed to find projects in ${directory}: ${error.message}`);
165 |     }
166 |     
167 |     logger.debug({ count: projects.length, directory }, 'Found Xcode projects');
168 |     return projects;
169 |   }
170 |   
171 |   /**
172 |    * Find all Swift packages in a directory
173 |    */
174 |   async findPackages(directory: string): Promise<SwiftPackage[]> {
175 |     const packages: SwiftPackage[] = [];
176 |     
177 |     try {
178 |       const files = await readdir(directory, { withFileTypes: true });
179 |       
180 |       // Check if this directory itself is a package
181 |       if (files.some(f => f.name === 'Package.swift')) {
182 |         packages.push(new SwiftPackage(directory));
183 |       }
184 |       
185 |       // Search subdirectories
186 |       for (const file of files) {
187 |         if (file.isDirectory() && !file.name.startsWith('.')) {
188 |           const fullPath = path.join(directory, file.name);
189 |           const subPackages = await this.findPackages(fullPath);
190 |           packages.push(...subPackages);
191 |         }
192 |       }
193 |     } catch (error: any) {
194 |       logger.error({ error: error.message, directory }, 'Failed to find packages');
195 |       throw new Error(`Failed to find packages in ${directory}: ${error.message}`);
196 |     }
197 |     
198 |     logger.debug({ count: packages.length, directory }, 'Found Swift packages');
199 |     return packages;
200 |   }
201 |   
202 |   /**
203 |    * Find all projects and packages in a directory
204 |    */
205 |   async findAll(directory: string): Promise<(XcodeProject | SwiftPackage)[]> {
206 |     const [projects, packages] = await Promise.all([
207 |       this.findProjects(directory),
208 |       this.findPackages(directory)
209 |     ]);
210 |     
211 |     const all = [...projects, ...packages];
212 |     logger.debug({ 
213 |       totalCount: all.length, 
214 |       projectCount: projects.length, 
215 |       packageCount: packages.length,
216 |       directory 
217 |     }, 'Found all projects and packages');
218 |     
219 |     return all;
220 |   }
221 | }
222 | 
223 | // Export a default instance for convenience
224 | export const xcode = new Xcode();
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/unit/ShutdownSimulatorUseCase.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  2 | import { ShutdownSimulatorUseCase } from '../../use-cases/ShutdownSimulatorUseCase.js';
  3 | import { ShutdownRequest } from '../../domain/ShutdownRequest.js';
  4 | import { DeviceId } from '../../../../shared/domain/DeviceId.js';
  5 | import { ShutdownResult, ShutdownOutcome, SimulatorNotFoundError, ShutdownCommandFailedError } from '../../domain/ShutdownResult.js';
  6 | import { SimulatorState } from '../../domain/SimulatorState.js';
  7 | import { ISimulatorLocator, ISimulatorControl, SimulatorInfo } from '../../../../application/ports/SimulatorPorts.js';
  8 | 
  9 | describe('ShutdownSimulatorUseCase', () => {
 10 |   let useCase: ShutdownSimulatorUseCase;
 11 |   let mockLocator: jest.Mocked<ISimulatorLocator>;
 12 |   let mockControl: jest.Mocked<ISimulatorControl>;
 13 | 
 14 |   beforeEach(() => {
 15 |     jest.clearAllMocks();
 16 |     
 17 |     mockLocator = {
 18 |       findSimulator: jest.fn<ISimulatorLocator['findSimulator']>(),
 19 |       findBootedSimulator: jest.fn<ISimulatorLocator['findBootedSimulator']>()
 20 |     };
 21 |     
 22 |     mockControl = {
 23 |       boot: jest.fn<ISimulatorControl['boot']>(),
 24 |       shutdown: jest.fn<ISimulatorControl['shutdown']>()
 25 |     };
 26 |     
 27 |     useCase = new ShutdownSimulatorUseCase(mockLocator, mockControl);
 28 |   });
 29 | 
 30 |   describe('execute', () => {
 31 |     it('should shutdown a booted simulator', async () => {
 32 |       // Arrange
 33 |       const request = ShutdownRequest.create(DeviceId.create('iPhone-15'));
 34 |       const simulatorInfo: SimulatorInfo = {
 35 |         id: 'ABC123',
 36 |         name: 'iPhone 15',
 37 |         state: SimulatorState.Booted,
 38 |         platform: 'iOS',
 39 |         runtime: 'iOS-17.0'
 40 |       };
 41 |       
 42 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
 43 |       mockControl.shutdown.mockResolvedValue(undefined);
 44 | 
 45 |       // Act
 46 |       const result = await useCase.execute(request);
 47 | 
 48 |       // Assert
 49 |       expect(mockLocator.findSimulator).toHaveBeenCalledWith('iPhone-15');
 50 |       expect(mockControl.shutdown).toHaveBeenCalledWith('ABC123');
 51 |       expect(result.outcome).toBe(ShutdownOutcome.Shutdown);
 52 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
 53 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
 54 |     });
 55 | 
 56 |     it('should handle already shutdown simulator', async () => {
 57 |       // Arrange
 58 |       const request = ShutdownRequest.create(DeviceId.create('iPhone-15'));
 59 |       const simulatorInfo: SimulatorInfo = {
 60 |         id: 'ABC123',
 61 |         name: 'iPhone 15',
 62 |         state: SimulatorState.Shutdown,
 63 |         platform: 'iOS',
 64 |         runtime: 'iOS-17.0'
 65 |       };
 66 |       
 67 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
 68 | 
 69 |       // Act
 70 |       const result = await useCase.execute(request);
 71 | 
 72 |       // Assert
 73 |       expect(mockControl.shutdown).not.toHaveBeenCalled();
 74 |       expect(result.outcome).toBe(ShutdownOutcome.AlreadyShutdown);
 75 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
 76 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
 77 |     });
 78 | 
 79 |     it('should shutdown a simulator in Booting state', async () => {
 80 |       // Arrange
 81 |       const request = ShutdownRequest.create(DeviceId.create('iPhone-15'));
 82 |       const simulatorInfo: SimulatorInfo = {
 83 |         id: 'ABC123',
 84 |         name: 'iPhone 15',
 85 |         state: SimulatorState.Booting,
 86 |         platform: 'iOS',
 87 |         runtime: 'iOS-17.0'
 88 |       };
 89 |       
 90 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
 91 |       mockControl.shutdown.mockResolvedValue(undefined);
 92 | 
 93 |       // Act
 94 |       const result = await useCase.execute(request);
 95 | 
 96 |       // Assert
 97 |       expect(mockControl.shutdown).toHaveBeenCalledWith('ABC123');
 98 |       expect(result.outcome).toBe(ShutdownOutcome.Shutdown);
 99 |     });
100 | 
101 |     it('should handle simulator in ShuttingDown state as already shutdown', async () => {
102 |       // Arrange
103 |       const request = ShutdownRequest.create(DeviceId.create('iPhone-15'));
104 |       const simulatorInfo: SimulatorInfo = {
105 |         id: 'ABC123',
106 |         name: 'iPhone 15',
107 |         state: SimulatorState.ShuttingDown,
108 |         platform: 'iOS',
109 |         runtime: 'iOS-17.0'
110 |       };
111 |       
112 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
113 | 
114 |       // Act
115 |       const result = await useCase.execute(request);
116 | 
117 |       // Assert
118 |       expect(mockControl.shutdown).not.toHaveBeenCalled();
119 |       expect(result.outcome).toBe(ShutdownOutcome.AlreadyShutdown);
120 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
121 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
122 |     });
123 | 
124 |     it('should return failure when simulator not found', async () => {
125 |       // Arrange
126 |       const request = ShutdownRequest.create(DeviceId.create('non-existent'));
127 |       mockLocator.findSimulator.mockResolvedValue(null);
128 | 
129 |       // Act
130 |       const result = await useCase.execute(request);
131 |       
132 |       // Assert - Test behavior: simulator not found error
133 |       expect(mockControl.shutdown).not.toHaveBeenCalled();
134 |       expect(result.outcome).toBe(ShutdownOutcome.Failed);
135 |       expect(result.diagnostics.error).toBeInstanceOf(SimulatorNotFoundError);
136 |       expect((result.diagnostics.error as SimulatorNotFoundError).deviceId).toBe('non-existent');
137 |     });
138 | 
139 |     it('should return failure on shutdown error', async () => {
140 |       // Arrange
141 |       const request = ShutdownRequest.create(DeviceId.create('iPhone-15'));
142 |       const simulatorInfo: SimulatorInfo = {
143 |         id: 'ABC123',
144 |         name: 'iPhone 15',
145 |         state: SimulatorState.Booted,
146 |         platform: 'iOS',
147 |         runtime: 'iOS-17.0'
148 |       };
149 |       
150 |       const shutdownError = new Error('Device is busy');
151 |       (shutdownError as any).stderr = 'Device is busy';
152 |       
153 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
154 |       mockControl.shutdown.mockRejectedValue(shutdownError);
155 | 
156 |       // Act
157 |       const result = await useCase.execute(request);
158 | 
159 |       // Assert
160 |       expect(result.outcome).toBe(ShutdownOutcome.Failed);
161 |       expect(result.diagnostics.error).toBeInstanceOf(ShutdownCommandFailedError);
162 |       expect((result.diagnostics.error as ShutdownCommandFailedError).stderr).toBe('Device is busy');
163 |       expect(result.diagnostics.simulatorId).toBe('ABC123');
164 |       expect(result.diagnostics.simulatorName).toBe('iPhone 15');
165 |     });
166 | 
167 |     it('should handle shutdown error without stderr', async () => {
168 |       // Arrange
169 |       const request = ShutdownRequest.create(DeviceId.create('iPhone-15'));
170 |       const simulatorInfo: SimulatorInfo = {
171 |         id: 'ABC123',
172 |         name: 'iPhone 15',
173 |         state: SimulatorState.Booted,
174 |         platform: 'iOS',
175 |         runtime: 'iOS-17.0'
176 |       };
177 |       
178 |       const shutdownError = new Error('Unknown error');
179 |       
180 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
181 |       mockControl.shutdown.mockRejectedValue(shutdownError);
182 | 
183 |       // Act
184 |       const result = await useCase.execute(request);
185 | 
186 |       // Assert
187 |       expect(result.outcome).toBe(ShutdownOutcome.Failed);
188 |       expect(result.diagnostics.error).toBeInstanceOf(ShutdownCommandFailedError);
189 |       expect((result.diagnostics.error as ShutdownCommandFailedError).stderr).toBe('Unknown error');
190 |     });
191 | 
192 |     it('should handle shutdown error with empty message', async () => {
193 |       // Arrange
194 |       const request = ShutdownRequest.create(DeviceId.create('iPhone-15'));
195 |       const simulatorInfo: SimulatorInfo = {
196 |         id: 'ABC123',
197 |         name: 'iPhone 15',
198 |         state: SimulatorState.Booted,
199 |         platform: 'iOS',
200 |         runtime: 'iOS-17.0'
201 |       };
202 |       
203 |       const shutdownError = {};
204 |       
205 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
206 |       mockControl.shutdown.mockRejectedValue(shutdownError);
207 | 
208 |       // Act
209 |       const result = await useCase.execute(request);
210 | 
211 |       // Assert
212 |       expect(result.outcome).toBe(ShutdownOutcome.Failed);
213 |       expect(result.diagnostics.error).toBeInstanceOf(ShutdownCommandFailedError);
214 |       expect((result.diagnostics.error as ShutdownCommandFailedError).stderr).toBe('');
215 |     });
216 | 
217 |     it('should shutdown simulator by UUID', async () => {
218 |       // Arrange
219 |       const uuid = '550e8400-e29b-41d4-a716-446655440000';
220 |       const request = ShutdownRequest.create(DeviceId.create(uuid));
221 |       const simulatorInfo: SimulatorInfo = {
222 |         id: uuid,
223 |         name: 'iPhone 15 Pro',
224 |         state: SimulatorState.Booted,
225 |         platform: 'iOS',
226 |         runtime: 'iOS-17.0'
227 |       };
228 |       
229 |       mockLocator.findSimulator.mockResolvedValue(simulatorInfo);
230 |       mockControl.shutdown.mockResolvedValue(undefined);
231 | 
232 |       // Act
233 |       const result = await useCase.execute(request);
234 | 
235 |       // Assert
236 |       expect(mockLocator.findSimulator).toHaveBeenCalledWith(uuid);
237 |       expect(mockControl.shutdown).toHaveBeenCalledWith(uuid);
238 |       expect(result.outcome).toBe(ShutdownOutcome.Shutdown);
239 |       expect(result.diagnostics.simulatorId).toBe(uuid);
240 |     });
241 |   });
242 | });
```
Page 3/5FirstPrevNextLast