Skip to content

Commit f5da07f

Browse files
committed
feat(simulator): migrate test_sim and get_sim_app_path to session defaults
1 parent 2f73662 commit f5da07f

4 files changed

Lines changed: 356 additions & 40 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* Tests for get_sim_app_path plugin (session-aware version)
3+
* Mirrors patterns from other simulator session-aware migrations.
4+
*/
5+
6+
import { describe, it, expect, beforeEach } from 'vitest';
7+
import { ChildProcess } from 'child_process';
8+
import { z } from 'zod';
9+
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
10+
import { sessionStore } from '../../../../utils/session-store.ts';
11+
import getSimAppPath, { get_sim_app_pathLogic } from '../get_sim_app_path.ts';
12+
import type { CommandExecutor } from '../../../../utils/CommandExecutor.ts';
13+
14+
describe('get_sim_app_path tool', () => {
15+
beforeEach(() => {
16+
sessionStore.clear();
17+
});
18+
19+
describe('Export Field Validation (Literal)', () => {
20+
it('should have correct name', () => {
21+
expect(getSimAppPath.name).toBe('get_sim_app_path');
22+
});
23+
24+
it('should have concise description', () => {
25+
expect(getSimAppPath.description).toBe('Retrieves the built app path for an iOS simulator.');
26+
});
27+
28+
it('should have handler function', () => {
29+
expect(typeof getSimAppPath.handler).toBe('function');
30+
});
31+
32+
it('should expose only platform in public schema', () => {
33+
const schema = z.object(getSimAppPath.schema);
34+
35+
expect(schema.safeParse({ platform: 'iOS Simulator' }).success).toBe(true);
36+
expect(schema.safeParse({}).success).toBe(false);
37+
expect(schema.safeParse({ platform: 'iOS' }).success).toBe(false);
38+
39+
const schemaKeys = Object.keys(getSimAppPath.schema).sort();
40+
expect(schemaKeys).toEqual(['platform']);
41+
});
42+
});
43+
44+
describe('Handler Requirements', () => {
45+
it('should require scheme when not provided', async () => {
46+
const result = await getSimAppPath.handler({
47+
platform: 'iOS Simulator',
48+
});
49+
50+
expect(result.isError).toBe(true);
51+
expect(result.content[0].text).toContain('scheme is required');
52+
});
53+
54+
it('should require project or workspace when scheme default exists', async () => {
55+
sessionStore.setDefaults({ scheme: 'MyScheme' });
56+
57+
const result = await getSimAppPath.handler({
58+
platform: 'iOS Simulator',
59+
});
60+
61+
expect(result.isError).toBe(true);
62+
expect(result.content[0].text).toContain('Provide a project or workspace');
63+
});
64+
65+
it('should require simulator identifier when scheme and project defaults exist', async () => {
66+
sessionStore.setDefaults({
67+
scheme: 'MyScheme',
68+
projectPath: '/path/to/project.xcodeproj',
69+
});
70+
71+
const result = await getSimAppPath.handler({
72+
platform: 'iOS Simulator',
73+
});
74+
75+
expect(result.isError).toBe(true);
76+
expect(result.content[0].text).toContain('Provide simulatorId or simulatorName');
77+
});
78+
79+
it('should error when both projectPath and workspacePath provided explicitly', async () => {
80+
sessionStore.setDefaults({ scheme: 'MyScheme' });
81+
82+
const result = await getSimAppPath.handler({
83+
platform: 'iOS Simulator',
84+
projectPath: '/path/project.xcodeproj',
85+
workspacePath: '/path/workspace.xcworkspace',
86+
});
87+
88+
expect(result.isError).toBe(true);
89+
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
90+
expect(result.content[0].text).toContain('projectPath');
91+
expect(result.content[0].text).toContain('workspacePath');
92+
});
93+
94+
it('should error when both simulatorId and simulatorName provided explicitly', async () => {
95+
sessionStore.setDefaults({
96+
scheme: 'MyScheme',
97+
workspacePath: '/path/to/workspace.xcworkspace',
98+
});
99+
100+
const result = await getSimAppPath.handler({
101+
platform: 'iOS Simulator',
102+
simulatorId: 'SIM-UUID',
103+
simulatorName: 'iPhone 16',
104+
});
105+
106+
expect(result.isError).toBe(true);
107+
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
108+
expect(result.content[0].text).toContain('simulatorId');
109+
expect(result.content[0].text).toContain('simulatorName');
110+
});
111+
});
112+
113+
describe('Logic Behavior', () => {
114+
it('should return app path with simulator name destination', async () => {
115+
const callHistory: Array<{
116+
command: string[];
117+
logPrefix?: string;
118+
useShell?: boolean;
119+
opts?: unknown;
120+
}> = [];
121+
122+
const trackingExecutor: CommandExecutor = async (
123+
command,
124+
logPrefix,
125+
useShell,
126+
opts,
127+
): Promise<{
128+
success: boolean;
129+
output: string;
130+
process: ChildProcess;
131+
}> => {
132+
callHistory.push({ command, logPrefix, useShell, opts });
133+
return {
134+
success: true,
135+
output:
136+
' BUILT_PRODUCTS_DIR = /tmp/DerivedData/Build\n FULL_PRODUCT_NAME = MyApp.app\n',
137+
process: { pid: 12345 } as unknown as ChildProcess,
138+
};
139+
};
140+
141+
const result = await get_sim_app_pathLogic(
142+
{
143+
workspacePath: '/path/to/workspace.xcworkspace',
144+
scheme: 'MyScheme',
145+
platform: 'iOS Simulator',
146+
simulatorName: 'iPhone 16',
147+
useLatestOS: true,
148+
},
149+
trackingExecutor,
150+
);
151+
152+
expect(callHistory).toHaveLength(1);
153+
expect(callHistory[0].logPrefix).toBe('Get App Path');
154+
expect(callHistory[0].useShell).toBe(true);
155+
expect(callHistory[0].command).toEqual([
156+
'xcodebuild',
157+
'-showBuildSettings',
158+
'-workspace',
159+
'/path/to/workspace.xcworkspace',
160+
'-scheme',
161+
'MyScheme',
162+
'-configuration',
163+
'Debug',
164+
'-destination',
165+
'platform=iOS Simulator,name=iPhone 16,OS=latest',
166+
]);
167+
168+
expect(result.isError).toBe(false);
169+
expect(result.content[0].text).toContain(
170+
'✅ App path retrieved successfully: /tmp/DerivedData/Build/MyApp.app',
171+
);
172+
});
173+
174+
it('should surface executor failures when build settings cannot be retrieved', async () => {
175+
const mockExecutor = createMockExecutor({
176+
success: false,
177+
error: 'Failed to run xcodebuild',
178+
});
179+
180+
const result = await get_sim_app_pathLogic(
181+
{
182+
projectPath: '/path/to/project.xcodeproj',
183+
scheme: 'MyScheme',
184+
platform: 'iOS Simulator',
185+
simulatorId: 'SIM-UUID',
186+
},
187+
mockExecutor,
188+
);
189+
190+
expect(result.isError).toBe(true);
191+
expect(result.content[0].text).toContain('Failed to get app path');
192+
expect(result.content[0].text).toContain('Failed to run xcodebuild');
193+
});
194+
});
195+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Tests for test_sim plugin (session-aware version)
3+
* Follows CLAUDE.md guidance: dependency injection, no vi-mocks, literal validation.
4+
*/
5+
6+
import { describe, it, expect, beforeEach } from 'vitest';
7+
import { z } from 'zod';
8+
import { sessionStore } from '../../../../utils/session-store.ts';
9+
import testSim from '../test_sim.ts';
10+
11+
describe('test_sim tool', () => {
12+
beforeEach(() => {
13+
sessionStore.clear();
14+
});
15+
16+
describe('Export Field Validation (Literal)', () => {
17+
it('should have correct name', () => {
18+
expect(testSim.name).toBe('test_sim');
19+
});
20+
21+
it('should have concise description', () => {
22+
expect(testSim.description).toBe('Runs tests on an iOS simulator.');
23+
});
24+
25+
it('should have handler function', () => {
26+
expect(typeof testSim.handler).toBe('function');
27+
});
28+
29+
it('should expose only non-session fields in public schema', () => {
30+
const schema = z.object(testSim.schema);
31+
32+
expect(schema.safeParse({}).success).toBe(true);
33+
expect(
34+
schema.safeParse({
35+
derivedDataPath: '/tmp/derived',
36+
extraArgs: ['--quiet'],
37+
preferXcodebuild: true,
38+
testRunnerEnv: { FOO: 'BAR' },
39+
}).success,
40+
).toBe(true);
41+
42+
expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false);
43+
expect(schema.safeParse({ extraArgs: ['--ok', 42] }).success).toBe(false);
44+
expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);
45+
expect(schema.safeParse({ testRunnerEnv: { FOO: 123 } }).success).toBe(false);
46+
47+
const schemaKeys = Object.keys(testSim.schema).sort();
48+
expect(schemaKeys).toEqual(
49+
['derivedDataPath', 'extraArgs', 'preferXcodebuild', 'testRunnerEnv'].sort(),
50+
);
51+
});
52+
});
53+
54+
describe('Handler Requirements', () => {
55+
it('should require scheme when not provided', async () => {
56+
const result = await testSim.handler({});
57+
58+
expect(result.isError).toBe(true);
59+
expect(result.content[0].text).toContain('scheme is required');
60+
});
61+
62+
it('should require project or workspace when scheme default exists', async () => {
63+
sessionStore.setDefaults({ scheme: 'MyScheme' });
64+
65+
const result = await testSim.handler({});
66+
67+
expect(result.isError).toBe(true);
68+
expect(result.content[0].text).toContain('Provide a project or workspace');
69+
});
70+
71+
it('should require simulator identifier when scheme and project defaults exist', async () => {
72+
sessionStore.setDefaults({
73+
scheme: 'MyScheme',
74+
projectPath: '/path/to/project.xcodeproj',
75+
});
76+
77+
const result = await testSim.handler({});
78+
79+
expect(result.isError).toBe(true);
80+
expect(result.content[0].text).toContain('Provide simulatorId or simulatorName');
81+
});
82+
83+
it('should error when both simulatorId and simulatorName provided explicitly', async () => {
84+
sessionStore.setDefaults({
85+
scheme: 'MyScheme',
86+
workspacePath: '/path/to/workspace.xcworkspace',
87+
});
88+
89+
const result = await testSim.handler({
90+
simulatorId: 'SIM-UUID',
91+
simulatorName: 'iPhone 16',
92+
});
93+
94+
expect(result.isError).toBe(true);
95+
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
96+
expect(result.content[0].text).toContain('simulatorId');
97+
expect(result.content[0].text).toContain('simulatorName');
98+
});
99+
});
100+
});

