Skip to content

Commit 6a5b120

Browse files
committed
Infer simulator platform with fast path and fallback
1 parent ced279e commit 6a5b120

9 files changed

Lines changed: 717 additions & 24 deletions

File tree

src/mcp/tools/simulator/__tests__/build_run_sim.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,49 @@ describe('build_run_sim tool', () => {
491491
]);
492492
expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
493493
});
494+
495+
it('should infer tvOS platform from simulator name for build command', async () => {
496+
const callHistory: Array<{
497+
command: string[];
498+
logPrefix?: string;
499+
useShell?: boolean;
500+
opts?: { env?: Record<string, string>; cwd?: string };
501+
}> = [];
502+
503+
const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
504+
callHistory.push({ command, logPrefix, useShell, opts });
505+
return createMockCommandResponse({
506+
success: false,
507+
output: '',
508+
error: 'Test error to stop execution early',
509+
});
510+
};
511+
512+
await build_run_simLogic(
513+
{
514+
workspacePath: '/path/to/MyProject.xcworkspace',
515+
scheme: 'MyTVScheme',
516+
simulatorName: 'Apple TV 4K',
517+
},
518+
trackingExecutor,
519+
);
520+
521+
expect(callHistory).toHaveLength(1);
522+
expect(callHistory[0].command).toEqual([
523+
'xcodebuild',
524+
'-workspace',
525+
'/path/to/MyProject.xcworkspace',
526+
'-scheme',
527+
'MyTVScheme',
528+
'-configuration',
529+
'Debug',
530+
'-skipMacroValidation',
531+
'-destination',
532+
'platform=tvOS Simulator,name=Apple TV 4K,OS=latest',
533+
'build',
534+
]);
535+
expect(callHistory[0].logPrefix).toBe('tvOS Simulator Build');
536+
});
494537
});
495538

496539
describe('XOR Validation', () => {

src/mcp/tools/simulator/__tests__/build_sim.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,49 @@ describe('build_sim tool', () => {
414414
]);
415415
expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
416416
});
417+
418+
it('should infer watchOS platform from simulator name', async () => {
419+
const callHistory: Array<{
420+
command: string[];
421+
logPrefix?: string;
422+
useShell?: boolean;
423+
opts?: { env?: Record<string, string>; cwd?: string };
424+
}> = [];
425+
426+
const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
427+
callHistory.push({ command, logPrefix, useShell, opts });
428+
return createMockCommandResponse({
429+
success: false,
430+
output: '',
431+
error: 'Test error to stop execution early',
432+
});
433+
};
434+
435+
await build_simLogic(
436+
{
437+
workspacePath: '/path/to/MyProject.xcworkspace',
438+
scheme: 'MyWatchScheme',
439+
simulatorName: 'Apple Watch Ultra 2',
440+
},
441+
trackingExecutor,
442+
);
443+
444+
expect(callHistory).toHaveLength(1);
445+
expect(callHistory[0].command).toEqual([
446+
'xcodebuild',
447+
'-workspace',
448+
'/path/to/MyProject.xcworkspace',
449+
'-scheme',
450+
'MyWatchScheme',
451+
'-configuration',
452+
'Debug',
453+
'-skipMacroValidation',
454+
'-destination',
455+
'platform=watchOS Simulator,name=Apple Watch Ultra 2,OS=latest',
456+
'build',
457+
]);
458+
expect(callHistory[0].logPrefix).toBe('watchOS Simulator Build');
459+
});
417460
});
418461

