Skip to content

Commit 0fd562f

Browse files
cameroncookecodex
andcommitted
fix(simctl): Require names for cloned simulators
Require clone_sims callers to provide the new simulator name that simctl expects. Avoid reporting the source simulator as a clone artifact when the clone command fails. Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent 4f8fef6 commit 0fd562f

2 files changed

Lines changed: 54 additions & 32 deletions

File tree

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import { schema, clone_simsLogic } from '../clone_sims.ts';
2+
import { schema, clone_simsLogic, createCloneSimsExecutor } from '../clone_sims.ts';
33
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
44
import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
55

@@ -14,16 +14,6 @@ describe('clone_sims tool', () => {
1414
it('clones a simulator and captures the new UDID', async () => {
1515
const newUdid = '00000000-0000-0000-0000-000000000001';
1616
const mock = createMockExecutor({ success: true, output: `${newUdid}\n` });
17-
const res = await runLogic(() =>
18-
clone_simsLogic({ sourceSimulatorId: '00000000-0000-0000-0000-000000000000' }, mock),
19-
);
20-
expect(res.isError).toBeFalsy();
21-
const text = allText(res);
22-
expect(text).toContain('Simulator cloned successfully');
23-
});
24-
25-
it('clones with a custom name', async () => {
26-
const mock = createMockExecutor({ success: true, output: 'UUID1\n' });
2717
const res = await runLogic(() =>
2818
clone_simsLogic(
2919
{
@@ -34,19 +24,60 @@ describe('clone_sims tool', () => {
3424
),
3525
);
3626
expect(res.isError).toBeFalsy();
27+
const text = allText(res);
28+
expect(text).toContain('Simulator cloned successfully');
29+
});
30+
31+
it('passes the required custom name to simctl clone', async () => {
32+
const calls: string[][] = [];
33+
const mock = createMockExecutor({
34+
success: true,
35+
output: 'UUID1\n',
36+
onExecute: (command) => calls.push(command),
37+
});
38+
39+
const executeCloneSims = createCloneSimsExecutor(mock);
40+
const result = await executeCloneSims({
41+
sourceSimulatorId: '00000000-0000-0000-0000-000000000000',
42+
newName: 'My Clone',
43+
});
44+
45+
expect(result.didError).toBe(false);
46+
expect(calls).toEqual([
47+
['xcrun', 'simctl', 'clone', '00000000-0000-0000-0000-000000000000', 'My Clone'],
48+
]);
3749
});
3850
});
3951

4052
describe('Failure path', () => {
4153
it('returns failure when clone fails', async () => {
4254
const mock = createMockExecutor({ success: false, error: 'No such device' });
4355
const res = await runLogic(() =>
44-
clone_simsLogic({ sourceSimulatorId: '00000000-0000-0000-0000-000000000000' }, mock),
56+
clone_simsLogic(
57+
{
58+
sourceSimulatorId: '00000000-0000-0000-0000-000000000000',
59+
newName: 'My Clone',
60+
},
61+
mock,
62+
),
4563
);
4664
expect(res.isError).toBe(true);
4765
const text = allText(res);
4866
expect(text).toContain('Clone simulator failed');
4967
expect(text).toContain('No such device');
5068
});
69+
70+
it('omits artifacts when clone fails before producing a cloned simulator ID', async () => {
71+
const mock = createMockExecutor({ success: false, error: 'No such device' });
72+
const executeCloneSims = createCloneSimsExecutor(mock);
73+
74+
const result = await executeCloneSims({
75+
sourceSimulatorId: '00000000-0000-0000-0000-000000000000',
76+
newName: 'My Clone',
77+
});
78+
79+
expect(result.didError).toBe(true);
80+
expect(result.artifacts).toBeUndefined();
81+
});
5182
});
5283
});

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

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,13 @@ import { toErrorMessage } from '../../../utils/errors.ts';
1515
import { createBasicDiagnostics } from '../../../utils/diagnostics.ts';
1616

1717
const baseSchemaObject = z.object({
18-
sourceSimulatorId: z.string().uuid().describe('UDID of the simulator to clone'),
19-
newName: z
20-
.string()
21-
.optional()
22-
.describe('Name for the cloned simulator. If omitted, simctl auto-generates one.'),
18+
sourceSimulatorId: z.uuid().describe('UDID of the simulator to clone'),
19+
newName: z.string().min(1).describe('Name for the cloned simulator.'),
2320
});
2421

2522
const internalSchemaObject = z.object({
26-
sourceSimulatorId: z.string().uuid(),
27-
newName: z.string().optional(),
23+
sourceSimulatorId: z.uuid(),
24+
newName: z.string().min(1),
2825
});
2926

3027
type CloneSimsParams = z.infer<typeof internalSchemaObject>;
@@ -51,9 +48,7 @@ function createCloneSimsResult(params: {
5148
...(params.diagnosticMessage
5249
? { diagnostics: createBasicDiagnostics({ errors: [params.diagnosticMessage] }) }
5350
: {}),
54-
artifacts: {
55-
simulatorId: params.clonedSimulatorId ?? params.sourceSimulatorId,
56-
},
51+
...(params.clonedSimulatorId ? { artifacts: { simulatorId: params.clonedSimulatorId } } : {}),
5752
};
5853
}
5954

@@ -70,12 +65,11 @@ export function createCloneSimsExecutor(
7065
): NonStreamingExecutor<CloneSimsParams, CloneSimsResult> {
7166
return async (params) => {
7267
try {
73-
const command = ['xcrun', 'simctl', 'clone', params.sourceSimulatorId];
74-
if (params.newName) {
75-
command.push(params.newName);
76-
}
77-
78-
const result = await executor(command, 'Clone Simulator', false);
68+
const result = await executor(
69+
['xcrun', 'simctl', 'clone', params.sourceSimulatorId, params.newName],
70+
'Clone Simulator',
71+
false,
72+
);
7973

8074
if (!result.success) {
8175
const diagnosticMessage = result.error ?? 'Unknown error';
@@ -109,10 +103,7 @@ export async function clone_simsLogic(
109103
params: CloneSimsParams,
110104
executor: CommandExecutor,
111105
): Promise<void> {
112-
log(
113-
'info',
114-
`Cloning simulator ${params.sourceSimulatorId}${params.newName ? ` as "${params.newName}"` : ''}`,
115-
);
106+
log('info', `Cloning simulator ${params.sourceSimulatorId} as "${params.newName}"`);
116107

117108
const ctx = getHandlerContext();
118109
const executeCloneSims = createCloneSimsExecutor(executor);

0 commit comments

Comments
 (0)