Skip to content

Commit 4a5ebd3

Browse files
committed
feat(simulator-management): add shutdownFirst option and tool hints for booted erase errors; update tests and docs generation
1 parent e93cf4c commit 4a5ebd3

2 files changed

Lines changed: 125 additions & 27 deletions

File tree

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ describe('erase_sims tool (UDID or ALL only)', () => {
1111

1212
it('should have correct description', () => {
1313
expect(eraseSims.description).toContain('Provide exactly one of: simulatorUuid or all=true');
14+
expect(eraseSims.description).toContain('shutdownFirst');
1415
});
1516

1617
it('should have handler function', () => {
@@ -42,6 +43,31 @@ describe('erase_sims tool (UDID or ALL only)', () => {
4243
content: [{ type: 'text', text: 'Failed to erase simulator: Booted device' }],
4344
});
4445
});
46+
47+
it('adds tool hint when booted error occurs without shutdownFirst', async () => {
48+
const bootedError =
49+
'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';
50+
const mock = createMockExecutor({ success: false, error: bootedError });
51+
const res = await erase_simsLogic({ simulatorUuid: 'UD1' }, mock);
52+
expect((res.content?.[1] as any).text).toContain('Tool hint');
53+
expect((res.content?.[1] as any).text).toContain('shutdownFirst: true');
54+
});
55+
56+
it('performs shutdown first when shutdownFirst=true', async () => {
57+
const calls: any[] = [];
58+
const exec = async (cmd: string[]) => {
59+
calls.push(cmd);
60+
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
61+
};
62+
const res = await erase_simsLogic({ simulatorUuid: 'UD1', shutdownFirst: true }, exec as any);
63+
expect(calls).toEqual([
64+
['xcrun', 'simctl', 'shutdown', 'UD1'],
65+
['xcrun', 'simctl', 'erase', 'UD1'],
66+
]);
67+
expect(res).toEqual({
68+
content: [{ type: 'text', text: 'Successfully erased simulator UD1' }],
69+
});
70+
});
4571
});
4672

4773
describe('All mode', () => {
@@ -60,5 +86,29 @@ describe('erase_sims tool (UDID or ALL only)', () => {
6086
content: [{ type: 'text', text: 'Failed to erase all simulators: Denied' }],
6187
});
6288
});
89+
90+
it('performs shutdown all when shutdownFirst=true', async () => {
91+
const calls: any[] = [];
92+
const exec = async (cmd: string[]) => {
93+
calls.push(cmd);
94+
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
95+
};
96+
const res = await erase_simsLogic({ all: true, shutdownFirst: true }, exec as any);
97+
expect(calls).toEqual([
98+
['xcrun', 'simctl', 'shutdown', 'all'],
99+
['xcrun', 'simctl', 'erase', 'all'],
100+
]);
101+
expect(res).toEqual({
102+
content: [{ type: 'text', text: 'Successfully erased all simulators' }],
103+
});
104+
});
105+
106+
it('adds tool hint on booted error without shutdownFirst (all mode)', async () => {
107+
const bootedError = 'Unable to erase contents and settings in current state: Booted';
108+
const exec = createMockExecutor({ success: false, error: bootedError });
109+
const res = await erase_simsLogic({ all: true }, exec);
110+
expect((res.content?.[1] as any).text).toContain('Tool hint');
111+
expect((res.content?.[1] as any).text).toContain('shutdownFirst: true');
112+
});
63113
});
64114
});

src/mcp/tools/simulator-management/erase_sims.ts

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
77
const eraseSimsBaseSchema = z.object({
88
simulatorUuid: z.string().optional().describe('UUID of the simulator to erase.'),
99
all: z.boolean().optional().describe('When true, erases all simulators.'),
10+
shutdownFirst: z
11+
.boolean()
12+
.optional()
13+
.describe('If true, shuts down the target (UDID or all) before erasing.'),
1014
});
1115