419462
describe('Response Processing', () => {

src/mcp/tools/simulator/build_run_sim.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
2020
import type { CommandExecutor } from '../../../utils/execution/index.ts';
2121
import { determineSimulatorUuid } from '../../../utils/simulator-utils.ts';
2222
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
23+
import { inferPlatform } from '../../../utils/infer-platform.ts';
2324

2425
// Unified schema: XOR between projectPath and workspacePath, and XOR between simulatorId and simulatorName
2526
const baseOptions = {
@@ -82,7 +83,7 @@ async function _handleSimulatorBuildLogic(
8283
params: BuildRunSimulatorParams,
8384
executor: CommandExecutor,
8485
executeXcodeBuildCommandFn: typeof executeXcodeBuildCommand = executeXcodeBuildCommand,
85-
): Promise<ToolResponse> {
86+
): Promise<{ response: ToolResponse; detectedPlatform: XcodePlatform }> {
8687
const projectType = params.projectPath ? 'project' : 'workspace';
8788
const filePath = params.projectPath ?? params.workspacePath;
8889

@@ -94,10 +95,22 @@ async function _handleSimulatorBuildLogic(
9495
);
9596
}
9697

97-
log(
98-
'info',
99-
`Starting iOS Simulator build for scheme ${params.scheme} from ${projectType}: ${filePath}`,
98+
const inferred = await inferPlatform(
99+
{
100+
projectPath: params.projectPath,
101+
workspacePath: params.workspacePath,
102+
scheme: params.scheme,
103+
simulatorId: params.simulatorId,
104+
simulatorName: params.simulatorName,
105+
},
106+
executor,
100107
);
108+
const detectedPlatform = inferred.platform;
109+
const platformName = detectedPlatform.replace(' Simulator', '');
110+
const logPrefix = `${platformName} Simulator Build`;
111+
112+
log('info', `Starting ${logPrefix} for scheme ${params.scheme} from ${projectType}: ${filePath}`);
113+
log('info', `Inferred simulator platform: ${detectedPlatform} (source: ${inferred.source})`);
101114

102115
// Create SharedBuildParams object with required configuration property
103116
const sharedBuildParams: SharedBuildParams = {
@@ -109,19 +122,21 @@ async function _handleSimulatorBuildLogic(
109122
extraArgs: params.extraArgs,
110123
};
111124

112-
return executeXcodeBuildCommandFn(
125+
const response = await executeXcodeBuildCommandFn(
113126
sharedBuildParams,
114127
{
115-
platform: XcodePlatform.iOSSimulator,
128+
platform: detectedPlatform,
116129
simulatorId: params.simulatorId,
117130
simulatorName: params.simulatorName,
118131
useLatestOS: params.simulatorId ? false : params.useLatestOS,
119-
logPrefix: 'iOS Simulator Build',
132+
logPrefix,
120133
},
121134
params.preferXcodebuild as boolean,
122135
'build',
123136
executor,
124137
);
138+
139+
return { response, detectedPlatform };
125140
}
126141

127142
// Exported business logic function for building and running iOS Simulator apps.
@@ -135,12 +150,12 @@ export async function build_run_simLogic(
135150

136151
log(
137152
'info',
138-
`Starting iOS Simulator build and run for scheme ${params.scheme} from ${projectType}: ${filePath}`,
153+
`Starting Simulator build and run for scheme ${params.scheme} from ${projectType}: ${filePath}`,
139154
);
140155

141156
try {
142157
// --- Build Step ---
143-
const buildResult = await _handleSimulatorBuildLogic(
158+
const { response: buildResult, detectedPlatform } = await _handleSimulatorBuildLogic(
144159
params,
145160
executor,
146161
executeXcodeBuildCommandFn,
@@ -150,6 +165,9 @@ export async function build_run_simLogic(
150165
return buildResult; // Return the build error
151166
}
152167

168+
const platformDestination = detectedPlatform;
169+
const platformName = detectedPlatform.replace(' Simulator', '');
170+
153171
// --- Get App Path Step ---
154172
// Create the command array for xcodebuild with -showBuildSettings option
155173
const command = ['xcodebuild', '-showBuildSettings'];
@@ -168,12 +186,12 @@ export async function build_run_simLogic(
168186
// Handle destination for simulator
169187
let destinationString: string;
170188
if (params.simulatorId) {
171-
destinationString = `platform=iOS Simulator,id=${params.simulatorId}`;
189+
destinationString = `platform=${platformDestination},id=${params.simulatorId}`;
172190
} else if (params.simulatorName) {
173-
destinationString = `platform=iOS Simulator,name=${params.simulatorName}${(params.useLatestOS ?? true) ? ',OS=latest' : ''}`;
191+
destinationString = `platform=${platformDestination},name=${params.simulatorName}${(params.useLatestOS ?? true) ? ',OS=latest' : ''}`;
174192
} else {
175193
// This shouldn't happen due to validation, but handle it
176-
destinationString = 'platform=iOS Simulator';
194+
destinationString = `platform=${platformDestination}`;
177195
}
178196
command.push('-destination', destinationString);
179197

@@ -450,7 +468,7 @@ export async function build_run_simLogic(
450468
}
451469

452470
// --- Success ---
453-
log('info', '✅ iOS simulator build & run succeeded.');
471+
log('info', `✅ ${platformName} simulator build & run succeeded.`);
454472

455473
const target = params.simulatorId
456474
? `simulator UUID '${params.simulatorId}'`
@@ -462,7 +480,7 @@ export async function build_run_simLogic(
462480
content: [
463481
{
464482
type: 'text',
465-
text: `✅ iOS simulator build and run succeeded for scheme ${params.scheme} from ${sourceType} ${sourcePath} targeting ${target}.\n\nThe app (${bundleId}) is now running in the iOS Simulator.\nIf you don't see the simulator window, it may be hidden behind other windows. The Simulator app should be open.`,
483+
text: `✅ ${platformName} simulator build and run succeeded for scheme ${params.scheme} from ${sourceType} ${sourcePath} targeting ${target}.\n\nThe app (${bundleId}) is now running in the ${platformName} Simulator.\nIf you don't see the simulator window, it may be hidden behind other windows. The Simulator app should be open.`,
466484
},
467485
],
468486
nextSteps: [
@@ -489,8 +507,8 @@ export async function build_run_simLogic(
489507
};
490508
} catch (error) {
491509
const errorMessage = error instanceof Error ? error.message : String(error);
492-
log('error', `Error in iOS Simulator build and run: ${errorMessage}`);
493-
return createTextResponse(`Error in iOS Simulator build and run: ${errorMessage}`, true);
510+
log('error', `Error in Simulator build and run: ${errorMessage}`);
511+
return createTextResponse(`Error in Simulator build and run: ${errorMessage}`, true);
494512
}
495513
}
496514

src/mcp/tools/simulator/build_sim.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import * as z from 'zod';
1010
import { log } from '../../../utils/logging/index.ts';
1111
import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
1212
import type { ToolResponse } from '../../../types/common.ts';
13-
import { XcodePlatform } from '../../../types/common.ts';
1413
import type { CommandExecutor } from '../../../utils/execution/index.ts';
1514
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
1615
import {
1716
createSessionAwareTool,
1817
getSessionAwareToolSchemaShape,
1918
} from '../../../utils/typed-tool-factory.ts';
2019
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
20+
import { inferPlatform } from '../../../utils/infer-platform.ts';
2121

2222
// Unified schema: XOR between projectPath and workspacePath, and XOR between simulatorId and simulatorName
2323
const baseOptions = {
@@ -91,10 +91,22 @@ async function _handleSimulatorBuildLogic(
9191
);
9292
}
9393

94-
log(
95-
'info',
96-
`Starting iOS Simulator build for scheme ${params.scheme} from ${projectType}: ${filePath}`,
94+
const inferred = await inferPlatform(
95+
{
96+
projectPath: params.projectPath,
97+
workspacePath: params.workspacePath,
98+
scheme: params.scheme,
99+
simulatorId: params.simulatorId,
100+
simulatorName: params.simulatorName,
101+
},
102+
executor,
97103
);
104+
const detectedPlatform = inferred.platform;
105+
const platformName = detectedPlatform.replace(' Simulator', '');
106+
const logPrefix = `${platformName} Simulator Build`;
107+
108+
log('info', `Starting ${logPrefix} for scheme ${params.scheme} from ${projectType}: ${filePath}`);
109+
log('info', `Inferred simulator platform: ${detectedPlatform} (source: ${inferred.source})`);
98110

99111
// Ensure configuration has a default value for SharedBuildParams compatibility
100112
const sharedBuildParams = {
@@ -106,11 +118,11 @@ async function _handleSimulatorBuildLogic(
106118
return executeXcodeBuildCommand(
107119
sharedBuildParams,
108120
{
109-
platform: XcodePlatform.iOSSimulator,
121+
platform: detectedPlatform,
110122
simulatorName: params.simulatorName,
111123
simulatorId: params.simulatorId,
112124
useLatestOS: params.simulatorId ? false : params.useLatestOS, // Ignore useLatestOS with ID
113-
logPrefix: 'iOS Simulator Build',
125+
logPrefix,
114126
},
115127
params.preferXcodebuild ?? false,
116128
'build',

src/mcp/tools/simulator/test_sim.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import * as z from 'zod';
1010
import { handleTestLogic } from '../../../utils/test/index.ts';
1111
import { log } from '../../../utils/logging/index.ts';
1212
import type { ToolResponse } from '../../../types/common.ts';
13-
import { XcodePlatform } from '../../../types/common.ts';
1413
import type { CommandExecutor } from '../../../utils/execution/index.ts';
1514
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
1615
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
1716
import {
1817
createSessionAwareTool,
1918
getSessionAwareToolSchemaShape,
2019
} from '../../../utils/typed-tool-factory.ts';
20+
import { inferPlatform } from '../../../utils/infer-platform.ts';
2121

2222
// Define base schema object with all fields
2323
const baseSchemaObject = z.object({
@@ -91,6 +91,21 @@ export async function test_simLogic(
9191
);
9292
}
9393

94+
const inferred = await inferPlatform(
95+
{
96+
projectPath: params.projectPath,
97+
workspacePath: params.workspacePath,
98+
scheme: params.scheme,
99+
simulatorId: params.simulatorId,
100+
simulatorName: params.simulatorName,
101+
},
102+
executor,
103+
);
104+
log(
105+
'info',
106+
`Inferred simulator platform for tests: ${inferred.platform} (source: ${inferred.source})`,
107+
);
108+
94109
return handleTestLogic(
95110
{
96111
projectPath: params.projectPath,
@@ -103,7 +118,7 @@ export async function test_simLogic(
103118
extraArgs: params.extraArgs,
104119
useLatestOS: params.simulatorId ? false : (params.useLatestOS ?? false),
105120
preferXcodebuild: params.preferXcodebuild ?? false,
106-
platform: XcodePlatform.iOSSimulator,
121+
platform: inferred.platform,
107122
testRunnerEnv: params.testRunnerEnv,
108123
},
109124
executor,

0 commit comments

Comments
 (0)