Skip to content

Commit 89cd11c

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

5 files changed

Lines changed: 420 additions & 40 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Session-Aware Migration TODO
2+
3+
_Audit date: October 6, 2025_
4+
5+
Reference: `docs/session_management_plan.md`
6+
7+
## Utilities
8+
- [ ] `src/mcp/tools/utilities/clean.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
9+
10+
## Project Discovery
11+
- [ ] `src/mcp/tools/project-discovery/list_schemes.ts` — session defaults: `projectPath`, `workspacePath`.
12+
- [ ] `src/mcp/tools/project-discovery/show_build_settings.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`.
13+
14+
## Device Workflows
15+
- [ ] `src/mcp/tools/device/build_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
16+
- [ ] `src/mcp/tools/device/test_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `deviceId`, `configuration`.
17+
- [ ] `src/mcp/tools/device/get_device_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
18+
- [ ] `src/mcp/tools/device/install_app_device.ts` — session defaults: `deviceId`.
19+
- [ ] `src/mcp/tools/device/launch_app_device.ts` — session defaults: `deviceId`.
20+
- [ ] `src/mcp/tools/device/stop_app_device.ts` — session defaults: `deviceId`.
21+
22+
## Device Logging
23+
- [ ] `src/mcp/tools/logging/start_device_log_cap.ts` — session defaults: `deviceId`.
24+
25+
## macOS Workflows
26+
- [ ] `src/mcp/tools/macos/build_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
27+
- [ ] `src/mcp/tools/macos/build_run_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
28+
- [ ] `src/mcp/tools/macos/test_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
29+
- [ ] `src/mcp/tools/macos/get_mac_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
30+
31+
## Simulator Build/Test/Path
32+
- [x] `src/mcp/tools/simulator/test_sim.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `simulatorId`, `simulatorName`, `configuration`, `useLatestOS`.
33+
- [x] `src/mcp/tools/simulator/get_sim_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `simulatorId`, `simulatorName`, `configuration`, `useLatestOS`, `arch`.
34+
35+
## Simulator Runtime Actions
36+
- [ ] `src/mcp/tools/simulator/boot_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
37+
- [ ] `src/mcp/tools/simulator/install_app_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
38+
- [ ] `src/mcp/tools/simulator/launch_app_sim.ts` — session defaults: `simulatorId`, `simulatorName` (hydrate `simulatorUuid`).
39+
- [ ] `src/mcp/tools/simulator/launch_app_logs_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
40+
- [ ] `src/mcp/tools/simulator/stop_app_sim.ts` — session defaults: `simulatorId`, `simulatorName` (hydrate `simulatorUuid`).
41+
- [ ] `src/mcp/tools/simulator/record_sim_video.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
42+
43+
## Simulator Management
44+
- [ ] `src/mcp/tools/simulator-management/erase_sims.ts` — session defaults: `simulatorId` (covers `simulatorUdid`).
45+
- [ ] `src/mcp/tools/simulator-management/set_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
46+
- [ ] `src/mcp/tools/simulator-management/reset_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
47+
- [ ] `src/mcp/tools/simulator-management/set_sim_appearance.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
48+
- [ ] `src/mcp/tools/simulator-management/sim_statusbar.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
49+
50+
## Simulator Logging
51+
- [ ] `src/mcp/tools/logging/start_sim_log_cap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
52+
53+
## AXe UI Testing Tools
54+
- [ ] `src/mcp/tools/ui-testing/button.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
55+
- [ ] `src/mcp/tools/ui-testing/describe_ui.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
56+
- [ ] `src/mcp/tools/ui-testing/gesture.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
57+
- [ ] `src/mcp/tools/ui-testing/key_press.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
58+
- [ ] `src/mcp/tools/ui-testing/key_sequence.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
59+
- [ ] `src/mcp/tools/ui-testing/long_press.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
60+
- [ ] `src/mcp/tools/ui-testing/screenshot.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
61+
- [ ] `src/mcp/tools/ui-testing/swipe.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
62+
- [ ] `src/mcp/tools/ui-testing/tap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
63+
- [ ] `src/mcp/tools/ui-testing/touch.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
64+
- [ ] `src/mcp/tools/ui-testing/type_text.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`).
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+
});

0 commit comments

Comments
 (0)