src/mcp/tools/simulator/get_sim_app_path.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { createTextResponse } from '../../../utils/responses/index.ts';
1212
import type { CommandExecutor } from '../../../utils/execution/index.ts';
1313
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
1414
import { ToolResponse } from '../../../types/common.ts';
15-
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
15+
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
1616
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
1717

1818
const XcodePlatform = {
@@ -289,14 +289,33 @@ export async function get_sim_app_pathLogic(
289289
}
290290
}
291291

292+
const publicSchemaObject = baseGetSimulatorAppPathSchema.omit({
293+
projectPath: true,
294+
workspacePath: true,
295+
scheme: true,
296+
simulatorId: true,
297+
simulatorName: true,
298+
configuration: true,
299+
useLatestOS: true,
300+
arch: true,
301+
} as const);
302+
292303
export default {
293304
name: 'get_sim_app_path',
294-
description:
295-
"Gets the app bundle path for a simulator by UUID or name using either a project or workspace file. IMPORTANT: Requires either projectPath OR workspacePath (not both), plus scheme, platform, and either simulatorId OR simulatorName (not both). Example: get_sim_app_path({ projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme', platform: 'iOS Simulator', simulatorName: 'iPhone 16' })",
296-
schema: baseGetSimulatorAppPathSchema.shape, // MCP SDK compatibility
297-
handler: createTypedTool<GetSimulatorAppPathParams>(
298-
getSimulatorAppPathSchema as z.ZodType<GetSimulatorAppPathParams>,
299-
get_sim_app_pathLogic,
300-
getDefaultCommandExecutor,
301-
),
305+
description: 'Retrieves the built app path for an iOS simulator.',
306+
schema: publicSchemaObject.shape,
307+
handler: createSessionAwareTool<GetSimulatorAppPathParams>({
308+
internalSchema: getSimulatorAppPathSchema as unknown as z.ZodType<GetSimulatorAppPathParams>,
309+
logicFunction: get_sim_app_pathLogic,
310+
getExecutor: getDefaultCommandExecutor,
311+
requirements: [
312+
{ allOf: ['scheme'], message: 'scheme is required' },
313+
{ oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
314+
{ oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
315+
],
316+
exclusivePairs: [
317+
['projectPath', 'workspacePath'],
318+
['simulatorId', 'simulatorName'],
319+
],
320+
}),
302321
};

0 commit comments

Comments
 (0)