#
tokens: 48217/50000 13/195 files (page 4/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 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/presentation/tests/unit/BuildIssuesStrategy.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { BuildIssuesStrategy } from '../../formatters/strategies/BuildIssuesStrategy.js';
  2 | import { BuildIssue } from '../../../features/build/domain/BuildIssue.js';
  3 | 
  4 | describe('BuildIssuesStrategy', () => {
  5 |   function createSUT(): BuildIssuesStrategy {
  6 |     return new BuildIssuesStrategy();
  7 |   }
  8 | 
  9 |   function createErrorWithIssues(issues: BuildIssue[], message?: string) {
 10 |     return { issues, message };
 11 |   }
 12 | 
 13 |   describe('canFormat', () => {
 14 |     it('should return true for error with BuildIssue array', () => {
 15 |       const sut = createSUT();
 16 |       const error = createErrorWithIssues([
 17 |         BuildIssue.error('Test error')
 18 |       ]);
 19 |       
 20 |       const result = sut.canFormat(error);
 21 |       
 22 |       expect(result).toBe(true);
 23 |     });
 24 | 
 25 |     it('should return true when at least one issue is BuildIssue instance', () => {
 26 |       const sut = createSUT();
 27 |       const error = {
 28 |         issues: [
 29 |           BuildIssue.error('Real issue'),
 30 |           { message: 'Not a BuildIssue' }
 31 |         ]
 32 |       };
 33 |       
 34 |       const result = sut.canFormat(error);
 35 |       
 36 |       expect(result).toBe(true);
 37 |     });
 38 | 
 39 |     it('should return false for error without issues property', () => {
 40 |       const sut = createSUT();
 41 |       const error = { message: 'Plain error' };
 42 |       
 43 |       const result = sut.canFormat(error);
 44 |       
 45 |       expect(result).toBe(false);
 46 |     });
 47 | 
 48 |     it('should return false when issues is not an array', () => {
 49 |       const sut = createSUT();
 50 |       const error = { issues: 'not an array' };
 51 |       
 52 |       const result = sut.canFormat(error);
 53 |       
 54 |       expect(result).toBe(false);
 55 |     });
 56 | 
 57 |     it('should return false when issues array is empty', () => {
 58 |       const sut = createSUT();
 59 |       const error = { issues: [] };
 60 |       
 61 |       const result = sut.canFormat(error);
 62 |       
 63 |       expect(result).toBe(false);
 64 |     });
 65 | 
 66 |     it('should return false when no issues are BuildIssue instances', () => {
 67 |       const sut = createSUT();
 68 |       const error = {
 69 |         issues: [
 70 |           { message: 'Plain object 1' },
 71 |           { message: 'Plain object 2' }
 72 |         ]
 73 |       };
 74 |       
 75 |       const result = sut.canFormat(error);
 76 |       
 77 |       expect(result).toBe(false);
 78 |     });
 79 |   });
 80 | 
 81 |   describe('format', () => {
 82 |     describe('when formatting errors only', () => {
 83 |       it('should format single error correctly', () => {
 84 |         const sut = createSUT();
 85 |         const error = createErrorWithIssues([
 86 |           BuildIssue.error('Cannot find module', 'src/main.ts', 10, 5)
 87 |         ]);
 88 |         
 89 |         const result = sut.format(error);
 90 |         
 91 |         const expected = 
 92 | `❌ Errors (1):
 93 |   • src/main.ts:10:5: Cannot find module`;
 94 |         
 95 |         expect(result).toBe(expected);
 96 |       });
 97 | 
 98 |       it('should format multiple errors with file information', () => {
 99 |         const sut = createSUT();
100 |         const error = createErrorWithIssues([
101 |           BuildIssue.error('Cannot find module', 'src/main.ts', 10, 5),
102 |           BuildIssue.error('Type mismatch', 'src/utils.ts', 20, 3)
103 |         ]);
104 |         
105 |         const result = sut.format(error);
106 |         
107 |         const expected = 
108 | `❌ Errors (2):
109 |   • src/main.ts:10:5: Cannot find module
110 |   • src/utils.ts:20:3: Type mismatch`;
111 |         
112 |         expect(result).toBe(expected);
113 |       });
114 | 
115 |       it('should limit to 5 errors and show count for more', () => {
116 |         const sut = createSUT();
117 |         const issues = Array.from({ length: 10 }, (_, i) => 
118 |           BuildIssue.error(`Error ${i + 1}`, `file${i}.ts`)
119 |         );
120 |         const error = createErrorWithIssues(issues);
121 |         
122 |         const result = sut.format(error);
123 |         
124 |         expect(result).toContain('❌ Errors (10):');
125 |         expect(result).toContain('file0.ts: Error 1');
126 |         expect(result).toContain('file4.ts: Error 5');
127 |         expect(result).not.toContain('file5.ts: Error 6');
128 |         expect(result).toContain('... and 5 more errors');
129 |       });
130 | 
131 |       it('should handle errors without file information', () => {
132 |         const sut = createSUT();
133 |         const error = createErrorWithIssues([
134 |           BuildIssue.error('General build error'),
135 |           BuildIssue.error('Another error without file')
136 |         ]);
137 |         
138 |         const result = sut.format(error);
139 |         
140 |         const expected = 
141 | `❌ Errors (2):
142 |   • General build error
143 |   • Another error without file`;
144 |         
145 |         expect(result).toBe(expected);
146 |       });
147 |     });
148 | 
149 |     describe('when formatting warnings only', () => {
150 |       it('should format single warning correctly', () => {
151 |         const sut = createSUT();
152 |         const error = createErrorWithIssues([
153 |           BuildIssue.warning('Deprecated API usage', 'src/legacy.ts', 15)
154 |         ]);
155 |         
156 |         const result = sut.format(error);
157 |         
158 |         const expected = 
159 | `⚠️ Warnings (1):
160 |   • src/legacy.ts:15: Deprecated API usage`;
161 |         
162 |         expect(result).toBe(expected);
163 |       });
164 | 
165 |       it('should limit to 3 warnings and show count for more', () => {
166 |         const sut = createSUT();
167 |         const issues = Array.from({ length: 6 }, (_, i) => 
168 |           BuildIssue.warning(`Warning ${i + 1}`, `file${i}.ts`)
169 |         );
170 |         const error = createErrorWithIssues(issues);
171 |         
172 |         const result = sut.format(error);
173 |         
174 |         expect(result).toContain('⚠️ Warnings (6):');
175 |         expect(result).toContain('file0.ts: Warning 1');
176 |         expect(result).toContain('file2.ts: Warning 3');
177 |         expect(result).not.toContain('file3.ts: Warning 4');
178 |         expect(result).toContain('... and 3 more warnings');
179 |       });
180 |     });
181 | 
182 |     describe('when formatting mixed errors and warnings', () => {
183 |       it('should show both sections separated by blank line', () => {
184 |         const sut = createSUT();
185 |         const error = createErrorWithIssues([
186 |           BuildIssue.error('Error message', 'error.ts'),
187 |           BuildIssue.warning('Warning message', 'warning.ts')
188 |         ]);
189 |         
190 |         const result = sut.format(error);
191 |         
192 |         const expected = 
193 | `❌ Errors (1):
194 |   • error.ts: Error message
195 | 
196 | ⚠️ Warnings (1):
197 |   • warning.ts: Warning message`;
198 |         
199 |         expect(result).toBe(expected);
200 |       });
201 | 
202 |       it('should handle many mixed issues correctly', () => {
203 |         const sut = createSUT();
204 |         const issues = [
205 |           ...Array.from({ length: 7 }, (_, i) => 
206 |             BuildIssue.error(`Error ${i + 1}`, `error${i}.ts`)
207 |           ),
208 |           ...Array.from({ length: 5 }, (_, i) => 
209 |             BuildIssue.warning(`Warning ${i + 1}`, `warn${i}.ts`)
210 |           )
211 |         ];
212 |         const error = createErrorWithIssues(issues);
213 |         
214 |         const result = sut.format(error);
215 |         
216 |         expect(result).toContain('❌ Errors (7):');
217 |         expect(result).toContain('... and 2 more errors');
218 |         expect(result).toContain('⚠️ Warnings (5):');
219 |         expect(result).toContain('... and 2 more warnings');
220 |         expect(result.split('\n\n')).toHaveLength(2); // Two sections
221 |       });
222 |     });
223 | 
224 |     describe('when handling edge cases', () => {
225 |       it('should return fallback message when no issues', () => {
226 |         const sut = createSUT();
227 |         const error = createErrorWithIssues([]);
228 |         
229 |         const result = sut.format(error);
230 |         
231 |         expect(result).toBe('Build failed');
232 |       });
233 | 
234 |       it('should use provided message as fallback when no issues', () => {
235 |         const sut = createSUT();
236 |         const error = createErrorWithIssues([], 'Custom build failure');
237 |         
238 |         const result = sut.format(error);
239 |         
240 |         expect(result).toBe('Custom build failure');
241 |       });
242 | 
243 |       it('should handle mix of BuildIssue and non-BuildIssue objects', () => {
244 |         const sut = createSUT();
245 |         const error = {
246 |           issues: [
247 |             BuildIssue.error('Real error'),
248 |             { type: 'error', message: 'Not a BuildIssue' }, // Will be filtered out
249 |             BuildIssue.warning('Real warning')
250 |           ]
251 |         };
252 |         
253 |         const result = sut.format(error);
254 |         
255 |         // Only real BuildIssues should be processed
256 |         expect(result).toContain('❌ Errors (1):');
257 |         expect(result).toContain('Real error');
258 |         expect(result).toContain('⚠️ Warnings (1):');
259 |         expect(result).toContain('Real warning');
260 |       });
261 | 
262 |       it('should handle issues with unknown types gracefully', () => {
263 |         const sut = createSUT();
264 |         const issues = [
265 |           BuildIssue.error('Error'),
266 |           BuildIssue.warning('Warning'),
267 |           Object.assign(BuildIssue.error('Info'), { type: 'info' as any }) // Unknown type
268 |         ];
269 |         const error = createErrorWithIssues(issues);
270 |         
271 |         const result = sut.format(error);
272 |         
273 |         // Unknown type should be ignored
274 |         expect(result).toContain('❌ Errors (1):');
275 |         expect(result).toContain('⚠️ Warnings (1):');
276 |         expect(result).not.toContain('info');
277 |       });
278 |     });
279 |   });
280 | });
```

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

```typescript
  1 | import { execAsync } from '../../utils.js';
  2 | import { SimulatorDevice } from './SimulatorDevice.js';
  3 | import { createModuleLogger } from '../../logger.js';
  4 | import { Platform } from '../../types.js';
  5 | 
  6 | const logger = createModuleLogger('Devices');
  7 | 
  8 | /**
  9 |  * Device discovery and management.
 10 |  * Provides methods to find and list simulator devices.
 11 |  * Future-ready for physical device support.
 12 |  */
 13 | export class Devices {
 14 |   /**
 15 |    * Find a device by name or UDID
 16 |    */
 17 |   async find(nameOrId: string): Promise<SimulatorDevice | null> {
 18 |     try {
 19 |       const { stdout } = await execAsync('xcrun simctl list devices --json');
 20 |       const data = JSON.parse(stdout);
 21 |       
 22 |       // Collect all matching devices with their raw data for sorting
 23 |       const matchingDevices: Array<{device: SimulatorDevice, state: string, isAvailable: boolean}> = [];
 24 |       
 25 |       for (const [runtime, deviceList] of Object.entries(data.devices)) {
 26 |         for (const device of deviceList as any[]) {
 27 |           if (device.udid === nameOrId || device.name === nameOrId) {
 28 |             matchingDevices.push({
 29 |               device: new SimulatorDevice(
 30 |                 device.udid,
 31 |                 device.name,
 32 |                 this.extractPlatformFromRuntime(runtime),
 33 |                 runtime
 34 |               ),
 35 |               state: device.state,
 36 |               isAvailable: device.isAvailable
 37 |             });
 38 |           }
 39 |         }
 40 |       }
 41 |       
 42 |       if (matchingDevices.length === 0) {
 43 |         logger.debug({ nameOrId }, 'Device not found');
 44 |         return null;
 45 |       }
 46 |       
 47 |       // Sort to prefer available, booted, and newer devices
 48 |       this.sortDevices(matchingDevices);
 49 |       
 50 |       const selected = matchingDevices[0];
 51 |       
 52 |       // Warn if selected device is not available
 53 |       if (!selected.isAvailable) {
 54 |         logger.warn({ 
 55 |           nameOrId, 
 56 |           deviceId: selected.device.id,
 57 |           runtime: selected.device.runtime 
 58 |         }, 'Selected device is not available - may fail to boot');
 59 |       }
 60 |       
 61 |       return selected.device;
 62 |     } catch (error: any) {
 63 |       logger.error({ error: error.message }, 'Failed to find device');
 64 |       throw new Error(`Failed to find device: ${error.message}`);
 65 |     }
 66 |   }
 67 | 
 68 |   /**
 69 |    * List all available simulators, optionally filtered by platform
 70 |    */
 71 |   async listSimulators(platform?: Platform): Promise<SimulatorDevice[]> {
 72 |     try {
 73 |       const { stdout } = await execAsync('xcrun simctl list devices --json');
 74 |       const data = JSON.parse(stdout);
 75 |       const devices: SimulatorDevice[] = [];
 76 |       
 77 |       for (const [runtime, deviceList] of Object.entries(data.devices)) {
 78 |         const extractedPlatform = this.extractPlatformFromRuntime(runtime);
 79 |         
 80 |         // Filter by platform if specified
 81 |         if (platform && !this.matchesPlatform(extractedPlatform, platform)) {
 82 |           continue;
 83 |         }
 84 |         
 85 |         for (const device of deviceList as any[]) {
 86 |           if (device.isAvailable) {
 87 |             devices.push(new SimulatorDevice(
 88 |               device.udid,
 89 |               device.name,
 90 |               extractedPlatform,
 91 |               runtime
 92 |             ));
 93 |           }
 94 |         }
 95 |       }
 96 |       
 97 |       return devices;
 98 |     } catch (error: any) {
 99 |       logger.error({ error: error.message }, 'Failed to list simulators');
100 |       throw new Error(`Failed to list simulators: ${error.message}`);
101 |     }
102 |   }
103 | 
104 |   /**
105 |    * Get the currently booted simulator, if any
106 |    */
107 |   async getBooted(): Promise<SimulatorDevice | null> {
108 |     try {
109 |       const { stdout } = await execAsync('xcrun simctl list devices --json');
110 |       const data = JSON.parse(stdout);
111 |       
112 |       for (const [runtime, deviceList] of Object.entries(data.devices)) {
113 |         for (const device of deviceList as any[]) {
114 |           if (device.state === 'Booted' && device.isAvailable) {
115 |             return new SimulatorDevice(
116 |               device.udid,
117 |               device.name,
118 |               this.extractPlatformFromRuntime(runtime),
119 |               runtime
120 |             );
121 |           }
122 |         }
123 |       }
124 |       
125 |       logger.debug('No booted simulator found');
126 |       return null;
127 |     } catch (error: any) {
128 |       logger.error({ error: error.message }, 'Failed to get booted device');
129 |       throw new Error(`Failed to get booted device: ${error.message}`);
130 |     }
131 |   }
132 | 
133 |   /**
134 |    * Find the first available device for a platform
135 |    */
136 |   async findFirstAvailable(platform: Platform): Promise<SimulatorDevice | null> {
137 |     const devices = await this.listSimulators(platform);
138 |     
139 |     // First, look for an already booted device
140 |     const booted = devices.find((d: SimulatorDevice) => d.isBooted());
141 |     if (booted) {
142 |       logger.debug({ device: booted.name, id: booted.id }, 'Using already booted device');
143 |       return booted;
144 |     }
145 |     
146 |     // Otherwise, return the first available device
147 |     const available = devices[0];
148 |     if (available) {
149 |       logger.debug({ device: available.name, id: available.id }, 'Found available device');
150 |       return available;
151 |     }
152 |     
153 |     logger.debug({ platform }, 'No available devices for platform');
154 |     return null;
155 |   }
156 | 
157 |   /**
158 |    * Extract platform from runtime string
159 |    */
160 |   private extractPlatformFromRuntime(runtime: string): string {
161 |     const runtimeLower = runtime.toLowerCase();
162 |     
163 |     if (runtimeLower.includes('ios')) return 'iOS';
164 |     if (runtimeLower.includes('tvos')) return 'tvOS';
165 |     if (runtimeLower.includes('watchos')) return 'watchOS';
166 |     if (runtimeLower.includes('xros') || runtimeLower.includes('visionos')) return 'visionOS';
167 |     
168 |     // Default fallback
169 |     return 'iOS';
170 |   }
171 | 
172 |   /**
173 |    * Extract version number from runtime string
174 |    */
175 |   private getVersionFromRuntime(runtime: string): number {
176 |     const match = runtime.match(/(\d+)[.-](\d+)/);
177 |     return match ? parseFloat(`${match[1]}.${match[2]}`) : 0;
178 |   }
179 | 
180 |   /**
181 |    * Sort devices preferring: available > booted > newer iOS versions
182 |    */
183 |   private sortDevices(devices: Array<{device: SimulatorDevice, state: string, isAvailable: boolean}>): void {
184 |     devices.sort((a, b) => {
185 |       if (a.isAvailable !== b.isAvailable) return a.isAvailable ? -1 : 1;
186 |       if (a.state === 'Booted' !== (b.state === 'Booted')) return a.state === 'Booted' ? -1 : 1;
187 |       return this.getVersionFromRuntime(b.device.runtime) - this.getVersionFromRuntime(a.device.runtime);
188 |     });
189 |   }
190 | 
191 |   /**
192 |    * Check if a runtime matches a platform
193 |    */
194 |   private matchesPlatform(extractedPlatform: string, targetPlatform: Platform): boolean {
195 |     const extractedLower = extractedPlatform.toLowerCase();
196 |     const targetLower = targetPlatform.toLowerCase();
197 |     
198 |     // Handle visionOS special case (internally called xrOS)
199 |     if (targetLower === 'visionos') {
200 |       return extractedLower === 'visionos' || extractedLower === 'xros';
201 |     }
202 |     
203 |     return extractedLower === targetLower;
204 |   }
205 | 
206 |   /**
207 |    * Find an available device for a specific platform
208 |    * Returns the first available device, preferring already booted ones
209 |    */
210 |   async findForPlatform(platform: Platform): Promise<SimulatorDevice | null> {
211 |     logger.debug({ platform }, 'Finding device for platform');
212 |     
213 |     try {
214 |       const devices = await this.listSimulators();
215 |       
216 |       // Filter devices for the requested platform
217 |       const platformDevices = devices.filter((device: SimulatorDevice) => 
218 |         this.matchesPlatform(this.extractPlatformFromRuntime(device.runtime), platform)
219 |       );
220 |       
221 |       if (platformDevices.length === 0) {
222 |         logger.warn({ platform }, 'No devices found for platform');
223 |         return null;
224 |       }
225 |       
226 |       // Try to find a booted device first
227 |       const booted = await this.getBooted();
228 |       if (booted && platformDevices.some(d => d.id === booted.id)) {
229 |         logger.debug({ 
230 |           device: booted.name, 
231 |           id: booted.id
232 |         }, 'Selected already booted device for platform');
233 |         return booted;
234 |       }
235 |       
236 |       // Sort by runtime version (prefer newer) and return the first
237 |       platformDevices.sort((a, b) => 
238 |         this.getVersionFromRuntime(b.runtime) - this.getVersionFromRuntime(a.runtime)
239 |       );
240 |       
241 |       const selected = platformDevices[0];
242 |       logger.debug({ 
243 |         device: selected.name, 
244 |         id: selected.id
245 |       }, 'Selected device for platform');
246 |       
247 |       return selected;
248 |     } catch (error: any) {
249 |       logger.error({ error: error.message, platform }, 'Failed to find device for platform');
250 |       throw new Error(`Failed to find device for platform ${platform}: ${error.message}`);
251 |     }
252 |   }
253 | 
254 |   /**
255 |    * Future: List physical devices connected to the system
256 |    * Currently returns empty array as physical device support is not yet implemented
257 |    */
258 |   async listPhysical(): Promise<any[]> {
259 |     // Future implementation for physical devices
260 |     // Would use xcrun devicectl or ios-deploy
261 |     return [];
262 |   }
263 | }
264 | 
265 | // Export a default instance for convenience
266 | export const devices = new Devices();
```

--------------------------------------------------------------------------------
/src/features/app-management/tests/e2e/InstallAppController.e2e.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * E2E Test for InstallAppController
  3 |  * 
  4 |  * Tests CRITICAL USER PATH with REAL simulators:
  5 |  * - Can the controller actually install apps on real simulators?
  6 |  * - Does it properly validate inputs through Clean Architecture layers?
  7 |  * - Does error handling work with real simulator failures?
  8 |  * 
  9 |  * NO MOCKS - uses real simulators and real test apps
 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 |  * It may be skipped in CI environments without proper setup
 14 |  */
 15 | 
 16 | import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
 17 | import { MCPController } from '../../../../presentation/interfaces/MCPController.js';
 18 | import { InstallAppControllerFactory } from '../../factories/InstallAppControllerFactory.js';
 19 | import { TestProjectManager } from '../../../../shared/tests/utils/TestProjectManager.js';
 20 | import { exec } from 'child_process';
 21 | import { promisify } from 'util';
 22 | import * as fs from 'fs';
 23 | import { SimulatorState } from '../../../simulator/domain/SimulatorState.js';
 24 | import { bootAndWaitForSimulator } from '../../../../shared/tests/utils/testHelpers.js';
 25 | 
 26 | const execAsync = promisify(exec);
 27 | 
 28 | describe('InstallAppController E2E', () => {
 29 |   let controller: MCPController;
 30 |   let testManager: TestProjectManager;
 31 |   let testDeviceId: string;
 32 |   let testAppPath: string;
 33 |   
 34 |   beforeAll(async () => {
 35 |     // Set up test project with built app
 36 |     testManager = new TestProjectManager();
 37 |     await testManager.setup();
 38 |     
 39 |     // Build the test app using TestProjectManager
 40 |     testAppPath = await testManager.buildApp('xcodeProject');
 41 |     
 42 |     // Get the latest iOS runtime
 43 |     const runtimesResult = await execAsync('xcrun simctl list runtimes --json');
 44 |     const runtimes = JSON.parse(runtimesResult.stdout);
 45 |     const iosRuntime = runtimes.runtimes.find((r: { platform: string }) => r.platform === 'iOS');
 46 |     
 47 |     if (!iosRuntime) {
 48 |       throw new Error('No iOS runtime found. Please install an iOS simulator runtime.');
 49 |     }
 50 |     
 51 |     // Create and boot a test simulator
 52 |     const createResult = await execAsync(
 53 |       `xcrun simctl create "TestSimulator-InstallApp" "iPhone 15" "${iosRuntime.identifier}"`
 54 |     );
 55 |     testDeviceId = createResult.stdout.trim();
 56 |     
 57 |     // Boot the simulator and wait for it to be ready
 58 |     await bootAndWaitForSimulator(testDeviceId, 30);
 59 |   });
 60 |   
 61 |   afterAll(async () => {
 62 |     // Clean up simulator
 63 |     if (testDeviceId) {
 64 |       try {
 65 |         await execAsync(`xcrun simctl shutdown "${testDeviceId}"`);
 66 |         await execAsync(`xcrun simctl delete "${testDeviceId}"`);
 67 |       } catch (error) {
 68 |         // Ignore cleanup errors
 69 |       }
 70 |     }
 71 |     
 72 |     // Clean up test project
 73 |     await testManager.cleanup();
 74 |   });
 75 |   
 76 |   beforeEach(() => {
 77 |     // Create controller with all real dependencies
 78 |     controller = InstallAppControllerFactory.create();
 79 |   });
 80 | 
 81 |   describe('install real apps on simulators', () => {
 82 |     it('should successfully install app on booted simulator', async () => {
 83 |       // Arrange - simulator is already booted from beforeAll
 84 |       
 85 |       // Act
 86 |       const result = await controller.execute({
 87 |         appPath: testAppPath,
 88 |         simulatorId: testDeviceId
 89 |       });
 90 | 
 91 |       // Assert
 92 |       expect(result).toMatchObject({
 93 |         content: expect.arrayContaining([
 94 |           expect.objectContaining({
 95 |             type: 'text',
 96 |             text: expect.stringContaining('Successfully installed')
 97 |           })
 98 |         ])
 99 |       });
100 |       
101 |       // Verify app is actually installed
102 |       const listAppsResult = await execAsync(
103 |         `xcrun simctl listapps "${testDeviceId}" | grep -i test || true`
104 |       );
105 |       expect(listAppsResult.stdout).toBeTruthy();
106 |     });
107 | 
108 |     it('should install app on booted simulator when no ID specified', async () => {
109 |       // Arrange - ensure our test simulator is the only booted one
110 |       const devicesResult = await execAsync('xcrun simctl list devices --json');
111 |       const devices = JSON.parse(devicesResult.stdout);
112 |       
113 |       // Shutdown all other booted simulators
114 |       interface Device {
115 |         state: string;
116 |         udid: string;
117 |       }
118 |       for (const runtime of Object.values(devices.devices) as Device[][]) {
119 |         for (const device of runtime) {
120 |           if (device.state === SimulatorState.Booted && device.udid !== testDeviceId) {
121 |             await execAsync(`xcrun simctl shutdown "${device.udid}"`);
122 |           }
123 |         }
124 |       }
125 |       
126 |       // Act - install without specifying simulator ID
127 |       const result = await controller.execute({
128 |         appPath: testAppPath
129 |       });
130 | 
131 |       // Assert
132 |       expect(result).toMatchObject({
133 |         content: expect.arrayContaining([
134 |           expect.objectContaining({
135 |             type: 'text',
136 |             text: expect.stringContaining('Successfully installed')
137 |           })
138 |         ])
139 |       });
140 |     });
141 | 
142 |     it('should boot and install when simulator is shutdown', async () => {
143 |       // Arrange - get iOS runtime for creating simulator
144 |       const runtimesResult = await execAsync('xcrun simctl list runtimes --json');
145 |       const runtimes = JSON.parse(runtimesResult.stdout);
146 |       const iosRuntime = runtimes.runtimes.find((r: { platform: string }) => r.platform === 'iOS');
147 | 
148 |       // Create a new shutdown simulator
149 |       const createResult = await execAsync(
150 |         `xcrun simctl create "TestSimulator-Shutdown" "iPhone 14" "${iosRuntime.identifier}"`
151 |       );
152 |       const shutdownSimId = createResult.stdout.trim();
153 | 
154 |       try {
155 |         // Act
156 |         const result = await controller.execute({
157 |           appPath: testAppPath,
158 |           simulatorId: shutdownSimId
159 |         });
160 | 
161 |         // Assert
162 |         expect(result).toMatchObject({
163 |           content: expect.arrayContaining([
164 |             expect.objectContaining({
165 |               type: 'text',
166 |               text: expect.stringContaining('Successfully installed')
167 |             })
168 |           ])
169 |         });
170 | 
171 |         // Verify simulator was booted
172 |         const stateResult = await execAsync(
173 |           `xcrun simctl list devices --json | jq -r '.devices[][] | select(.udid=="${shutdownSimId}") | .state'`
174 |         );
175 |         expect(stateResult.stdout.trim()).toBe(SimulatorState.Booted);
176 |       } finally {
177 |         // Clean up
178 |         await execAsync(`xcrun simctl shutdown "${shutdownSimId}" || true`);
179 |         await execAsync(`xcrun simctl delete "${shutdownSimId}"`);
180 |       }
181 |     }, 300000);
182 |   });
183 | 
184 |   describe('error handling with real simulators', () => {
185 |     it('should fail when app path does not exist', async () => {
186 |       // Arrange
187 |       const nonExistentPath = '/path/that/does/not/exist.app';
188 |       
189 |       // Act
190 |       const result = await controller.execute({
191 |         appPath: nonExistentPath,
192 |         simulatorId: testDeviceId
193 |       });
194 |       
195 |       // Assert - error message from xcrun simctl install (multi-line in real E2E)
196 |       expect(result.content[0].text).toContain('❌');
197 |       expect(result.content[0].text).toContain('No such file or directory');  
198 |     });
199 | 
200 |     it('should fail when app path is not an app bundle', async () => {
201 |       // Arrange - use a regular file instead of .app
202 |       const invalidAppPath = testManager.paths.xcodeProjectXCTestPath;
203 |       
204 |       // Act
205 |       const result = await controller.execute({
206 |         appPath: invalidAppPath,
207 |         simulatorId: testDeviceId
208 |       });
209 |       
210 |       // Assert - validation error formatted with ❌
211 |       expect(result.content[0].text).toBe('❌ App path must end with .app');
212 |     });
213 | 
214 |     it('should fail when simulator does not exist', async () => {
215 |       // Arrange
216 |       const nonExistentSimulator = 'non-existent-simulator-id';
217 |       
218 |       // Act
219 |       const result = await controller.execute({
220 |         appPath: testAppPath,
221 |         simulatorId: nonExistentSimulator
222 |       });
223 |       
224 |       // Assert
225 |       expect(result.content[0].text).toBe('❌ Simulator not found: non-existent-simulator-id');
226 |     });
227 | 
228 |     it('should fail when no booted simulator and no ID specified', async () => {
229 |       // Arrange - shutdown all simulators
230 |       await execAsync('xcrun simctl shutdown all');
231 |       
232 |       try {
233 |         // Act
234 |         const result = await controller.execute({
235 |           appPath: testAppPath
236 |         });
237 |         
238 |         // Assert
239 |         expect(result.content[0].text).toBe('❌ No booted simulator found. Please boot a simulator first or specify a simulator ID.');
240 |       } finally {
241 |         // Re-boot our test simulator for other tests
242 |         await execAsync(`xcrun simctl boot "${testDeviceId}"`);
243 |         await new Promise(resolve => setTimeout(resolve, 3000));
244 |       }
245 |     });
246 |   });
247 | 
248 |   describe('simulator name handling', () => {
249 |     it('should handle simulator specified by name', async () => {
250 |       // Act - use simulator name instead of UUID
251 |       const result = await controller.execute({
252 |         appPath: testAppPath,
253 |         simulatorId: 'TestSimulator-InstallApp'
254 |       });
255 | 
256 |       // Assert
257 |       expect(result).toMatchObject({
258 |         content: expect.arrayContaining([
259 |           expect.objectContaining({
260 |             type: 'text',
261 |             text: expect.stringContaining('Successfully installed')
262 |           })
263 |         ])
264 |       });
265 |     });
266 |   });
267 | });
```

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

```typescript
  1 | /**
  2 |  * Mock helpers for unit testing
  3 |  * Provides utilities to mock subprocess execution, filesystem operations, and MCP interactions
  4 |  */
  5 | 
  6 | import { jest } from '@jest/globals';
  7 | import type { ExecSyncOptions } from 'child_process';
  8 | 
  9 | /**
 10 |  * Mock response builder for subprocess commands
 11 |  */
 12 | export class SubprocessMock {
 13 |   private responses = new Map<string, { stdout?: string; stderr?: string; error?: Error }>();
 14 | 
 15 |   /**
 16 |    * Register a mock response for a command pattern
 17 |    */
 18 |   mockCommand(pattern: string | RegExp, response: { stdout?: string; stderr?: string; error?: Error }) {
 19 |     const key = pattern instanceof RegExp ? pattern.source : pattern;
 20 |     this.responses.set(key, response);
 21 |   }
 22 | 
 23 |   /**
 24 |    * Get mock implementation for execSync
 25 |    */
 26 |   getExecSyncMock() {
 27 |     return jest.fn((command: string, options?: ExecSyncOptions) => {
 28 |       // Find matching response
 29 |       for (const [pattern, response] of this.responses) {
 30 |         const regex = new RegExp(pattern);
 31 |         if (regex.test(command)) {
 32 |           if (response.error) {
 33 |             throw response.error;
 34 |           }
 35 |           return response.stdout || '';
 36 |         }
 37 |       }
 38 |       throw new Error(`No mock defined for command: ${command}`);
 39 |     });
 40 |   }
 41 | 
 42 |   /**
 43 |    * Get mock implementation for spawn
 44 |    */
 45 |   getSpawnMock() {
 46 |     return jest.fn((command: string, args: string[], options?: any) => {
 47 |       const fullCommand = `${command} ${args.join(' ')}`;
 48 |       
 49 |       // Find matching response
 50 |       for (const [pattern, response] of this.responses) {
 51 |         const regex = new RegExp(pattern);
 52 |         if (regex.test(fullCommand)) {
 53 |           return {
 54 |             stdout: {
 55 |               on: jest.fn((event: string, cb: Function) => {
 56 |                 if (event === 'data' && response.stdout) {
 57 |                   cb(Buffer.from(response.stdout));
 58 |                 }
 59 |               })
 60 |             },
 61 |             stderr: {
 62 |               on: jest.fn((event: string, cb: Function) => {
 63 |                 if (event === 'data' && response.stderr) {
 64 |                   cb(Buffer.from(response.stderr));
 65 |                 }
 66 |               })
 67 |             },
 68 |             on: jest.fn((event: string, cb: Function) => {
 69 |               if (event === 'close') {
 70 |                 cb(response.error ? 1 : 0);
 71 |               }
 72 |               if (event === 'error' && response.error) {
 73 |                 cb(response.error);
 74 |               }
 75 |             }),
 76 |             kill: jest.fn()
 77 |           };
 78 |         }
 79 |       }
 80 |       
 81 |       throw new Error(`No mock defined for command: ${fullCommand}`);
 82 |     });
 83 |   }
 84 | 
 85 |   /**
 86 |    * Clear all mocked responses
 87 |    */
 88 |   clear() {
 89 |     this.responses.clear();
 90 |   }
 91 | }
 92 | 
 93 | /**
 94 |  * Mock filesystem operations
 95 |  */
 96 | export class FilesystemMock {
 97 |   private files = new Map<string, string | Buffer>();
 98 |   private directories = new Set<string>();
 99 | 
100 |   /**
101 |    * Mock a file with content
102 |    */
103 |   mockFile(path: string, content: string | Buffer) {
104 |     this.files.set(path, content);
105 |     // Also add parent directories
106 |     const parts = path.split('/');
107 |     for (let i = 1; i < parts.length; i++) {
108 |       this.directories.add(parts.slice(0, i).join('/'));
109 |     }
110 |   }
111 | 
112 |   /**
113 |    * Mock a directory
114 |    */
115 |   mockDirectory(path: string) {
116 |     this.directories.add(path);
117 |   }
118 | 
119 |   /**
120 |    * Get mock for existsSync
121 |    */
122 |   getExistsSyncMock() {
123 |     return jest.fn((path: string) => {
124 |       return this.files.has(path) || this.directories.has(path);
125 |     });
126 |   }
127 | 
128 |   /**
129 |    * Get mock for readFileSync
130 |    */
131 |   getReadFileSyncMock() {
132 |     return jest.fn((path: string, encoding?: BufferEncoding) => {
133 |       if (!this.files.has(path)) {
134 |         const error: any = new Error(`ENOENT: no such file or directory, open '${path}'`);
135 |         error.code = 'ENOENT';
136 |         throw error;
137 |       }
138 |       const content = this.files.get(path)!;
139 |       return encoding && content instanceof Buffer ? content.toString(encoding) : content;
140 |     });
141 |   }
142 | 
143 |   /**
144 |    * Get mock for readdirSync
145 |    */
146 |   getReaddirSyncMock() {
147 |     return jest.fn((path: string) => {
148 |       if (!this.directories.has(path)) {
149 |         const error: any = new Error(`ENOENT: no such file or directory, scandir '${path}'`);
150 |         error.code = 'ENOENT';
151 |         throw error;
152 |       }
153 |       
154 |       // Return files and subdirectories in this directory
155 |       const items = new Set<string>();
156 |       const pathWithSlash = path.endsWith('/') ? path : `${path}/`;
157 |       
158 |       for (const file of this.files.keys()) {
159 |         if (file.startsWith(pathWithSlash)) {
160 |           const relative = file.slice(pathWithSlash.length);
161 |           const firstPart = relative.split('/')[0];
162 |           items.add(firstPart);
163 |         }
164 |       }
165 |       
166 |       for (const dir of this.directories) {
167 |         if (dir.startsWith(pathWithSlash) && dir !== path) {
168 |           const relative = dir.slice(pathWithSlash.length);
169 |           const firstPart = relative.split('/')[0];
170 |           items.add(firstPart);
171 |         }
172 |       }
173 |       
174 |       return Array.from(items);
175 |     });
176 |   }
177 | 
178 |   /**
179 |    * Clear all mocked files and directories
180 |    */
181 |   clear() {
182 |     this.files.clear();
183 |     this.directories.clear();
184 |   }
185 | }
186 | 
187 | /**
188 |  * Common mock responses for Xcode/simulator commands
189 |  */
190 | export const commonMockResponses = {
191 |   /**
192 |    * Mock successful xcodebuild
193 |    */
194 |   xcodebuildSuccess: (scheme: string = 'TestApp') => ({
195 |     stdout: `Build succeeded\nScheme: ${scheme}\n** BUILD SUCCEEDED **`,
196 |     stderr: ''
197 |   }),
198 | 
199 |   /**
200 |    * Mock xcodebuild failure
201 |    */
202 |   xcodebuildFailure: (error: string = 'Build failed') => ({
203 |     stdout: '',
204 |     stderr: `error: ${error}\n** BUILD FAILED **`,
205 |     error: new Error(`Command failed: xcodebuild\n${error}`)
206 |   }),
207 | 
208 |   /**
209 |    * Mock scheme not found error
210 |    */
211 |   schemeNotFound: (scheme: string) => ({
212 |     stdout: '',
213 |     stderr: `xcodebuild: error: The project does not contain a scheme named "${scheme}".`,
214 |     error: new Error(`xcodebuild: error: The project does not contain a scheme named "${scheme}".`)
215 |   }),
216 | 
217 |   /**
218 |    * Mock simulator list
219 |    */
220 |   simulatorList: (devices: Array<{ name: string; udid: string; state: string }> = []) => ({
221 |     stdout: JSON.stringify({
222 |       devices: {
223 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': devices.map(d => ({
224 |           ...d,
225 |           isAvailable: true,
226 |           deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'
227 |         }))
228 |       }
229 |     }),
230 |     stderr: ''
231 |   }),
232 | 
233 |   /**
234 |    * Mock simulator boot success
235 |    */
236 |   simulatorBootSuccess: (deviceId: string) => ({
237 |     stdout: `Device ${deviceId} booted successfully`,
238 |     stderr: ''
239 |   }),
240 | 
241 |   /**
242 |    * Mock simulator already booted
243 |    */
244 |   simulatorAlreadyBooted: (deviceId: string) => ({
245 |     stdout: '',
246 |     stderr: `Device ${deviceId} is already booted`,
247 |     error: new Error(`Device ${deviceId} is already booted`)
248 |   }),
249 | 
250 |   /**
251 |    * Mock app installation success
252 |    */
253 |   appInstallSuccess: (appPath: string, deviceId: string) => ({
254 |     stdout: `Successfully installed ${appPath} on ${deviceId}`,
255 |     stderr: ''
256 |   }),
257 | 
258 |   /**
259 |    * Mock list schemes
260 |    */
261 |   schemesList: (schemes: string[] = ['TestApp', 'TestAppTests']) => ({
262 |     stdout: JSON.stringify({ project: { schemes: schemes } }),
263 |     stderr: ''
264 |   }),
265 | 
266 |   /**
267 |    * Mock swift build success
268 |    */
269 |   swiftBuildSuccess: () => ({
270 |     stdout: 'Building for debugging...\nBuild complete!',
271 |     stderr: ''
272 |   }),
273 | 
274 |   /**
275 |    * Mock swift test success
276 |    */
277 |   swiftTestSuccess: (passed: number = 10, failed: number = 0) => ({
278 |     stdout: `Test Suite 'All tests' passed at 2024-01-01\nExecuted ${passed + failed} tests, with ${failed} failures`,
279 |     stderr: ''
280 |   })
281 | };
282 | 
283 | /**
284 |  * Create a mock MCP client for testing
285 |  */
286 | export function createMockMCPClient() {
287 |   return {
288 |     request: jest.fn(),
289 |     notify: jest.fn(),
290 |     close: jest.fn(),
291 |     on: jest.fn(),
292 |     off: jest.fn()
293 |   };
294 | }
295 | 
296 | /**
297 |  * Helper to setup common mocks for a test
298 |  */
299 | export function setupCommonMocks() {
300 |   const subprocess = new SubprocessMock();
301 |   const filesystem = new FilesystemMock();
302 |   
303 |   // Mock child_process
304 |   jest.mock('child_process', () => ({
305 |     execSync: subprocess.getExecSyncMock(),
306 |     spawn: subprocess.getSpawnMock()
307 |   }));
308 |   
309 |   // Mock fs
310 |   jest.mock('fs', () => ({
311 |     existsSync: filesystem.getExistsSyncMock(),
312 |     readFileSync: filesystem.getReadFileSyncMock(),
313 |     readdirSync: filesystem.getReaddirSyncMock()
314 |   }));
315 |   
316 |   return { subprocess, filesystem };
317 | }
318 | 
319 | /**
320 |  * Helper to create a mock Xcode instance
321 |  */
322 | export function createMockXcode() {
323 |   return {
324 |     open: jest.fn().mockReturnValue({
325 |       buildWithConfiguration: jest.fn<() => Promise<any>>().mockResolvedValue({
326 |         success: true,
327 |         stdout: 'Build succeeded',
328 |         stderr: ''
329 |       }),
330 |       test: jest.fn<() => Promise<any>>().mockResolvedValue({
331 |         success: true,
332 |         stdout: 'Test succeeded',
333 |         stderr: ''
334 |       }),
335 |       run: jest.fn<() => Promise<any>>().mockResolvedValue({
336 |         success: true,
337 |         stdout: 'Run succeeded',
338 |         stderr: ''
339 |       }),
340 |       clean: jest.fn<() => Promise<any>>().mockResolvedValue({
341 |         success: true,
342 |         stdout: 'Clean succeeded',
343 |         stderr: ''
344 |       }),
345 |       archive: jest.fn<() => Promise<any>>().mockResolvedValue({
346 |         success: true,
347 |         stdout: 'Archive succeeded',
348 |         stderr: ''
349 |       })
350 |     })
351 |   };
352 | }
```

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

```typescript
  1 | import { existsSync, rmSync, readdirSync, statSync } from 'fs';
  2 | import { join, resolve } from 'path';
  3 | import { exec } from 'child_process';
  4 | import { promisify } from 'util';
  5 | import { createModuleLogger } from '../../../logger';
  6 | import { config } from '../../../config';
  7 | import { TestEnvironmentCleaner } from './TestEnvironmentCleaner';
  8 | import { gitResetTestArtifacts } from './gitResetTestArtifacts';
  9 | 
 10 | const execAsync = promisify(exec);
 11 | 
 12 | const logger = createModuleLogger('TestProjectManager');
 13 | 
 14 | export class TestProjectManager {
 15 |   private testArtifactsDir: string;
 16 |   private xcodeProjectPath: string;
 17 |   private xcodeProjectSwiftTestingPath: string;
 18 |   private swiftPackageXCTestPath: string;
 19 |   private swiftPackageSwiftTestingPath: string;
 20 |   private workspacePath: string;
 21 |   private watchOSProjectPath: string;
 22 | 
 23 |   constructor() {
 24 |     // Use the actual test artifacts directory
 25 |     this.testArtifactsDir = resolve(process.cwd(), 'test_artifacts');
 26 |     
 27 |     // Set up paths to real test projects
 28 |     // Xcode projects
 29 |     this.xcodeProjectPath = join(this.testArtifactsDir, 'TestProjectXCTest', 'TestProjectXCTest.xcodeproj');
 30 |     this.xcodeProjectSwiftTestingPath = join(this.testArtifactsDir, 'TestProjectSwiftTesting', 'TestProjectSwiftTesting.xcodeproj');
 31 |     
 32 |     // Swift packages
 33 |     this.swiftPackageXCTestPath = join(this.testArtifactsDir, 'TestSwiftPackageXCTest');
 34 |     this.swiftPackageSwiftTestingPath = join(this.testArtifactsDir, 'TestSwiftPackageSwiftTesting');
 35 |     
 36 |     // Workspace and other projects
 37 |     this.workspacePath = join(this.testArtifactsDir, 'Test.xcworkspace');
 38 |     this.watchOSProjectPath = join(this.testArtifactsDir, 'TestProjectWatchOS', 'TestProjectWatchOS.xcodeproj');
 39 |   }
 40 | 
 41 |   get paths() {
 42 |     return {
 43 |       testProjectDir: this.testArtifactsDir,
 44 |       // Xcode projects
 45 |       xcodeProjectXCTestDir: join(this.testArtifactsDir, 'TestProjectXCTest'),
 46 |       xcodeProjectXCTestPath: this.xcodeProjectPath,
 47 |       xcodeProjectSwiftTestingDir: join(this.testArtifactsDir, 'TestProjectSwiftTesting'),
 48 |       xcodeProjectSwiftTestingPath: this.xcodeProjectSwiftTestingPath,
 49 |       // Swift packages
 50 |       swiftPackageXCTestDir: this.swiftPackageXCTestPath, // Default to XCTest for backward compat
 51 |       swiftPackageSwiftTestingDir: this.swiftPackageSwiftTestingPath,
 52 |       // Other
 53 |       workspaceDir: this.testArtifactsDir,
 54 |       derivedDataPath: join(this.testArtifactsDir, 'DerivedData'),
 55 |       workspacePath: this.workspacePath,
 56 |       watchOSProjectPath: this.watchOSProjectPath,
 57 |       watchOSProjectDir: join(this.testArtifactsDir, 'TestProjectWatchOS')
 58 |     };
 59 |   }
 60 | 
 61 |   get schemes() {
 62 |     return {
 63 |       xcodeProject: 'TestProjectXCTest',
 64 |       xcodeProjectSwiftTesting: 'TestProjectSwiftTesting',
 65 |       workspace: 'TestProjectXCTest',  // The workspace uses the same scheme
 66 |       swiftPackageXCTest: 'TestSwiftPackageXCTest',
 67 |       swiftPackageSwiftTesting: 'TestSwiftPackageSwiftTesting',
 68 |       watchOSProject: 'TestProjectWatchOS Watch App'  // The watchOS app scheme
 69 |     };
 70 |   }
 71 | 
 72 |   get targets() {
 73 |     return {
 74 |       xcodeProject: {
 75 |         app: 'TestProjectXCTest',
 76 |         unitTests: 'TestProjectXCTestTests',
 77 |         uiTests: 'TestProjectXCTestUITests'
 78 |       },
 79 |       xcodeProjectSwiftTesting: {
 80 |         app: 'TestProjectSwiftTesting',
 81 |         unitTests: 'TestProjectSwiftTestingTests',
 82 |         uiTests: 'TestProjectSwiftTestingUITests'
 83 |       },
 84 |       watchOSProject: {
 85 |         app: 'TestProjectWatchOS Watch App',
 86 |         tests: 'TestProjectWatchOS Watch AppTests'
 87 |       }
 88 |     };
 89 |   }
 90 | 
 91 |   async setup() {
 92 |     // Clean up any leftover build artifacts before starting
 93 |     this.cleanBuildArtifacts();
 94 |   }
 95 | 
 96 |   /**
 97 |    * Build a test app for simulator testing
 98 |    * Uses optimized settings to avoid hanging on code signing or large output
 99 |    * @param projectType Which test project to build (defaults to 'xcodeProject')
100 |    * @returns Path to the built .app bundle
101 |    */
102 |   async buildApp(projectType: 'xcodeProject' | 'xcodeProjectSwiftTesting' | 'watchOSProject' = 'xcodeProject'): Promise<string> {
103 |     let projectPath: string;
104 |     let scheme: string;
105 | 
106 |     switch (projectType) {
107 |       case 'xcodeProject':
108 |         projectPath = this.xcodeProjectPath;
109 |         scheme = this.schemes.xcodeProject;
110 |         break;
111 |       case 'xcodeProjectSwiftTesting':
112 |         projectPath = this.xcodeProjectSwiftTestingPath;
113 |         scheme = this.schemes.xcodeProjectSwiftTesting;
114 |         break;
115 |       case 'watchOSProject':
116 |         projectPath = this.watchOSProjectPath;
117 |         scheme = this.schemes.watchOSProject;
118 |         break;
119 |     }
120 | 
121 |     // Build with optimized settings for testing
122 |     // Use generic/platform but with ONLY_ACTIVE_ARCH to build for current architecture only
123 |     await execAsync(
124 |       `xcodebuild -project "${projectPath}" ` +
125 |       `-scheme "${scheme}" ` +
126 |       `-configuration Debug ` +
127 |       `-destination 'generic/platform=iOS Simulator' ` +
128 |       `-derivedDataPath "${this.paths.derivedDataPath}" ` +
129 |       `ONLY_ACTIVE_ARCH=YES ` +
130 |       `CODE_SIGNING_ALLOWED=NO ` +
131 |       `CODE_SIGNING_REQUIRED=NO ` +
132 |       `build`,
133 |       { maxBuffer: 50 * 1024 * 1024 }
134 |     );
135 | 
136 |     // Find the built app
137 |     const findResult = await execAsync(
138 |       `find "${this.paths.derivedDataPath}" -name "*.app" -type d | head -1`
139 |     );
140 |     const appPath = findResult.stdout.trim();
141 | 
142 |     if (!appPath || !existsSync(appPath)) {
143 |       throw new Error('Failed to find built app');
144 |     }
145 | 
146 |     return appPath;
147 |   }
148 | 
149 |   private cleanBuildArtifacts() {
150 |     // Clean DerivedData
151 |     TestEnvironmentCleaner.cleanupTestEnvironment()
152 |     
153 |     // Clean .build directories (for SPM)
154 |     const buildDirs = [
155 |       join(this.swiftPackageXCTestPath, '.build'),
156 |       join(this.swiftPackageSwiftTestingPath, '.build'),
157 |       join(this.testArtifactsDir, '.build')
158 |     ];
159 | 
160 |     buildDirs.forEach(dir => {
161 |       if (existsSync(dir)) {
162 |         rmSync(dir, { recursive: true, force: true });
163 |       }
164 |     });
165 | 
166 |     // Clean xcresult bundles (test results)
167 |     this.cleanTestResults();
168 | 
169 |     // Clean any .swiftpm directories
170 |     const swiftpmDirs = this.findDirectories(this.testArtifactsDir, '.swiftpm');
171 |     swiftpmDirs.forEach(dir => {
172 |       rmSync(dir, { recursive: true, force: true });
173 |     });
174 | 
175 |     // Clean build folders in Xcode projects
176 |     const xcodeProjects = [
177 |       join(this.testArtifactsDir, 'TestProjectXCTest'),
178 |       join(this.testArtifactsDir, 'TestProjectSwiftTesting'),
179 |       join(this.testArtifactsDir, 'TestProjectWatchOS')
180 |     ];
181 | 
182 |     xcodeProjects.forEach(projectDir => {
183 |       const buildDir = join(projectDir, 'build');
184 |       if (existsSync(buildDir)) {
185 |         rmSync(buildDir, { recursive: true, force: true });
186 |       }
187 |     });
188 |   }
189 | 
190 |   cleanTestResults() {
191 |     // Find and remove all .xcresult bundles
192 |     const xcresultFiles = this.findFiles(this.testArtifactsDir, '.xcresult');
193 |     xcresultFiles.forEach(file => {
194 |       rmSync(file, { recursive: true, force: true });
195 |     });
196 | 
197 |     // Clean test output files
198 |     const testOutputFiles = [
199 |       join(this.swiftPackageXCTestPath, 'test-output.txt'),
200 |       join(this.swiftPackageSwiftTestingPath, 'test-output.txt'),
201 |       join(this.testArtifactsDir, 'test-results.json')
202 |     ];
203 | 
204 |     testOutputFiles.forEach(file => {
205 |       if (existsSync(file)) {
206 |         rmSync(file, { force: true });
207 |       }
208 |     });
209 |   }
210 | 
211 |   cleanup() {
212 |     // Use git to restore test_artifacts to pristine state
213 |     gitResetTestArtifacts();
214 |     
215 |     // ALWAYS clean build artifacts including MCP-Xcode DerivedData
216 |     this.cleanBuildArtifacts();
217 |     
218 |     // Also clean DerivedData in project root
219 |     const projectDerivedData = join(process.cwd(), 'DerivedData');
220 |     if (existsSync(projectDerivedData)) {
221 |       rmSync(projectDerivedData, { recursive: true, force: true });
222 |     }
223 |   }
224 | 
225 |   private findFiles(dir: string, extension: string): string[] {
226 |     const results: string[] = [];
227 |     
228 |     if (!existsSync(dir)) {
229 |       return results;
230 |     }
231 | 
232 |     try {
233 |       const files = readdirSync(dir);
234 |       
235 |       for (const file of files) {
236 |         const fullPath = join(dir, file);
237 |         const stat = statSync(fullPath);
238 |         
239 |         if (stat.isDirectory()) {
240 |           // Skip hidden directories and node_modules
241 |           if (!file.startsWith('.') && file !== 'node_modules') {
242 |             results.push(...this.findFiles(fullPath, extension));
243 |           }
244 |         } else if (file.endsWith(extension)) {
245 |           results.push(fullPath);
246 |         }
247 |       }
248 |     } catch (error) {
249 |       logger.error({ error, dir }, 'Error scanning directory');
250 |     }
251 |     
252 |     return results;
253 |   }
254 | 
255 |   private findDirectories(dir: string, name: string): string[] {
256 |     const results: string[] = [];
257 |     
258 |     if (!existsSync(dir)) {
259 |       return results;
260 |     }
261 | 
262 |     try {
263 |       const files = readdirSync(dir);
264 |       
265 |       for (const file of files) {
266 |         const fullPath = join(dir, file);
267 |         const stat = statSync(fullPath);
268 |         
269 |         if (stat.isDirectory()) {
270 |           if (file === name) {
271 |             results.push(fullPath);
272 |           } else if (!file.startsWith('.') && file !== 'node_modules') {
273 |             // Recursively search subdirectories
274 |             results.push(...this.findDirectories(fullPath, name));
275 |           }
276 |         }
277 |       }
278 |     } catch (error) {
279 |       logger.error({ error, dir }, 'Error scanning directory');
280 |     }
281 |     
282 |     return results;
283 |   }
284 | }
```

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

```typescript
  1 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  2 | import { SimulatorLocatorAdapter } from '../../infrastructure/SimulatorLocatorAdapter.js';
  3 | import { ICommandExecutor } from '../../../../application/ports/CommandPorts.js';
  4 | import { SimulatorState } from '../../domain/SimulatorState.js';
  5 | 
  6 | describe('SimulatorLocatorAdapter', () => {
  7 |   beforeEach(() => {
  8 |     jest.clearAllMocks();
  9 |   });
 10 | 
 11 |   function createSUT() {
 12 |     const mockExecute = jest.fn<ICommandExecutor['execute']>();
 13 |     const mockExecutor: ICommandExecutor = {
 14 |       execute: mockExecute
 15 |     };
 16 |     const sut = new SimulatorLocatorAdapter(mockExecutor);
 17 |     return { sut, mockExecute };
 18 |   }
 19 | 
 20 |   function createDeviceListOutput(devices: any = {}) {
 21 |     return JSON.stringify({ devices });
 22 |   }
 23 | 
 24 |   describe('findSimulator', () => {
 25 |     describe('finding by UUID', () => {
 26 |       it('should find simulator by exact UUID match', async () => {
 27 |         // Arrange
 28 |         const { sut, mockExecute } = createSUT();
 29 |         const deviceList = {
 30 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
 31 |             udid: 'ABC-123-EXACT',
 32 |             name: 'iPhone 15',
 33 |             state: 'Shutdown',
 34 |             isAvailable: true
 35 |           }]
 36 |         };
 37 |         mockExecute.mockResolvedValue({
 38 |           stdout: createDeviceListOutput(deviceList),
 39 |           stderr: '',
 40 |           exitCode: 0
 41 |         });
 42 | 
 43 |         // Act
 44 |         const result = await sut.findSimulator('ABC-123-EXACT');
 45 | 
 46 |         // Assert
 47 |         expect(result).toEqual({
 48 |           id: 'ABC-123-EXACT',
 49 |           name: 'iPhone 15',
 50 |           state: SimulatorState.Shutdown,
 51 |           platform: 'iOS',
 52 |           runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-17-0'
 53 |         });
 54 |       });
 55 |     });
 56 | 
 57 |     describe('finding by name with multiple matches', () => {
 58 |       it('should prefer booted device when multiple devices have same name', async () => {
 59 |         // Arrange
 60 |         const { sut, mockExecute } = createSUT();
 61 |         const deviceList = {
 62 |           'com.apple.CoreSimulator.SimRuntime.iOS-16-0': [{
 63 |             udid: 'OLD-123',
 64 |             name: 'iPhone 15 Pro',
 65 |             state: 'Shutdown',
 66 |             isAvailable: true
 67 |           }],
 68 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
 69 |             udid: 'NEW-456',
 70 |             name: 'iPhone 15 Pro',
 71 |             state: 'Booted',
 72 |             isAvailable: true
 73 |           }]
 74 |         };
 75 |         mockExecute.mockResolvedValue({
 76 |           stdout: createDeviceListOutput(deviceList),
 77 |           stderr: '',
 78 |           exitCode: 0
 79 |         });
 80 | 
 81 |         // Act
 82 |         const result = await sut.findSimulator('iPhone 15 Pro');
 83 | 
 84 |         // Assert
 85 |         expect(result?.id).toBe('NEW-456'); // Should pick booted one
 86 |       });
 87 | 
 88 |       it('should prefer newer runtime when multiple shutdown devices have same name', async () => {
 89 |         // Arrange
 90 |         const { sut, mockExecute } = createSUT();
 91 |         const deviceList = {
 92 |           'com.apple.CoreSimulator.SimRuntime.iOS-16-4': [{
 93 |             udid: 'OLD-123',
 94 |             name: 'iPhone 14',
 95 |             state: 'Shutdown',
 96 |             isAvailable: true
 97 |           }],
 98 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-2': [{
 99 |             udid: 'NEW-456',
100 |             name: 'iPhone 14',
101 |             state: 'Shutdown',
102 |             isAvailable: true
103 |           }],
104 |           'com.apple.CoreSimulator.SimRuntime.iOS-15-0': [{
105 |             udid: 'OLDER-789',
106 |             name: 'iPhone 14',
107 |             state: 'Shutdown',
108 |             isAvailable: true
109 |           }]
110 |         };
111 |         mockExecute.mockResolvedValue({
112 |           stdout: createDeviceListOutput(deviceList),
113 |           stderr: '',
114 |           exitCode: 0
115 |         });
116 | 
117 |         // Act
118 |         const result = await sut.findSimulator('iPhone 14');
119 | 
120 |         // Assert
121 |         expect(result?.id).toBe('NEW-456'); // Should pick iOS 17.2
122 |         expect(result?.runtime).toContain('iOS-17-2');
123 |       });
124 |     });
125 | 
126 |     describe('availability handling', () => {
127 |       it('should skip unavailable devices', async () => {
128 |         // Arrange
129 |         const { sut, mockExecute } = createSUT();
130 |         const deviceList = {
131 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
132 |             udid: 'UNAVAIL-123',
133 |             name: 'iPhone 15',
134 |             state: 'Shutdown',
135 |             isAvailable: false
136 |           }]
137 |         };
138 |         mockExecute.mockResolvedValue({
139 |           stdout: createDeviceListOutput(deviceList),
140 |           stderr: '',
141 |           exitCode: 0
142 |         });
143 | 
144 |         // Act
145 |         const result = await sut.findSimulator('iPhone 15');
146 | 
147 |         // Assert
148 |         expect(result).toBeNull();
149 |       });
150 |     });
151 | 
152 |     describe('platform extraction', () => {
153 |       it('should correctly identify iOS platform', async () => {
154 |         // Arrange
155 |         const { sut, mockExecute } = createSUT();
156 |         const deviceList = {
157 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
158 |             udid: 'IOS-123',
159 |             name: 'iPhone 15',
160 |             state: 'Shutdown',
161 |             isAvailable: true
162 |           }]
163 |         };
164 |         mockExecute.mockResolvedValue({
165 |           stdout: createDeviceListOutput(deviceList),
166 |           stderr: '',
167 |           exitCode: 0
168 |         });
169 | 
170 |         // Act
171 |         const result = await sut.findSimulator('IOS-123');
172 | 
173 |         // Assert
174 |         expect(result?.platform).toBe('iOS');
175 |       });
176 | 
177 |       it('should correctly identify tvOS platform', async () => {
178 |         // Arrange
179 |         const { sut, mockExecute } = createSUT();
180 |         const deviceList = {
181 |           'com.apple.CoreSimulator.SimRuntime.tvOS-17-0': [{
182 |             udid: 'TV-123',
183 |             name: 'Apple TV',
184 |             state: 'Shutdown',
185 |             isAvailable: true
186 |           }]
187 |         };
188 |         mockExecute.mockResolvedValue({
189 |           stdout: createDeviceListOutput(deviceList),
190 |           stderr: '',
191 |           exitCode: 0
192 |         });
193 | 
194 |         // Act
195 |         const result = await sut.findSimulator('TV-123');
196 | 
197 |         // Assert
198 |         expect(result?.platform).toBe('tvOS');
199 |       });
200 | 
201 |       it('should correctly identify visionOS platform', async () => {
202 |         // Arrange
203 |         const { sut, mockExecute } = createSUT();
204 |         const deviceList = {
205 |           'com.apple.CoreSimulator.SimRuntime.xrOS-1-0': [{
206 |             udid: 'VISION-123',
207 |             name: 'Apple Vision Pro',
208 |             state: 'Shutdown',
209 |             isAvailable: true
210 |           }]
211 |         };
212 |         mockExecute.mockResolvedValue({
213 |           stdout: createDeviceListOutput(deviceList),
214 |           stderr: '',
215 |           exitCode: 0
216 |         });
217 | 
218 |         // Act
219 |         const result = await sut.findSimulator('VISION-123');
220 | 
221 |         // Assert
222 |         expect(result?.platform).toBe('visionOS');
223 |       });
224 |     });
225 |   });
226 | 
227 |   describe('findBootedSimulator', () => {
228 |     describe('with single booted simulator', () => {
229 |       it('should return the booted simulator', async () => {
230 |         // Arrange
231 |         const { sut, mockExecute } = createSUT();
232 |         const deviceList = {
233 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
234 |             {
235 |               udid: 'SHUT-123',
236 |               name: 'iPhone 15',
237 |               state: 'Shutdown',
238 |               isAvailable: true
239 |             },
240 |             {
241 |               udid: 'BOOT-456',
242 |               name: 'iPhone 14',
243 |               state: 'Booted',
244 |               isAvailable: true
245 |             }
246 |           ]
247 |         };
248 |         mockExecute.mockResolvedValue({
249 |           stdout: createDeviceListOutput(deviceList),
250 |           stderr: '',
251 |           exitCode: 0
252 |         });
253 | 
254 |         // Act
255 |         const result = await sut.findBootedSimulator();
256 | 
257 |         // Assert
258 |         expect(result).toEqual({
259 |           id: 'BOOT-456',
260 |           name: 'iPhone 14',
261 |           state: SimulatorState.Booted,
262 |           platform: 'iOS',
263 |           runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-17-0'
264 |         });
265 |       });
266 |     });
267 | 
268 |     describe('with multiple booted simulators', () => {
269 |       it('should throw error indicating multiple booted simulators', async () => {
270 |         // Arrange
271 |         const { sut, mockExecute } = createSUT();
272 |         const deviceList = {
273 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
274 |             {
275 |               udid: 'BOOT-1',
276 |               name: 'iPhone 15',
277 |               state: 'Booted',
278 |               isAvailable: true
279 |             },
280 |             {
281 |               udid: 'BOOT-2',
282 |               name: 'iPhone 14',
283 |               state: 'Booted',
284 |               isAvailable: true
285 |             }
286 |           ]
287 |         };
288 |         mockExecute.mockResolvedValue({
289 |           stdout: createDeviceListOutput(deviceList),
290 |           stderr: '',
291 |           exitCode: 0
292 |         });
293 | 
294 |         // Act & Assert
295 |         await expect(sut.findBootedSimulator())
296 |           .rejects.toThrow('Multiple booted simulators found (2). Please specify a simulator ID.');
297 |       });
298 |     });
299 | 
300 |     describe('with no booted simulators', () => {
301 |       it('should return null', async () => {
302 |         // Arrange
303 |         const { sut, mockExecute } = createSUT();
304 |         const deviceList = {
305 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
306 |             udid: 'SHUT-123',
307 |             name: 'iPhone 15',
308 |             state: 'Shutdown',
309 |             isAvailable: true
310 |           }]
311 |         };
312 |         mockExecute.mockResolvedValue({
313 |           stdout: createDeviceListOutput(deviceList),
314 |           stderr: '',
315 |           exitCode: 0
316 |         });
317 | 
318 |         // Act
319 |         const result = await sut.findBootedSimulator();
320 | 
321 |         // Assert
322 |         expect(result).toBeNull();
323 |       });
324 |     });
325 | 
326 |     describe('with unavailable booted device', () => {
327 |       it('should skip unavailable devices even if booted', async () => {
328 |         // Arrange
329 |         const { sut, mockExecute } = createSUT();
330 |         const deviceList = {
331 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
332 |             udid: 'BOOT-123',
333 |             name: 'iPhone 15',
334 |             state: 'Booted',
335 |             isAvailable: false
336 |           }]
337 |         };
338 |         mockExecute.mockResolvedValue({
339 |           stdout: createDeviceListOutput(deviceList),
340 |           stderr: '',
341 |           exitCode: 0
342 |         });
343 | 
344 |         // Act
345 |         const result = await sut.findBootedSimulator();
346 | 
347 |         // Assert
348 |         expect(result).toBeNull();
349 |       });
350 |     });
351 |   });
352 | });
```

--------------------------------------------------------------------------------
/src/features/simulator/tests/integration/BootSimulatorController.integration.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  2 | import { MCPController } from '../../../../presentation/interfaces/MCPController.js';
  3 | import { BootSimulatorControllerFactory } from '../../factories/BootSimulatorControllerFactory.js';
  4 | import { SimulatorState } from '../../domain/SimulatorState.js';
  5 | import { exec } from 'child_process';
  6 | import type { NodeExecError } from '../../../../shared/tests/types/execTypes.js';
  7 | 
  8 | // Mock ONLY external boundaries
  9 | jest.mock('child_process');
 10 | 
 11 | // Mock promisify to return {stdout, stderr} for exec (as node's promisify does)
 12 | jest.mock('util', () => {
 13 |   const actualUtil = jest.requireActual('util') as typeof import('util');
 14 |   const { createPromisifiedExec } = require('../../../../shared/tests/mocks/promisifyExec');
 15 | 
 16 |   return {
 17 |     ...actualUtil,
 18 |     promisify: (fn: Function) =>
 19 |       fn?.name === 'exec' ? createPromisifiedExec(fn) : actualUtil.promisify(fn)
 20 |   };
 21 | });
 22 | 
 23 | // Mock DependencyChecker to always report dependencies are available in tests
 24 | jest.mock('../../../../infrastructure/services/DependencyChecker', () => ({
 25 |   DependencyChecker: jest.fn().mockImplementation(() => ({
 26 |     check: jest.fn<() => Promise<[]>>().mockResolvedValue([]) // No missing dependencies
 27 |   }))
 28 | }));
 29 | 
 30 | const mockExec = exec as jest.MockedFunction<typeof exec>;
 31 | 
 32 | /**
 33 |  * Integration tests for BootSimulatorController
 34 |  * 
 35 |  * Tests the integration between:
 36 |  * - Controller → Use Case → Adapters
 37 |  * - Input validation → Domain logic → Output formatting
 38 |  * 
 39 |  * Mocks only external boundaries (shell commands)
 40 |  * Tests behavior, not implementation details
 41 |  */
 42 | describe('BootSimulatorController Integration', () => {
 43 |   let controller: MCPController;
 44 |   let execCallIndex: number;
 45 |   let execMockResponses: Array<{ stdout: string; stderr: string; error?: NodeExecError }>;
 46 | 
 47 |   beforeEach(() => {
 48 |     jest.clearAllMocks();
 49 |     execCallIndex = 0;
 50 |     execMockResponses = [];
 51 |     
 52 |     // Setup exec mock to return responses sequentially
 53 |     mockExec.mockImplementation(((
 54 |       _cmd: string, 
 55 |       _options: any, 
 56 |       callback: (error: Error | null, stdout: string, stderr: string) => void
 57 |     ) => {
 58 |       const response = execMockResponses[execCallIndex++] || { stdout: '', stderr: '' };
 59 |       if (response.error) {
 60 |         callback(response.error, response.stdout, response.stderr);
 61 |       } else {
 62 |         callback(null, response.stdout, response.stderr);
 63 |       }
 64 |     }) as any);
 65 |     
 66 |     // Create controller with REAL components using factory
 67 |     controller = BootSimulatorControllerFactory.create();
 68 |   });
 69 | 
 70 |   describe('boot simulator workflow', () => {
 71 |     it('should boot a shutdown simulator', async () => {
 72 |       // Arrange
 73 |       const simulatorData = {
 74 |         devices: {
 75 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
 76 |             udid: 'ABC123',
 77 |             name: 'iPhone 15',
 78 |             state: SimulatorState.Shutdown,
 79 |             isAvailable: true,
 80 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'
 81 |           }]
 82 |         }
 83 |       };
 84 |       
 85 |       execMockResponses = [
 86 |         { stdout: JSON.stringify(simulatorData), stderr: '' },  // list devices
 87 |         { stdout: '', stderr: '' }  // boot command succeeds
 88 |       ];
 89 |       
 90 |       // Act
 91 |       const result = await controller.execute({ deviceId: 'iPhone 15' });
 92 |       
 93 |       // Assert - Test behavior: simulator was successfully booted
 94 |       expect(result.content[0].text).toBe('✅ Successfully booted simulator: iPhone 15 (ABC123)');
 95 |     });
 96 | 
 97 |     it('should handle already booted simulator', async () => {
 98 |       // Arrange
 99 |       const simulatorData = {
100 |         devices: {
101 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
102 |             udid: 'ABC123',
103 |             name: 'iPhone 15',
104 |             state: SimulatorState.Booted,
105 |             isAvailable: true,
106 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'
107 |           }]
108 |         }
109 |       };
110 |       
111 |       execMockResponses = [
112 |         { stdout: JSON.stringify(simulatorData), stderr: '' }  // list devices - already booted
113 |       ];
114 |       
115 |       // Act
116 |       const result = await controller.execute({ deviceId: 'iPhone 15' });
117 |       
118 |       // Assert - Test behavior: reports simulator is already running
119 |       expect(result.content[0].text).toBe('✅ Simulator already booted: iPhone 15 (ABC123)');
120 |     });
121 | 
122 |     it('should boot simulator by UUID', async () => {
123 |       // Arrange
124 |       const uuid = '550e8400-e29b-41d4-a716-446655440000';
125 |       const simulatorData = {
126 |         devices: {
127 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
128 |             udid: uuid,
129 |             name: 'iPhone 15 Pro',
130 |             state: SimulatorState.Shutdown,
131 |             isAvailable: true,
132 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro'
133 |           }]
134 |         }
135 |       };
136 |       
137 |       execMockResponses = [
138 |         { stdout: JSON.stringify(simulatorData), stderr: '' },  // list devices
139 |         { stdout: '', stderr: '' }  // boot command succeeds
140 |       ];
141 |       
142 |       // Act
143 |       const result = await controller.execute({ deviceId: uuid });
144 |       
145 |       // Assert - Test behavior: simulator was booted using UUID
146 |       expect(result.content[0].text).toBe(`✅ Successfully booted simulator: iPhone 15 Pro (${uuid})`);
147 |     });
148 |   });
149 | 
150 |   describe('error handling', () => {
151 |     it('should handle simulator not found', async () => {
152 |       // Arrange
153 |       execMockResponses = [
154 |         { stdout: JSON.stringify({ devices: {} }), stderr: '' }  // empty device list
155 |       ];
156 |       
157 |       // Act
158 |       const result = await controller.execute({ deviceId: 'NonExistent' });
159 |       
160 |       // Assert - Test behavior: appropriate error message shown
161 |       expect(result.content[0].text).toBe('❌ Simulator not found: NonExistent');
162 |     });
163 | 
164 |     it('should handle boot command failure', async () => {
165 |       // Arrange
166 |       const simulatorData = {
167 |         devices: {
168 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
169 |             udid: 'ABC123',
170 |             name: 'iPhone 15',
171 |             state: SimulatorState.Shutdown,
172 |             isAvailable: true,
173 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'
174 |           }]
175 |         }
176 |       };
177 |       
178 |       const bootError: NodeExecError = new Error('Command failed') as NodeExecError;
179 |       bootError.code = 1;
180 |       bootError.stdout = '';
181 |       bootError.stderr = 'Unable to boot device';
182 |       
183 |       execMockResponses = [
184 |         { stdout: JSON.stringify(simulatorData), stderr: '' },  // list devices
185 |         { stdout: '', stderr: 'Unable to boot device', error: bootError }  // boot fails
186 |       ];
187 |       
188 |       // Act
189 |       const result = await controller.execute({ deviceId: 'iPhone 15' });
190 |       
191 |       // Assert - Test behavior: error message includes context for found simulator
192 |       expect(result.content[0].text).toBe('❌ iPhone 15 (ABC123) - Unable to boot device');
193 |     });
194 |   });
195 | 
196 |   describe('validation', () => {
197 |     it('should validate required deviceId', async () => {
198 |       // Act
199 |       const result = await controller.execute({} as any);
200 |       
201 |       // Assert
202 |       expect(result.content[0].text).toBe('❌ Device ID is required');
203 |     });
204 | 
205 |     it('should validate empty deviceId', async () => {
206 |       // Act
207 |       const result = await controller.execute({ deviceId: '' });
208 |       
209 |       // Assert
210 |       expect(result.content[0].text).toBe('❌ Device ID cannot be empty');
211 |     });
212 | 
213 |     it('should validate whitespace-only deviceId', async () => {
214 |       // Act
215 |       const result = await controller.execute({ deviceId: '   ' });
216 |       
217 |       // Assert
218 |       expect(result.content[0].text).toBe('❌ Device ID cannot be whitespace only');
219 |     });
220 |   });
221 | 
222 |   describe('complex scenarios', () => {
223 |     it('should boot specific simulator when multiple exist with similar names', async () => {
224 |       // Arrange
225 |       const simulatorData = {
226 |         devices: {
227 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
228 |             {
229 |               udid: 'AAA111',
230 |               name: 'iPhone 15',
231 |               state: SimulatorState.Shutdown,
232 |               isAvailable: true,
233 |               deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'
234 |             },
235 |             {
236 |               udid: 'BBB222',
237 |               name: 'iPhone 15 Pro',
238 |               state: SimulatorState.Shutdown,
239 |               isAvailable: true,
240 |               deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro'
241 |             },
242 |             {
243 |               udid: 'CCC333',
244 |               name: 'iPhone 15 Pro Max',
245 |               state: SimulatorState.Shutdown,
246 |               isAvailable: true,
247 |               deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro-Max'
248 |             }
249 |           ]
250 |         }
251 |       };
252 |       
253 |       execMockResponses = [
254 |         { stdout: JSON.stringify(simulatorData), stderr: '' },  // list devices
255 |         { stdout: '', stderr: '' }  // boot command succeeds
256 |       ];
257 |       
258 |       // Act
259 |       const result = await controller.execute({ deviceId: 'iPhone 15 Pro' });
260 |       
261 |       // Assert - Test behavior: correct simulator was booted
262 |       expect(result.content[0].text).toBe('✅ Successfully booted simulator: iPhone 15 Pro (BBB222)');
263 |     });
264 | 
265 |     it('should handle mixed state simulators across runtimes', async () => {
266 |       // Arrange
267 |       const simulatorData = {
268 |         devices: {
269 |           'com.apple.CoreSimulator.SimRuntime.iOS-16-0': [{
270 |             udid: 'OLD123',
271 |             name: 'iPhone 14',
272 |             state: SimulatorState.Booted,
273 |             isAvailable: true,
274 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-14'
275 |           }],
276 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
277 |             udid: 'NEW456',
278 |             name: 'iPhone 14',
279 |             state: SimulatorState.Shutdown,
280 |             isAvailable: true,
281 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-14'
282 |           }]
283 |         }
284 |       };
285 |       
286 |       execMockResponses = [
287 |         { stdout: JSON.stringify(simulatorData), stderr: '' }  // list shows iOS 16 one is booted
288 |       ];
289 |       
290 |       // Act - should find the first matching by name regardless of runtime
291 |       const result = await controller.execute({ deviceId: 'iPhone 14' });
292 |       
293 |       // Assert - Test behavior: finds already booted simulator from any runtime
294 |       expect(result.content[0].text).toBe('✅ Simulator already booted: iPhone 14 (OLD123)');
295 |     });
296 |   });
297 | });
```

--------------------------------------------------------------------------------
/src/features/app-management/tests/unit/InstallAppUseCase.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { InstallResult, InstallOutcome, InstallCommandFailedError, SimulatorNotFoundError, NoBootedSimulatorError } from '../../domain/InstallResult.js';
  2 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  3 | import { InstallAppUseCase } from '../../use-cases/InstallAppUseCase.js';
  4 | import { InstallRequest } from '../../domain/InstallRequest.js';
  5 | import { SimulatorState } from '../../../simulator/domain/SimulatorState.js';
  6 | import { 
  7 |   ISimulatorLocator,
  8 |   ISimulatorControl,
  9 |   IAppInstaller, 
 10 |   SimulatorInfo 
 11 | } from '../../../../application/ports/SimulatorPorts.js';
 12 | import { ILogManager } from '../../../../application/ports/LoggingPorts.js';
 13 | 
 14 | describe('InstallAppUseCase', () => {
 15 |   beforeEach(() => {
 16 |     jest.clearAllMocks();
 17 |   });
 18 | 
 19 |   function createSUT() {
 20 |     const mockFindSimulator = jest.fn<ISimulatorLocator['findSimulator']>();
 21 |     const mockFindBootedSimulator = jest.fn<ISimulatorLocator['findBootedSimulator']>();
 22 |     const mockSimulatorLocator: ISimulatorLocator = {
 23 |       findSimulator: mockFindSimulator,
 24 |       findBootedSimulator: mockFindBootedSimulator
 25 |     };
 26 | 
 27 |     const mockBoot = jest.fn<ISimulatorControl['boot']>();
 28 |     const mockShutdown = jest.fn<ISimulatorControl['shutdown']>();
 29 |     const mockSimulatorControl: ISimulatorControl = {
 30 |       boot: mockBoot,
 31 |       shutdown: mockShutdown
 32 |     };
 33 | 
 34 |     const mockInstallApp = jest.fn<IAppInstaller['installApp']>();
 35 |     const mockAppInstaller: IAppInstaller = {
 36 |       installApp: mockInstallApp
 37 |     };
 38 | 
 39 |     const mockSaveDebugData = jest.fn<ILogManager['saveDebugData']>();
 40 |     const mockSaveLog = jest.fn<ILogManager['saveLog']>();
 41 |     const mockLogManager: ILogManager = {
 42 |       saveDebugData: mockSaveDebugData,
 43 |       saveLog: mockSaveLog
 44 |     };
 45 | 
 46 |     const sut = new InstallAppUseCase(
 47 |       mockSimulatorLocator,
 48 |       mockSimulatorControl,
 49 |       mockAppInstaller,
 50 |       mockLogManager
 51 |     );
 52 | 
 53 |     return {
 54 |       sut,
 55 |       mockFindSimulator,
 56 |       mockFindBootedSimulator,
 57 |       mockBoot,
 58 |       mockInstallApp,
 59 |       mockSaveDebugData,
 60 |       mockSaveLog
 61 |     };
 62 |   }
 63 | 
 64 |   function createTestSimulator(state: SimulatorState = SimulatorState.Booted): SimulatorInfo {
 65 |     return {
 66 |       id: 'test-simulator-id',
 67 |       name: 'iPhone 15',
 68 |       state,
 69 |       platform: 'iOS',
 70 |       runtime: 'iOS 17.0'
 71 |     };
 72 |   }
 73 | 
 74 |   describe('when installing with specific simulator ID', () => {
 75 |     it('should install app on already booted simulator', async () => {
 76 |       // Arrange
 77 |       const { sut, mockFindSimulator, mockInstallApp } = createSUT();
 78 |       const request = InstallRequest.create('/path/to/MyApp.app', 'test-simulator-id');
 79 |       const simulator = createTestSimulator(SimulatorState.Booted);
 80 |       
 81 |       mockFindSimulator.mockResolvedValue(simulator);
 82 |       mockInstallApp.mockResolvedValue(undefined);
 83 | 
 84 |       // Act
 85 |       const result = await sut.execute(request);
 86 | 
 87 |       // Assert
 88 |       expect(result.outcome).toBe(InstallOutcome.Succeeded);
 89 |       expect(result.diagnostics.bundleId).toBe('MyApp.app');
 90 |       expect(result.diagnostics.simulatorId?.toString()).toBe('test-simulator-id');
 91 |       expect(mockInstallApp).toHaveBeenCalledWith('/path/to/MyApp.app', 'test-simulator-id');
 92 |     });
 93 | 
 94 |     it('should auto-boot shutdown simulator before installing', async () => {
 95 |       // Arrange
 96 |       const { sut, mockFindSimulator, mockBoot, mockInstallApp } = createSUT();
 97 |       const request = InstallRequest.create('/path/to/MyApp.app', 'test-simulator-id');
 98 |       const simulator = createTestSimulator(SimulatorState.Shutdown);
 99 |       
100 |       mockFindSimulator.mockResolvedValue(simulator);
101 |       mockBoot.mockResolvedValue(undefined);
102 |       mockInstallApp.mockResolvedValue(undefined);
103 | 
104 |       // Act
105 |       const result = await sut.execute(request);
106 | 
107 |       // Assert
108 |       expect(mockBoot).toHaveBeenCalledWith('test-simulator-id');
109 |       expect(mockInstallApp).toHaveBeenCalledWith('/path/to/MyApp.app', 'test-simulator-id');
110 |       expect(result.outcome).toBe(InstallOutcome.Succeeded);
111 |     });
112 | 
113 |     it('should return failure when simulator not found', async () => {
114 |       // Arrange
115 |       const { sut, mockFindSimulator, mockSaveDebugData } = createSUT();
116 |       const request = InstallRequest.create('/path/to/MyApp.app', 'non-existent-id');
117 |       
118 |       mockFindSimulator.mockResolvedValue(null);
119 | 
120 |       // Act
121 |       const result = await sut.execute(request);
122 | 
123 |       // Assert
124 |       expect(result.outcome).toBe(InstallOutcome.Failed);
125 |       expect(result.diagnostics.error).toBeInstanceOf(SimulatorNotFoundError);
126 |       expect((result.diagnostics.error as SimulatorNotFoundError).simulatorId.toString()).toBe('non-existent-id');
127 |       expect(mockSaveDebugData).toHaveBeenCalledWith(
128 |         'install-app-failed',
129 |         expect.objectContaining({ reason: 'simulator_not_found' }),
130 |         'MyApp.app'
131 |       );
132 |     });
133 | 
134 |     it('should return failure when boot fails', async () => {
135 |       // Arrange  
136 |       const { sut, mockFindSimulator, mockBoot, mockSaveDebugData } = createSUT();
137 |       const request = InstallRequest.create('/path/to/MyApp.app', 'test-simulator-id');
138 |       const simulator = createTestSimulator(SimulatorState.Shutdown);
139 |       
140 |       mockFindSimulator.mockResolvedValue(simulator);
141 |       mockBoot.mockRejectedValue(new Error('Boot failed'));
142 | 
143 |       // Act
144 |       const result = await sut.execute(request);
145 | 
146 |       // Assert
147 |       expect(result.outcome).toBe(InstallOutcome.Failed);
148 |       expect(result.diagnostics.error).toBeInstanceOf(InstallCommandFailedError);
149 |       expect((result.diagnostics.error as InstallCommandFailedError).stderr).toBe('Boot failed');
150 |       expect(mockSaveDebugData).toHaveBeenCalledWith(
151 |         'simulator-boot-failed',
152 |         expect.objectContaining({ error: 'Boot failed' }),
153 |         'MyApp.app'
154 |       );
155 |     });
156 |   });
157 | 
158 |   describe('when installing without simulator ID', () => {
159 |     it('should use booted simulator', async () => {
160 |       // Arrange
161 |       const { sut, mockFindBootedSimulator, mockInstallApp } = createSUT();
162 |       const request = InstallRequest.create('/path/to/MyApp.app');
163 |       const simulator = createTestSimulator(SimulatorState.Booted);
164 |       
165 |       mockFindBootedSimulator.mockResolvedValue(simulator);
166 |       mockInstallApp.mockResolvedValue(undefined);
167 | 
168 |       // Act
169 |       const result = await sut.execute(request);
170 | 
171 |       // Assert
172 |       expect(result.outcome).toBe(InstallOutcome.Succeeded);
173 |       expect(result.diagnostics.simulatorId?.toString()).toBe('test-simulator-id');
174 |       expect(mockFindBootedSimulator).toHaveBeenCalled();
175 |       expect(mockInstallApp).toHaveBeenCalledWith('/path/to/MyApp.app', 'test-simulator-id');
176 |     });
177 | 
178 |     it('should return failure when no booted simulator found', async () => {
179 |       // Arrange
180 |       const { sut, mockFindBootedSimulator, mockSaveDebugData } = createSUT();
181 |       const request = InstallRequest.create('/path/to/MyApp.app');
182 |       
183 |       mockFindBootedSimulator.mockResolvedValue(null);
184 | 
185 |       // Act
186 |       const result = await sut.execute(request);
187 | 
188 |       // Assert
189 |       expect(result.outcome).toBe(InstallOutcome.Failed);
190 |       expect(result.diagnostics.error).toBeInstanceOf(NoBootedSimulatorError);
191 |       expect(mockSaveDebugData).toHaveBeenCalledWith(
192 |         'install-app-failed',
193 |         expect.objectContaining({ reason: 'simulator_not_found' }),
194 |         'MyApp.app'
195 |       );
196 |     });
197 |   });
198 | 
199 |   describe('when installation fails', () => {
200 |     it('should return failure with error message', async () => {
201 |       // Arrange
202 |       const { sut, mockFindSimulator, mockInstallApp, mockSaveDebugData } = createSUT();
203 |       const request = InstallRequest.create('/path/to/MyApp.app', 'test-simulator-id');
204 |       const simulator = createTestSimulator(SimulatorState.Booted);
205 |       
206 |       mockFindSimulator.mockResolvedValue(simulator);
207 |       mockInstallApp.mockRejectedValue(new Error('Code signing error'));
208 | 
209 |       // Act
210 |       const result = await sut.execute(request);
211 | 
212 |       // Assert
213 |       expect(result.outcome).toBe(InstallOutcome.Failed);
214 |       expect(result.diagnostics.error).toBeInstanceOf(InstallCommandFailedError);
215 |       expect((result.diagnostics.error as InstallCommandFailedError).stderr).toBe('Code signing error');
216 |       expect(mockSaveDebugData).toHaveBeenCalledWith(
217 |         'install-app-error',
218 |         expect.objectContaining({ error: 'Code signing error' }),
219 |         'MyApp.app'
220 |       );
221 |     });
222 | 
223 |     it('should handle generic error', async () => {
224 |       // Arrange
225 |       const { sut, mockFindSimulator, mockInstallApp } = createSUT();
226 |       const request = InstallRequest.create('/path/to/MyApp.app', 'test-simulator-id');
227 |       const simulator = createTestSimulator(SimulatorState.Booted);
228 |       
229 |       mockFindSimulator.mockResolvedValue(simulator);
230 |       mockInstallApp.mockRejectedValue('String error');
231 | 
232 |       // Act
233 |       const result = await sut.execute(request);
234 | 
235 |       // Assert
236 |       expect(result.outcome).toBe(InstallOutcome.Failed);
237 |       expect(result.diagnostics.error).toBeInstanceOf(InstallCommandFailedError);
238 |       expect((result.diagnostics.error as InstallCommandFailedError).stderr).toBe('String error');
239 |     });
240 |   });
241 | 
242 |   describe('debug data logging', () => {
243 |     it('should log success with app name and simulator info', async () => {
244 |       // Arrange
245 |       const { sut, mockFindSimulator, mockInstallApp, mockSaveDebugData } = createSUT();
246 |       const request = InstallRequest.create('/path/to/MyApp.app', 'test-simulator-id');
247 |       const simulator = createTestSimulator(SimulatorState.Booted);
248 |       
249 |       mockFindSimulator.mockResolvedValue(simulator);
250 |       mockInstallApp.mockResolvedValue(undefined);
251 | 
252 |       // Act
253 |       await sut.execute(request);
254 | 
255 |       // Assert
256 |       expect(mockSaveDebugData).toHaveBeenCalledWith(
257 |         'install-app-success',
258 |         expect.objectContaining({
259 |           simulator: 'iPhone 15',
260 |           simulatorId: 'test-simulator-id',
261 |           app: 'MyApp.app'
262 |         }),
263 |         'MyApp.app'
264 |       );
265 |     });
266 | 
267 |     it('should log auto-boot event', async () => {
268 |       // Arrange
269 |       const { sut, mockFindSimulator, mockBoot, mockInstallApp, mockSaveDebugData } = createSUT();
270 |       const request = InstallRequest.create('/path/to/MyApp.app', 'test-simulator-id');
271 |       const simulator = createTestSimulator(SimulatorState.Shutdown);
272 |       
273 |       mockFindSimulator.mockResolvedValue(simulator);
274 |       mockBoot.mockResolvedValue(undefined);
275 |       mockInstallApp.mockResolvedValue(undefined);
276 | 
277 |       // Act
278 |       await sut.execute(request);
279 | 
280 |       // Assert
281 |       expect(mockSaveDebugData).toHaveBeenCalledWith(
282 |         'simulator-auto-booted',
283 |         expect.objectContaining({
284 |           simulatorId: 'test-simulator-id',
285 |           simulatorName: 'iPhone 15'
286 |         }),
287 |         'MyApp.app'
288 |       );
289 |     });
290 |   });
291 | });
```

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