1216
const eraseSimsSchema = eraseSimsBaseSchema.refine(
@@ -19,51 +23,95 @@ const eraseSimsSchema = eraseSimsBaseSchema.refine(
1923

2024
type EraseSimsParams = z.infer<typeof eraseSimsSchema>;
2125

22-
async function eraseSingle(udid: string, executor: CommandExecutor): Promise<ToolResponse> {
23-
const result = await executor(
24-
['xcrun', 'simctl', 'erase', udid],
25-
'Erase Simulator',
26-
true,
27-
undefined,
28-
);
29-
if (result.success) {
30-
return { content: [{ type: 'text', text: `Successfully erased simulator ${udid}` }] };
31-
}
32-
return {
33-
content: [
34-
{ type: 'text', text: `Failed to erase simulator: ${result.error ?? 'Unknown error'}` },
35-
],
36-
};
37-
}
38-
3926
export async function erase_simsLogic(
4027
params: EraseSimsParams,
4128
executor: CommandExecutor,
4229
): Promise<ToolResponse> {
4330
try {
4431
if (params.simulatorUuid) {
45-
log('info', `Erasing simulator ${params.simulatorUuid}`);
46-
return await eraseSingle(params.simulatorUuid, executor);
47-
}
32+
const udid = params.simulatorUuid;
33+
log(
34+
'info',
35+
`Erasing simulator ${udid}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`,
36+
);
37+
38+
if (params.shutdownFirst) {
39+
try {
40+
await executor(
41+
['xcrun', 'simctl', 'shutdown', udid],
42+
'Shutdown Simulator',
43+
true,
44+
undefined,
45+
);
46+
} catch {
47+
// ignore shutdown errors; proceed to erase attempt
48+
}
49+
}
4850

49-
if (params.all === true) {
50-
log('info', 'Erasing ALL simulators');
5151
const result = await executor(
52-
['xcrun', 'simctl', 'erase', 'all'],
53-
'Erase All Simulators',
52+
['xcrun', 'simctl', 'erase', udid],
53+
'Erase Simulator',
5454
true,
5555
undefined,
5656
);
57-
if (!result.success) {
57+
if (result.success) {
58+
return { content: [{ type: 'text', text: `Successfully erased simulator ${udid}` }] };
59+
}
60+
61+
// Add tool hint if simulator is booted and shutdownFirst was not requested
62+
const errText = result.error ?? 'Unknown error';
63+
if (/Unable to erase contents and settings.*Booted/i.test(errText) && !params.shutdownFirst) {
5864
return {
5965
content: [
66+
{ type: 'text', text: `Failed to erase simulator: ${errText}` },
6067
{
6168
type: 'text',
62-
text: `Failed to erase all simulators: ${result.error ?? 'Unknown error'}`,
69+
text: `Tool hint: The simulator appears to be Booted. Re-run erase_sims with { simulatorUuid: '${udid}', shutdownFirst: true } to shut it down before erasing.`,
6370
},
6471
],
6572
};
6673
}
74+
75+
return {
76+
content: [{ type: 'text', text: `Failed to erase simulator: ${errText}` }],
77+
};
78+
}
79+
80+
if (params.all === true) {
81+
log('info', `Erasing ALL simulators${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`);
82+
if (params.shutdownFirst) {
83+
try {
84+
await executor(
85+
['xcrun', 'simctl', 'shutdown', 'all'],
86+
'Shutdown All Simulators',
87+
true,
88+
undefined,
89+
);
90+
} catch {
91+
// ignore and continue to erase
92+
}
93+
}
94+
95+
const result = await executor(
96+
['xcrun', 'simctl', 'erase', 'all'],
97+
'Erase All Simulators',
98+
true,
99+
undefined,
100+
);
101+
if (!result.success) {
102+
const errText = result.error ?? 'Unknown error';
103+
const content = [{ type: 'text', text: `Failed to erase all simulators: ${errText}` }];
104+
if (
105+
/Unable to erase contents and settings.*Booted/i.test(errText) &&
106+
!params.shutdownFirst
107+
) {
108+
content.push({
109+
type: 'text',
110+
text: 'Tool hint: One or more simulators appear to be Booted. Re-run erase_sims with { all: true, shutdownFirst: true } to shut them down before erasing.',
111+
});
112+
}
113+
return { content };
114+
}
67115
return { content: [{ type: 'text', text: 'Successfully erased all simulators' }] };
68116
}
69117

@@ -80,7 +128,7 @@ export async function erase_simsLogic(
80128
export default {
81129
name: 'erase_sims',
82130
description:
83-
'Erases simulator content and settings. Provide exactly one of: simulatorUuid or all=true.',
131+
'Erases simulator content and settings. Provide exactly one of: simulatorUuid or all=true. Optional: shutdownFirst to shut down before erasing.',
84132
schema: eraseSimsBaseSchema.shape,
85133
handler: createTypedTool(eraseSimsSchema, erase_simsLogic, getDefaultCommandExecutor),
86134
};

0 commit comments

Comments
 (0)