Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions manifests/tools/clone_sims.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
id: clone_sims
module: mcp/tools/simulator-management/clone_sims
names:
mcp: clone_sims
cli: clone
description: Clone an existing simulator.
outputSchema:
schema: xcodebuildmcp.output.simulator-action-result
version: "1"
annotations:
title: Clone Simulator
readOnlyHint: false
destructiveHint: false
openWorldHint: false
nextSteps:
- label: List simulators to see the clone
toolId: list_sims
priority: 1
when: success
- label: Boot the cloned simulator
toolId: boot_sim
params:
simulatorId: NEW_UDID
priority: 2
when: success
25 changes: 25 additions & 0 deletions manifests/tools/create_sim.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
id: create_sim
module: mcp/tools/simulator-management/create_sim
names:
mcp: create_sim
cli: create
description: Create a new simulator.
outputSchema:
schema: xcodebuildmcp.output.simulator-action-result
version: "1"
annotations:
title: Create Simulator
readOnlyHint: false
destructiveHint: false
openWorldHint: false
nextSteps:
- label: List simulators to see the new device
toolId: list_sims
priority: 1
when: success
- label: Boot the new simulator
toolId: boot_sim
params:
simulatorId: NEW_UDID
priority: 2
when: success
19 changes: 19 additions & 0 deletions manifests/tools/delete_sims.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
id: delete_sims
module: mcp/tools/simulator-management/delete_sims
names:
mcp: delete_sims
cli: delete
description: Delete simulators by UDID, all simulators, or unavailable simulators.
outputSchema:
schema: xcodebuildmcp.output.simulator-action-result
version: "1"
annotations:
title: Delete Simulators
readOnlyHint: false
destructiveHint: true
openWorldHint: false
nextSteps:
- label: List remaining simulators
toolId: list_sims
priority: 1
when: success
3 changes: 3 additions & 0 deletions manifests/workflows/simulator-management.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ tools:
- set_sim_location
- reset_sim_location
- set_sim_appearance
- clone_sims
- create_sim
- delete_sims
- sim_statusbar
- toggle_software_keyboard
- toggle_connect_hardware_keyboard
52 changes: 52 additions & 0 deletions src/mcp/tools/simulator-management/__tests__/clone_sims.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it, expect } from 'vitest';
import { schema, clone_simsLogic } from '../clone_sims.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';

describe('clone_sims tool', () => {
describe('Plugin Structure', () => {
it('should expose schema', () => {
expect(schema).toBeDefined();
});
});

describe('Happy path', () => {
it('clones a simulator and captures the new UDID', async () => {
const newUdid = '00000000-0000-0000-0000-000000000001';
const mock = createMockExecutor({ success: true, output: `${newUdid}\n` });
const res = await runLogic(() =>
clone_simsLogic({ sourceSimulatorId: '00000000-0000-0000-0000-000000000000' }, mock),
);
expect(res.isError).toBeFalsy();
const text = allText(res);
expect(text).toContain('Simulator cloned successfully');
});

it('clones with a custom name', async () => {
const mock = createMockExecutor({ success: true, output: 'UUID1\n' });
const res = await runLogic(() =>
clone_simsLogic(
{
sourceSimulatorId: '00000000-0000-0000-0000-000000000000',
newName: 'My Clone',
},
mock,
),
);
expect(res.isError).toBeFalsy();
});
});

describe('Failure path', () => {
it('returns failure when clone fails', async () => {
const mock = createMockExecutor({ success: false, error: 'No such device' });
const res = await runLogic(() =>
clone_simsLogic({ sourceSimulatorId: '00000000-0000-0000-0000-000000000000' }, mock),
);
expect(res.isError).toBe(true);
const text = allText(res);
expect(text).toContain('Clone simulator failed');
expect(text).toContain('No such device');
});
});
});
52 changes: 52 additions & 0 deletions src/mcp/tools/simulator-management/__tests__/create_sim.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it, expect } from 'vitest';
import { schema, create_simLogic } from '../create_sim.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';