```typescript
  1 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  2 | import { MCPController } from '../../../../presentation/interfaces/MCPController.js';
  3 | import { ShutdownSimulatorControllerFactory } from '../../factories/ShutdownSimulatorControllerFactory.js';
  4 | import { SimulatorState } from '../../domain/SimulatorState.js';
  5 | import { exec } from 'child_process';
  6 | import type { NodeExecError } from '../../../../shared/tests/types/execTypes.js';
  7 | 
  8 | // Mock ONLY external boundaries
  9 | jest.mock('child_process');
 10 | 
 11 | // Mock promisify to return {stdout, stderr} for exec (as node's promisify does)
 12 | jest.mock('util', () => {
 13 |   const actualUtil = jest.requireActual('util') as typeof import('util');
 14 |   const { createPromisifiedExec } = require('../../../../shared/tests/mocks/promisifyExec');
 15 | 
 16 |   return {
 17 |     ...actualUtil,
 18 |     promisify: (fn: Function) =>
 19 |       fn?.name === 'exec' ? createPromisifiedExec(fn) : actualUtil.promisify(fn)
 20 |   };
 21 | });
 22 | 
 23 | // Mock DependencyChecker to always report dependencies are available in tests
 24 | jest.mock('../../../../infrastructure/services/DependencyChecker', () => ({
 25 |   DependencyChecker: jest.fn().mockImplementation(() => ({
 26 |     check: jest.fn<() => Promise<[]>>().mockResolvedValue([]) // No missing dependencies
 27 |   }))
 28 | }));
 29 | 
 30 | const mockExec = exec as jest.MockedFunction<typeof exec>;
 31 | 
 32 | /**
 33 |  * Integration tests for ShutdownSimulatorController
 34 |  * 
 35 |  * Tests the integration between:
 36 |  * - Controller → Use Case → Adapters
 37 |  * - Input validation → Domain logic → Output formatting
 38 |  * 
 39 |  * Mocks only external boundaries (shell commands)
 40 |  * Tests behavior, not implementation details
 41 |  */
 42 | describe('ShutdownSimulatorController Integration', () => {
 43 |   let controller: MCPController;
 44 |   let execCallIndex: number;
 45 |   let execMockResponses: Array<{ stdout: string; stderr: string; error?: NodeExecError }>;
 46 | 
 47 |   beforeEach(() => {
 48 |     jest.clearAllMocks();
 49 |     execCallIndex = 0;
 50 |     execMockResponses = [];
 51 |     
 52 |     // Setup exec mock to return responses sequentially
 53 |     mockExec.mockImplementation(((
 54 |       _cmd: string, 
 55 |       _options: any, 
 56 |       callback: (error: Error | null, stdout: string, stderr: string) => void
 57 |     ) => {
 58 |       const response = execMockResponses[execCallIndex++] || { stdout: '', stderr: '' };
 59 |       if (response.error) {
 60 |         callback(response.error, response.stdout, response.stderr);
 61 |       } else {
 62 |         callback(null, response.stdout, response.stderr);
 63 |       }
 64 |     }) as any);
 65 |     
 66 |     // Create controller with REAL components using factory
 67 |     controller = ShutdownSimulatorControllerFactory.create();
 68 |   });
 69 | 
 70 |   describe('shutdown simulator workflow', () => {
 71 |     it('should shutdown a booted simulator', async () => {
 72 |       // Arrange
 73 |       const simulatorData = {
 74 |         devices: {
 75 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
 76 |             udid: 'ABC123',
 77 |             name: 'iPhone 15',
 78 |             state: SimulatorState.Booted,
 79 |             isAvailable: true,
 80 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'
 81 |           }]
 82 |         }
 83 |       };
 84 |       
 85 |       execMockResponses = [
 86 |         { stdout: JSON.stringify(simulatorData), stderr: '' },  // list devices
 87 |         { stdout: '', stderr: '' }  // shutdown command succeeds
 88 |       ];
 89 |       
 90 |       // Act
 91 |       const result = await controller.execute({ deviceId: 'iPhone 15' });
 92 |       
 93 |       // Assert - Test behavior: simulator was successfully shutdown
 94 |       expect(result.content[0].text).toBe('✅ Successfully shutdown simulator: iPhone 15 (ABC123)');
 95 |     });
 96 | 
 97 |     it('should handle already shutdown simulator', async () => {
 98 |       // Arrange
 99 |       const simulatorData = {
100 |         devices: {
101 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
102 |             udid: 'ABC123',
103 |             name: 'iPhone 15',
104 |             state: SimulatorState.Shutdown,
105 |             isAvailable: true,
106 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'
107 |           }]
108 |         }
109 |       };
110 |       
111 |       execMockResponses = [
112 |         { stdout: JSON.stringify(simulatorData), stderr: '' }  // list devices - already shutdown
113 |       ];
114 |       
115 |       // Act
116 |       const result = await controller.execute({ deviceId: 'iPhone 15' });
117 |       
118 |       // Assert - Test behavior: reports simulator is already shutdown
119 |       expect(result.content[0].text).toBe('✅ Simulator already shutdown: iPhone 15 (ABC123)');
120 |     });
121 | 
122 |     it('should shutdown simulator by UUID', async () => {
123 |       // Arrange
124 |       const uuid = '550e8400-e29b-41d4-a716-446655440000';
125 |       const simulatorData = {
126 |         devices: {
127 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
128 |             udid: uuid,
129 |             name: 'iPhone 15 Pro',
130 |             state: SimulatorState.Booted,
131 |             isAvailable: true,
132 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro'
133 |           }]
134 |         }
135 |       };
136 |       
137 |       execMockResponses = [
138 |         { stdout: JSON.stringify(simulatorData), stderr: '' },  // list devices
139 |         { stdout: '', stderr: '' }  // shutdown command succeeds
140 |       ];
141 |       
142 |       // Act
143 |       const result = await controller.execute({ deviceId: uuid });
144 |       
145 |       // Assert - Test behavior: simulator was shutdown using UUID
146 |       expect(result.content[0].text).toBe(`✅ Successfully shutdown simulator: iPhone 15 Pro (${uuid})`);
147 |     });
148 |   });
149 | 
150 |   describe('error handling', () => {
151 |     it('should handle simulator not found', async () => {
152 |       // Arrange
153 |       execMockResponses = [
154 |         { stdout: JSON.stringify({ devices: {} }), stderr: '' }  // empty device list
155 |       ];
156 |       
157 |       // Act
158 |       const result = await controller.execute({ deviceId: 'NonExistent' });
159 |       
160 |       // Assert - Test behavior: appropriate error message shown
161 |       expect(result.content[0].text).toBe('❌ Simulator not found: NonExistent');
162 |     });
163 | 
164 |     it('should handle shutdown command failure', async () => {
165 |       // Arrange
166 |       const simulatorData = {
167 |         devices: {
168 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
169 |             udid: 'ABC123',
170 |             name: 'iPhone 15',
171 |             state: SimulatorState.Booted,
172 |             isAvailable: true,
173 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'
174 |           }]
175 |         }
176 |       };
177 |       
178 |       const shutdownError: NodeExecError = new Error('Command failed') as NodeExecError;
179 |       shutdownError.code = 1;
180 |       shutdownError.stdout = '';
181 |       shutdownError.stderr = 'Unable to shutdown device';
182 |       
183 |       execMockResponses = [
184 |         { stdout: JSON.stringify(simulatorData), stderr: '' },  // list devices
185 |         { stdout: '', stderr: 'Unable to shutdown device', error: shutdownError }  // shutdown fails
186 |       ];
187 |       
188 |       // Act
189 |       const result = await controller.execute({ deviceId: 'iPhone 15' });
190 |       
191 |       // Assert - Test behavior: error message includes context for found simulator
192 |       expect(result.content[0].text).toBe('❌ iPhone 15 (ABC123) - Unable to shutdown device');
193 |     });
194 |   });
195 | 
196 |   describe('validation', () => {
197 |     it('should validate required deviceId', async () => {
198 |       // Act
199 |       const result = await controller.execute({} as any);
200 |       
201 |       // Assert
202 |       expect(result.content[0].text).toBe('❌ Device ID is required');
203 |     });
204 | 
205 |     it('should validate empty deviceId', async () => {
206 |       // Act
207 |       const result = await controller.execute({ deviceId: '' });
208 |       
209 |       // Assert
210 |       expect(result.content[0].text).toBe('❌ Device ID cannot be empty');
211 |     });
212 | 
213 |     it('should validate whitespace-only deviceId', async () => {
214 |       // Act
215 |       const result = await controller.execute({ deviceId: '   ' });
216 |       
217 |       // Assert
218 |       expect(result.content[0].text).toBe('❌ Device ID cannot be whitespace only');
219 |     });
220 |   });
221 | 
222 |   describe('complex scenarios', () => {
223 |     it('should shutdown specific simulator when multiple exist with similar names', async () => {
224 |       // Arrange
225 |       const simulatorData = {
226 |         devices: {
227 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
228 |             {
229 |               udid: 'AAA111',
230 |               name: 'iPhone 15',
231 |               state: SimulatorState.Booted,
232 |               isAvailable: true,
233 |               deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'
234 |             },
235 |             {
236 |               udid: 'BBB222',
237 |               name: 'iPhone 15 Pro',
238 |               state: SimulatorState.Booted,
239 |               isAvailable: true,
240 |               deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro'
241 |             },
242 |             {
243 |               udid: 'CCC333',
244 |               name: 'iPhone 15 Pro Max',
245 |               state: SimulatorState.Shutdown,
246 |               isAvailable: true,
247 |               deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro-Max'
248 |             }
249 |           ]
250 |         }
251 |       };
252 |       
253 |       execMockResponses = [
254 |         { stdout: JSON.stringify(simulatorData), stderr: '' },  // list devices
255 |         { stdout: '', stderr: '' }  // shutdown command succeeds
256 |       ];
257 |       
258 |       // Act
259 |       const result = await controller.execute({ deviceId: 'iPhone 15 Pro' });
260 |       
261 |       // Assert - Test behavior: correct simulator was shutdown
262 |       expect(result.content[0].text).toBe('✅ Successfully shutdown simulator: iPhone 15 Pro (BBB222)');
263 |     });
264 | 
265 |     it('should handle mixed state simulators across runtimes', async () => {
266 |       // Arrange
267 |       const simulatorData = {
268 |         devices: {
269 |           'com.apple.CoreSimulator.SimRuntime.iOS-16-0': [{
270 |             udid: 'OLD123',
271 |             name: 'iPhone 14',
272 |             state: SimulatorState.Shutdown,
273 |             isAvailable: true,
274 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-14'
275 |           }],
276 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
277 |             udid: 'NEW456',
278 |             name: 'iPhone 14',
279 |             state: SimulatorState.Booted,
280 |             isAvailable: true,
281 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-14'
282 |           }]
283 |         }
284 |       };
285 |       
286 |       execMockResponses = [
287 |         { stdout: JSON.stringify(simulatorData), stderr: '' },  // list shows iOS 17 one is booted
288 |         { stdout: '', stderr: '' }  // shutdown succeeds
289 |       ];
290 |       
291 |       // Act - should find the first matching by name (prioritizes newer runtime)
292 |       const result = await controller.execute({ deviceId: 'iPhone 14' });
293 |       
294 |       // Assert - Test behavior: finds and shuts down the iOS 17 device (newer runtime)
295 |       expect(result.content[0].text).toBe('✅ Successfully shutdown simulator: iPhone 14 (NEW456)');
296 |     });
297 | 
298 |     it('should shutdown simulator in Booting state', async () => {
299 |       // Arrange
300 |       const simulatorData = {
301 |         devices: {
302 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [{
303 |             udid: 'BOOT123',
304 |             name: 'iPhone 15',
305 |             state: SimulatorState.Booting,
306 |             isAvailable: true,
307 |             deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'
308 |           }]
309 |         }
310 |       };
311 |       
312 |       execMockResponses = [
313 |         { stdout: JSON.stringify(simulatorData), stderr: '' },  // list devices - in Booting state
314 |         { stdout: '', stderr: '' }  // shutdown command succeeds
315 |       ];
316 |       
317 |       // Act
318 |       const result = await controller.execute({ deviceId: 'iPhone 15' });
319 |       
320 |       // Assert - Test behavior: can shutdown a simulator that's booting
321 |       expect(result.content[0].text).toBe('✅ Successfully shutdown simulator: iPhone 15 (BOOT123)');
322 |     });
323 |   });
324 | });
```

