Skip to content

Commit daf00b3

Browse files
committed
refactor: migrate simulator and simulator-management tools to event-based handler contract
1 parent 089f6a3 commit daf00b3

36 files changed

+2919
-3078
lines changed
Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,69 @@
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, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
5+
6+
const runLogic = async (logic: () => Promise<unknown>) => {
7+
const { result, run } = createMockToolHandlerContext();
8+
const response = await run(logic);
9+
10+
if (
11+
response &&
12+
typeof response === 'object' &&
13+
'content' in (response as Record<string, unknown>)
14+
) {
15+
return response as {
16+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
17+
isError?: boolean;
18+
nextStepParams?: unknown;
19+
};
20+
}
21+
22+
const text = result.text();
23+
const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
24+
const imageContent = result.attachments.map((attachment) => ({
25+
type: 'image' as const,
26+
data: attachment.data,
27+
mimeType: attachment.mimeType,
28+
}));
29+
30+
return {
31+
content: [...textContent, ...imageContent],
32+
isError: result.isError() ? true : undefined,
33+
nextStepParams: result.nextStepParams,
34+
attachments: result.attachments,
35+
text,
36+
};
37+
};
538

639
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);
40+
describe('Plugin Structure', () => {
41+
it('should expose schema', () => {
42+
expect(schema).toBeDefined();
1243
});
1344
});
1445

1546
describe('Single mode', () => {
1647
it('erases a simulator successfully', async () => {
1748
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-
});
49+
const res = await runLogic(() => erase_simsLogic({ simulatorId: 'UD1' }, mock));
50+
expect(res.isError).toBeFalsy();
2251
});
2352

2453
it('returns failure when erase fails', async () => {
2554
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-
});
55+
const res = await runLogic(() => erase_simsLogic({ simulatorId: 'UD1' }, mock));
56+
expect(res.isError).toBe(true);
3057
});
3158

3259
it('adds tool hint when booted error occurs without shutdownFirst', async () => {
3360
const bootedError =
3461
'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';
3562
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');
63+
const res = await runLogic(() => erase_simsLogic({ simulatorId: 'UD1' }, mock));
64+
const text = allText(res);
65+
expect(text).toContain('shutdownFirst: true');
66+
expect(res.isError).toBe(true);
3967
});
4068

4169
it('performs shutdown first when shutdownFirst=true', async () => {
@@ -44,14 +72,14 @@ describe('erase_sims tool (single simulator)', () => {
4472
calls.push(cmd);
4573
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
4674
};
47-
const res = await erase_simsLogic({ simulatorId: 'UD1', shutdownFirst: true }, exec as any);
75+
const res = await runLogic(() =>
76+
erase_simsLogic({ simulatorId: 'UD1', shutdownFirst: true }, exec as any),
77+
);
4878
expect(calls).toEqual([
4979
['xcrun', 'simctl', 'shutdown', 'UD1'],
5080
['xcrun', 'simctl', 'erase', 'UD1'],
5181
]);
52-
expect(res).toEqual({
53-
content: [{ type: 'text', text: 'Successfully erased simulator UD1' }],
54-
});
82+
expect(res.isError).toBeFalsy();
5583
});
5684
});
5785
});

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

Lines changed: 65 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,40 @@ 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 { createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
6+
7+
const runLogic = async (logic: () => Promise<unknown>) => {
8+
const { result, run } = createMockToolHandlerContext();
9+
const response = await run(logic);
10+
11+
if (
12+
response &&
13+
typeof response === 'object' &&
14+
'content' in (response as Record<string, unknown>)
15+
) {
16+
return response as {
17+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
18+
isError?: boolean;
19+
nextStepParams?: unknown;
20+
};
21+
}
22+
23+
const text = result.text();
24+
const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
25+
const imageContent = result.attachments.map((attachment) => ({
26+
type: 'image' as const,
27+
data: attachment.data,
28+
mimeType: attachment.mimeType,
29+
}));
30+
31+
return {
32+
content: [...textContent, ...imageContent],
33+
isError: result.isError() ? true : undefined,
34+
nextStepParams: result.nextStepParams,
35+
attachments: result.attachments,
36+
text,
37+
};
38+
};
539

640
describe('reset_sim_location plugin', () => {
741
describe('Schema Validation', () => {
@@ -23,21 +57,16 @@ describe('reset_sim_location plugin', () => {
2357
output: 'Location reset successfully',
2458
});
2559

26-
const result = await reset_sim_locationLogic(
27-
{
28-
simulatorId: 'test-uuid-123',
29-
},
30-
mockExecutor,
31-
);
32-
33-
expect(result).toEqual({
34-
content: [
60+
const result = await runLogic(() =>
61+
reset_sim_locationLogic(
3562
{
36-
type: 'text',
37-
text: 'Successfully reset simulator test-uuid-123 location.',
63+
simulatorId: 'test-uuid-123',
3864
},
39-
],
40-
});
65+
mockExecutor,
66+
),
67+
);
68+
69+
expect(result.isError).toBeFalsy();
4170
});
4271

4372
it('should handle command failure', async () => {
@@ -46,41 +75,31 @@ describe('reset_sim_location plugin', () => {
4675
error: 'Command failed',
4776
});
4877

49-
const result = await reset_sim_locationLogic(
50-
{
51-
simulatorId: 'test-uuid-123',
52-
},
53-
mockExecutor,
54-
);
55-
56-
expect(result).toEqual({
57-
content: [
78+
const result = await runLogic(() =>
79+
reset_sim_locationLogic(
5880
{
59-
type: 'text',
60-
text: 'Failed to reset simulator location: Command failed',
81+
simulatorId: 'test-uuid-123',
6182
},
62-
],
63-
});
83+
mockExecutor,
84+
),
85+
);
86+
87+
expect(result.isError).toBe(true);
6488
});
6589

6690
it('should handle exception during execution', async () => {
6791
const mockExecutor = createMockExecutor(new Error('Network error'));
6892

69-
const result = await reset_sim_locationLogic(
70-
{
71-
simulatorId: 'test-uuid-123',
72-
},
73-
mockExecutor,
74-
);
75-
76-
expect(result).toEqual({
77-
content: [
93+
const result = await runLogic(() =>
94+
reset_sim_locationLogic(
7895
{
79-
type: 'text',
80-
text: 'Failed to reset simulator location: Network error',
96+
simulatorId: 'test-uuid-123',
8197
},
82-
],
83-
});
98+
mockExecutor,
99+
),
100+
);
101+
102+
expect(result.isError).toBe(true);
84103
});
85104

86105
it('should call correct command', async () => {
@@ -92,18 +111,19 @@ describe('reset_sim_location plugin', () => {
92111
output: 'Location reset successfully',
93112
});
94113

95-
// Create a wrapper to capture the command arguments
96114
const capturingExecutor = async (command: string[], logPrefix?: string) => {
97115
capturedCommand = command;
98116
capturedLogPrefix = logPrefix;
99117
return mockExecutor(command, logPrefix);
100118
};
101119

102-
await reset_sim_locationLogic(
103-
{
104-
simulatorId: 'test-uuid-123',
105-
},
106-
capturingExecutor,
120+
await runLogic(() =>
121+
reset_sim_locationLogic(
122+
{
123+
simulatorId: 'test-uuid-123',
124+
},
125+
capturingExecutor,
126+
),
107127
);
108128

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

0 commit comments

Comments
 (0)