Skip to content

Commit 6e51e5b

Browse files
authored
refactor(6/12): migrate simulator tools to event-based handlers (#324)
## Summary This is **PR 6 of 12** in a stacked PR series that decouples the rendering pipeline from MCP transport. Depends on PR 5 (handler contract change). Migrates all simulator and simulator-management tool handlers from the old `return toolResponse([...])` pattern to the new `ctx.emit(event)` pattern introduced in PR 5. This is the largest tool migration PR because simulators are the primary development target. ### Tools migrated (36 files) **Simulator tools**: `boot_sim`, `build_sim`, `build_run_sim`, `get_sim_app_path`, `install_app_sim`, `launch_app_sim`, `list_sims`, `open_sim`, `record_sim_video`, `stop_app_sim`, `test_sim`, `screenshot` **Simulator management tools**: `erase_sims`, `reset_sim_location`, `set_sim_appearance`, `set_sim_location`, `sim_statusbar` ### Pattern of change Each handler follows the same mechanical transformation: \`\`\`typescript // Before async function handler(params) { const result = await buildForSimulator(params); return toolResponse([header('Build', [...]), statusLine('success', '...')]); } // After async function handler(params, ctx) { const result = await buildForSimulator(params, ctx); ctx.emit(header('Build', [...])); ctx.emit(statusLine('success', '...')); } \`\`\` Build/test tools additionally pass `ctx.emit` to the xcodebuild pipeline so events stream in real-time during compilation. `test_sim` delegates to `simulator-test-execution.ts` and `test-preflight.ts` from PR 4. ### Supporting changes - `simulator-utils.ts`: Updated to work with the new handler context - `simulator-resolver.ts`: Simplified resolver that integrates with the step modules from PR 4 ## Stack navigation - PR 1-5/12: Foundation, utilities, runtime contract - **PR 6/12** (this PR): Simulator tool migrations - PR 7/12: Device + macOS tool migrations - PR 8/12: UI automation tool migrations - PR 9/12: Remaining tool migrations - PR 10-12/12: Boundaries, config, tests ## Test plan - [ ] `npx vitest run` passes -- all simulator tool tests updated - [ ] Each tool handler emits the expected event sequence - [ ] Build/test tools stream progress events during xcodebuild execution
1 parent bc56fa7 commit 6e51e5b

37 files changed

Lines changed: 2537 additions & 3081 deletions
Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,37 @@
11
import { describe, it, expect } from 'vitest';
2-
import * as z from 'zod';
32
import { schema, erase_simsLogic } from '../erase_sims.ts';
43
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
4+
import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
5+
56

67
describe('erase_sims tool (single simulator)', () => {
7-
describe('Schema Validation', () => {
8-
it('should validate schema fields (shape only)', () => {
9-
const schemaObj = z.object(schema);
10-
expect(schemaObj.safeParse({ shutdownFirst: true }).success).toBe(true);
11-
expect(schemaObj.safeParse({}).success).toBe(true);
8+
describe('Plugin Structure', () => {
9+
it('should expose schema', () => {
10+
expect(schema).toBeDefined();
1211
});
1312
});
1413

1514
describe('Single mode', () => {
1615
it('erases a simulator successfully', async () => {
1716
const mock = createMockExecutor({ success: true, output: 'OK' });
18-
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
19-
expect(res).toEqual({
20-
content: [{ type: 'text', text: 'Successfully erased simulator UD1' }],
21-
});
17+
const res = await runLogic(() => erase_simsLogic({ simulatorId: 'UD1' }, mock));
18+
expect(res.isError).toBeFalsy();
2219
});
2320

2421
it('returns failure when erase fails', async () => {
2522
const mock = createMockExecutor({ success: false, error: 'Booted device' });
26-
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
27-
expect(res).toEqual({
28-
content: [{ type: 'text', text: 'Failed to erase simulator: Booted device' }],
29-
});
23+
const res = await runLogic(() => erase_simsLogic({ simulatorId: 'UD1' }, mock));
24+
expect(res.isError).toBe(true);
3025
});
3126

3227
it('adds tool hint when booted error occurs without shutdownFirst', async () => {
3328
const bootedError =
3429
'An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405):\nUnable to erase contents and settings in current state: Booted\n';
3530
const mock = createMockExecutor({ success: false, error: bootedError });
36-
const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock);
37-
expect((res.content?.[1] as any).text).toContain('Tool hint');
38-
expect((res.content?.[1] as any).text).toContain('shutdownFirst: true');
31+
const res = await runLogic(() => erase_simsLogic({ simulatorId: 'UD1' }, mock));
32+
const text = allText(res);
33+
expect(text).toContain('shutdownFirst: true');
34+
expect(res.isError).toBe(true);
3935
});
4036

4137
it('performs shutdown first when shutdownFirst=true', async () => {
@@ -44,14 +40,14 @@ describe('erase_sims tool (single simulator)', () => {
4440
calls.push(cmd);
4541
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
4642
};
47-
const res = await erase_simsLogic({ simulatorId: 'UD1', shutdownFirst: true }, exec as any);
43+
const res = await runLogic(() =>
44+
erase_simsLogic({ simulatorId: 'UD1', shutdownFirst: true }, exec as any),
45+
);
4846
expect(calls).toEqual([
4947
['xcrun', 'simctl', 'shutdown', 'UD1'],
5048
['xcrun', 'simctl', 'erase', 'UD1'],
5149
]);
52-
expect(res).toEqual({
53-
content: [{ type: 'text', text: 'Successfully erased simulator UD1' }],
54-
});
50+
expect(res.isError).toBeFalsy();
5551
});
5652
});
5753
});

src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts

Lines changed: 33 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest';
22
import * as z from 'zod';
33
import { schema, reset_sim_locationLogic } from '../reset_sim_location.ts';
44
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
5+
import { runLogic } from '../../../../test-utils/test-helpers.ts';
6+
57

68
describe('reset_sim_location plugin', () => {
79
describe('Schema Validation', () => {
@@ -23,21 +25,16 @@ describe('reset_sim_location plugin', () => {
2325
output: 'Location reset successfully',
2426
});
2527

26-
const result = await reset_sim_locationLogic(
27-
{
28-
simulatorId: 'test-uuid-123',
29-
},
30-
mockExecutor,
31-
);
32-
33-
expect(result).toEqual({
34-
content: [
28+
const result = await runLogic(() =>
29+
reset_sim_locationLogic(
3530
{
36-
type: 'text',
37-
text: 'Successfully reset simulator test-uuid-123 location.',
31+
simulatorId: 'test-uuid-123',
3832
},
39-
],
40-
});
33+
mockExecutor,
34+
),
35+
);
36+
37+
expect(result.isError).toBeFalsy();
4138
});
4239

4340
it('should handle command failure', async () => {
@@ -46,41 +43,31 @@ describe('reset_sim_location plugin', () => {
4643
error: 'Command failed',
4744
});
4845

49-
const result = await reset_sim_locationLogic(
50-
{
51-
simulatorId: 'test-uuid-123',
52-
},
53-
mockExecutor,
54-
);
55-
56-
expect(result).toEqual({
57-
content: [
46+
const result = await runLogic(() =>
47+
reset_sim_locationLogic(
5848
{
59-
type: 'text',
60-
text: 'Failed to reset simulator location: Command failed',
49+
simulatorId: 'test-uuid-123',
6150
},
62-
],
63-
});
51+
mockExecutor,
52+
),
53+
);
54+
55+
expect(result.isError).toBe(true);
6456
});
6557

6658
it('should handle exception during execution', async () => {
6759
const mockExecutor = createMockExecutor(new Error('Network error'));
6860

69-
const result = await reset_sim_locationLogic(
70-
{
71-
simulatorId: 'test-uuid-123',
72-
},
73-
mockExecutor,
74-
);
75-
76-
expect(result).toEqual({
77-
content: [
61+
const result = await runLogic(() =>
62+
reset_sim_locationLogic(
7863
{
79-
type: 'text',
80-
text: 'Failed to reset simulator location: Network error',
64+
simulatorId: 'test-uuid-123',
8165
},
82-
],
83-
});
66+
mockExecutor,
67+
),
68+
);
69+
70+
expect(result.isError).toBe(true);
8471
});
8572

8673
it('should call correct command', async () => {
@@ -92,18 +79,19 @@ describe('reset_sim_location plugin', () => {
9279
output: 'Location reset successfully',
9380
});
9481

95-
// Create a wrapper to capture the command arguments
9682
const capturingExecutor = async (command: string[], logPrefix?: string) => {
9783
capturedCommand = command;
9884
capturedLogPrefix = logPrefix;
9985
return mockExecutor(command, logPrefix);
10086
};
10187

102-
await reset_sim_locationLogic(
103-
{
104-
simulatorId: 'test-uuid-123',
105-
},
106-
capturingExecutor,
88+
await runLogic(() =>
89+
reset_sim_locationLogic(
90+
{
91+
simulatorId: 'test-uuid-123',
92+
},
93+
capturingExecutor,
94+
),
10795
);
10896

10997
expect(capturedCommand).toEqual(['xcrun', 'simctl', 'location', 'test-uuid-123', 'clear']);

src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts

Lines changed: 39 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { describe, it, expect } from 'vitest';
22
import * as z from 'zod';
33
import { schema, handler, set_sim_appearanceLogic } from '../set_sim_appearance.ts';
4+
import { runLogic } from '../../../../test-utils/test-helpers.ts';
5+
6+
47
import {
58
createMockCommandResponse,
69
createMockExecutor,
@@ -33,22 +36,17 @@ describe('set_sim_appearance plugin', () => {
3336
error: '',
3437
});
3538

36-
const result = await set_sim_appearanceLogic(
37-
{
38-
simulatorId: 'test-uuid-123',
39-
mode: 'dark',
40-
},
41-
mockExecutor,
42-
);
43-
44-
expect(result).toEqual({
45-
content: [
39+
const result = await runLogic(() =>
40+
set_sim_appearanceLogic(
4641
{
47-
type: 'text',
48-
text: 'Successfully set simulator test-uuid-123 appearance to dark mode',
42+
simulatorId: 'test-uuid-123',
43+
mode: 'dark',
4944
},
50-
],
51-
});
45+
mockExecutor,
46+
),
47+
);
48+
49+
expect(result.isError).toBeFalsy();
5250
});
5351

5452
it('should handle appearance change failure', async () => {
@@ -57,52 +55,42 @@ describe('set_sim_appearance plugin', () => {
5755
error: 'Invalid device: invalid-uuid',
5856
});
5957

60-
const result = await set_sim_appearanceLogic(
61-
{
62-
simulatorId: 'invalid-uuid',
63-
mode: 'light',
64-
},
65-
mockExecutor,
66-
);
67-
68-
expect(result).toEqual({
69-
content: [
58+
const result = await runLogic(() =>
59+
set_sim_appearanceLogic(
7060
{
71-
type: 'text',
72-
text: 'Failed to set simulator appearance: Invalid device: invalid-uuid',
61+
simulatorId: 'invalid-uuid',
62+
mode: 'light',
7363
},
74-
],
75-
});
64+
mockExecutor,
65+
),
66+
);
67+
68+
expect(result.isError).toBe(true);
7669
});
7770

7871
it('should surface session default requirement when simulatorId is missing', async () => {
7972
const result = await handler({ mode: 'dark' });
8073

8174
const message = result.content?.[0]?.text ?? '';
82-
expect(message).toContain('Error: Missing required session defaults');
75+
expect(message).toContain('Missing required session defaults');
8376
expect(message).toContain('simulatorId is required');
8477
expect(result.isError).toBe(true);
8578
});
8679

8780
it('should handle exception during execution', async () => {
8881
const mockExecutor = createMockExecutor(new Error('Network error'));
8982

90-
const result = await set_sim_appearanceLogic(
91-
{
92-
simulatorId: 'test-uuid-123',
93-
mode: 'dark',
94-
},
95-
mockExecutor,
96-
);
97-
98-
expect(result).toEqual({
99-
content: [
83+
const result = await runLogic(() =>
84+
set_sim_appearanceLogic(
10085
{
101-
type: 'text',
102-
text: 'Failed to set simulator appearance: Network error',
86+
simulatorId: 'test-uuid-123',
87+
mode: 'dark',
10388
},
104-
],
105-
});
89+
mockExecutor,
90+
),
91+
);
92+
93+
expect(result.isError).toBe(true);
10694
});
10795

10896
it('should call correct command', async () => {
@@ -118,20 +106,21 @@ describe('set_sim_appearance plugin', () => {
118106
);
119107
};
120108

121-
await set_sim_appearanceLogic(
122-
{
123-
simulatorId: 'test-uuid-123',
124-
mode: 'dark',
125-
},
126-
mockExecutor,
109+
await runLogic(() =>
110+
set_sim_appearanceLogic(
111+
{
112+
simulatorId: 'test-uuid-123',
113+
mode: 'dark',
114+
},
115+
mockExecutor,
116+
),
127117
);
128118

129119
expect(commandCalls).toEqual([
130120
[
131121
['xcrun', 'simctl', 'ui', 'test-uuid-123', 'appearance', 'dark'],
132122
'Set Simulator Appearance',
133123
false,
134-
undefined,
135124
],
136125
]);
137126
});

0 commit comments

Comments
 (0)