--------------------------------------------------------------------------------
/src/features/app-management/tests/integration/InstallAppController.integration.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Integration Test for InstallAppController
  3 |  * 
  4 |  * Tests the controller with REAL use case, presenter, and adapters
  5 |  * but MOCKS external boundaries (filesystem, subprocess).
  6 |  * 
  7 |  * Following testing philosophy:
  8 |  * - Integration tests (60% of suite) test component interactions
  9 |  * - Mock only external boundaries
 10 |  * - Test behavior, not implementation
 11 |  */
 12 | 
 13 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
 14 | import { MCPController } from '../../../../presentation/interfaces/MCPController.js';
 15 | import { InstallAppControllerFactory } from '../../factories/InstallAppControllerFactory.js';
 16 | import { exec } from 'child_process';
 17 | import { existsSync, statSync } from 'fs';
 18 | import type { NodeExecError } from '../../../../shared/tests/types/execTypes.js';
 19 | 
 20 | // Mock ONLY external boundaries
 21 | jest.mock('child_process');
 22 | jest.mock('fs');
 23 | 
 24 | // Mock promisify to return {stdout, stderr} for exec (as node's promisify does)
 25 | jest.mock('util', () => {
 26 |   const actualUtil = jest.requireActual('util') as typeof import('util');
 27 |   const { createPromisifiedExec } = require('../../../../shared/tests/mocks/promisifyExec');
 28 | 
 29 |   return {
 30 |     ...actualUtil,
 31 |     promisify: (fn: Function) =>
 32 |       fn?.name === 'exec' ? createPromisifiedExec(fn) : actualUtil.promisify(fn)
 33 |   };
 34 | });
 35 | 
 36 | // Mock DependencyChecker to always report dependencies are available in tests
 37 | jest.mock('../../../../infrastructure/services/DependencyChecker', () => ({
 38 |   DependencyChecker: jest.fn().mockImplementation(() => ({
 39 |     check: jest.fn<() => Promise<[]>>().mockResolvedValue([]) // No missing dependencies
 40 |   }))
 41 | }));
 42 | 
 43 | const mockExec = exec as jest.MockedFunction<typeof exec>;
 44 | const mockExistsSync = existsSync as jest.MockedFunction<typeof existsSync>;
 45 | const mockStatSync = statSync as jest.MockedFunction<typeof statSync>;
 46 | 
 47 | describe('InstallAppController Integration', () => {
 48 |   let controller: MCPController;
 49 |   let execCallIndex: number;
 50 |   let execMockResponses: Array<{ stdout: string; stderr: string; error?: NodeExecError }>;
 51 |   
 52 |   // Helper to create device list JSON response
 53 |   const createDeviceListResponse = (devices: Array<{udid: string, name: string, state: string}>) => ({
 54 |     stdout: JSON.stringify({
 55 |       devices: {
 56 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': devices.map(d => ({
 57 |           ...d,
 58 |           isAvailable: true
 59 |         }))
 60 |       }
 61 |     }),
 62 |     stderr: ''
 63 |   });
 64 | 
 65 |   beforeEach(() => {
 66 |     jest.clearAllMocks();
 67 |     execCallIndex = 0;
 68 |     execMockResponses = [];
 69 |     
 70 |     // Setup selective exec mock for xcrun simctl commands
 71 |     const actualExec = (jest.requireActual('child_process') as typeof import('child_process')).exec;
 72 |     const { createSelectiveExecMock } = require('../../../../shared/tests/mocks/selectiveExecMock');
 73 |     
 74 |     const isSimctlCommand = (cmd: string) => 
 75 |       cmd.includes('xcrun simctl');
 76 |     
 77 |     mockExec.mockImplementation(
 78 |       createSelectiveExecMock(
 79 |         isSimctlCommand,
 80 |         () => execMockResponses[execCallIndex++],
 81 |         actualExec
 82 |       )
 83 |     );
 84 |     
 85 |     // Default filesystem mocks
 86 |     mockExistsSync.mockImplementation((path) => {
 87 |       const pathStr = String(path);
 88 |       return pathStr.endsWith('.app');
 89 |     });
 90 |     
 91 |     mockStatSync.mockImplementation((path) => ({
 92 |       isDirectory: () => String(path).endsWith('.app'),
 93 |       isFile: () => false,
 94 |       // Add other stat properties as needed
 95 |     } as any));
 96 |     
 97 |     // Create controller with REAL components using factory
 98 |     controller = InstallAppControllerFactory.create();
 99 |   });