describe('create_sim tool', () => {
describe('Plugin Structure', () => {
it('should expose schema', () => {
expect(schema).toBeDefined();
});
});

describe('Happy path', () => {
it('creates a simulator and captures the new UDID', async () => {
const newUdid = '00000000-0000-0000-0000-000000000001';
const mock = createMockExecutor({ success: true, output: `${newUdid}\n` });
const res = await runLogic(() =>
create_simLogic(
{
name: 'Test Sim',
deviceType: 'iPhone 17',
runtime: 'iOS 26.4',
},
mock,
),
);
expect(res.isError).toBeFalsy();
const text = allText(res);
expect(text).toContain('Simulator created successfully');
});
});

describe('Failure path', () => {
it('returns failure when create fails', async () => {
const mock = createMockExecutor({ success: false, error: 'Invalid device type' });
const res = await runLogic(() =>
create_simLogic(
{
name: 'Bad Sim',
deviceType: 'NonExistent',
runtime: 'iOS 99',
},
mock,
),
);
expect(res.isError).toBe(true);
const text = allText(res);
expect(text).toContain('Create simulator failed');
expect(text).toContain('Invalid device type');
});
});
});
113 changes: 113 additions & 0 deletions src/mcp/tools/simulator-management/__tests__/delete_sims.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, it, expect } from 'vitest';
import { schema, delete_simsLogic } from '../delete_sims.ts';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';

describe('delete_sims tool', () => {
describe('Plugin Structure', () => {
it('should expose schema', () => {
expect(schema).toBeDefined();
});
});

describe('Happy path', () => {
it('deletes a simulator by UDID', async () => {
const mock = createMockExecutor({ success: true, output: 'OK' });
const res = await runLogic(() =>
delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, mock),
);
expect(res.isError).toBeFalsy();
const text = allText(res);
expect(text).toContain('Simulator(s) deleted successfully');
});

it('deletes all simulators', async () => {
const mock = createMockExecutor({ success: true, output: 'OK' });
const res = await runLogic(() => delete_simsLogic({ target: 'all' }, mock));
expect(res.isError).toBeFalsy();
});

it('deletes unavailable simulators', async () => {
const mock = createMockExecutor({ success: true, output: 'OK' });
const res = await runLogic(() => delete_simsLogic({ target: 'unavailable' }, mock));
expect(res.isError).toBeFalsy();
});
});

describe('Shutdown first', () => {
it('shuts down before deleting when shutdownFirst=true', async () => {
const calls: any[] = [];
const exec = async (cmd: string[]) => {
calls.push(cmd);
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
};
const res = await runLogic(() =>
delete_simsLogic(
{ target: '00000000-0000-0000-0000-000000000000', shutdownFirst: true },
exec as any,
),
);
expect(calls).toEqual([
['xcrun', 'simctl', 'shutdown', '00000000-0000-0000-0000-000000000000'],
['xcrun', 'simctl', 'delete', '00000000-0000-0000-0000-000000000000'],
]);
expect(res.isError).toBeFalsy();
});

it('shuts down all before deleting when target=all', async () => {
const calls: any[] = [];
const exec = async (cmd: string[]) => {
calls.push(cmd);
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
};
const res = await runLogic(() =>
delete_simsLogic({ target: 'all', shutdownFirst: true }, exec as any),
);
expect(calls).toEqual([
['xcrun', 'simctl', 'shutdown', 'all'],
['xcrun', 'simctl', 'delete', 'all'],
]);
expect(res.isError).toBeFalsy();
});

it('skips shutdown when target=unavailable', async () => {
const calls: any[] = [];
const exec = async (cmd: string[]) => {
calls.push(cmd);
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
};
const res = await runLogic(() =>
delete_simsLogic({ target: 'unavailable', shutdownFirst: true }, exec as any),
);
expect(calls).toEqual([['xcrun', 'simctl', 'delete', 'unavailable']]);
expect(res.isError).toBeFalsy();
});

it('does not shut down when shutdownFirst is not set', async () => {
const calls: any[] = [];
const exec = async (cmd: string[]) => {
calls.push(cmd);
return { success: true, output: 'OK', error: '', process: { pid: 1 } as any };
};
await runLogic(() =>
delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, exec as any),
);
expect(calls).toEqual([
['xcrun', 'simctl', 'delete', '00000000-0000-0000-0000-000000000000'],
]);
});
});

describe('Failure path', () => {
it('returns failure when delete fails', async () => {
const mock = createMockExecutor({ success: false, error: 'Unable to delete' });
const res = await runLogic(() =>
delete_simsLogic({ target: '00000000-0000-0000-0000-000000000000' }, mock),
);
expect(res.isError).toBe(true);
const text = allText(res);
expect(text).toContain('Failed to delete simulator');
expect(text).toContain('Unable to delete');
});
});
});
Loading
Loading