100 | 
101 |   describe('successful app installation', () => {
102 |     it('should install app on booted simulator', async () => {
103 |       // Arrange
104 |       const appPath = '/Users/dev/MyApp.app';
105 |       const simulatorId = 'test-simulator-id';
106 |       
107 |       execMockResponses = [
108 |         // Find simulator
109 |         createDeviceListResponse([
110 |           { udid: simulatorId, name: 'iPhone 15', state: 'Booted' }
111 |         ]),
112 |         // Install app
113 |         { stdout: '', stderr: '' }
114 |       ];
115 |       
116 |       // Act
117 |       const result = await controller.execute({
118 |         appPath,
119 |         simulatorId
120 |       });
121 |       
122 |       // Assert
123 |       expect(result).toMatchObject({
124 |         content: expect.arrayContaining([
125 |           expect.objectContaining({
126 |             type: 'text',
127 |             text: expect.stringContaining('Successfully installed')
128 |           })
129 |         ])
130 |       });
131 |       
132 |     });
133 | 
134 |     it('should find and use booted simulator when no ID specified', async () => {
135 |       // Arrange
136 |       const appPath = '/Users/dev/MyApp.app';
137 |       
138 |       execMockResponses = [
139 |         // xcrun simctl list devices --json (to find booted simulator)
140 |         { 
141 |           stdout: JSON.stringify({
142 |             devices: {
143 |               'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
144 |                 {
145 |                   udid: 'booted-sim-id',
146 |                   name: 'iPhone 15',
147 |                   state: 'Booted',
148 |                   isAvailable: true
149 |                 },
150 |                 {
151 |                   udid: 'shutdown-sim-id',
152 |                   name: 'iPhone 14',
153 |                   state: 'Shutdown',
154 |                   isAvailable: true
155 |                 }
156 |               ]
157 |             }
158 |           }),
159 |           stderr: ''
160 |         },
161 |         // xcrun simctl install command
162 |         { stdout: '', stderr: '' }
163 |       ];
164 |       
165 |       // Act
166 |       const result = await controller.execute({
167 |         appPath
168 |       });
169 |       
170 |       // Assert
171 |       expect(result).toMatchObject({
172 |         content: expect.arrayContaining([
173 |           expect.objectContaining({
174 |             type: 'text',
175 |             text: expect.stringContaining('Successfully installed')
176 |           })
177 |         ])
178 |       });
179 |       
180 |     });
181 | 
182 |     it('should boot simulator if shutdown', async () => {
183 |       // Arrange
184 |       const appPath = '/Users/dev/MyApp.app';
185 |       const simulatorId = 'shutdown-sim-id';
186 |       
187 |       execMockResponses = [
188 |         // Find simulator
189 |         {
190 |           stdout: JSON.stringify({
191 |             devices: {
192 |               'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
193 |                 {
194 |                   udid: simulatorId,
195 |                   name: 'iPhone 15',
196 |                   state: 'Shutdown',
197 |                   isAvailable: true
198 |                 }
199 |               ]
200 |             }
201 |           }),
202 |           stderr: ''
203 |         },
204 |         // Boot simulator
205 |         { stdout: '', stderr: '' },
206 |         // Install app
207 |         { stdout: '', stderr: '' }
208 |       ];
209 |       
210 |       // Act
211 |       const result = await controller.execute({
212 |         appPath,
213 |         simulatorId
214 |       });
215 |       
216 |       // Assert
217 |       expect(result).toMatchObject({
218 |         content: expect.arrayContaining([
219 |           expect.objectContaining({
220 |             type: 'text',
221 |             text: expect.stringContaining('Successfully installed')
222 |           })
223 |         ])
224 |       });
225 |       
226 |     });
227 |   });
228 | 
229 |   describe('error handling', () => {
230 |     it('should fail when app path does not exist', async () => {
231 |       // Arrange
232 |       const nonExistentPath = '/path/that/does/not/exist.app';
233 |       mockExistsSync.mockReturnValue(false);
234 |       
235 |       execMockResponses = [
236 |         // Find simulator
237 |         createDeviceListResponse([
238 |           { udid: 'test-sim', name: 'iPhone 15', state: 'Booted' }
239 |         ]),
240 |         // Install command would fail with file not found
241 |         {
242 |           error: Object.assign(new Error('Failed to install app'), {
243 |             code: 1,
244 |             stdout: '',
245 |             stderr: 'xcrun simctl install: No such file or directory'
246 |           }),
247 |           stdout: '',
248 |           stderr: 'xcrun simctl install: No such file or directory'
249 |         }
250 |       ];
251 |       
252 |       // Act
253 |       const result = await controller.execute({
254 |         appPath: nonExistentPath,
255 |         simulatorId: 'test-sim'
256 |       });
257 |       
258 |       // Assert
259 |       expect(result.content[0].text).toBe('❌ iPhone 15 (test-sim) - xcrun simctl install: No such file or directory');
260 |     });
261 | 
262 |     it('should fail when app path is not an app bundle', async () => {
263 |       // Arrange
264 |       const invalidPath = '/Users/dev/file.txt';
265 |       mockExistsSync.mockReturnValue(true);
266 |       mockStatSync.mockReturnValue({
267 |         isDirectory: () => false,
268 |         isFile: () => true
269 |       } as any);
270 |       
271 |       // Act
272 |       const result = await controller.execute({
273 |         appPath: invalidPath,
274 |         simulatorId: 'test-sim'
275 |       });
276 |       
277 |       // Assert
278 |       expect(result.content[0].text).toBe('❌ App path must end with .app');
279 |     });
280 | 
281 |     it('should fail when simulator does not exist', async () => {
282 |       // Arrange
283 |       const appPath = '/Users/dev/MyApp.app';
284 |       const nonExistentSim = 'non-existent-id';
285 |       
286 |       execMockResponses = [
287 |         // List devices - simulator not found
288 |         {
289 |           stdout: JSON.stringify({
290 |             devices: {
291 |               'com.apple.CoreSimulator.SimRuntime.iOS-17-0': []
292 |             }
293 |           }),
294 |           stderr: ''
295 |         }
296 |       ];
297 |       
298 |       // Act
299 |       const result = await controller.execute({
300 |         appPath,
301 |         simulatorId: nonExistentSim
302 |       });
303 |       
304 |       // Assert
305 |       expect(result.content[0].text).toBe(`❌ Simulator not found: ${nonExistentSim}`);
306 |     });
307 | 
308 |     it('should fail when no booted simulator and no ID specified', async () => {
309 |       // Arrange
310 |       const appPath = '/Users/dev/MyApp.app';
311 |       
312 |       execMockResponses = [
313 |         // List devices - no booted simulators
314 |         {
315 |           stdout: JSON.stringify({
316 |             devices: {
317 |               'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
318 |                 {
319 |                   udid: 'shutdown-sim',
320 |                   name: 'iPhone 15',
321 |                   state: 'Shutdown',
322 |                   isAvailable: true
323 |                 }
324 |               ]
325 |             }
326 |           }),
327 |           stderr: ''
328 |         }
329 |       ];
330 |       
331 |       // Act
332 |       const result = await controller.execute({
333 |         appPath
334 |       });
335 |       
336 |       // Assert
337 |       expect(result.content[0].text).toBe('❌ No booted simulator found. Please boot a simulator first or specify a simulator ID.');
338 |     });
339 | 
340 |     it('should handle installation failure gracefully', async () => {
341 |       // Arrange
342 |       const appPath = '/Users/dev/MyApp.app';
343 |       const simulatorId = 'test-sim';
344 |       
345 |       const error = new Error('Failed to install app: incompatible architecture') as NodeExecError;
346 |       error.code = 1;
347 |       error.stdout = '';
348 |       error.stderr = 'Error: incompatible architecture';
349 |       
350 |       execMockResponses = [
351 |         // Find simulator
352 |         createDeviceListResponse([
353 |           { udid: simulatorId, name: 'iPhone 15', state: 'Booted' }
354 |         ]),
355 |         // Install fails
356 |         { error, stdout: '', stderr: error.stderr }
357 |       ];
358 |       
359 |       // Act
360 |       const result = await controller.execute({
361 |         appPath,
362 |         simulatorId
363 |       });
364 |       
365 |       // Assert
366 |       expect(result.content[0].text).toBe('❌ iPhone 15 (test-sim) - incompatible architecture');
367 |     });
368 |   });
369 | 
370 |   describe('input validation', () => {
371 |     it('should accept simulator name instead of UUID', async () => {
372 |       // Arrange
373 |       const appPath = '/Users/dev/MyApp.app';
374 |       const simulatorName = 'iPhone 15 Pro';
375 |       
376 |       execMockResponses = [
377 |         // Find simulator by name
378 |         createDeviceListResponse([
379 |           { udid: 'sim-id-123', name: simulatorName, state: 'Booted' }
380 |         ]),
381 |         // Install succeeds
382 |         { stdout: '', stderr: '' }
383 |       ];
384 |       
385 |       // Act
386 |       const result = await controller.execute({
387 |         appPath,
388 |         simulatorId: simulatorName
389 |       });
390 |       
391 |       // Assert
392 |       expect(result).toMatchObject({
393 |         content: expect.arrayContaining([
394 |           expect.objectContaining({
395 |             type: 'text',
396 |             text: expect.stringContaining('Successfully installed')
397 |           })
398 |         ])
399 |       });
400 |       // Should show both simulator name and ID in format: "name (id)"
401 |       expect(result.content[0].text).toContain(`${simulatorName} (sim-id-123)`);
402 |     });
403 | 
404 |     it('should handle paths with spaces', async () => {
405 |       // Arrange
406 |       const appPath = '/Users/dev/My iOS App/MyApp.app';
407 |       const simulatorId = 'test-sim';
408 |       
409 |       execMockResponses = [
410 |         // Find simulator
411 |         createDeviceListResponse([
412 |           { udid: simulatorId, name: 'iPhone 15', state: 'Booted' }
413 |         ]),
414 |         // Install app
415 |         { stdout: '', stderr: '' }
416 |       ];
417 |       
418 |       // Act
419 |       const result = await controller.execute({
420 |         appPath,
421 |         simulatorId
422 |       });
423 |       
424 |       // Assert
425 |       expect(result).toMatchObject({
426 |         content: expect.arrayContaining([
427 |           expect.objectContaining({
428 |             type: 'text',
429 |             text: expect.stringContaining('Successfully installed')
430 |           })
431 |         ])
432 |       });
433 |       // Path with spaces should be handled correctly
434 |       expect(result.content[0].text).toContain('iPhone 15 (test-sim)');
435 |       
436 |     });
437 |   });
438 | });
```

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

```typescript
  1 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  2 | import { MCPController } from '../../../../presentation/interfaces/MCPController.js';
  3 | import { ListSimulatorsControllerFactory } from '../../factories/ListSimulatorsControllerFactory.js';
  4 | import { SimulatorState } from '../../domain/SimulatorState.js';
  5 | import { exec } from 'child_process';
  6 | import type { NodeExecError } from '../../../../shared/tests/types/execTypes.js';
  7 | 
  8 | // Mock ONLY external boundaries
  9 | jest.mock('child_process');
 10 | 
 11 | // Mock promisify to return {stdout, stderr} for exec (as node's promisify does)
 12 | jest.mock('util', () => {
 13 |   const actualUtil = jest.requireActual('util') as typeof import('util');
 14 |   const { createPromisifiedExec } = require('../../../../shared/tests/mocks/promisifyExec');
 15 | 
 16 |   return {
 17 |     ...actualUtil,
 18 |     promisify: (fn: Function) =>
 19 |       fn?.name === 'exec' ? createPromisifiedExec(fn) : actualUtil.promisify(fn)
 20 |   };
 21 | });
 22 | 
 23 | // Mock DependencyChecker to always report dependencies are available in tests
 24 | jest.mock('../../../../infrastructure/services/DependencyChecker', () => ({
 25 |   DependencyChecker: jest.fn().mockImplementation(() => ({
 26 |     check: jest.fn<() => Promise<[]>>().mockResolvedValue([]) // No missing dependencies
 27 |   }))
 28 | }));
 29 | 
 30 | const mockExec = exec as jest.MockedFunction<typeof exec>;
 31 | 
 32 | describe('ListSimulatorsController Integration', () => {
 33 |   let controller: MCPController;
 34 |   let execCallIndex: number;
 35 |   let execMockResponses: Array<{ stdout: string; stderr: string; error?: NodeExecError }>;
 36 | 
 37 |   beforeEach(() => {
 38 |     jest.clearAllMocks();
 39 |     execCallIndex = 0;
 40 |     execMockResponses = [];
 41 | 
 42 |     // Setup exec mock to return responses sequentially
 43 |     mockExec.mockImplementation(((
 44 |       _cmd: string,
 45 |       _options: any,
 46 |       callback: (error: Error | null, stdout: string, stderr: string) => void
 47 |     ) => {
 48 |       const response = execMockResponses[execCallIndex++] || { stdout: '', stderr: '' };
 49 |       if (response.error) {
 50 |         callback(response.error, response.stdout, response.stderr);
 51 |       } else {
 52 |         callback(null, response.stdout, response.stderr);
 53 |       }
 54 |     }) as any);
 55 | 
 56 |     // Create controller with REAL components using factory
 57 |     controller = ListSimulatorsControllerFactory.create();
 58 |   });
 59 | 
 60 |   describe('with mocked shell commands', () => {
 61 |     it('should list all simulators', async () => {
 62 |       // Arrange
 63 |       const mockDeviceList = {
 64 |         devices: {
 65 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
 66 |             {
 67 |               dataPath: '/path/to/data',
 68 |               dataPathSize: 1000000,
 69 |               logPath: '/path/to/logs',
 70 |               udid: 'ABC123',
 71 |               isAvailable: true,
 72 |               deviceTypeIdentifier: 'com.apple.iPhone15',
 73 |               state: 'Booted',
 74 |               name: 'iPhone 15'
 75 |             },
 76 |             {
 77 |               dataPath: '/path/to/data2',
 78 |               dataPathSize: 2000000,
 79 |               logPath: '/path/to/logs2',
 80 |               udid: 'DEF456',
 81 |               isAvailable: true,
 82 |               deviceTypeIdentifier: 'com.apple.iPadPro',
 83 |               state: 'Shutdown',
 84 |               name: 'iPad Pro'
 85 |             }
 86 |           ],
 87 |           'com.apple.CoreSimulator.SimRuntime.tvOS-17-0': [
 88 |             {
 89 |               dataPath: '/path/to/data3',
 90 |               dataPathSize: 3000000,
 91 |               logPath: '/path/to/logs3',
 92 |               udid: 'GHI789',
 93 |               isAvailable: true,
 94 |               deviceTypeIdentifier: 'com.apple.AppleTV',
 95 |               state: 'Shutdown',
 96 |               name: 'Apple TV'
 97 |             }
 98 |           ]
 99 |         }
100 |       };
101 | 
102 |       execMockResponses = [
103 |         { stdout: JSON.stringify(mockDeviceList), stderr: '' }
104 |       ];
105 | 
106 |       // Act
107 |       const result = await controller.execute({});
108 | 
109 |       // Assert - Test behavior: lists all simulators
110 |       expect(result.content[0].text).toContain('Found 3 simulators');
111 |       expect(result.content[0].text).toContain('iPhone 15');
112 |       expect(result.content[0].text).toContain('iPad Pro');
113 |       expect(result.content[0].text).toContain('Apple TV');
114 |     });
115 | 
116 |     it('should filter by iOS platform', async () => {
117 |       // Arrange
118 |       const mockDeviceList = {
119 |         devices: {
120 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
121 |             {
122 |               udid: 'ABC123',
123 |               isAvailable: true,
124 |               state: 'Booted',
125 |               name: 'iPhone 15'
126 |             }
127 |           ],
128 |           'com.apple.CoreSimulator.SimRuntime.tvOS-17-0': [
129 |             {
130 |               udid: 'GHI789',
131 |               isAvailable: true,
132 |               state: 'Shutdown',
133 |               name: 'Apple TV'
134 |             }
135 |           ]
136 |         }
137 |       };
138 | 
139 |       execMockResponses = [
140 |         { stdout: JSON.stringify(mockDeviceList), stderr: '' }
141 |       ];
142 | 
143 |       // Act
144 |       const result = await controller.execute({ platform: 'iOS' });
145 | 
146 |       // Assert
147 |       expect(result.content[0].text).toContain('Found 1 simulator');
148 |       expect(result.content[0].text).toContain('iPhone 15');
149 |       expect(result.content[0].text).not.toContain('Apple TV');
150 |     });
151 | 
152 |     it('should filter by booted state', async () => {
153 |       // Arrange
154 |       const mockDeviceList = {
155 |         devices: {
156 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
157 |             {
158 |               udid: 'ABC123',
159 |               isAvailable: true,
160 |               state: 'Booted',
161 |               name: 'iPhone 15'
162 |             },
163 |             {
164 |               udid: 'DEF456',
165 |               isAvailable: true,
166 |               state: 'Shutdown',
167 |               name: 'iPad Pro'
168 |             }
169 |           ]
170 |         }
171 |       };
172 | 
173 |       execMockResponses = [
174 |         { stdout: JSON.stringify(mockDeviceList), stderr: '' }
175 |       ];
176 | 
177 |       // Act
178 |       const result = await controller.execute({ state: 'Booted' });
179 | 
180 |       // Assert
181 |       expect(result.content[0].text).toContain('Found 1 simulator');
182 |       expect(result.content[0].text).toContain('iPhone 15');
183 |       expect(result.content[0].text).not.toContain('iPad Pro');
184 |     });
185 | 
186 |     it('should return error when command execution fails', async () => {
187 |       // Arrange
188 |       const error = new Error('xcrun not found') as NodeExecError;
189 |       error.code = 1;
190 |       execMockResponses = [
191 |         { stdout: '', stderr: 'xcrun not found', error }
192 |       ];
193 | 
194 |       // Act
195 |       const result = await controller.execute({});
196 | 
197 |       // Assert
198 |       expect(result.content[0].type).toBe('text');
199 |       expect(result.content[0].text).toMatch(/^❌.*JSON/); // Error about JSON parsing
200 |     });
201 | 
202 |     it('should show warning when no simulators exist', async () => {
203 |       // Arrange
204 |       execMockResponses = [
205 |         { stdout: JSON.stringify({ devices: {} }), stderr: '' }
206 |       ];
207 | 
208 |       // Act
209 |       const result = await controller.execute({});
210 | 
211 |       // Assert
212 |       expect(result.content[0].text).toBe('🔍 No simulators found');
213 |     });
214 | 
215 |     it('should filter by multiple criteria', async () => {
216 |       // Arrange
217 |       const mockDeviceList = {
218 |         devices: {
219 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
220 |             {
221 |               udid: 'ABC123',
222 |               isAvailable: true,
223 |               state: 'Booted',
224 |               name: 'iPhone 15'
225 |             },
226 |             {
227 |               udid: 'DEF456',
228 |               isAvailable: true,
229 |               state: 'Shutdown',
230 |               name: 'iPad Pro'
231 |             }
232 |           ],
233 |           'com.apple.CoreSimulator.SimRuntime.tvOS-17-0': [
234 |             {
235 |               udid: 'GHI789',
236 |               isAvailable: true,
237 |               state: 'Booted',
238 |               name: 'Apple TV'
239 |             }
240 |           ]
241 |         }
242 |       };
243 | 
244 |       execMockResponses = [
245 |         { stdout: JSON.stringify(mockDeviceList), stderr: '' }
246 |       ];
247 | 
248 |       // Act
249 |       const result = await controller.execute({
250 |         platform: 'iOS',
251 |         state: 'Booted'
252 |       });
253 | 
254 |       // Assert
255 |       expect(result.content[0].text).toContain('Found 1 simulator');
256 |       expect(result.content[0].text).toContain('iPhone 15');
257 |       expect(result.content[0].text).not.toContain('iPad Pro');
258 |       expect(result.content[0].text).not.toContain('Apple TV');
259 |     });
260 | 
261 |     it('should return JSON parse error for malformed response', async () => {
262 |       // Arrange
263 |       execMockResponses = [
264 |         { stdout: 'not valid json', stderr: '' }
265 |       ];
266 | 
267 |       // Act
268 |       const result = await controller.execute({});
269 | 
270 |       // Assert
271 |       expect(result.content[0].type).toBe('text');
272 |       expect(result.content[0].text).toMatch(/^❌.*not valid JSON/);
273 |     });
274 | 
275 |     it('should return error for invalid platform', async () => {
276 |       // Arrange, Act, Assert
277 |       const result = await controller.execute({
278 |         platform: 'Android'
279 |       });
280 | 
281 |       expect(result.content[0].text).toBe('❌ Invalid platform: Android. Valid values are: iOS, macOS, tvOS, watchOS, visionOS');
282 |     });
283 | 
284 |     it('should return error for invalid state', async () => {
285 |       // Arrange, Act, Assert
286 |       const result = await controller.execute({
287 |         state: 'Running'
288 |       });
289 | 
290 |       expect(result.content[0].text).toBe('❌ Invalid simulator state: Running. Valid values are: Booted, Booting, Shutdown, Shutting Down');
291 |     });
292 | 
293 |     it('should filter by device name with partial match', async () => {
294 |       // Arrange
295 |       const mockDeviceList = {
296 |         devices: {
297 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
298 |             {
299 |               udid: 'ABC123',
300 |               isAvailable: true,
301 |               state: 'Booted',
302 |               name: 'iPhone 15 Pro'
303 |             },
304 |             {
305 |               udid: 'DEF456',
306 |               isAvailable: true,
307 |               state: 'Shutdown',
308 |               name: 'iPhone 14'
309 |             },
310 |             {
311 |               udid: 'GHI789',
312 |               isAvailable: true,
313 |               state: 'Shutdown',
314 |               name: 'iPad Pro'
315 |             }
316 |           ]
317 |         }
318 |       };
319 | 
320 |       execMockResponses = [
321 |         { stdout: JSON.stringify(mockDeviceList), stderr: '' }
322 |       ];
323 | 
324 |       // Act
325 |       const result = await controller.execute({ name: '15' });
326 | 
327 |       // Assert - Tests actual behavior: only devices with "15" in name
328 |       expect(result.content[0].text).toContain('Found 1 simulator');
329 |       expect(result.content[0].text).toContain('iPhone 15 Pro');
330 |       expect(result.content[0].text).not.toContain('iPhone 14');
331 |       expect(result.content[0].text).not.toContain('iPad Pro');
332 |     });
333 | 
334 |     it('should filter by device name case-insensitive', async () => {
335 |       // Arrange
336 |       const mockDeviceList = {
337 |         devices: {
338 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
339 |             {
340 |               udid: 'ABC123',
341 |               isAvailable: true,
342 |               state: 'Booted',
343 |               name: 'iPhone 15 Pro'
344 |             },
345 |             {
346 |               udid: 'DEF456',
347 |               isAvailable: true,
348 |               state: 'Shutdown',
349 |               name: 'iPad Air'
350 |             }
351 |           ]
352 |         }
353 |       };
354 | 
355 |       execMockResponses = [
356 |         { stdout: JSON.stringify(mockDeviceList), stderr: '' }
357 |       ];
358 | 
359 |       // Act
360 |       const result = await controller.execute({ name: 'iphone' });
361 | 
362 |       // Assert - Case-insensitive matching
363 |       expect(result.content[0].text).toContain('Found 1 simulator');
364 |       expect(result.content[0].text).toContain('iPhone 15 Pro');
365 |       expect(result.content[0].text).not.toContain('iPad Air');
366 |     });
367 | 
368 |     it('should combine all filters (platform, state, and name)', async () => {
369 |       // Arrange
370 |       const mockDeviceList = {
371 |         devices: {
372 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
373 |             {
374 |               udid: 'ABC123',
375 |               isAvailable: true,
376 |               state: 'Booted',
377 |               name: 'iPhone 15 Pro'
378 |             },
379 |             {
380 |               udid: 'DEF456',
381 |               isAvailable: true,
382 |               state: 'Shutdown',
383 |               name: 'iPhone 15'
384 |             },
385 |             {
386 |               udid: 'GHI789',
387 |               isAvailable: true,
388 |               state: 'Booted',
389 |               name: 'iPhone 14'
390 |             }
391 |           ],
392 |           'com.apple.CoreSimulator.SimRuntime.tvOS-17-0': [
393 |             {
394 |               udid: 'TV123',
395 |               isAvailable: true,
396 |               state: 'Booted',
397 |               name: 'Apple TV 15'
398 |             }
399 |           ]
400 |         }
401 |       };
402 | 
403 |       execMockResponses = [
404 |         { stdout: JSON.stringify(mockDeviceList), stderr: '' }
405 |       ];
406 | 
407 |       // Act
408 |       const result = await controller.execute({
409 |         platform: 'iOS',
410 |         state: 'Booted',
411 |         name: '15'
412 |       });
413 | 
414 |       // Assert - All filters applied together
415 |       expect(result.content[0].text).toContain('Found 1 simulator');
416 |       expect(result.content[0].text).toContain('iPhone 15 Pro');
417 |       expect(result.content[0].text).not.toContain('iPhone 14'); // Wrong name
418 |       expect(result.content[0].text).not.toContain('Apple TV'); // Wrong platform
419 |     });
420 | 
421 |     it('should show no simulators when name filter matches nothing', async () => {
422 |       // Arrange
423 |       const mockDeviceList = {
424 |         devices: {
425 |           'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
426 |             {
427 |               udid: 'ABC123',
428 |               isAvailable: true,
429 |               state: 'Booted',
430 |               name: 'iPhone 15 Pro'
431 |             }
432 |           ]
433 |         }
434 |       };
435 | 
436 |       execMockResponses = [
437 |         { stdout: JSON.stringify(mockDeviceList), stderr: '' }
438 |       ];
439 | 
440 |       // Act
441 |       const result = await controller.execute({ name: 'Galaxy' });
442 | 
443 |       // Assert
444 |       expect(result.content[0].text).toBe('🔍 No simulators found');
445 |     });
446 |   });
447 | });
```

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

```typescript
  1 | import { describe, it, expect, jest, beforeEach } from '@jest/globals';
  2 | import { ListSimulatorsUseCase } from '../../use-cases/ListSimulatorsUseCase.js';
  3 | import { DeviceRepository } from '../../../../infrastructure/repositories/DeviceRepository.js';
  4 | import { ListSimulatorsRequest } from '../../domain/ListSimulatorsRequest.js';
  5 | import { SimulatorListParseError } from '../../domain/ListSimulatorsResult.js';
  6 | import { Platform } from '../../../../shared/domain/Platform.js';
  7 | import { SimulatorState } from '../../domain/SimulatorState.js';
  8 | 
  9 | describe('ListSimulatorsUseCase', () => {
 10 |   let mockDeviceRepository: jest.Mocked<DeviceRepository>;
 11 |   let sut: ListSimulatorsUseCase;
 12 | 
 13 |   beforeEach(() => {
 14 |     mockDeviceRepository = {
 15 |       getAllDevices: jest.fn()
 16 |     } as any;
 17 | 
 18 |     sut = new ListSimulatorsUseCase(mockDeviceRepository);
 19 |   });
 20 | 
 21 |   describe('execute', () => {
 22 |     it('should return all available simulators when no filters', async () => {
 23 |       // Arrange
 24 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
 25 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
 26 |           {
 27 |             udid: 'ABC123',
 28 |             name: 'iPhone 15',
 29 |             state: 'Booted',
 30 |             isAvailable: true
 31 |           },
 32 |           {
 33 |             udid: 'DEF456',
 34 |             name: 'iPad Pro',
 35 |             state: 'Shutdown',
 36 |             isAvailable: true
 37 |           },
 38 |           {
 39 |             udid: 'NOTAVAIL',
 40 |             name: 'Old iPhone',
 41 |             state: 'Shutdown',
 42 |             isAvailable: false
 43 |           }
 44 |         ]
 45 |       });
 46 | 
 47 |       const request = ListSimulatorsRequest.create();
 48 | 
 49 |       // Act
 50 |       const result = await sut.execute(request);
 51 | 
 52 |       // Assert
 53 |       expect(result.isSuccess).toBe(true);
 54 |       expect(result.count).toBe(2); // Only available devices
 55 |       expect(result.simulators).toHaveLength(2);
 56 |       expect(result.simulators[0]).toMatchObject({
 57 |         udid: 'ABC123',
 58 |         name: 'iPhone 15',
 59 |         state: SimulatorState.Booted,
 60 |         platform: 'iOS',
 61 |         runtime: 'iOS 17.0'
 62 |       });
 63 |     });
 64 | 
 65 |     it('should filter by platform', async () => {
 66 |       // Arrange
 67 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
 68 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
 69 |           {
 70 |             udid: 'IOS1',
 71 |             name: 'iPhone 15',
 72 |             state: 'Booted',
 73 |             isAvailable: true
 74 |           }
 75 |         ],
 76 |         'com.apple.CoreSimulator.SimRuntime.tvOS-17-0': [
 77 |           {
 78 |             udid: 'TV1',
 79 |             name: 'Apple TV',
 80 |             state: 'Shutdown',
 81 |             isAvailable: true
 82 |           }
 83 |         ]
 84 |       });
 85 | 
 86 |       const request = ListSimulatorsRequest.create(Platform.iOS);
 87 | 
 88 |       // Act
 89 |       const result = await sut.execute(request);
 90 | 
 91 |       // Assert
 92 |       expect(result.isSuccess).toBe(true);
 93 |       expect(result.count).toBe(1);
 94 |       expect(result.simulators[0].udid).toBe('IOS1');
 95 |     });
 96 | 
 97 |     it('should filter by state', async () => {
 98 |       // Arrange
 99 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
100 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
101 |           {
102 |             udid: 'BOOTED1',
103 |             name: 'iPhone 15',
104 |             state: 'Booted',
105 |             isAvailable: true
106 |           },
107 |           {
108 |             udid: 'SHUTDOWN1',
109 |             name: 'iPad Pro',
110 |             state: 'Shutdown',
111 |             isAvailable: true
112 |           }
113 |         ]
114 |       });
115 | 
116 |       const request = ListSimulatorsRequest.create(undefined, SimulatorState.Booted);
117 | 
118 |       // Act
119 |       const result = await sut.execute(request);
120 | 
121 |       // Assert
122 |       expect(result.isSuccess).toBe(true);
123 |       expect(result.count).toBe(1);
124 |       expect(result.simulators[0].udid).toBe('BOOTED1');
125 |     });
126 | 
127 |     it('should handle watchOS platform detection', async () => {
128 |       // Arrange
129 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
130 |         'com.apple.CoreSimulator.SimRuntime.watchOS-10-0': [
131 |           {
132 |             udid: 'WATCH1',
133 |             name: 'Apple Watch Series 9',
134 |             state: 'Shutdown',
135 |             isAvailable: true
136 |           }
137 |         ]
138 |       });
139 | 
140 |       const request = ListSimulatorsRequest.create();
141 | 
142 |       // Act
143 |       const result = await sut.execute(request);
144 | 
145 |       // Assert
146 |       expect(result.isSuccess).toBe(true);
147 |       expect(result.simulators[0].platform).toBe('watchOS');
148 |       expect(result.simulators[0].runtime).toBe('watchOS 10.0');
149 |     });
150 | 
151 |     it('should handle visionOS platform detection', async () => {
152 |       // Arrange
153 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
154 |         'com.apple.CoreSimulator.SimRuntime.visionOS-1-0': [
155 |           {
156 |             udid: 'VISION1',
157 |             name: 'Apple Vision Pro',
158 |             state: 'Shutdown',
159 |             isAvailable: true
160 |           }
161 |         ]
162 |       });
163 | 
164 |       const request = ListSimulatorsRequest.create();
165 | 
166 |       // Act
167 |       const result = await sut.execute(request);
168 | 
169 |       // Assert
170 |       expect(result.isSuccess).toBe(true);
171 |       expect(result.simulators[0].platform).toBe('visionOS');
172 |       expect(result.simulators[0].runtime).toBe('visionOS 1.0');
173 |     });
174 | 
175 |     it('should handle xrOS platform detection (legacy name for visionOS)', async () => {
176 |       // Arrange
177 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
178 |         'com.apple.CoreSimulator.SimRuntime.xrOS-1-0': [
179 |           {
180 |             udid: 'XR1',
181 |             name: 'Apple Vision Pro',
182 |             state: 'Shutdown',
183 |             isAvailable: true
184 |           }
185 |         ]
186 |       });
187 | 
188 |       const request = ListSimulatorsRequest.create();
189 | 
190 |       // Act
191 |       const result = await sut.execute(request);
192 | 
193 |       // Assert
194 |       expect(result.isSuccess).toBe(true);
195 |       expect(result.simulators[0].platform).toBe('visionOS');
196 |       expect(result.simulators[0].runtime).toBe('visionOS 1.0');
197 |     });
198 | 
199 |     it('should handle macOS platform detection', async () => {
200 |       // Arrange
201 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
202 |         'com.apple.CoreSimulator.SimRuntime.macOS-14-0': [
203 |           {
204 |             udid: 'MAC1',
205 |             name: 'Mac',
206 |             state: 'Shutdown',
207 |             isAvailable: true
208 |           }
209 |         ]
210 |       });
211 | 
212 |       const request = ListSimulatorsRequest.create();
213 | 
214 |       // Act
215 |       const result = await sut.execute(request);
216 | 
217 |       // Assert
218 |       expect(result.isSuccess).toBe(true);
219 |       expect(result.simulators[0].platform).toBe('macOS');
220 |       expect(result.simulators[0].runtime).toBe('macOS 14.0');
221 |     });
222 | 
223 |     it('should handle unknown platform', async () => {
224 |       // Arrange
225 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
226 |         'com.apple.CoreSimulator.SimRuntime.unknown-1-0': [
227 |           {
228 |             udid: 'UNKNOWN1',
229 |             name: 'Unknown Device',
230 |             state: 'Shutdown',
231 |             isAvailable: true
232 |           }
233 |         ]
234 |       });
235 | 
236 |       const request = ListSimulatorsRequest.create();
237 | 
238 |       // Act
239 |       const result = await sut.execute(request);
240 | 
241 |       // Assert
242 |       expect(result.isSuccess).toBe(true);
243 |       expect(result.simulators[0].platform).toBe('Unknown');
244 |       expect(result.simulators[0].runtime).toBe('Unknown 1.0');
245 |     });
246 | 
247 |     it('should handle Booting state', async () => {
248 |       // Arrange
249 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
250 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
251 |           {
252 |             udid: 'BOOT1',
253 |             name: 'iPhone 15',
254 |             state: 'Booting',
255 |             isAvailable: true
256 |           }
257 |         ]
258 |       });
259 | 
260 |       const request = ListSimulatorsRequest.create();
261 | 
262 |       // Act
263 |       const result = await sut.execute(request);
264 | 
265 |       // Assert
266 |       expect(result.isSuccess).toBe(true);
267 |       expect(result.simulators[0].state).toBe(SimulatorState.Booting);
268 |     });
269 | 
270 |     it('should handle Shutting Down state', async () => {
271 |       // Arrange
272 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
273 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
274 |           {
275 |             udid: 'SHUTTING1',
276 |             name: 'iPhone 15',
277 |             state: 'Shutting Down',
278 |             isAvailable: true
279 |           }
280 |         ]
281 |       });
282 | 
283 |       const request = ListSimulatorsRequest.create();
284 | 
285 |       // Act
286 |       const result = await sut.execute(request);
287 | 
288 |       // Assert
289 |       expect(result.isSuccess).toBe(true);
290 |       expect(result.simulators[0].state).toBe(SimulatorState.ShuttingDown);
291 |     });
292 | 
293 |     it('should handle unknown device state by throwing error', async () => {
294 |       // Arrange
295 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
296 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
297 |           {
298 |             udid: 'WEIRD1',
299 |             name: 'iPhone 15',
300 |             state: 'WeirdState',
301 |             isAvailable: true
302 |           }
303 |         ]
304 |       });
305 | 
306 |       const request = ListSimulatorsRequest.create();
307 | 
308 |       // Act
309 |       const result = await sut.execute(request);
310 | 
311 |       // Assert - should fail with error about unrecognized state
312 |       expect(result.isSuccess).toBe(false);
313 |       expect(result.error?.message).toContain('Invalid simulator state: WeirdState');
314 |     });
315 | 
316 |     it('should handle runtime without version number', async () => {
317 |       // Arrange
318 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
319 |         'com.apple.CoreSimulator.SimRuntime.iOS': [
320 |           {
321 |             udid: 'NOVERSION1',
322 |             name: 'iPhone',
323 |             state: 'Shutdown',
324 |             isAvailable: true
325 |           }
326 |         ]
327 |       });
328 | 
329 |       const request = ListSimulatorsRequest.create();
330 | 
331 |       // Act
332 |       const result = await sut.execute(request);
333 | 
334 |       // Assert
335 |       expect(result.isSuccess).toBe(true);
336 |       expect(result.simulators[0].runtime).toBe('iOS Unknown');
337 |     });
338 | 
339 |     it('should handle repository errors', async () => {
340 |       // Arrange
341 |       const error = new Error('Repository failed');
342 |       mockDeviceRepository.getAllDevices.mockRejectedValue(error);
343 | 
344 |       const request = ListSimulatorsRequest.create();
345 | 
346 |       // Act
347 |       const result = await sut.execute(request);
348 | 
349 |       // Assert
350 |       expect(result.isSuccess).toBe(false);
351 |       expect(result.error).toBeInstanceOf(SimulatorListParseError);
352 |       expect(result.error?.message).toBe('Failed to parse simulator list: not valid JSON');
353 |     });
354 | 
355 |     it('should return empty list when no simulators available', async () => {
356 |       // Arrange
357 |       mockDeviceRepository.getAllDevices.mockResolvedValue({});
358 | 
359 |       const request = ListSimulatorsRequest.create();
360 | 
361 |       // Act
362 |       const result = await sut.execute(request);
363 | 
364 |       // Assert
365 |       expect(result.isSuccess).toBe(true);
366 |       expect(result.count).toBe(0);
367 |       expect(result.simulators).toHaveLength(0);
368 |     });
369 | 
370 |     it('should filter by device name with partial match', async () => {
371 |       // Arrange
372 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
373 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
374 |           {
375 |             udid: 'ABC123',
376 |             name: 'iPhone 15 Pro',
377 |             state: 'Booted',
378 |             isAvailable: true
379 |           },
380 |           {
381 |             udid: 'DEF456',
382 |             name: 'iPhone 14',
383 |             state: 'Shutdown',
384 |             isAvailable: true
385 |           },
386 |           {
387 |             udid: 'GHI789',
388 |             name: 'iPad Pro',
389 |             state: 'Shutdown',
390 |             isAvailable: true
391 |           }
392 |         ]
393 |       });
394 | 
395 |       const request = ListSimulatorsRequest.create(undefined, undefined, '15');
396 | 
397 |       // Act
398 |       const result = await sut.execute(request);
399 | 
400 |       // Assert
401 |       expect(result.isSuccess).toBe(true);
402 |       expect(result.count).toBe(1);
403 |       expect(result.simulators[0].name).toBe('iPhone 15 Pro');
404 |     });
405 | 
406 |     it('should filter by device name case-insensitive', async () => {
407 |       // Arrange
408 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
409 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
410 |           {
411 |             udid: 'ABC123',
412 |             name: 'iPhone 15 Pro',
413 |             state: 'Booted',
414 |             isAvailable: true
415 |           },
416 |           {
417 |             udid: 'DEF456',
418 |             name: 'iPad Air',
419 |             state: 'Shutdown',
420 |             isAvailable: true
421 |           }
422 |         ]
423 |       });
424 | 
425 |       const request = ListSimulatorsRequest.create(undefined, undefined, 'iphone');
426 | 
427 |       // Act
428 |       const result = await sut.execute(request);
429 | 
430 |       // Assert
431 |       expect(result.isSuccess).toBe(true);
432 |       expect(result.count).toBe(1);
433 |       expect(result.simulators[0].name).toBe('iPhone 15 Pro');
434 |     });
435 | 
436 |     it('should combine all filters (platform, state, and name)', async () => {
437 |       // Arrange
438 |       mockDeviceRepository.getAllDevices.mockResolvedValue({
439 |         'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
440 |           {
441 |             udid: 'ABC123',
442 |             name: 'iPhone 15 Pro',
443 |             state: 'Booted',
444 |             isAvailable: true
445 |           },
446 |           {
447 |             udid: 'DEF456',
448 |             name: 'iPhone 15',
449 |             state: 'Shutdown',
450 |             isAvailable: true
451 |           }
452 |         ],
453 |         'com.apple.CoreSimulator.SimRuntime.tvOS-17-0': [
454 |           {
455 |             udid: 'GHI789',
456 |             name: 'Apple TV 15',
457 |             state: 'Booted',
458 |             isAvailable: true
459 |           }
460 |         ]
461 |       });
462 | 
463 |       const request = ListSimulatorsRequest.create(Platform.iOS, SimulatorState.Booted, '15');
464 | 
465 |       // Act
466 |       const result = await sut.execute(request);
467 | 
468 |       // Assert
469 |       expect(result.isSuccess).toBe(true);
470 |       expect(result.count).toBe(1);
471 |       expect(result.simulators[0].name).toBe('iPhone 15 Pro');
472 |       expect(result.simulators[0].platform).toBe('iOS');
473 |       expect(result.simulators[0].state).toBe(SimulatorState.Booted);
474 |     });
475 |   });
476 | });
```

--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { program } from 'commander';
  4 | import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
  5 | import { join, resolve, dirname } from 'path';
  6 | import { fileURLToPath } from 'url';
  7 | import { homedir } from 'os';
  8 | import { execSync } from 'child_process';
  9 | import * as readline from 'readline/promises';
 10 | 
 11 | const __filename = fileURLToPath(import.meta.url);
 12 | const __dirname = dirname(__filename);
 13 | const PACKAGE_ROOT = resolve(__dirname, '..');
 14 | 
 15 | interface ClaudeConfig {
 16 |   mcpServers?: Record<string, any>;
 17 |   [key: string]: any;
 18 | }
 19 | 
 20 | interface ClaudeSettings {
 21 |   hooks?: any;
 22 |   model?: string;
 23 |   [key: string]: any;
 24 | }
 25 | 
 26 | class MCPXcodeSetup {
 27 |   private rl = readline.createInterface({
 28 |     input: process.stdin,
 29 |     output: process.stdout
 30 |   });
 31 | 
 32 |   async setup() {
 33 |     console.log('🔧 MCP Xcode Setup\n');
 34 |     
 35 |     // 1. Ask about MCP server
 36 |     console.log('📡 MCP Server Configuration');
 37 |     const setupMCP = await this.askYesNo('Would you like to install the MCP Xcode server?');
 38 |     let mcpScope: 'global' | 'project' | null = null;
 39 |     if (setupMCP) {
 40 |       mcpScope = await this.askMCPScope();
 41 |       await this.setupMCPServer(mcpScope);
 42 |     }
 43 |     
 44 |     // 2. Ask about hooks (independent of MCP choice)
 45 |     console.log('\n📝 Xcode Sync Hook Configuration');
 46 |     console.log('The hook will automatically sync file operations with Xcode projects.');
 47 |     console.log('It syncs when:');
 48 |     console.log('  - Files are created, modified, deleted, or moved');
 49 |     console.log('  - An .xcodeproj file exists in the parent directories');
 50 |     console.log('  - The project hasn\'t opted out (via .no-xcode-sync or .no-xcode-autoadd file)');
 51 |     
 52 |     const setupHooks = await this.askYesNo('\nWould you like to enable Xcode file sync?');
 53 |     let hookScope: 'global' | 'project' | null = null;
 54 |     if (setupHooks) {
 55 |       hookScope = await this.askHookScope();
 56 |       await this.setupHooks(hookScope);
 57 |     }
 58 |     
 59 |     // 3. Build helper tools if anything was installed
 60 |     if (setupMCP || setupHooks) {
 61 |       console.log('\n📦 Building helper tools...');
 62 |       await this.buildHelperTools();
 63 |     }
 64 |     
 65 |     // 4. Show completion message
 66 |     console.log('\n✅ Setup complete!');
 67 |     console.log('\nNext steps:');
 68 |     console.log('1. Restart Claude Code for changes to take effect');
 69 |     
 70 |     const hasProjectConfig = (mcpScope === 'project' || hookScope === 'project');
 71 |     if (hasProjectConfig) {
 72 |       console.log('2. Commit .claude/settings.json to share with your team');
 73 |     }
 74 |     
 75 |     this.rl.close();
 76 |   }
 77 | 
 78 |   private async askMCPScope(): Promise<'global' | 'project'> {
 79 |     const answer = await this.rl.question(
 80 |       'Where should the MCP server be installed?\n' +
 81 |       '1) Global (~/.claude.json)\n' +
 82 |       '2) Project (.claude/settings.json)\n' +
 83 |       'Choice (1 or 2): '
 84 |     );
 85 |     
 86 |     return answer === '2' ? 'project' : 'global';
 87 |   }
 88 | 
 89 |   private async askHookScope(): Promise<'global' | 'project'> {
 90 |     const answer = await this.rl.question(
 91 |       'Where should the Xcode sync hook be installed?\n' +
 92 |       '1) Global (~/.claude/settings.json)\n' +
 93 |       '2) Project (.claude/settings.json)\n' +
 94 |       'Choice (1 or 2): '
 95 |     );
 96 |     
 97 |     return answer === '2' ? 'project' : 'global';
 98 |   }
 99 | 
100 |   private async askYesNo(question: string): Promise<boolean> {
101 |     const answer = await this.rl.question(`${question} (y/n): `);
102 |     return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
103 |   }
104 | 
105 |   private getMCPConfigPath(scope: 'global' | 'project'): string {
106 |     if (scope === 'global') {
107 |       // MCP servers go in ~/.claude.json for global
108 |       return join(homedir(), '.claude.json');
109 |     } else {
110 |       // Project scope - everything in .claude/settings.json
111 |       return join(process.cwd(), '.claude', 'settings.json');
112 |     }
113 |   }
114 | 
115 |   private getHooksConfigPath(scope: 'global' | 'project'): string {
116 |     if (scope === 'global') {
117 |       // Hooks go in ~/.claude/settings.json for global
118 |       return join(homedir(), '.claude', 'settings.json');
119 |     } else {
120 |       // Project scope - everything in .claude/settings.json
121 |       return join(process.cwd(), '.claude', 'settings.json');
122 |     }
123 |   }
124 | 
125 |   private loadConfig(path: string): any {
126 |     if (existsSync(path)) {
127 |       try {
128 |         return JSON.parse(readFileSync(path, 'utf8'));
129 |       } catch (error) {
130 |         console.warn(`⚠️  Warning: Could not parse existing config at ${path}`);
131 |         return {};
132 |       }
133 |     }
134 |     return {};
135 |   }
136 | 
137 |   private saveConfig(path: string, config: any) {
138 |     const dir = dirname(path);
139 |     if (!existsSync(dir)) {
140 |       mkdirSync(dir, { recursive: true });
141 |     }
142 |     writeFileSync(path, JSON.stringify(config, null, 2), 'utf8');
143 |   }
144 | 
145 |   private async setupMCPServer(scope: 'global' | 'project') {
146 |     const configPath = this.getMCPConfigPath(scope);
147 |     const config = this.loadConfig(configPath);
148 |     
149 |     // Determine the command based on installation type
150 |     const isGlobalInstall = await this.checkGlobalInstall();
151 |     const serverPath = isGlobalInstall 
152 |       ? 'mcp-xcode-server'
153 |       : resolve(PACKAGE_ROOT, 'dist', 'index.js');
154 |     
155 |     const serverConfig = {
156 |       type: 'stdio',
157 |       command: isGlobalInstall ? 'mcp-xcode-server' : 'node',
158 |       args: isGlobalInstall ? ['serve'] : [serverPath],
159 |       env: {}
160 |     };
161 |     
162 |     // Add to mcpServers
163 |     if (!config.mcpServers) {
164 |       config.mcpServers = {};
165 |     }
166 |     
167 |     if (config.mcpServers['mcp-xcode-server']) {
168 |       const overwrite = await this.askYesNo('MCP Xcode server already configured. Overwrite?');
169 |       if (!overwrite) {
170 |         console.log('Skipping MCP server configuration.');
171 |         return;
172 |       }
173 |     }
174 |     
175 |     config.mcpServers['mcp-xcode-server'] = serverConfig;
176 |     
177 |     this.saveConfig(configPath, config);
178 |     console.log(`✅ MCP server configured in ${configPath}`);
179 |   }
180 | 
181 |   private async setupHooks(scope: 'global' | 'project') {
182 |     const configPath = this.getHooksConfigPath(scope);
183 |     const config = this.loadConfig(configPath) as ClaudeSettings;
184 |     
185 |     const hookScriptPath = resolve(PACKAGE_ROOT, 'scripts', 'xcode-sync.swift');
186 |     
187 |     // Set up hooks using the correct Claude settings format
188 |     if (!config.hooks) {
189 |       config.hooks = {};
190 |     }
191 |     
192 |     if (!config.hooks.PostToolUse) {
193 |       config.hooks.PostToolUse = [];
194 |     }
195 |     
196 |     // Check if hook already exists
197 |     const existingHookIndex = config.hooks.PostToolUse.findIndex((hook: any) => 
198 |       hook.matcher === 'Write|Edit|MultiEdit|Bash' && 
199 |       (hook.hooks?.[0]?.command?.includes('xcode-sync.swift') || hook.hooks?.[0]?.command?.includes('xcode-sync.js'))
200 |     );
201 |     
202 |     if (existingHookIndex >= 0) {
203 |       const overwrite = await this.askYesNo('PostToolUse hook for Xcode sync already exists. Overwrite?');
204 |       if (!overwrite) {
205 |         console.log('Skipping hook configuration.');
206 |         return;
207 |       }
208 |       // Remove existing hook
209 |       config.hooks.PostToolUse.splice(existingHookIndex, 1);
210 |     }
211 |     
212 |     // Add the new hook in Claude's expected format
213 |     config.hooks.PostToolUse.push({
214 |       matcher: 'Write|Edit|MultiEdit|Bash',
215 |       hooks: [{
216 |         type: 'command',
217 |         command: hookScriptPath
218 |       }]
219 |     });
220 |     
221 |     this.saveConfig(configPath, config);
222 |     console.log(`✅ Xcode sync hook configured in ${configPath}`);
223 |   }
224 | 
225 |   private async checkGlobalInstall(): Promise<boolean> {
226 |     try {
227 |       execSync('which mcp-xcode-server', { stdio: 'ignore' });
228 |       return true;
229 |     } catch {
230 |       return false;
231 |     }
232 |   }
233 | 
234 |   private async buildHelperTools() {
235 |     try {
236 |       // Build TypeScript
237 |       console.log('  Building TypeScript...');
238 |       execSync('npm run build', { 
239 |         cwd: PACKAGE_ROOT,
240 |         stdio: 'inherit' 
241 |       });
242 |       
243 |       // Build XcodeProjectModifier for the sync hook
244 |       console.log('  Building XcodeProjectModifier for sync hook...');
245 |       await this.buildXcodeProjectModifier();
246 |       
247 |     } catch (error) {
248 |       console.error('❌ Failed to build:', error);
249 |       process.exit(1);
250 |     }
251 |   }
252 |   
253 |   private async buildXcodeProjectModifier() {
254 |     const modifierDir = '/tmp/XcodeProjectModifier';
255 |     const modifierBinary = join(modifierDir, '.build', 'release', 'XcodeProjectModifier');
256 |     
257 |     // Check if already built
258 |     if (existsSync(modifierBinary)) {
259 |       // Check if it's the real modifier or just a mock
260 |       try {
261 |         const output = execSync(`"${modifierBinary}" --help 2>&1 || true`, { encoding: 'utf8' });
262 |         if (output.includes('Mock XcodeProjectModifier')) {
263 |           console.log('    Detected mock modifier, rebuilding with real implementation...');
264 |           // Remove the mock
265 |           execSync(`rm -rf "${modifierDir}"`, { stdio: 'ignore' });
266 |         } else {
267 |           console.log('    XcodeProjectModifier already built');
268 |           return;
269 |         }
270 |       } catch {
271 |         // If --help fails, rebuild
272 |         execSync(`rm -rf "${modifierDir}"`, { stdio: 'ignore' });
273 |       }
274 |     }
275 |     
276 |     console.log('    Creating XcodeProjectModifier...');
277 |     
278 |     // Create directory structure
279 |     mkdirSync(join(modifierDir, 'Sources', 'XcodeProjectModifier'), { recursive: true });
280 |     
281 |     // Create Package.swift
282 |     const packageSwift = `// swift-tools-version: 5.9
283 | import PackageDescription
284 | 
285 | let package = Package(
286 |     name: "XcodeProjectModifier",
287 |     platforms: [.macOS(.v10_15)],
288 |     dependencies: [
289 |         .package(url: "https://github.com/tuist/XcodeProj.git", from: "8.0.0"),
290 |         .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")
291 |     ],
292 |     targets: [
293 |         .executableTarget(
294 |             name: "XcodeProjectModifier",
295 |             dependencies: [
296 |                 "XcodeProj",
297 |                 .product(name: "ArgumentParser", package: "swift-argument-parser")
298 |             ]
299 |         )
300 |     ]
301 | )`;
302 |     
303 |     writeFileSync(join(modifierDir, 'Package.swift'), packageSwift);
304 |     
305 |     // Create main.swift (simplified version for the hook)
306 |     const mainSwift = `import Foundation
307 | import XcodeProj
308 | import ArgumentParser
309 | 
310 | struct XcodeProjectModifier: ParsableCommand {
311 |     @Argument(help: "Path to the .xcodeproj file")
312 |     var projectPath: String
313 |     
314 |     @Argument(help: "Action to perform: add or remove")
315 |     var action: String
316 |     
317 |     @Argument(help: "Path to the file to add/remove")
318 |     var filePath: String
319 |     
320 |     @Argument(help: "Target name")
321 |     var targetName: String
322 |     
323 |     @Option(name: .long, help: "Group path for the file")
324 |     var groupPath: String = ""
325 |     
326 |     func run() throws {
327 |         let project = try XcodeProj(pathString: projectPath)
328 |         let pbxproj = project.pbxproj
329 |         
330 |         guard let target = pbxproj.nativeTargets.first(where: { $0.name == targetName }) else {
331 |             print("Error: Target '\\(targetName)' not found")
332 |             throw ExitCode.failure
333 |         }
334 |         
335 |         let fileName = URL(fileURLWithPath: filePath).lastPathComponent
336 |         
337 |         if action == "remove" {
338 |             // Remove file reference
339 |             if let fileRef = pbxproj.fileReferences.first(where: { $0.path == fileName || $0.path == filePath }) {
340 |                 pbxproj.delete(object: fileRef)
341 |                 print("Removed \\(fileName) from project")
342 |             }
343 |         } else if action == "add" {
344 |             // Remove existing reference if it exists
345 |             if let existingRef = pbxproj.fileReferences.first(where: { $0.path == fileName || $0.path == filePath }) {
346 |                 pbxproj.delete(object: existingRef)
347 |             }
348 |             
349 |             // Add new file reference
350 |             let fileRef = PBXFileReference(
351 |                 sourceTree: .group,
352 |                 name: fileName,
353 |                 path: filePath
354 |             )
355 |             pbxproj.add(object: fileRef)
356 |             
357 |             // Add to appropriate build phase based on file type
358 |             let fileExtension = URL(fileURLWithPath: filePath).pathExtension.lowercased()
359 |             
360 |             if ["swift", "m", "mm", "c", "cpp", "cc", "cxx"].contains(fileExtension) {
361 |                 // Add to sources build phase
362 |                 if let sourcesBuildPhase = target.buildPhases.compactMap({ $0 as? PBXSourcesBuildPhase }).first {
363 |                     let buildFile = PBXBuildFile(file: fileRef)
364 |                     pbxproj.add(object: buildFile)
365 |                     sourcesBuildPhase.files?.append(buildFile)
366 |                 }
367 |             } else if ["png", "jpg", "jpeg", "gif", "pdf", "json", "plist", "xib", "storyboard", "xcassets"].contains(fileExtension) {
368 |                 // Add to resources build phase
369 |                 if let resourcesBuildPhase = target.buildPhases.compactMap({ $0 as? PBXResourcesBuildPhase }).first {
370 |                     let buildFile = PBXBuildFile(file: fileRef)
371 |                     pbxproj.add(object: buildFile)
372 |                     resourcesBuildPhase.files?.append(buildFile)
373 |                 }
374 |             }
375 |             
376 |             // Add to group
377 |             if let mainGroup = try? pbxproj.rootProject()?.mainGroup {
378 |                 mainGroup.children.append(fileRef)
379 |             }
380 |             
381 |             print("Added \\(fileName) to project")
382 |         }
383 |         
384 |         try project.write(path: Path(projectPath))
385 |     }
386 | }
387 | 
388 | XcodeProjectModifier.main()`;
389 |     
390 |     writeFileSync(join(modifierDir, 'Sources', 'XcodeProjectModifier', 'main.swift'), mainSwift);
391 |     
392 |     // Build the tool
393 |     console.log('    Building with Swift Package Manager...');
394 |     try {
395 |       execSync('swift build -c release', {
396 |         cwd: modifierDir,
397 |         stdio: 'pipe'
398 |       });
399 |       console.log('    ✅ XcodeProjectModifier built successfully');
400 |     } catch (error) {
401 |       console.warn('    ⚠️  Warning: Could not build XcodeProjectModifier. Sync hook may not work until first MCP server use.');
402 |     }
403 |   }
404 | }
405 | 
406 | // CLI Commands
407 | program
408 |   .name('mcp-xcode-server')
409 |   .description('MCP Xcode Server - Setup and management')
410 |   .version('2.4.0');
411 | 
412 | program
413 |   .command('setup')
414 |   .description('Interactive setup for MCP Xcode server and hooks')
415 |   .action(async () => {
416 |     const setup = new MCPXcodeSetup();
417 |     await setup.setup();
418 |   });
419 | 
420 | program
421 |   .command('serve')
422 |   .description('Start the MCP server')
423 |   .action(async () => {
424 |     // Simply run the server
425 |     await import('./index.js');
426 |   });
427 | 
428 | // Parse command line arguments
429 | program.parse();
430 | 
431 | // If no command specified, show help
432 | if (!process.argv.slice(2).length) {
433 |   program.outputHelp();
434 | }
```
Page 4/5FirstPrevNextLast