diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 4c402dde..60dac51f 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -69,7 +69,7 @@ XcodeBuildMCP provides 61 tools organized into 12 workflow groups for comprehens ### Simulator Management (`simulator-management`) **Purpose**: Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators, erasing simulator content and settings, and setting simulator environment options like location, network, statusbar and appearance. (5 tools) -- `erase_sims` - Erases simulator content and settings. Provide exactly one of: simulatorUdid or all=true. Optional: shutdownFirst to shut down before erasing. +- `erase_sims` - Erases simulator content and settings for a specific simulator. Requires simulatorUdid. Optional: shutdownFirst to shut down before erasing. - `reset_sim_location` - Resets the simulator's location to default. - `set_sim_appearance` - Sets the appearance mode (dark/light) of an iOS simulator. - `set_sim_location` - Sets a custom GPS location for the simulator. diff --git a/docs/session-aware-migration-todo.md b/docs/session-aware-migration-todo.md index 4f49e753..5ba98104 100644 --- a/docs/session-aware-migration-todo.md +++ b/docs/session-aware-migration-todo.md @@ -41,14 +41,14 @@ Reference: `docs/session_management_plan.md` - [x] `src/mcp/tools/simulator/record_sim_video.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). ## Simulator Management -- [ ] `src/mcp/tools/simulator-management/erase_sims.ts` — session defaults: `simulatorId` (covers `simulatorUdid`). -- [ ] `src/mcp/tools/simulator-management/set_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [ ] `src/mcp/tools/simulator-management/reset_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [ ] `src/mcp/tools/simulator-management/set_sim_appearance.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [ ] `src/mcp/tools/simulator-management/sim_statusbar.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). +- [x] `src/mcp/tools/simulator-management/erase_sims.ts` — session defaults: `simulatorId` (covers `simulatorUdid`). +- [x] `src/mcp/tools/simulator-management/set_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). +- [x] `src/mcp/tools/simulator-management/reset_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). +- [x] `src/mcp/tools/simulator-management/set_sim_appearance.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). +- [x] `src/mcp/tools/simulator-management/sim_statusbar.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). ## Simulator Logging -- [ ] `src/mcp/tools/logging/start_sim_log_cap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). +- [x] `src/mcp/tools/logging/start_sim_log_cap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). ## AXe UI Testing Tools - [ ] `src/mcp/tools/ui-testing/button.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). diff --git a/example_projects/iOS_Calculator/.gitignore b/example_projects/iOS_Calculator/.gitignore new file mode 100644 index 00000000..ad146c76 --- /dev/null +++ b/example_projects/iOS_Calculator/.gitignore @@ -0,0 +1,7 @@ + +# xcode-build-server files +buildServer.json +.compile + +# Local build artifacts +.build/ diff --git a/src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts b/src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts index 8880c7db..cb53413b 100644 --- a/src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts +++ b/src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts @@ -1,7 +1,7 @@ /** * Tests for start_sim_log_cap plugin */ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import plugin, { start_sim_log_capLogic } from '../start_sim_log_cap.ts'; import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; @@ -33,51 +33,30 @@ describe('start_sim_log_cap plugin', () => { it('should validate schema with valid parameters', () => { const schema = z.object(plugin.schema); - expect( - schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: 'com.example.app' }).success, - ).toBe(true); - expect( - schema.safeParse({ - simulatorUuid: 'test-uuid', - bundleId: 'com.example.app', - captureConsole: true, - }).success, - ).toBe(true); - expect( - schema.safeParse({ - simulatorUuid: 'test-uuid', - bundleId: 'com.example.app', - captureConsole: false, - }).success, - ).toBe(true); + expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true); + expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: true }).success).toBe( + true, + ); + expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: false }).success).toBe( + true, + ); }); it('should reject invalid schema parameters', () => { const schema = z.object(plugin.schema); - expect(schema.safeParse({ simulatorUuid: null, bundleId: 'com.example.app' }).success).toBe( + expect(schema.safeParse({ bundleId: null }).success).toBe(false); + expect(schema.safeParse({ captureConsole: true }).success).toBe(false); + expect(schema.safeParse({}).success).toBe(false); + expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 'yes' }).success).toBe( false, ); - expect( - schema.safeParse({ simulatorUuid: undefined, bundleId: 'com.example.app' }).success, - ).toBe(false); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: null }).success).toBe(false); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: undefined }).success).toBe( + expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 123 }).success).toBe( false, ); - expect( - schema.safeParse({ - simulatorUuid: 'test-uuid', - bundleId: 'com.example.app', - captureConsole: 'yes', - }).success, - ).toBe(false); - expect( - schema.safeParse({ - simulatorUuid: 'test-uuid', - bundleId: 'com.example.app', - captureConsole: 123, - }).success, - ).toBe(false); + + const withSimId = schema.safeParse({ simulatorId: 'test-uuid', bundleId: 'com.example.app' }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as any)).toBe(false); }); }); @@ -98,7 +77,7 @@ describe('start_sim_log_cap plugin', () => { const result = await start_sim_log_capLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', bundleId: 'com.example.app', }, mockExecutor, @@ -122,7 +101,7 @@ describe('start_sim_log_cap plugin', () => { const result = await start_sim_log_capLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', bundleId: 'com.example.app', }, mockExecutor, @@ -148,7 +127,7 @@ describe('start_sim_log_cap plugin', () => { const result = await start_sim_log_capLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', bundleId: 'com.example.app', captureConsole: true, }, @@ -208,7 +187,7 @@ describe('start_sim_log_cap plugin', () => { await start_sim_log_capLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', bundleId: 'com.example.app', captureConsole: true, }, @@ -277,7 +256,7 @@ describe('start_sim_log_cap plugin', () => { await start_sim_log_capLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', bundleId: 'com.example.app', captureConsole: false, }, diff --git a/src/mcp/tools/logging/start_sim_log_cap.ts b/src/mcp/tools/logging/start_sim_log_cap.ts index 2a0ea651..cc7b415d 100644 --- a/src/mcp/tools/logging/start_sim_log_cap.ts +++ b/src/mcp/tools/logging/start_sim_log_cap.ts @@ -8,12 +8,13 @@ import { z } from 'zod'; import { startLogCapture } from '../../../utils/log-capture/index.ts'; import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.ts'; import { ToolResponse, createTextContent } from '../../../types/common.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const startSimLogCapSchema = z.object({ - simulatorUuid: z + simulatorId: z .string() + .uuid() .describe('UUID of the simulator to capture logs from (obtained from list_simulators).'), bundleId: z.string().describe('Bundle identifier of the app to capture logs for.'), captureConsole: z @@ -30,11 +31,15 @@ export async function start_sim_log_capLogic( _executor: CommandExecutor = getDefaultCommandExecutor(), logCaptureFunction: typeof startLogCapture = startLogCapture, ): Promise { - const paramsWithDefaults = { - ...params, - captureConsole: params.captureConsole ?? false, - }; - const { sessionId, error } = await logCaptureFunction(paramsWithDefaults, _executor); + const captureConsole = params.captureConsole ?? false; + const { sessionId, error } = await logCaptureFunction( + { + simulatorUuid: params.simulatorId, + bundleId: params.bundleId, + captureConsole, + }, + _executor, + ); if (error) { return { content: [createTextContent(`Error starting log capture: ${error}`)], @@ -44,16 +49,23 @@ export async function start_sim_log_capLogic( return { content: [ createTextContent( - `Log capture started successfully. Session ID: ${sessionId}.\n\n${paramsWithDefaults.captureConsole ? 'Note: Your app was relaunched to capture console output.' : 'Note: Only structured logs are being captured.'}\n\nNext Steps:\n1. Interact with your simulator and app.\n2. Use 'stop_sim_log_cap' with session ID '${sessionId}' to stop capture and retrieve logs.`, + `Log capture started successfully. Session ID: ${sessionId}.\n\n${captureConsole ? 'Note: Your app was relaunched to capture console output.' : 'Note: Only structured logs are being captured.'}\n\nNext Steps:\n1. Interact with your simulator and app.\n2. Use 'stop_sim_log_cap' with session ID '${sessionId}' to stop capture and retrieve logs.`, ), ], }; } +const publicSchemaObject = startSimLogCapSchema.omit({ simulatorId: true } as const).strict(); + export default { name: 'start_sim_log_cap', description: 'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.', - schema: startSimLogCapSchema.shape, // MCP SDK compatibility - handler: createTypedTool(startSimLogCapSchema, start_sim_log_capLogic, getDefaultCommandExecutor), + schema: publicSchemaObject.shape, // MCP SDK compatibility + handler: createSessionAwareTool({ + internalSchema: startSimLogCapSchema as unknown as z.ZodType, + logicFunction: start_sim_log_capLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; diff --git a/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts b/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts index 5861287e..5a398be8 100644 --- a/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/erase_sims.test.ts @@ -3,15 +3,14 @@ import { z } from 'zod'; import eraseSims, { erase_simsLogic } from '../erase_sims.ts'; import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; -describe('erase_sims tool (UDID or ALL only)', () => { +describe('erase_sims tool (single simulator)', () => { describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(eraseSims.name).toBe('erase_sims'); }); it('should have correct description', () => { - expect(eraseSims.description).toContain('Provide exactly one of: simulatorUdid or all=true'); - expect(eraseSims.description).toContain('shutdownFirst'); + expect(eraseSims.description).toBe('Erases a simulator by UDID.'); }); it('should have handler function', () => { @@ -20,19 +19,15 @@ describe('erase_sims tool (UDID or ALL only)', () => { it('should validate schema fields (shape only)', () => { const schema = z.object(eraseSims.schema); - // Valid - expect( - schema.safeParse({ simulatorUdid: '123e4567-e89b-12d3-a456-426614174000' }).success, - ).toBe(true); - expect(schema.safeParse({ all: true }).success).toBe(true); - // Shape-level schema does not enforce selection rules; handler validation covers that. + expect(schema.safeParse({ shutdownFirst: true }).success).toBe(true); + expect(schema.safeParse({}).success).toBe(true); }); }); describe('Single mode', () => { it('erases a simulator successfully', async () => { const mock = createMockExecutor({ success: true, output: 'OK' }); - const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock); + const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock); expect(res).toEqual({ content: [{ type: 'text', text: 'Successfully erased simulator UD1' }], }); @@ -40,7 +35,7 @@ describe('erase_sims tool (UDID or ALL only)', () => { it('returns failure when erase fails', async () => { const mock = createMockExecutor({ success: false, error: 'Booted device' }); - const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock); + const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock); expect(res).toEqual({ content: [{ type: 'text', text: 'Failed to erase simulator: Booted device' }], }); @@ -50,7 +45,7 @@ describe('erase_sims tool (UDID or ALL only)', () => { const bootedError = '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'; const mock = createMockExecutor({ success: false, error: bootedError }); - const res = await erase_simsLogic({ simulatorUdid: 'UD1' }, mock); + const res = await erase_simsLogic({ simulatorId: 'UD1' }, mock); expect((res.content?.[1] as any).text).toContain('Tool hint'); expect((res.content?.[1] as any).text).toContain('shutdownFirst: true'); }); @@ -61,7 +56,7 @@ describe('erase_sims tool (UDID or ALL only)', () => { calls.push(cmd); return { success: true, output: 'OK', error: '', process: { pid: 1 } as any }; }; - const res = await erase_simsLogic({ simulatorUdid: 'UD1', shutdownFirst: true }, exec as any); + const res = await erase_simsLogic({ simulatorId: 'UD1', shutdownFirst: true }, exec as any); expect(calls).toEqual([ ['xcrun', 'simctl', 'shutdown', 'UD1'], ['xcrun', 'simctl', 'erase', 'UD1'], @@ -71,46 +66,4 @@ describe('erase_sims tool (UDID or ALL only)', () => { }); }); }); - - describe('All mode', () => { - it('erases all simulators successfully', async () => { - const exec = createMockExecutor({ success: true, output: 'OK' }); - const res = await erase_simsLogic({ all: true }, exec); - expect(res).toEqual({ - content: [{ type: 'text', text: 'Successfully erased all simulators' }], - }); - }); - - it('returns failure when erase all fails', async () => { - const exec = createMockExecutor({ success: false, error: 'Denied' }); - const res = await erase_simsLogic({ all: true }, exec); - expect(res).toEqual({ - content: [{ type: 'text', text: 'Failed to erase all simulators: Denied' }], - }); - }); - - it('performs shutdown all 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 erase_simsLogic({ all: true, shutdownFirst: true }, exec as any); - expect(calls).toEqual([ - ['xcrun', 'simctl', 'shutdown', 'all'], - ['xcrun', 'simctl', 'erase', 'all'], - ]); - expect(res).toEqual({ - content: [{ type: 'text', text: 'Successfully erased all simulators' }], - }); - }); - - it('adds tool hint on booted error without shutdownFirst (all mode)', async () => { - const bootedError = 'Unable to erase contents and settings in current state: Booted'; - const exec = createMockExecutor({ success: false, error: bootedError }); - const res = await erase_simsLogic({ all: true }, exec); - expect((res.content?.[1] as any).text).toContain('Tool hint'); - expect((res.content?.[1] as any).text).toContain('shutdownFirst: true'); - }); - }); }); diff --git a/src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts b/src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts index 38d51f07..f2e4be9a 100644 --- a/src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts @@ -19,22 +19,14 @@ describe('reset_sim_location plugin', () => { expect(typeof resetSimLocationPlugin.handler).toBe('function'); }); - it('should have correct schema validation', () => { + it('should hide simulatorId from public schema', () => { const schema = z.object(resetSimLocationPlugin.schema); - expect( - schema.safeParse({ - simulatorUuid: 'abc123', - }).success, - ).toBe(true); + expect(schema.safeParse({}).success).toBe(true); - expect( - schema.safeParse({ - simulatorUuid: 123, - }).success, - ).toBe(false); - - expect(schema.safeParse({}).success).toBe(false); + const withSimId = schema.safeParse({ simulatorId: 'abc123' }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as any)).toBe(false); }); }); @@ -47,7 +39,7 @@ describe('reset_sim_location plugin', () => { const result = await reset_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', }, mockExecutor, ); @@ -70,7 +62,7 @@ describe('reset_sim_location plugin', () => { const result = await reset_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', }, mockExecutor, ); @@ -90,7 +82,7 @@ describe('reset_sim_location plugin', () => { const result = await reset_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', }, mockExecutor, ); @@ -123,7 +115,7 @@ describe('reset_sim_location plugin', () => { await reset_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', }, capturingExecutor, ); diff --git a/src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts b/src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts index 1866a9f2..9a051abf 100644 --- a/src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts @@ -19,36 +19,16 @@ describe('set_sim_appearance plugin', () => { expect(typeof setSimAppearancePlugin.handler).toBe('function'); }); - it('should have correct schema validation', () => { + it('should expose public schema without simulatorId field', () => { const schema = z.object(setSimAppearancePlugin.schema); - expect( - schema.safeParse({ - simulatorUuid: 'abc123', - mode: 'dark', - }).success, - ).toBe(true); + expect(schema.safeParse({ mode: 'dark' }).success).toBe(true); + expect(schema.safeParse({ mode: 'light' }).success).toBe(true); + expect(schema.safeParse({ mode: 'invalid' }).success).toBe(false); - expect( - schema.safeParse({ - simulatorUuid: 'abc123', - mode: 'light', - }).success, - ).toBe(true); - - expect( - schema.safeParse({ - simulatorUuid: 'abc123', - mode: 'invalid', - }).success, - ).toBe(false); - - expect( - schema.safeParse({ - simulatorUuid: 123, - mode: 'dark', - }).success, - ).toBe(false); + const withSimId = schema.safeParse({ simulatorId: 'abc123', mode: 'dark' }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as any)).toBe(false); }); }); @@ -62,7 +42,7 @@ describe('set_sim_appearance plugin', () => { const result = await set_sim_appearanceLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', mode: 'dark', }, mockExecutor, @@ -86,7 +66,7 @@ describe('set_sim_appearance plugin', () => { const result = await set_sim_appearanceLogic( { - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', mode: 'light', }, mockExecutor, @@ -102,25 +82,13 @@ describe('set_sim_appearance plugin', () => { }); }); - it('should handle missing simulatorUuid via Zod validation', async () => { - const mockExecutor = createMockExecutor({ - success: true, - output: '', - error: '', - }); - - // Test the handler directly to trigger Zod validation + it('should surface session default requirement when simulatorId is missing', async () => { const result = await setSimAppearancePlugin.handler({ mode: 'dark' }); - expect(result).toEqual({ - content: [ - { - type: 'text', - text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Required', - }, - ], - isError: true, - }); + const message = result.content?.[0]?.text ?? ''; + expect(message).toContain('Error: Missing required session defaults'); + expect(message).toContain('simulatorId is required'); + expect(result.isError).toBe(true); }); it('should handle exception during execution', async () => { @@ -128,7 +96,7 @@ describe('set_sim_appearance plugin', () => { const result = await set_sim_appearanceLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', mode: 'dark', }, mockExecutor, @@ -158,7 +126,7 @@ describe('set_sim_appearance plugin', () => { await set_sim_appearanceLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', mode: 'dark', }, mockExecutor, diff --git a/src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts b/src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts index 606fae5d..6819fcec 100644 --- a/src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts @@ -25,53 +25,20 @@ describe('set_sim_location tool', () => { expect(typeof setSimLocation.handler).toBe('function'); }); - it('should have correct schema with simulatorUuid string field and latitude/longitude number fields', () => { + it('should expose public schema without simulatorId field', () => { const schema = z.object(setSimLocation.schema); - // Valid inputs - expect( - schema.safeParse({ - simulatorUuid: 'test-uuid-123', - latitude: 37.7749, - longitude: -122.4194, - }).success, - ).toBe(true); - expect( - schema.safeParse({ simulatorUuid: 'ABC123-DEF456', latitude: 0, longitude: 0 }).success, - ).toBe(true); - expect( - schema.safeParse({ simulatorUuid: 'test-uuid', latitude: 90, longitude: 180 }).success, - ).toBe(true); - expect( - schema.safeParse({ simulatorUuid: 'test-uuid', latitude: -90, longitude: -180 }).success, - ).toBe(true); - expect( - schema.safeParse({ simulatorUuid: 'test-uuid', latitude: 45.5, longitude: -73.6 }).success, - ).toBe(true); - - // Invalid inputs - expect( - schema.safeParse({ simulatorUuid: 123, latitude: 37.7749, longitude: -122.4194 }).success, - ).toBe(false); - expect( - schema.safeParse({ simulatorUuid: 'test-uuid', latitude: 'invalid', longitude: -122.4194 }) - .success, - ).toBe(false); - expect( - schema.safeParse({ simulatorUuid: 'test-uuid', latitude: 37.7749, longitude: 'invalid' }) - .success, - ).toBe(false); - expect( - schema.safeParse({ simulatorUuid: null, latitude: 37.7749, longitude: -122.4194 }).success, - ).toBe(false); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', longitude: -122.4194 }).success).toBe( - false, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', latitude: 37.7749 }).success).toBe( - false, - ); - expect(schema.safeParse({ latitude: 37.7749, longitude: -122.4194 }).success).toBe(false); - expect(schema.safeParse({}).success).toBe(false); + expect(schema.safeParse({ latitude: 37.7749, longitude: -122.4194 }).success).toBe(true); + expect(schema.safeParse({ latitude: 0, longitude: 0 }).success).toBe(true); + expect(schema.safeParse({ latitude: 37.7749 }).success).toBe(false); + expect(schema.safeParse({ longitude: -122.4194 }).success).toBe(false); + const withSimId = schema.safeParse({ + simulatorId: 'test-uuid-123', + latitude: 37.7749, + longitude: -122.4194, + }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as any)).toBe(false); }); }); @@ -91,7 +58,7 @@ describe('set_sim_location tool', () => { await set_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', latitude: 37.7749, longitude: -122.4194, }, @@ -123,7 +90,7 @@ describe('set_sim_location tool', () => { await set_sim_locationLogic( { - simulatorUuid: 'different-uuid', + simulatorId: 'different-uuid', latitude: 45.5, longitude: -73.6, }, @@ -155,7 +122,7 @@ describe('set_sim_location tool', () => { await set_sim_locationLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', latitude: -90, longitude: -180, }, @@ -183,7 +150,7 @@ describe('set_sim_location tool', () => { const result = await set_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', latitude: 37.7749, longitude: -122.4194, }, @@ -203,7 +170,7 @@ describe('set_sim_location tool', () => { it('should handle latitude validation failure', async () => { const result = await set_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', latitude: 95, longitude: -122.4194, }, @@ -223,7 +190,7 @@ describe('set_sim_location tool', () => { it('should handle longitude validation failure', async () => { const result = await set_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', latitude: 37.7749, longitude: -185, }, @@ -249,7 +216,7 @@ describe('set_sim_location tool', () => { const result = await set_sim_locationLogic( { - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', latitude: 37.7749, longitude: -122.4194, }, @@ -271,7 +238,7 @@ describe('set_sim_location tool', () => { const result = await set_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', latitude: 37.7749, longitude: -122.4194, }, @@ -293,7 +260,7 @@ describe('set_sim_location tool', () => { const result = await set_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', latitude: 37.7749, longitude: -122.4194, }, @@ -319,7 +286,7 @@ describe('set_sim_location tool', () => { const result = await set_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', latitude: 90, longitude: 180, }, @@ -345,7 +312,7 @@ describe('set_sim_location tool', () => { const result = await set_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', latitude: -90, longitude: -180, }, @@ -371,7 +338,7 @@ describe('set_sim_location tool', () => { const result = await set_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', latitude: 0, longitude: 0, }, @@ -403,7 +370,7 @@ describe('set_sim_location tool', () => { await set_sim_locationLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', latitude: 37.7749, longitude: -122.4194, }, diff --git a/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts b/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts index ba364d85..e57b1ade 100644 --- a/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts @@ -25,59 +25,16 @@ describe('sim_statusbar tool', () => { expect(typeof simStatusbar.handler).toBe('function'); }); - it('should have correct schema with simulatorUuid string field and dataNetwork enum field', () => { + it('should expose public schema without simulatorId field', () => { const schema = z.object(simStatusbar.schema); - // Valid inputs - expect( - schema.safeParse({ simulatorUuid: 'test-uuid-123', dataNetwork: 'wifi' }).success, - ).toBe(true); - expect(schema.safeParse({ simulatorUuid: 'ABC123-DEF456', dataNetwork: '3g' }).success).toBe( - true, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '4g' }).success).toBe( - true, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'lte' }).success).toBe( - true, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'lte-a' }).success).toBe( - true, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'lte+' }).success).toBe( - true, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g' }).success).toBe( - true, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g+' }).success).toBe( - true, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g-uwb' }).success).toBe( - true, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g-uc' }).success).toBe( - true, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'hide' }).success).toBe( - true, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'clear' }).success).toBe( - true, - ); + expect(schema.safeParse({ dataNetwork: 'wifi' }).success).toBe(true); + expect(schema.safeParse({ dataNetwork: 'clear' }).success).toBe(true); + expect(schema.safeParse({ dataNetwork: 'invalid' }).success).toBe(false); - // Invalid inputs - expect(schema.safeParse({ simulatorUuid: 123, dataNetwork: 'wifi' }).success).toBe(false); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'invalid' }).success).toBe( - false, - ); - expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 123 }).success).toBe( - false, - ); - expect(schema.safeParse({ simulatorUuid: null, dataNetwork: 'wifi' }).success).toBe(false); - expect(schema.safeParse({ simulatorUuid: 'test-uuid' }).success).toBe(false); - expect(schema.safeParse({ dataNetwork: 'wifi' }).success).toBe(false); - expect(schema.safeParse({}).success).toBe(false); + const withSimId = schema.safeParse({ simulatorId: 'test-uuid', dataNetwork: 'wifi' }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as any)).toBe(false); }); }); @@ -90,7 +47,7 @@ describe('sim_statusbar tool', () => { const result = await sim_statusbarLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', dataNetwork: 'wifi', }, mockExecutor, @@ -116,7 +73,7 @@ describe('sim_statusbar tool', () => { const result = await sim_statusbarLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', dataNetwork: 'wifi', }, mockExecutor, @@ -136,7 +93,7 @@ describe('sim_statusbar tool', () => { const result = await sim_statusbarLogic( { - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', dataNetwork: '3g', }, mockExecutor, @@ -160,7 +117,7 @@ describe('sim_statusbar tool', () => { const result = await sim_statusbarLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', dataNetwork: '4g', }, mockExecutor, @@ -184,7 +141,7 @@ describe('sim_statusbar tool', () => { const result = await sim_statusbarLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', dataNetwork: 'lte', }, mockExecutor, @@ -226,7 +183,7 @@ describe('sim_statusbar tool', () => { await sim_statusbarLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', dataNetwork: 'wifi', }, mockExecutor, @@ -274,7 +231,7 @@ describe('sim_statusbar tool', () => { await sim_statusbarLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', dataNetwork: 'clear', }, mockExecutor, @@ -297,7 +254,7 @@ describe('sim_statusbar tool', () => { const result = await sim_statusbarLogic( { - simulatorUuid: 'test-uuid-123', + simulatorId: 'test-uuid-123', dataNetwork: 'clear', }, mockExecutor, diff --git a/src/mcp/tools/simulator-management/erase_sims.ts b/src/mcp/tools/simulator-management/erase_sims.ts index 4723e9d1..feb61e19 100644 --- a/src/mcp/tools/simulator-management/erase_sims.ts +++ b/src/mcp/tools/simulator-management/erase_sims.ts @@ -1,25 +1,20 @@ import { z } from 'zod'; -import { ToolResponse, type ToolResponseContent } from '../../../types/common.ts'; +import { ToolResponse } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; -const eraseSimsBaseSchema = z.object({ - simulatorUdid: z.string().uuid().optional().describe('UDID of the simulator to erase.'), - all: z.boolean().optional().describe('When true, erases all simulators.'), - shutdownFirst: z - .boolean() - .optional() - .describe('If true, shuts down the target (UDID or all) before erasing.'), -}); +const eraseSimsBaseSchema = z + .object({ + simulatorId: z.string().uuid().describe('UDID of the simulator to erase.'), + shutdownFirst: z + .boolean() + .optional() + .describe('If true, shuts down the simulator before erasing.'), + }) + .passthrough(); -const eraseSimsSchema = eraseSimsBaseSchema.refine( - (v) => { - const selectors = (v.simulatorUdid ? 1 : 0) + (v.all === true ? 1 : 0); - return selectors === 1; - }, - { message: 'Provide exactly one of: simulatorUdid or all=true.' }, -); +const eraseSimsSchema = eraseSimsBaseSchema; type EraseSimsParams = z.infer; @@ -28,97 +23,53 @@ export async function erase_simsLogic( executor: CommandExecutor, ): Promise { try { - if (params.simulatorUdid) { - const udid = params.simulatorUdid; - log( - 'info', - `Erasing simulator ${udid}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`, - ); + const simulatorId = params.simulatorId; + log( + 'info', + `Erasing simulator ${simulatorId}${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`, + ); - if (params.shutdownFirst) { - try { - await executor( - ['xcrun', 'simctl', 'shutdown', udid], - 'Shutdown Simulator', - true, - undefined, - ); - } catch { - // ignore shutdown errors; proceed to erase attempt - } - } - - const result = await executor( - ['xcrun', 'simctl', 'erase', udid], - 'Erase Simulator', - true, - undefined, - ); - if (result.success) { - return { content: [{ type: 'text', text: `Successfully erased simulator ${udid}` }] }; - } - - // Add tool hint if simulator is booted and shutdownFirst was not requested - const errText = result.error ?? 'Unknown error'; - if (/Unable to erase contents and settings.*Booted/i.test(errText) && !params.shutdownFirst) { - return { - content: [ - { type: 'text', text: `Failed to erase simulator: ${errText}` }, - { - type: 'text', - text: `Tool hint: The simulator appears to be Booted. Re-run erase_sims with { simulatorUdid: '${udid}', shutdownFirst: true } to shut it down before erasing.`, - }, - ], - }; + if (params.shutdownFirst) { + try { + await executor( + ['xcrun', 'simctl', 'shutdown', simulatorId], + 'Shutdown Simulator', + true, + undefined, + ); + } catch { + // ignore shutdown errors; proceed to erase attempt } + } + const result = await executor( + ['xcrun', 'simctl', 'erase', simulatorId], + 'Erase Simulator', + true, + undefined, + ); + if (result.success) { return { - content: [{ type: 'text', text: `Failed to erase simulator: ${errText}` }], + content: [{ type: 'text', text: `Successfully erased simulator ${simulatorId}` }], }; } - if (params.all === true) { - log('info', `Erasing ALL simulators${params.shutdownFirst ? ' (shutdownFirst=true)' : ''}`); - if (params.shutdownFirst) { - try { - await executor( - ['xcrun', 'simctl', 'shutdown', 'all'], - 'Shutdown All Simulators', - true, - undefined, - ); - } catch { - // ignore and continue to erase - } - } - - const result = await executor( - ['xcrun', 'simctl', 'erase', 'all'], - 'Erase All Simulators', - true, - undefined, - ); - if (!result.success) { - const errText = result.error ?? 'Unknown error'; - const content: ToolResponseContent[] = [ - { type: 'text', text: `Failed to erase all simulators: ${errText}` }, - ]; - if ( - /Unable to erase contents and settings.*Booted/i.test(errText) && - !params.shutdownFirst - ) { - content.push({ + // Add tool hint if simulator is booted and shutdownFirst was not requested + const errText = result.error ?? 'Unknown error'; + if (/Unable to erase contents and settings.*Booted/i.test(errText) && !params.shutdownFirst) { + return { + content: [ + { type: 'text', text: `Failed to erase simulator: ${errText}` }, + { type: 'text', - 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.', - }); - } - return { content }; - } - return { content: [{ type: 'text', text: 'Successfully erased all simulators' }] }; + text: `Tool hint: The simulator appears to be Booted. Re-run erase_sims with { simulatorId: '${simulatorId}', shutdownFirst: true } to shut it down before erasing.`, + }, + ], + }; } return { - content: [{ type: 'text', text: 'Invalid parameters: provide simulatorUdid or all=true.' }], + content: [{ type: 'text', text: `Failed to erase simulator: ${errText}` }], }; } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); @@ -127,10 +78,16 @@ export async function erase_simsLogic( } } +const publicSchemaObject = eraseSimsSchema.omit({ simulatorId: true } as const).passthrough(); + export default { name: 'erase_sims', - description: - 'Erases simulator content and settings. Provide exactly one of: simulatorUdid or all=true. Optional: shutdownFirst to shut down before erasing.', - schema: eraseSimsBaseSchema.shape, - handler: createTypedTool(eraseSimsSchema, erase_simsLogic, getDefaultCommandExecutor), + description: 'Erases a simulator by UDID.', + schema: publicSchemaObject.shape, + handler: createSessionAwareTool({ + internalSchema: eraseSimsSchema as unknown as z.ZodType, + logicFunction: erase_simsLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; diff --git a/src/mcp/tools/simulator-management/reset_sim_location.ts b/src/mcp/tools/simulator-management/reset_sim_location.ts index f6c55fc7..e08e303d 100644 --- a/src/mcp/tools/simulator-management/reset_sim_location.ts +++ b/src/mcp/tools/simulator-management/reset_sim_location.ts @@ -2,12 +2,13 @@ import { z } from 'zod'; import { ToolResponse } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const resetSimulatorLocationSchema = z.object({ - simulatorUuid: z + simulatorId: z .string() + .uuid() .describe('UUID of the simulator to use (obtained from list_simulators)'), }); @@ -40,7 +41,7 @@ async function executeSimctlCommandAndRespond( const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`; log( 'error', - `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, + `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`, ); return { content: [{ type: 'text', text: fullFailureMessage }], @@ -49,7 +50,7 @@ async function executeSimctlCommandAndRespond( log( 'info', - `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, + `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`, ); return { content: [{ type: 'text', text: successMessage }], @@ -59,7 +60,7 @@ async function executeSimctlCommandAndRespond( const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`; log( 'error', - `Error during ${operationLogContext} for simulator ${params.simulatorUuid}: ${errorMessage}`, + `Error during ${operationLogContext} for simulator ${params.simulatorId}: ${errorMessage}`, ); return { content: [{ type: 'text', text: fullFailureMessage }], @@ -71,26 +72,32 @@ export async function reset_sim_locationLogic( params: ResetSimulatorLocationParams, executor: CommandExecutor, ): Promise { - log('info', `Resetting simulator ${params.simulatorUuid} location`); + log('info', `Resetting simulator ${params.simulatorId} location`); return executeSimctlCommandAndRespond( params, - ['location', params.simulatorUuid, 'clear'], + ['location', params.simulatorId, 'clear'], 'Reset Simulator Location', - `Successfully reset simulator ${params.simulatorUuid} location.`, + `Successfully reset simulator ${params.simulatorId} location.`, 'Failed to reset simulator location', 'reset simulator location', executor, ); } +const publicSchemaObject = resetSimulatorLocationSchema + .omit({ simulatorId: true } as const) + .strict(); + export default { name: 'reset_sim_location', description: "Resets the simulator's location to default.", - schema: resetSimulatorLocationSchema.shape, // MCP SDK compatibility - handler: createTypedTool( - resetSimulatorLocationSchema, - reset_sim_locationLogic, - getDefaultCommandExecutor, - ), + schema: publicSchemaObject.shape, // MCP SDK compatibility + handler: createSessionAwareTool({ + internalSchema: + resetSimulatorLocationSchema as unknown as z.ZodType, + logicFunction: reset_sim_locationLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; diff --git a/src/mcp/tools/simulator-management/set_sim_appearance.ts b/src/mcp/tools/simulator-management/set_sim_appearance.ts index b8d65eeb..156ae871 100644 --- a/src/mcp/tools/simulator-management/set_sim_appearance.ts +++ b/src/mcp/tools/simulator-management/set_sim_appearance.ts @@ -2,12 +2,13 @@ import { z } from 'zod'; import { ToolResponse } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const setSimAppearanceSchema = z.object({ - simulatorUuid: z + simulatorId: z .string() + .uuid() .describe('UUID of the simulator to use (obtained from list_simulators)'), mode: z.enum(['dark', 'light']).describe('The appearance mode to set (either "dark" or "light")'), }); @@ -41,7 +42,7 @@ async function executeSimctlCommandAndRespond( const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`; log( 'error', - `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, + `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`, ); return { content: [{ type: 'text', text: fullFailureMessage }], @@ -50,7 +51,7 @@ async function executeSimctlCommandAndRespond( log( 'info', - `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, + `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`, ); return { content: [{ type: 'text', text: successMessage }], @@ -60,7 +61,7 @@ async function executeSimctlCommandAndRespond( const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`; log( 'error', - `Error during ${operationLogContext} for simulator ${params.simulatorUuid}: ${errorMessage}`, + `Error during ${operationLogContext} for simulator ${params.simulatorId}: ${errorMessage}`, ); return { content: [{ type: 'text', text: fullFailureMessage }], @@ -72,13 +73,13 @@ export async function set_sim_appearanceLogic( params: SetSimAppearanceParams, executor: CommandExecutor, ): Promise { - log('info', `Setting simulator ${params.simulatorUuid} appearance to ${params.mode} mode`); + log('info', `Setting simulator ${params.simulatorId} appearance to ${params.mode} mode`); return executeSimctlCommandAndRespond( params, - ['ui', params.simulatorUuid, 'appearance', params.mode], + ['ui', params.simulatorId, 'appearance', params.mode], 'Set Simulator Appearance', - `Successfully set simulator ${params.simulatorUuid} appearance to ${params.mode} mode`, + `Successfully set simulator ${params.simulatorId} appearance to ${params.mode} mode`, 'Failed to set simulator appearance', 'set simulator appearance', undefined, @@ -86,13 +87,16 @@ export async function set_sim_appearanceLogic( ); } +const publicSchemaObject = setSimAppearanceSchema.omit({ simulatorId: true } as const).strict(); + export default { name: 'set_sim_appearance', description: 'Sets the appearance mode (dark/light) of an iOS simulator.', - schema: setSimAppearanceSchema.shape, // MCP SDK compatibility - handler: createTypedTool( - setSimAppearanceSchema, - set_sim_appearanceLogic, - getDefaultCommandExecutor, - ), + schema: publicSchemaObject.shape, // MCP SDK compatibility + handler: createSessionAwareTool({ + internalSchema: setSimAppearanceSchema as unknown as z.ZodType, + logicFunction: set_sim_appearanceLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; diff --git a/src/mcp/tools/simulator-management/set_sim_location.ts b/src/mcp/tools/simulator-management/set_sim_location.ts index df2047b2..1121231e 100644 --- a/src/mcp/tools/simulator-management/set_sim_location.ts +++ b/src/mcp/tools/simulator-management/set_sim_location.ts @@ -2,12 +2,13 @@ import { z } from 'zod'; import { ToolResponse } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const setSimulatorLocationSchema = z.object({ - simulatorUuid: z + simulatorId: z .string() + .uuid() .describe('UUID of the simulator to use (obtained from list_simulators)'), latitude: z.number().describe('The latitude for the custom location.'), longitude: z.number().describe('The longitude for the custom location.'), @@ -42,7 +43,7 @@ async function executeSimctlCommandAndRespond( const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`; log( 'error', - `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, + `${fullFailureMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`, ); return { content: [{ type: 'text', text: fullFailureMessage }], @@ -51,7 +52,7 @@ async function executeSimctlCommandAndRespond( log( 'info', - `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorUuid})`, + `${successMessage} (operation: ${operationLogContext}, simulator: ${params.simulatorId})`, ); return { content: [{ type: 'text', text: successMessage }], @@ -61,7 +62,7 @@ async function executeSimctlCommandAndRespond( const fullFailureMessage = `${failureMessagePrefix}: ${errorMessage}`; log( 'error', - `Error during ${operationLogContext} for simulator ${params.simulatorUuid}: ${errorMessage}`, + `Error during ${operationLogContext} for simulator ${params.simulatorId}: ${errorMessage}`, ); return { content: [{ type: 'text', text: fullFailureMessage }], @@ -99,14 +100,14 @@ export async function set_sim_locationLogic( log( 'info', - `Setting simulator ${params.simulatorUuid} location to ${params.latitude},${params.longitude}`, + `Setting simulator ${params.simulatorId} location to ${params.latitude},${params.longitude}`, ); return executeSimctlCommandAndRespond( params, - ['location', params.simulatorUuid, 'set', `${params.latitude},${params.longitude}`], + ['location', params.simulatorId, 'set', `${params.latitude},${params.longitude}`], 'Set Simulator Location', - `Successfully set simulator ${params.simulatorUuid} location to ${params.latitude},${params.longitude}`, + `Successfully set simulator ${params.simulatorId} location to ${params.latitude},${params.longitude}`, 'Failed to set simulator location', 'set simulator location', executor, @@ -114,13 +115,16 @@ export async function set_sim_locationLogic( ); } +const publicSchemaObject = setSimulatorLocationSchema.omit({ simulatorId: true } as const).strict(); + export default { name: 'set_sim_location', description: 'Sets a custom GPS location for the simulator.', - schema: setSimulatorLocationSchema.shape, // MCP SDK compatibility - handler: createTypedTool( - setSimulatorLocationSchema, - set_sim_locationLogic, - getDefaultCommandExecutor, - ), + schema: publicSchemaObject.shape, // MCP SDK compatibility + handler: createSessionAwareTool({ + internalSchema: setSimulatorLocationSchema as unknown as z.ZodType, + logicFunction: set_sim_locationLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; diff --git a/src/mcp/tools/simulator-management/sim_statusbar.ts b/src/mcp/tools/simulator-management/sim_statusbar.ts index 261ea800..cb271cbd 100644 --- a/src/mcp/tools/simulator-management/sim_statusbar.ts +++ b/src/mcp/tools/simulator-management/sim_statusbar.ts @@ -2,12 +2,13 @@ import { z } from 'zod'; import { ToolResponse } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const simStatusbarSchema = z.object({ - simulatorUuid: z + simulatorId: z .string() + .uuid() .describe('UUID of the simulator to use (obtained from list_simulators)'), dataNetwork: z .enum([ @@ -38,7 +39,7 @@ export async function sim_statusbarLogic( ): Promise { log( 'info', - `Setting simulator ${params.simulatorUuid} status bar data network to ${params.dataNetwork}`, + `Setting simulator ${params.simulatorId} status bar data network to ${params.dataNetwork}`, ); try { @@ -46,40 +47,40 @@ export async function sim_statusbarLogic( let successMessage: string; if (params.dataNetwork === 'clear') { - command = ['xcrun', 'simctl', 'status_bar', params.simulatorUuid, 'clear']; - successMessage = `Successfully cleared status bar overrides for simulator ${params.simulatorUuid}`; + command = ['xcrun', 'simctl', 'status_bar', params.simulatorId, 'clear']; + successMessage = `Successfully cleared status bar overrides for simulator ${params.simulatorId}`; } else { command = [ 'xcrun', 'simctl', 'status_bar', - params.simulatorUuid, + params.simulatorId, 'override', '--dataNetwork', params.dataNetwork, ]; - successMessage = `Successfully set simulator ${params.simulatorUuid} status bar data network to ${params.dataNetwork}`; + successMessage = `Successfully set simulator ${params.simulatorId} status bar data network to ${params.dataNetwork}`; } const result = await executor(command, 'Set Status Bar', true, undefined); if (!result.success) { const failureMessage = `Failed to set status bar: ${result.error}`; - log('error', `${failureMessage} (simulator: ${params.simulatorUuid})`); + log('error', `${failureMessage} (simulator: ${params.simulatorId})`); return { content: [{ type: 'text', text: failureMessage }], isError: true, }; } - log('info', `${successMessage} (simulator: ${params.simulatorUuid})`); + log('info', `${successMessage} (simulator: ${params.simulatorId})`); return { content: [{ type: 'text', text: successMessage }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const failureMessage = `Failed to set status bar: ${errorMessage}`; - log('error', `Error setting status bar for simulator ${params.simulatorUuid}: ${errorMessage}`); + log('error', `Error setting status bar for simulator ${params.simulatorId}: ${errorMessage}`); return { content: [{ type: 'text', text: failureMessage }], isError: true, @@ -87,10 +88,17 @@ export async function sim_statusbarLogic( } } +const publicSchemaObject = simStatusbarSchema.omit({ simulatorId: true } as const).strict(); + export default { name: 'sim_statusbar', description: 'Sets the data network indicator in the iOS simulator status bar. Use "clear" to reset all overrides, or specify a network type (hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc).', - schema: simStatusbarSchema.shape, // MCP SDK compatibility - handler: createTypedTool(simStatusbarSchema, sim_statusbarLogic, getDefaultCommandExecutor), + schema: publicSchemaObject.shape, // MCP SDK compatibility + handler: createSessionAwareTool({ + internalSchema: simStatusbarSchema as unknown as z.ZodType, + logicFunction: sim_statusbarLogic, + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; diff --git a/src/mcp/tools/simulator/__tests__/boot_sim.test.ts b/src/mcp/tools/simulator/__tests__/boot_sim.test.ts index 1fe630ed..2fa8ed24 100644 --- a/src/mcp/tools/simulator/__tests__/boot_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/boot_sim.test.ts @@ -27,6 +27,10 @@ describe('boot_sim tool', () => { const schema = z.object(bootSim.schema); expect(schema.safeParse({}).success).toBe(true); expect(Object.keys(bootSim.schema)).toHaveLength(0); + + const withSimId = schema.safeParse({ simulatorId: 'abc' }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as Record)).toBe(false); }); }); @@ -35,9 +39,10 @@ describe('boot_sim tool', () => { const result = await bootSim.handler({}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Missing required session defaults'); - expect(result.content[0].text).toContain('simulatorId is required'); - expect(result.content[0].text).toContain('session-set-defaults'); + const message = result.content[0].text; + expect(message).toContain('Missing required session defaults'); + expect(message).toContain('simulatorId is required'); + expect(message).toContain('session-set-defaults'); }); }); @@ -54,12 +59,7 @@ describe('boot_sim tool', () => { content: [ { type: 'text', - text: `✅ Simulator booted successfully. To make it visible, use: open_sim() - -Next steps: -1. Open the Simulator app (makes it visible): open_sim() -2. Install an app: install_app_sim({ simulatorId: "test-uuid-123", appPath: "PATH_TO_YOUR_APP" }) -3. Launch an app: launch_app_sim({ simulatorId: "test-uuid-123", bundleId: "YOUR_APP_BUNDLE_ID" })`, + text: `✅ Simulator booted successfully. To make it visible, use: open_sim()\n\nNext steps:\n1. Open the Simulator app (makes it visible): open_sim()\n2. Install an app: install_app_sim({ simulatorId: "test-uuid-123", appPath: "PATH_TO_YOUR_APP" })\n3. Launch an app: launch_app_sim({ simulatorId: "test-uuid-123", bundleId: "YOUR_APP_BUNDLE_ID" })`, }, ], }); diff --git a/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts b/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts index 5d49f757..c9920205 100644 --- a/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts @@ -30,6 +30,13 @@ describe('install_app_sim tool', () => { expect(schema.safeParse({}).success).toBe(false); expect(Object.keys(installAppSim.schema)).toEqual(['appPath']); + + const withSimId = schema.safeParse({ + simulatorId: 'test-uuid-123', + appPath: '/path/app.app', + }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as Record)).toBe(false); }); }); @@ -167,11 +174,69 @@ describe('install_app_sim tool', () => { }); }); - it('should handle successful install', async () => { - let callCount = 0; - const mockExecutor = () => { - callCount++; - if (callCount === 1) { + it('should handle bundle id extraction failure gracefully', async () => { + const bundleIdCalls: unknown[] = []; + const mockExecutor = (...args: unknown[]) => { + bundleIdCalls.push(args); + if ( + Array.isArray(args[0]) && + (args[0] as string[])[0] === 'xcrun' && + (args[0] as string[])[1] === 'simctl' + ) { + return Promise.resolve({ + success: true, + output: 'App installed', + error: undefined, + process: { pid: 12345 }, + }); + } + return Promise.resolve({ + success: false, + output: '', + error: 'Failed to read bundle ID', + process: { pid: 12345 }, + }); + }; + + const mockFileSystem = createMockFileSystemExecutor({ + existsSync: () => true, + }); + + const result = await install_app_simLogic( + { + simulatorId: 'test-uuid-123', + appPath: '/path/to/app.app', + }, + mockExecutor, + mockFileSystem, + ); + + expect(result).toEqual({ + content: [ + { + type: 'text', + text: 'App installed successfully in simulator test-uuid-123', + }, + { + type: 'text', + text: `Next Steps: +1. Open the Simulator app: open_sim({}) +2. Launch the app: launch_app_sim({ simulatorId: "test-uuid-123", bundleId: "YOUR_APP_BUNDLE_ID" })`, + }, + ], + }); + expect(bundleIdCalls).toHaveLength(2); + }); + + it('should include bundle id when extraction succeeds', async () => { + const bundleIdCalls: unknown[] = []; + const mockExecutor = (...args: unknown[]) => { + bundleIdCalls.push(args); + if ( + Array.isArray(args[0]) && + (args[0] as string[])[0] === 'xcrun' && + (args[0] as string[])[1] === 'simctl' + ) { return Promise.resolve({ success: true, output: 'App installed', @@ -214,6 +279,7 @@ describe('install_app_sim tool', () => { }, ], }); + expect(bundleIdCalls).toHaveLength(2); }); it('should handle command failure', async () => { diff --git a/src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts b/src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts index ff50d8e7..8148d9e3 100644 --- a/src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts @@ -36,6 +36,13 @@ describe('launch_app_logs_sim tool', () => { expect(schema.safeParse({ bundleId: 42 }).success).toBe(false); expect(Object.keys(launchAppLogsSim.schema).sort()).toEqual(['args', 'bundleId'].sort()); + + const withSimId = schema.safeParse({ + simulatorId: 'abc123', + bundleId: 'com.example.app', + }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as Record)).toBe(false); }); }); @@ -104,7 +111,7 @@ describe('launch_app_logs_sim tool', () => { }); }); - it('should ignore args for log capture setup', async () => { + it('should include passthrough args in log capture setup', async () => { let capturedParams: unknown = null; const logCaptureStub: LogCaptureFunction = async (params) => { capturedParams = params; diff --git a/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts b/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts index 8d4dcd20..8a6440f9 100644 --- a/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts @@ -36,6 +36,16 @@ describe('launch_app_sim tool', () => { expect(schema.safeParse({ args: ['--debug'] }).success).toBe(false); expect(Object.keys(launchAppSim.schema).sort()).toEqual(['args', 'bundleId'].sort()); + + const withSimDefaults = schema.safeParse({ + simulatorId: 'sim-default', + simulatorName: 'iPhone 16', + bundleId: 'com.example.testapp', + }); + expect(withSimDefaults.success).toBe(true); + const parsed = withSimDefaults.data as Record; + expect(parsed.simulatorId).toBeUndefined(); + expect(parsed.simulatorName).toBeUndefined(); }); }); @@ -109,13 +119,7 @@ describe('launch_app_sim tool', () => { content: [ { type: 'text', - text: `✅ App launched successfully in simulator test-uuid-123. - -Next Steps: -1. To see simulator: open_sim() -2. Log capture: start_sim_log_cap({ simulatorUuid: "test-uuid-123", bundleId: "com.example.testapp" }) - With console: start_sim_log_cap({ simulatorUuid: "test-uuid-123", bundleId: "com.example.testapp", captureConsole: true }) -3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`, + text: `✅ App launched successfully in simulator test-uuid-123.\n\nNext Steps:\n1. To see simulator: open_sim()\n2. Log capture: start_sim_log_cap({ simulatorId: "test-uuid-123", bundleId: "com.example.testapp" })\n With console: start_sim_log_cap({ simulatorId: "test-uuid-123", bundleId: "com.example.testapp", captureConsole: true })\n3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`, }, ], }); @@ -126,8 +130,8 @@ Next Steps: const commands: string[][] = []; const sequencedExecutor = async (command: string[]) => { - commands.push(command); callCount++; + commands.push(command); if (callCount === 1) { return { success: true, @@ -153,23 +157,62 @@ Next Steps: sequencedExecutor, ); - expect(commands[1]).toEqual([ - 'xcrun', - 'simctl', - 'launch', - 'test-uuid-123', - 'com.example.testapp', - '--debug', - '--verbose', + expect(commands).toEqual([ + ['xcrun', 'simctl', 'get_app_container', 'test-uuid-123', 'com.example.testapp', 'app'], + [ + 'xcrun', + 'simctl', + 'launch', + 'test-uuid-123', + 'com.example.testapp', + '--debug', + '--verbose', + ], ]); }); - it('should surface app-not-installed error', async () => { - const mockExecutor = createMockExecutor({ - success: false, - output: '', - error: 'App not found', + it('should surface error when simulatorId missing after lookup', async () => { + const result = await launch_app_simLogic( + { + simulatorId: undefined, + bundleId: 'com.example.testapp', + } as any, + async () => ({ + success: true, + output: '', + error: '', + process: {} as any, + }), + ); + + expect(result).toEqual({ + content: [ + { + type: 'text', + text: 'No simulator identifier provided', + }, + ], + isError: true, }); + }); + + it('should detect missing app container on install check', async () => { + const mockExecutor = async (command: string[]) => { + if (command.includes('get_app_container')) { + return { + success: false, + output: '', + error: 'App container not found', + process: {} as any, + }; + } + return { + success: true, + output: '', + error: '', + process: {} as any, + }; + }; const result = await launch_app_simLogic( { @@ -183,16 +226,48 @@ Next Steps: content: [ { type: 'text', - text: 'App is not installed on the simulator. Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.', + text: `App is not installed on the simulator. Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.`, }, ], isError: true, }); }); - it('should return launch failure message when simctl launch fails', async () => { + it('should return error when install check throws', async () => { + const mockExecutor = async (command: string[]) => { + if (command.includes('get_app_container')) { + throw new Error('Simctl command failed'); + } + return { + success: true, + output: '', + error: '', + process: {} as any, + }; + }; + + const result = await launch_app_simLogic( + { + simulatorId: 'test-uuid-123', + bundleId: 'com.example.testapp', + }, + mockExecutor, + ); + + expect(result).toEqual({ + content: [ + { + type: 'text', + text: `App is not installed on the simulator (check failed). Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.`, + }, + ], + isError: true, + }); + }); + + it('should handle launch failure', async () => { let callCount = 0; - const sequencedExecutor = async (command: string[]) => { + const mockExecutor = async (command: string[]) => { callCount++; if (callCount === 1) { return { @@ -215,7 +290,7 @@ Next Steps: simulatorId: 'test-uuid-123', bundleId: 'com.example.testapp', }, - sequencedExecutor, + mockExecutor, ); expect(result).toEqual({ @@ -279,13 +354,7 @@ Next Steps: content: [ { type: 'text', - text: `✅ App launched successfully in simulator "iPhone 16" (resolved-uuid). - -Next Steps: -1. To see simulator: open_sim() -2. Log capture: start_sim_log_cap({ simulatorName: "iPhone 16", bundleId: "com.example.testapp" }) - With console: start_sim_log_cap({ simulatorName: "iPhone 16", bundleId: "com.example.testapp", captureConsole: true }) -3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`, + text: `✅ App launched successfully in simulator "iPhone 16" (resolved-uuid).\n\nNext Steps:\n1. To see simulator: open_sim()\n2. Log capture: start_sim_log_cap({ simulatorName: "iPhone 16", bundleId: "com.example.testapp" })\n With console: start_sim_log_cap({ simulatorName: "iPhone 16", bundleId: "com.example.testapp", captureConsole: true })\n3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`, }, ], }); diff --git a/src/mcp/tools/simulator/__tests__/list_sims.test.ts b/src/mcp/tools/simulator/__tests__/list_sims.test.ts index ccd1b3cd..36e0707f 100644 --- a/src/mcp/tools/simulator/__tests__/list_sims.test.ts +++ b/src/mcp/tools/simulator/__tests__/list_sims.test.ts @@ -121,7 +121,7 @@ iOS 17.0: - iPhone 15 (test-uuid-123) Next Steps: -1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' }) +1. Boot a simulator: boot_sim({ simulatorId: 'UUID_FROM_ABOVE' }) 2. Open the simulator UI: open_sim({}) 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' }) 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`, @@ -177,7 +177,7 @@ iOS 17.0: - iPhone 15 (test-uuid-123) [Booted] Next Steps: -1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' }) +1. Boot a simulator: boot_sim({ simulatorId: 'UUID_FROM_ABOVE' }) 2. Open the simulator UI: open_sim({}) 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' }) 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`, @@ -239,7 +239,7 @@ iOS 26.0: - iPhone 17 Pro (text-uuid-456) Next Steps: -1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' }) +1. Boot a simulator: boot_sim({ simulatorId: 'UUID_FROM_ABOVE' }) 2. Open the simulator UI: open_sim({}) 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' }) 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`, @@ -306,7 +306,7 @@ iOS 17.0: - iPhone 15 (test-uuid-456) Next Steps: -1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' }) +1. Boot a simulator: boot_sim({ simulatorId: 'UUID_FROM_ABOVE' }) 2. Open the simulator UI: open_sim({}) 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' }) 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`, diff --git a/src/mcp/tools/simulator/__tests__/open_sim.test.ts b/src/mcp/tools/simulator/__tests__/open_sim.test.ts index fc82b1de..5ec09a16 100644 --- a/src/mcp/tools/simulator/__tests__/open_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/open_sim.test.ts @@ -62,15 +62,15 @@ describe('open_sim tool', () => { { type: 'text', text: `Next Steps: -1. Boot a simulator if needed: boot_sim({ simulatorUuid: 'UUID_FROM_LIST_SIMULATORS' }) +1. Boot a simulator if needed: boot_sim({ simulatorId: 'UUID_FROM_LIST_SIMULATORS' }) 2. Launch your app and interact with it 3. Log capture options: - Option 1: Capture structured logs only (app continues running): - start_sim_log_cap({ simulatorUuid: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' }) + start_sim_log_cap({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' }) - Option 2: Capture both console and structured logs (app will restart): - start_sim_log_cap({ simulatorUuid: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID', captureConsole: true }) + start_sim_log_cap({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID', captureConsole: true }) - Option 3: Launch app with logs in one step: - launch_app_logs_sim({ simulatorUuid: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' })`, + launch_app_logs_sim({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' })`, }, ], }); diff --git a/src/mcp/tools/simulator/__tests__/screenshot.test.ts b/src/mcp/tools/simulator/__tests__/screenshot.test.ts index 244c445f..3251b059 100644 --- a/src/mcp/tools/simulator/__tests__/screenshot.test.ts +++ b/src/mcp/tools/simulator/__tests__/screenshot.test.ts @@ -37,13 +37,13 @@ describe('screenshot plugin', () => { expect( schema.safeParse({ - simulatorUuid: '550e8400-e29b-41d4-a716-446655440000', + simulatorId: '550e8400-e29b-41d4-a716-446655440000', }).success, ).toBe(true); expect( schema.safeParse({ - simulatorUuid: 123, + simulatorId: 123, }).success, ).toBe(false); @@ -81,7 +81,7 @@ describe('screenshot plugin', () => { await screenshotLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', }, capturingExecutor, mockFileSystemExecutor, @@ -148,7 +148,7 @@ describe('screenshot plugin', () => { await screenshotLogic( { - simulatorUuid: 'another-uuid', + simulatorId: 'another-uuid', }, capturingExecutor, mockFileSystemExecutor, @@ -206,7 +206,7 @@ describe('screenshot plugin', () => { await screenshotLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', }, capturingExecutor, mockFileSystemExecutor, @@ -261,7 +261,7 @@ describe('screenshot plugin', () => { const result = await screenshotLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', }, mockExecutor, mockFileSystemExecutor, @@ -281,7 +281,7 @@ describe('screenshot plugin', () => { }); }); - it('should handle missing simulatorUuid via handler', async () => { + it('should handle missing simulatorId via handler', async () => { // Test Zod validation by calling the handler with invalid params const result = await screenshotPlugin.handler({}); @@ -289,7 +289,7 @@ describe('screenshot plugin', () => { content: [ { type: 'text', - text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Required', + text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorId: Required', }, ], isError: true, @@ -314,7 +314,7 @@ describe('screenshot plugin', () => { const result = await screenshotLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', }, mockExecutor, createMockFileSystemExecutor(), @@ -357,7 +357,7 @@ describe('screenshot plugin', () => { const result = await screenshotLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', }, mockExecutor, mockFileSystemExecutor, @@ -405,7 +405,7 @@ describe('screenshot plugin', () => { await screenshotLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', }, capturingExecutor, mockFileSystemExecutor, @@ -458,7 +458,7 @@ describe('screenshot plugin', () => { const result = await screenshotLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', }, mockExecutor, createMockFileSystemExecutor(), @@ -491,7 +491,7 @@ describe('screenshot plugin', () => { const result = await screenshotLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', }, mockExecutor, createMockFileSystemExecutor(), @@ -524,7 +524,7 @@ describe('screenshot plugin', () => { const result = await screenshotLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', }, mockExecutor, createMockFileSystemExecutor(), @@ -567,7 +567,7 @@ describe('screenshot plugin', () => { const result = await screenshotLogic( { - simulatorUuid: 'test-uuid', + simulatorId: 'test-uuid', }, mockExecutor, mockFileSystemExecutor, diff --git a/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts b/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts index 25ad8ae3..1c0c666c 100644 --- a/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts @@ -22,6 +22,16 @@ describe('stop_app_sim tool', () => { expect(schema.safeParse({}).success).toBe(false); expect(schema.safeParse({ bundleId: 42 }).success).toBe(false); expect(Object.keys(plugin.schema)).toEqual(['bundleId']); + + const withSessionDefaults = schema.safeParse({ + simulatorId: 'SIM-UUID', + simulatorName: 'iPhone 16', + bundleId: 'com.example.app', + }); + expect(withSessionDefaults.success).toBe(true); + const parsed = withSessionDefaults.data as Record; + expect(parsed.simulatorId).toBeUndefined(); + expect(parsed.simulatorName).toBeUndefined(); }); }); @@ -128,26 +138,25 @@ describe('stop_app_sim tool', () => { }); }); - it('should handle simulator lookup failure', async () => { - const listExecutor = createMockExecutor({ - success: true, - output: JSON.stringify({ devices: {} }), - error: '', - }); - + it('should surface error when simulator name is missing', async () => { const result = await stop_app_simLogic( { - simulatorName: 'Unknown Simulator', + simulatorName: 'Missing Simulator', bundleId: 'com.example.App', }, - listExecutor, + async () => ({ + success: true, + output: JSON.stringify({ devices: {} }), + error: '', + process: {} as any, + }), ); expect(result).toEqual({ content: [ { type: 'text', - text: 'Simulator named "Unknown Simulator" not found. Use list_sims to see available simulators.', + text: 'Simulator named "Missing Simulator" not found. Use list_sims to see available simulators.', }, ], isError: true, diff --git a/src/mcp/tools/simulator/boot_sim.ts b/src/mcp/tools/simulator/boot_sim.ts index 350ecaa3..e8acd9c2 100644 --- a/src/mcp/tools/simulator/boot_sim.ts +++ b/src/mcp/tools/simulator/boot_sim.ts @@ -6,15 +6,16 @@ import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; const bootSimSchemaObject = z.object({ - simulatorId: z.string().describe('UUID of the simulator to boot'), + simulatorId: z.string().describe('UUID of the simulator to use (obtained from list_sims)'), }); -// Use z.infer for type safety type BootSimParams = z.infer; -const publicSchemaObject = bootSimSchemaObject.omit({ - simulatorId: true, -} as const); +const publicSchemaObject = bootSimSchemaObject + .omit({ + simulatorId: true, + } as const) + .strict(); export async function boot_simLogic( params: BootSimParams, diff --git a/src/mcp/tools/simulator/build_run_sim.ts b/src/mcp/tools/simulator/build_run_sim.ts index 81642d25..c1e876e5 100644 --- a/src/mcp/tools/simulator/build_run_sim.ts +++ b/src/mcp/tools/simulator/build_run_sim.ts @@ -237,7 +237,7 @@ export async function build_run_simLogic( // --- Find/Boot Simulator Step --- // Use our helper to determine the simulator UUID const uuidResult = await determineSimulatorUuid( - { simulatorUuid: params.simulatorId, simulatorName: params.simulatorName }, + { simulatorId: params.simulatorId, simulatorName: params.simulatorName }, executor, ); @@ -249,9 +249,9 @@ export async function build_run_simLogic( log('warning', uuidResult.warning); } - const simulatorUuid = uuidResult.uuid; + const simulatorId = uuidResult.uuid; - if (!simulatorUuid) { + if (!simulatorId) { return createTextResponse( 'Build succeeded, but no simulator specified and failed to find a suitable one.', true, @@ -260,7 +260,7 @@ export async function build_run_simLogic( // Check simulator state and boot if needed try { - log('info', `Checking simulator state for UUID: ${simulatorUuid}`); + log('info', `Checking simulator state for UUID: ${simulatorId}`); const simulatorListResult = await executor( ['xcrun', 'simctl', 'list', 'devices', 'available', '--json'], 'List Simulators', @@ -288,7 +288,7 @@ export async function build_run_simLogic( typeof device.udid === 'string' && typeof device.name === 'string' && typeof device.state === 'string' && - device.udid === simulatorUuid + device.udid === simulatorId ) { targetSimulator = { udid: device.udid, @@ -304,7 +304,7 @@ export async function build_run_simLogic( if (!targetSimulator) { return createTextResponse( - `Build succeeded, but could not find simulator with UUID: ${simulatorUuid}`, + `Build succeeded, but could not find simulator with UUID: ${simulatorId}`, true, ); } @@ -313,14 +313,14 @@ export async function build_run_simLogic( if (targetSimulator.state !== 'Booted') { log('info', `Booting simulator ${targetSimulator.name}...`); const bootResult = await executor( - ['xcrun', 'simctl', 'boot', simulatorUuid], + ['xcrun', 'simctl', 'boot', simulatorId], 'Boot Simulator', ); if (!bootResult.success) { throw new Error(bootResult.error ?? 'Failed to boot simulator'); } } else { - log('info', `Simulator ${simulatorUuid} is already booted`); + log('info', `Simulator ${simulatorId} is already booted`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -346,9 +346,9 @@ export async function build_run_simLogic( // --- Install App Step --- try { - log('info', `Installing app at path: ${appBundlePath} to simulator: ${simulatorUuid}`); + log('info', `Installing app at path: ${appBundlePath} to simulator: ${simulatorId}`); const installResult = await executor( - ['xcrun', 'simctl', 'install', simulatorUuid, appBundlePath], + ['xcrun', 'simctl', 'install', simulatorId, appBundlePath], 'Install App', ); if (!installResult.success) { @@ -435,9 +435,9 @@ export async function build_run_simLogic( // --- Launch App Step --- try { - log('info', `Launching app with bundle ID: ${bundleId} on simulator: ${simulatorUuid}`); + log('info', `Launching app with bundle ID: ${bundleId} on simulator: ${simulatorId}`); const launchResult = await executor( - ['xcrun', 'simctl', 'launch', simulatorUuid, bundleId], + ['xcrun', 'simctl', 'launch', simulatorId, bundleId], 'Launch App', ); if (!launchResult.success) { @@ -472,11 +472,11 @@ If you don't see the simulator window, it may be hidden behind other windows. Th Next Steps: - Option 1: Capture structured logs only (app continues running): - start_simulator_log_capture({ simulatorUuid: '${simulatorUuid}', bundleId: '${bundleId}' }) + start_simulator_log_capture({ simulatorId: '${simulatorId}', bundleId: '${bundleId}' }) - Option 2: Capture both console and structured logs (app will restart): - start_simulator_log_capture({ simulatorUuid: '${simulatorUuid}', bundleId: '${bundleId}', captureConsole: true }) + start_simulator_log_capture({ simulatorId: '${simulatorId}', bundleId: '${bundleId}', captureConsole: true }) - Option 3: Launch app with logs in one step (for a fresh start): - launch_app_with_logs_in_simulator({ simulatorUuid: '${simulatorUuid}', bundleId: '${bundleId}' }) + launch_app_with_logs_in_simulator({ simulatorId: '${simulatorId}', bundleId: '${bundleId}' }) When done with any option, use: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`, }, diff --git a/src/mcp/tools/simulator/get_sim_app_path.ts b/src/mcp/tools/simulator/get_sim_app_path.ts index 5c3b78d1..2f7b591e 100644 --- a/src/mcp/tools/simulator/get_sim_app_path.ts +++ b/src/mcp/tools/simulator/get_sim_app_path.ts @@ -247,9 +247,9 @@ export async function get_sim_app_pathLogic( } else if (isSimulatorPlatform) { nextStepsText = `Next Steps: 1. Get bundle ID: get_app_bundle_id({ appPath: "${appPath}" }) -2. Boot simulator: boot_sim({ simulatorUuid: "SIMULATOR_UUID" }) -3. Install app: install_app_sim({ simulatorUuid: "SIMULATOR_UUID", appPath: "${appPath}" }) -4. Launch app: launch_app_sim({ simulatorUuid: "SIMULATOR_UUID", bundleId: "BUNDLE_ID" })`; +2. Boot simulator: boot_sim({ simulatorId: "SIMULATOR_UUID" }) +3. Install app: install_app_sim({ simulatorId: "SIMULATOR_UUID", appPath: "${appPath}" }) +4. Launch app: launch_app_sim({ simulatorId: "SIMULATOR_UUID", bundleId: "BUNDLE_ID" })`; } else if ( [ XcodePlatform.iOS, diff --git a/src/mcp/tools/simulator/install_app_sim.ts b/src/mcp/tools/simulator/install_app_sim.ts index 2b537683..bcc03fee 100644 --- a/src/mcp/tools/simulator/install_app_sim.ts +++ b/src/mcp/tools/simulator/install_app_sim.ts @@ -7,15 +7,19 @@ import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; const installAppSimSchemaObject = z.object({ - simulatorId: z.string().describe('UUID of the simulator to target'), - appPath: z.string().describe('Path to the .app bundle to install'), + simulatorId: z.string().describe('UUID of the simulator to use (obtained from list_sims)'), + appPath: z + .string() + .describe('Path to the .app bundle to install (full path to the .app directory)'), }); type InstallAppSimParams = z.infer; -const publicSchemaObject = installAppSimSchemaObject.omit({ - simulatorId: true, -} as const); +const publicSchemaObject = installAppSimSchemaObject + .omit({ + simulatorId: true, + } as const) + .strict(); export async function install_app_simLogic( params: InstallAppSimParams, diff --git a/src/mcp/tools/simulator/launch_app_logs_sim.ts b/src/mcp/tools/simulator/launch_app_logs_sim.ts index 7532612c..a3be0641 100644 --- a/src/mcp/tools/simulator/launch_app_logs_sim.ts +++ b/src/mcp/tools/simulator/launch_app_logs_sim.ts @@ -17,7 +17,7 @@ export type LogCaptureFunction = ( ) => Promise<{ sessionId: string; logFilePath: string; processes: unknown[]; error?: string }>; const launchAppLogsSimSchemaObject = z.object({ - simulatorId: z.string().describe('UUID of the simulator to target'), + simulatorId: z.string().describe('UUID of the simulator to use (obtained from list_sims)'), bundleId: z .string() .describe("Bundle identifier of the app to launch (e.g., 'com.example.MyApp')"), @@ -26,9 +26,11 @@ const launchAppLogsSimSchemaObject = z.object({ type LaunchAppLogsSimParams = z.infer; -const publicSchemaObject = launchAppLogsSimSchemaObject.omit({ - simulatorId: true, -} as const); +const publicSchemaObject = launchAppLogsSimSchemaObject + .omit({ + simulatorId: true, + } as const) + .strict(); export async function launch_app_logs_simLogic( params: LaunchAppLogsSimParams, diff --git a/src/mcp/tools/simulator/launch_app_sim.ts b/src/mcp/tools/simulator/launch_app_sim.ts index 1de4a70c..d9cd767a 100644 --- a/src/mcp/tools/simulator/launch_app_sim.ts +++ b/src/mcp/tools/simulator/launch_app_sim.ts @@ -165,20 +165,14 @@ export async function launch_app_simLogic( }; } - const userParamName = params.simulatorName ? 'simulatorName' : 'simulatorUuid'; - const userParamValue = params.simulatorName ?? simulatorId; + const userParamName = params.simulatorId ? 'simulatorId' : 'simulatorName'; + const userParamValue = params.simulatorId ?? params.simulatorName ?? simulatorId; return { content: [ { type: 'text', - text: `✅ App launched successfully in simulator ${simulatorDisplayName || simulatorId}. - -Next Steps: -1. To see simulator: open_sim() -2. Log capture: start_sim_log_cap({ ${userParamName}: "${userParamValue}", bundleId: "${params.bundleId}" }) - With console: start_sim_log_cap({ ${userParamName}: "${userParamValue}", bundleId: "${params.bundleId}", captureConsole: true }) -3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`, + text: `✅ App launched successfully in simulator ${simulatorDisplayName || simulatorId}.\n\nNext Steps:\n1. To see simulator: open_sim()\n2. Log capture: start_sim_log_cap({ ${userParamName}: "${userParamValue}", bundleId: "${params.bundleId}" })\n With console: start_sim_log_cap({ ${userParamName}: "${userParamValue}", bundleId: "${params.bundleId}", captureConsole: true })\n3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`, }, ], }; @@ -196,10 +190,12 @@ Next Steps: } } -const publicSchemaObject = baseSchemaObject.omit({ - simulatorId: true, - simulatorName: true, -} as const); +const publicSchemaObject = baseSchemaObject + .omit({ + simulatorId: true, + simulatorName: true, + } as const) + .strict(); export default { name: 'launch_app_sim', @@ -209,6 +205,7 @@ export default { internalSchema: launchAppSimSchema as unknown as z.ZodType, logicFunction: launch_app_simLogic, getExecutor: getDefaultCommandExecutor, + sessionKeys: ['simulatorId', 'simulatorName'], requirements: [ { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' }, ], diff --git a/src/mcp/tools/simulator/list_sims.ts b/src/mcp/tools/simulator/list_sims.ts index da74aea1..12d42118 100644 --- a/src/mcp/tools/simulator/list_sims.ts +++ b/src/mcp/tools/simulator/list_sims.ts @@ -184,7 +184,7 @@ export async function list_simsLogic( } responseText += 'Next Steps:\n'; - responseText += "1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' })\n"; + responseText += "1. Boot a simulator: boot_sim({ simulatorId: 'UUID_FROM_ABOVE' })\n"; responseText += '2. Open the simulator UI: open_sim({})\n'; responseText += "3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' })\n"; diff --git a/src/mcp/tools/simulator/open_sim.ts b/src/mcp/tools/simulator/open_sim.ts index 1970895c..ec7aad4c 100644 --- a/src/mcp/tools/simulator/open_sim.ts +++ b/src/mcp/tools/simulator/open_sim.ts @@ -41,15 +41,15 @@ export async function open_simLogic( { type: 'text', text: `Next Steps: -1. Boot a simulator if needed: boot_sim({ simulatorUuid: 'UUID_FROM_LIST_SIMULATORS' }) +1. Boot a simulator if needed: boot_sim({ simulatorId: 'UUID_FROM_LIST_SIMULATORS' }) 2. Launch your app and interact with it 3. Log capture options: - Option 1: Capture structured logs only (app continues running): - start_sim_log_cap({ simulatorUuid: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' }) + start_sim_log_cap({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' }) - Option 2: Capture both console and structured logs (app will restart): - start_sim_log_cap({ simulatorUuid: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID', captureConsole: true }) + start_sim_log_cap({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID', captureConsole: true }) - Option 3: Launch app with logs in one step: - launch_app_logs_sim({ simulatorUuid: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' })`, + launch_app_logs_sim({ simulatorId: 'UUID', bundleId: 'YOUR_APP_BUNDLE_ID' })`, }, ], }; diff --git a/src/mcp/tools/simulator/record_sim_video.ts b/src/mcp/tools/simulator/record_sim_video.ts index 270aecc6..8632dcf1 100644 --- a/src/mcp/tools/simulator/record_sim_video.ts +++ b/src/mcp/tools/simulator/record_sim_video.ts @@ -218,9 +218,7 @@ record_sim_video({ simulatorId: "${params.simulatorId}", stop: true, outputFile: }; } -const publicSchemaObject = recordSimVideoSchemaObject.omit({ - simulatorId: true, -} as const); +const publicSchemaObject = recordSimVideoSchemaObject.omit({ simulatorId: true } as const).strict(); export default { name: 'record_sim_video', diff --git a/src/mcp/tools/simulator/stop_app_sim.ts b/src/mcp/tools/simulator/stop_app_sim.ts index b8d37a5e..c6f516b0 100644 --- a/src/mcp/tools/simulator/stop_app_sim.ts +++ b/src/mcp/tools/simulator/stop_app_sim.ts @@ -125,9 +125,7 @@ export async function stop_app_simLogic( content: [ { type: 'text', - text: `✅ App ${params.bundleId} stopped successfully in simulator ${ - simulatorDisplayName || simulatorId - }`, + text: `✅ App ${params.bundleId} stopped successfully in simulator ${simulatorDisplayName || simulatorId}`, }, ], }; @@ -146,10 +144,12 @@ export async function stop_app_simLogic( } } -const publicSchemaObject = baseSchemaObject.omit({ - simulatorId: true, - simulatorName: true, -} as const); +const publicSchemaObject = baseSchemaObject + .omit({ + simulatorId: true, + simulatorName: true, + } as const) + .strict(); export default { name: 'stop_app_sim', @@ -159,6 +159,7 @@ export default { internalSchema: stopAppSimSchema as unknown as z.ZodType, logicFunction: stop_app_simLogic, getExecutor: getDefaultCommandExecutor, + sessionKeys: ['simulatorId', 'simulatorName'], requirements: [ { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' }, ], diff --git a/src/mcp/tools/ui-testing/__tests__/button.test.ts b/src/mcp/tools/ui-testing/__tests__/button.test.ts index 4d350b9a..1b1cd88b 100644 --- a/src/mcp/tools/ui-testing/__tests__/button.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/button.test.ts @@ -2,7 +2,7 @@ * Tests for button tool plugin */ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; import buttonPlugin, { buttonLogic } from '../button.ts'; @@ -23,61 +23,22 @@ describe('Button Plugin', () => { expect(typeof buttonPlugin.handler).toBe('function'); }); - it('should validate schema fields with safeParse', () => { + it('should expose public schema without simulatorId field', () => { const schema = z.object(buttonPlugin.schema); - // Valid case - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - buttonType: 'home', - }).success, - ).toBe(true); - - // Invalid simulatorUuid - expect( - schema.safeParse({ - simulatorUuid: 'invalid-uuid', - buttonType: 'home', - }).success, - ).toBe(false); - - // Invalid buttonType - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - buttonType: 'invalid-button', - }).success, - ).toBe(false); - - // Valid with duration - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - buttonType: 'home', - duration: 2.5, - }).success, - ).toBe(true); + expect(schema.safeParse({ buttonType: 'home' }).success).toBe(true); + expect(schema.safeParse({ buttonType: 'home', duration: 2.5 }).success).toBe(true); + expect(schema.safeParse({ buttonType: 'invalid-button' }).success).toBe(false); + expect(schema.safeParse({ buttonType: 'home', duration: -1 }).success).toBe(false); - // Invalid duration (negative) - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - buttonType: 'home', - duration: -1, - }).success, - ).toBe(false); - - // Test all valid button types - const validButtons = ['apple-pay', 'home', 'lock', 'side-button', 'siri']; - validButtons.forEach((buttonType) => { - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - buttonType, - }).success, - ).toBe(true); + const withSimId = schema.safeParse({ + simulatorId: '12345678-1234-1234-1234-123456789012', + buttonType: 'home', }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as any)).toBe(false); + + expect(schema.safeParse({}).success).toBe(false); }); }); @@ -105,7 +66,7 @@ describe('Button Plugin', () => { await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, trackingExecutor, @@ -144,7 +105,7 @@ describe('Button Plugin', () => { await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'side-button', duration: 2.5, }, @@ -186,7 +147,7 @@ describe('Button Plugin', () => { await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'apple-pay', }, trackingExecutor, @@ -221,7 +182,7 @@ describe('Button Plugin', () => { await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'siri', }, trackingExecutor, @@ -239,17 +200,17 @@ describe('Button Plugin', () => { }); describe('Handler Behavior (Complete Literal Returns)', () => { - it('should return error for missing simulatorUuid', async () => { + it('should surface session default requirement when simulatorId is missing', async () => { const result = await buttonPlugin.handler({ buttonType: 'home' }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Parameter validation failed'); - expect(result.content[0].text).toContain('simulatorUuid: Required'); + expect(result.content[0].text).toContain('Missing required session defaults'); + expect(result.content[0].text).toContain('simulatorId is required'); }); it('should return error for missing buttonType', async () => { const result = await buttonPlugin.handler({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }); expect(result.isError).toBe(true); @@ -257,9 +218,9 @@ describe('Button Plugin', () => { expect(result.content[0].text).toContain('buttonType: Required'); }); - it('should return error for invalid simulatorUuid format', async () => { + it('should return error for invalid simulatorId format', async () => { const result = await buttonPlugin.handler({ - simulatorUuid: 'invalid-uuid-format', + simulatorId: 'invalid-uuid-format', buttonType: 'home', }); @@ -270,7 +231,7 @@ describe('Button Plugin', () => { it('should return error for invalid buttonType', async () => { const result = await buttonPlugin.handler({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'invalid-button', }); @@ -280,7 +241,7 @@ describe('Button Plugin', () => { it('should return error for negative duration', async () => { const result = await buttonPlugin.handler({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'home', duration: -1, }); @@ -309,7 +270,7 @@ describe('Button Plugin', () => { const result = await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, mockExecutor, @@ -341,7 +302,7 @@ describe('Button Plugin', () => { const result = await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'side-button', duration: 2.5, }, @@ -372,7 +333,7 @@ describe('Button Plugin', () => { const result = await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, createNoopExecutor(), @@ -409,7 +370,7 @@ describe('Button Plugin', () => { const result = await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, mockExecutor, @@ -443,7 +404,7 @@ describe('Button Plugin', () => { const result = await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, mockExecutor, @@ -472,7 +433,7 @@ describe('Button Plugin', () => { const result = await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, mockExecutor, @@ -501,7 +462,7 @@ describe('Button Plugin', () => { const result = await buttonLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', buttonType: 'home', }, mockExecutor, diff --git a/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts b/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts index 8004555e..dde9344b 100644 --- a/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts @@ -2,7 +2,7 @@ * Tests for describe_ui tool plugin */ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; import describeUIPlugin, { describe_uiLogic } from '../describe_ui.ts'; @@ -27,43 +27,30 @@ describe('Describe UI Plugin', () => { expect(typeof describeUIPlugin.handler).toBe('function'); }); - it('should validate schema fields with safeParse', () => { + it('should expose public schema without simulatorId field', () => { const schema = z.object(describeUIPlugin.schema); - // Valid case - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - }).success, - ).toBe(true); - - // Invalid simulatorUuid - expect( - schema.safeParse({ - simulatorUuid: 'invalid-uuid', - }).success, - ).toBe(false); - - // Missing simulatorUuid - expect(schema.safeParse({}).success).toBe(false); + expect(schema.safeParse({}).success).toBe(true); + + const withSimId = schema.safeParse({ simulatorId: '12345678-1234-1234-1234-123456789012' }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as any)).toBe(false); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { - it('should handle missing simulatorUuid via schema validation', async () => { - // Test the actual handler (not just the logic function) - // This demonstrates that Zod validation catches missing parameters + it('should surface session default requirement when simulatorId is missing', async () => { const result = await describeUIPlugin.handler({}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Parameter validation failed'); - expect(result.content[0].text).toContain('simulatorUuid: Required'); + expect(result.content[0].text).toContain('Missing required session defaults'); + expect(result.content[0].text).toContain('simulatorId is required'); }); - it('should handle invalid simulatorUuid format via schema validation', async () => { + it('should handle invalid simulatorId format via schema validation', async () => { // Test the actual handler with invalid UUID format const result = await describeUIPlugin.handler({ - simulatorUuid: 'invalid-uuid-format', + simulatorId: 'invalid-uuid-format', }); expect(result.isError).toBe(true); @@ -97,7 +84,7 @@ describe('Describe UI Plugin', () => { const result = await describe_uiLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, trackingExecutor, mockAxeHelpers, @@ -145,7 +132,7 @@ describe('Describe UI Plugin', () => { const result = await describe_uiLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, createNoopExecutor(), mockAxeHelpers, @@ -178,7 +165,7 @@ describe('Describe UI Plugin', () => { const result = await describe_uiLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockAxeHelpers, @@ -206,7 +193,7 @@ describe('Describe UI Plugin', () => { const result = await describe_uiLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockAxeHelpers, @@ -236,7 +223,7 @@ describe('Describe UI Plugin', () => { const result = await describe_uiLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockAxeHelpers, @@ -266,7 +253,7 @@ describe('Describe UI Plugin', () => { const result = await describe_uiLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockAxeHelpers, diff --git a/src/mcp/tools/ui-testing/__tests__/gesture.test.ts b/src/mcp/tools/ui-testing/__tests__/gesture.test.ts index 7a683f1a..65e93041 100644 --- a/src/mcp/tools/ui-testing/__tests__/gesture.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/gesture.test.ts @@ -2,7 +2,7 @@ * Tests for gesture tool plugin */ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, @@ -27,37 +27,12 @@ describe('Gesture Plugin', () => { expect(typeof gesturePlugin.handler).toBe('function'); }); - it('should validate schema fields with safeParse', () => { + it('should expose public schema without simulatorId field', () => { const schema = z.object(gesturePlugin.schema); - // Valid case + expect(schema.safeParse({ preset: 'scroll-up' }).success).toBe(true); expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - preset: 'scroll-up', - }).success, - ).toBe(true); - - // Invalid simulatorUuid - expect( - schema.safeParse({ - simulatorUuid: 'invalid-uuid', - preset: 'scroll-up', - }).success, - ).toBe(false); - - // Invalid preset - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - preset: 'invalid-preset', - }).success, - ).toBe(false); - - // Valid optional parameters - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', preset: 'scroll-up', screenWidth: 375, screenHeight: 667, @@ -67,23 +42,16 @@ describe('Gesture Plugin', () => { postDelay: 0.2, }).success, ).toBe(true); + expect(schema.safeParse({ preset: 'invalid-preset' }).success).toBe(false); + expect(schema.safeParse({ preset: 'scroll-up', screenWidth: 0 }).success).toBe(false); + expect(schema.safeParse({ preset: 'scroll-up', duration: -1 }).success).toBe(false); - // Invalid optional parameters - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - preset: 'scroll-up', - screenWidth: 0, - }).success, - ).toBe(false); - - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - preset: 'scroll-up', - duration: -1, - }).success, - ).toBe(false); + const withSimId = schema.safeParse({ + simulatorId: '12345678-1234-1234-1234-123456789012', + preset: 'scroll-up', + }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as any)).toBe(false); }); }); @@ -107,7 +75,7 @@ describe('Gesture Plugin', () => { await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'scroll-up', }, trackingExecutor, @@ -142,7 +110,7 @@ describe('Gesture Plugin', () => { await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'swipe-from-left-edge', screenWidth: 375, screenHeight: 667, @@ -183,7 +151,7 @@ describe('Gesture Plugin', () => { await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'scroll-down', screenWidth: 414, screenHeight: 896, @@ -236,7 +204,7 @@ describe('Gesture Plugin', () => { await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'swipe-from-bottom-edge', }, trackingExecutor, @@ -273,7 +241,7 @@ describe('Gesture Plugin', () => { const result = await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'scroll-up', }, mockExecutor, @@ -301,7 +269,7 @@ describe('Gesture Plugin', () => { const result = await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'swipe-from-left-edge', screenWidth: 375, screenHeight: 667, @@ -337,7 +305,7 @@ describe('Gesture Plugin', () => { const result = await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'scroll-up', }, createNoopExecutor(), @@ -370,7 +338,7 @@ describe('Gesture Plugin', () => { const result = await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'scroll-up', }, mockExecutor, @@ -398,7 +366,7 @@ describe('Gesture Plugin', () => { const result = await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'scroll-up', }, mockExecutor, @@ -421,7 +389,7 @@ describe('Gesture Plugin', () => { const result = await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'scroll-up', }, mockExecutor, @@ -444,7 +412,7 @@ describe('Gesture Plugin', () => { const result = await gestureLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', preset: 'scroll-up', }, mockExecutor, diff --git a/src/mcp/tools/ui-testing/__tests__/key_press.test.ts b/src/mcp/tools/ui-testing/__tests__/key_press.test.ts index 26c28a21..aad4e81c 100644 --- a/src/mcp/tools/ui-testing/__tests__/key_press.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/key_press.test.ts @@ -27,66 +27,23 @@ describe('Key Press Plugin', () => { expect(typeof keyPressPlugin.handler).toBe('function'); }); - it('should validate schema fields with safeParse', () => { + it('should expose public schema without simulatorId field', () => { const schema = z.object(keyPressPlugin.schema); - // Valid case - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCode: 40, - }).success, - ).toBe(true); + expect(schema.safeParse({ keyCode: 40 }).success).toBe(true); + expect(schema.safeParse({ keyCode: 40, duration: 1.5 }).success).toBe(true); + expect(schema.safeParse({ keyCode: 'invalid' }).success).toBe(false); + expect(schema.safeParse({ keyCode: -1 }).success).toBe(false); + expect(schema.safeParse({ keyCode: 256 }).success).toBe(false); - // Invalid simulatorUuid - expect( - schema.safeParse({ - simulatorUuid: 'invalid-uuid', - keyCode: 40, - }).success, - ).toBe(false); - - // Invalid keyCode (string) - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCode: 'invalid', - }).success, - ).toBe(false); - - // Invalid keyCode (below range) - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCode: -1, - }).success, - ).toBe(false); - - // Invalid keyCode (above range) - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCode: 256, - }).success, - ).toBe(false); - - // Valid with duration - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCode: 40, - duration: 1.5, - }).success, - ).toBe(true); + const withSimId = schema.safeParse({ + simulatorId: '12345678-1234-1234-1234-123456789012', + keyCode: 40, + }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as any)).toBe(false); - // Invalid duration (negative) - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCode: 40, - duration: -1, - }).success, - ).toBe(false); + expect(schema.safeParse({}).success).toBe(false); }); }); @@ -119,7 +76,7 @@ describe('Key Press Plugin', () => { await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, trackingExecutor, @@ -163,7 +120,7 @@ describe('Key Press Plugin', () => { await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 42, duration: 1.5, }, @@ -210,7 +167,7 @@ describe('Key Press Plugin', () => { await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 255, }, trackingExecutor, @@ -254,7 +211,7 @@ describe('Key Press Plugin', () => { await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 44, }, trackingExecutor, @@ -298,7 +255,7 @@ describe('Key Press Plugin', () => { const result = await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, mockExecutor, @@ -334,7 +291,7 @@ describe('Key Press Plugin', () => { const result = await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 42, duration: 1.5, }, @@ -365,7 +322,7 @@ describe('Key Press Plugin', () => { const result = await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, createNoopExecutor(), @@ -409,7 +366,7 @@ describe('Key Press Plugin', () => { const result = await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, mockExecutor, @@ -448,7 +405,7 @@ describe('Key Press Plugin', () => { const result = await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, mockExecutor, @@ -482,7 +439,7 @@ describe('Key Press Plugin', () => { const result = await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, mockExecutor, @@ -516,7 +473,7 @@ describe('Key Press Plugin', () => { const result = await key_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCode: 40, }, mockExecutor, diff --git a/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts b/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts index 1921c0e4..f8d6812c 100644 --- a/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts @@ -23,67 +23,23 @@ describe('Key Sequence Plugin', () => { expect(typeof keySequencePlugin.handler).toBe('function'); }); - it('should validate schema fields with safeParse', () => { + it('should expose public schema without simulatorId field', () => { const schema = z.object(keySequencePlugin.schema); - // Valid case - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCodes: [40, 42, 44], - }).success, - ).toBe(true); + expect(schema.safeParse({ keyCodes: [40, 42, 44] }).success).toBe(true); + expect(schema.safeParse({ keyCodes: [40], delay: 0.1 }).success).toBe(true); + expect(schema.safeParse({ keyCodes: [] }).success).toBe(false); + expect(schema.safeParse({ keyCodes: [-1] }).success).toBe(false); + expect(schema.safeParse({ keyCodes: [256] }).success).toBe(false); + expect(schema.safeParse({ keyCodes: [40], delay: -0.1 }).success).toBe(false); - // Invalid simulatorUuid - expect( - schema.safeParse({ - simulatorUuid: 'invalid-uuid', - keyCodes: [40], - }).success, - ).toBe(false); - - // Invalid keyCodes - empty array - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCodes: [], - }).success, - ).toBe(false); - - // Invalid keyCodes - out of range - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCodes: [-1], - }).success, - ).toBe(false); - - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCodes: [256], - }).success, - ).toBe(false); - - // Invalid delay - negative - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCodes: [40], - delay: -0.1, - }).success, - ).toBe(false); - - // Valid with optional delay - expect( - schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', - keyCodes: [40], - delay: 0.1, - }).success, - ).toBe(true); + const withSimId = schema.safeParse({ + simulatorId: '12345678-1234-1234-1234-123456789012', + keyCodes: [40], + }); + expect(withSimId.success).toBe(true); + expect('simulatorId' in (withSimId.data as any)).toBe(false); - // Missing required fields expect(schema.safeParse({}).success).toBe(false); }); }); @@ -117,7 +73,7 @@ describe('Key Sequence Plugin', () => { await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [40, 42, 44], }, trackingExecutor, @@ -162,7 +118,7 @@ describe('Key Sequence Plugin', () => { await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [58, 59, 60], delay: 0.5, }, @@ -210,7 +166,7 @@ describe('Key Sequence Plugin', () => { await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [255], }, trackingExecutor, @@ -255,7 +211,7 @@ describe('Key Sequence Plugin', () => { await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [0, 1, 2, 3, 4], delay: 1.0, }, @@ -277,6 +233,14 @@ describe('Key Sequence Plugin', () => { }); describe('Handler Behavior (Complete Literal Returns)', () => { + it('should surface session default requirement when simulatorId is missing', async () => { + const result = await keySequencePlugin.handler({ keyCodes: [40] }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('Missing required session defaults'); + expect(result.content[0].text).toContain('simulatorId is required'); + }); + it('should return success for valid key sequence execution', async () => { const mockExecutor = createMockExecutor({ success: true, @@ -300,7 +264,7 @@ describe('Key Sequence Plugin', () => { const result = await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [40, 42, 44], delay: 0.1, }, @@ -337,7 +301,7 @@ describe('Key Sequence Plugin', () => { const result = await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, mockExecutor, @@ -367,7 +331,7 @@ describe('Key Sequence Plugin', () => { const result = await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, createNoopExecutor(), @@ -408,7 +372,7 @@ describe('Key Sequence Plugin', () => { const result = await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, mockExecutor, @@ -447,7 +411,7 @@ describe('Key Sequence Plugin', () => { const result = await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, mockExecutor, @@ -481,7 +445,7 @@ describe('Key Sequence Plugin', () => { const result = await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, mockExecutor, @@ -515,7 +479,7 @@ describe('Key Sequence Plugin', () => { const result = await key_sequenceLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', keyCodes: [40], }, mockExecutor, diff --git a/src/mcp/tools/ui-testing/__tests__/long_press.test.ts b/src/mcp/tools/ui-testing/__tests__/long_press.test.ts index d14e730e..eaea3a59 100644 --- a/src/mcp/tools/ui-testing/__tests__/long_press.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/long_press.test.ts @@ -31,17 +31,17 @@ describe('Long Press Plugin', () => { // Valid case expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, duration: 1500, }).success, ).toBe(true); - // Invalid simulatorUuid + // Invalid simulatorId expect( schema.safeParse({ - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', x: 100, y: 200, duration: 1500, @@ -51,7 +51,7 @@ describe('Long Press Plugin', () => { // Invalid x (not integer) expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100.5, y: 200, duration: 1500, @@ -61,7 +61,7 @@ describe('Long Press Plugin', () => { // Invalid y (not integer) expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200.5, duration: 1500, @@ -71,7 +71,7 @@ describe('Long Press Plugin', () => { // Invalid duration (not positive) expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, duration: 0, @@ -81,7 +81,7 @@ describe('Long Press Plugin', () => { // Invalid duration (negative) expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, duration: -100, @@ -114,7 +114,7 @@ describe('Long Press Plugin', () => { await long_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, duration: 1500, @@ -162,7 +162,7 @@ describe('Long Press Plugin', () => { await long_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 50, y: 75, duration: 2000, @@ -210,7 +210,7 @@ describe('Long Press Plugin', () => { await long_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 300, y: 400, duration: 500, @@ -258,7 +258,7 @@ describe('Long Press Plugin', () => { await long_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 150, y: 250, duration: 3000, @@ -303,7 +303,7 @@ describe('Long Press Plugin', () => { const result = await long_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, duration: 1500, @@ -347,7 +347,7 @@ describe('Long Press Plugin', () => { const result = await long_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, duration: 1500, @@ -386,7 +386,7 @@ describe('Long Press Plugin', () => { const result = await long_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, duration: 1500, @@ -422,7 +422,7 @@ describe('Long Press Plugin', () => { const result = await long_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, duration: 1500, @@ -460,7 +460,7 @@ describe('Long Press Plugin', () => { const result = await long_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, duration: 1500, @@ -498,7 +498,7 @@ describe('Long Press Plugin', () => { const result = await long_pressLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, duration: 1500, diff --git a/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts b/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts index 78a10e21..bf0ecab0 100644 --- a/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts @@ -34,31 +34,31 @@ describe('Screenshot Plugin', () => { // Valid case expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }).success, ).toBe(true); - // Invalid simulatorUuid + // Invalid simulatorId expect( schema.safeParse({ - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', }).success, ).toBe(false); - // Missing simulatorUuid + // Missing simulatorId expect(schema.safeParse({}).success).toBe(false); }); }); describe('Plugin Handler Validation', () => { - it('should return Zod validation error for missing simulatorUuid', async () => { + it('should return Zod validation error for missing simulatorId', async () => { const result = await screenshotPlugin.handler({}); expect(result).toEqual({ content: [ { type: 'text', - text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Required', + text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorId: Required', }, ], isError: true, @@ -67,14 +67,14 @@ describe('Screenshot Plugin', () => { it('should return Zod validation error for invalid UUID format', async () => { const result = await screenshotPlugin.handler({ - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', }); expect(result).toEqual({ content: [ { type: 'text', - text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Invalid Simulator UUID format', + text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorId: Invalid Simulator UUID format', }, ], isError: true, @@ -102,7 +102,7 @@ describe('Screenshot Plugin', () => { await screenshotLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, trackingExecutor, mockFileSystemExecutor, @@ -140,7 +140,7 @@ describe('Screenshot Plugin', () => { await screenshotLogic( { - simulatorUuid: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF', + simulatorId: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF', }, trackingExecutor, mockFileSystemExecutor, @@ -177,7 +177,7 @@ describe('Screenshot Plugin', () => { await screenshotLogic( { - simulatorUuid: '98765432-1098-7654-3210-987654321098', + simulatorId: '98765432-1098-7654-3210-987654321098', }, trackingExecutor, mockFileSystemExecutor, @@ -217,7 +217,7 @@ describe('Screenshot Plugin', () => { await screenshotLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, trackingExecutor, mockFileSystemExecutor, @@ -244,7 +244,7 @@ describe('Screenshot Plugin', () => { // This test documents that screenshotLogic assumes valid parameters. const result = await screenshotLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, createMockExecutor({ success: true, @@ -275,7 +275,7 @@ describe('Screenshot Plugin', () => { const result = await screenshotLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockFileSystemExecutor, @@ -302,7 +302,7 @@ describe('Screenshot Plugin', () => { const result = await screenshotLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, createMockFileSystemExecutor(), @@ -334,7 +334,7 @@ describe('Screenshot Plugin', () => { const result = await screenshotLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockFileSystemExecutor, @@ -368,7 +368,7 @@ describe('Screenshot Plugin', () => { const result = await screenshotLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockFileSystemExecutor, @@ -394,7 +394,7 @@ describe('Screenshot Plugin', () => { const result = await screenshotLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, createMockFileSystemExecutor(), @@ -415,7 +415,7 @@ describe('Screenshot Plugin', () => { const result = await screenshotLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, createMockFileSystemExecutor(), @@ -434,7 +434,7 @@ describe('Screenshot Plugin', () => { const result = await screenshotLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', }, mockExecutor, createMockFileSystemExecutor(), diff --git a/src/mcp/tools/ui-testing/__tests__/swipe.test.ts b/src/mcp/tools/ui-testing/__tests__/swipe.test.ts index e4fbf3f6..a2f74c28 100644 --- a/src/mcp/tools/ui-testing/__tests__/swipe.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/swipe.test.ts @@ -66,7 +66,7 @@ describe('Swipe Plugin', () => { // Valid case expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, @@ -74,10 +74,10 @@ describe('Swipe Plugin', () => { }).success, ).toBe(true); - // Invalid simulatorUuid + // Invalid simulatorId expect( schema.safeParse({ - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', x1: 100, y1: 200, x2: 300, @@ -88,7 +88,7 @@ describe('Swipe Plugin', () => { // Invalid x1 (not integer) expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100.5, y1: 200, x2: 300, @@ -99,7 +99,7 @@ describe('Swipe Plugin', () => { // Valid with optional parameters expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, @@ -114,7 +114,7 @@ describe('Swipe Plugin', () => { // Invalid duration (negative) expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, @@ -142,7 +142,7 @@ describe('Swipe Plugin', () => { await swipeLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, @@ -184,7 +184,7 @@ describe('Swipe Plugin', () => { await swipeLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 50, y1: 75, x2: 250, @@ -229,7 +229,7 @@ describe('Swipe Plugin', () => { await swipeLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 0, y1: 0, x2: 500, @@ -290,7 +290,7 @@ describe('Swipe Plugin', () => { await swipeLogic( { - simulatorUuid: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF', + simulatorId: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF', x1: 150, y1: 250, x2: 400, @@ -321,18 +321,18 @@ describe('Swipe Plugin', () => { }); describe('Handler Behavior (Complete Literal Returns)', () => { - it('should return error for missing simulatorUuid via handler', async () => { + it('should return error for missing simulatorId via handler', async () => { const result = await swipePlugin.handler({ x1: 100, y1: 200, x2: 300, y2: 400 }); expect(result.isError).toBe(true); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Parameter validation failed'); - expect(result.content[0].text).toContain('simulatorUuid'); + expect(result.content[0].text).toContain('simulatorId'); }); it('should return error for missing x1 via handler', async () => { const result = await swipePlugin.handler({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', y1: 200, x2: 300, y2: 400, @@ -355,7 +355,7 @@ describe('Swipe Plugin', () => { const result = await swipeLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, @@ -387,7 +387,7 @@ describe('Swipe Plugin', () => { const result = await swipeLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, @@ -420,7 +420,7 @@ describe('Swipe Plugin', () => { const result = await swipeLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, @@ -452,7 +452,7 @@ describe('Swipe Plugin', () => { const result = await swipeLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, @@ -483,7 +483,7 @@ describe('Swipe Plugin', () => { const result = await swipeLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, @@ -511,7 +511,7 @@ describe('Swipe Plugin', () => { const result = await swipeLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, @@ -539,7 +539,7 @@ describe('Swipe Plugin', () => { const result = await swipeLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x1: 100, y1: 200, x2: 300, diff --git a/src/mcp/tools/ui-testing/__tests__/tap.test.ts b/src/mcp/tools/ui-testing/__tests__/tap.test.ts index e52fe6f7..85ee7c13 100644 --- a/src/mcp/tools/ui-testing/__tests__/tap.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/tap.test.ts @@ -64,16 +64,16 @@ describe('Tap Plugin', () => { // Valid case expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, }).success, ).toBe(true); - // Invalid simulatorUuid + // Invalid simulatorId expect( schema.safeParse({ - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', x: 100, y: 200, }).success, @@ -82,7 +82,7 @@ describe('Tap Plugin', () => { // Invalid x coordinate - non-integer expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 3.14, y: 200, }).success, @@ -91,7 +91,7 @@ describe('Tap Plugin', () => { // Invalid y coordinate - non-integer expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 3.14, }).success, @@ -100,7 +100,7 @@ describe('Tap Plugin', () => { // Invalid preDelay - negative expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, preDelay: -1, @@ -110,7 +110,7 @@ describe('Tap Plugin', () => { // Invalid postDelay - negative expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, postDelay: -1, @@ -120,7 +120,7 @@ describe('Tap Plugin', () => { // Valid with optional delays expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, preDelay: 0.5, @@ -165,7 +165,7 @@ describe('Tap Plugin', () => { await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, }, @@ -211,7 +211,7 @@ describe('Tap Plugin', () => { await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 150, y: 300, preDelay: 0.5, @@ -260,7 +260,7 @@ describe('Tap Plugin', () => { await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 250, y: 400, postDelay: 1.0, @@ -309,7 +309,7 @@ describe('Tap Plugin', () => { await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 350, y: 500, preDelay: 0.3, @@ -353,7 +353,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, }, @@ -382,7 +382,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '87654321-4321-4321-4321-210987654321', + simulatorId: '87654321-4321-4321-4321-210987654321', x: 150, y: 300, }, @@ -411,7 +411,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 250, y: 400, preDelay: 0.5, @@ -442,7 +442,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 0, y: 0, }, @@ -471,7 +471,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 1920, y: 1080, }, @@ -492,7 +492,7 @@ describe('Tap Plugin', () => { }); describe('Plugin Handler Validation', () => { - it('should return Zod validation error for missing simulatorUuid', async () => { + it('should return Zod validation error for missing simulatorId', async () => { const result = await tapPlugin.handler({ x: 100, y: 200, @@ -502,7 +502,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Required', + text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorId: Required', }, ], isError: true, @@ -511,7 +511,7 @@ describe('Tap Plugin', () => { it('should return Zod validation error for missing x coordinate', async () => { const result = await tapPlugin.handler({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', y: 200, }); @@ -528,7 +528,7 @@ describe('Tap Plugin', () => { it('should return Zod validation error for missing y coordinate', async () => { const result = await tapPlugin.handler({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, }); @@ -545,7 +545,7 @@ describe('Tap Plugin', () => { it('should return Zod validation error for invalid UUID format', async () => { const result = await tapPlugin.handler({ - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', x: 100, y: 200, }); @@ -554,7 +554,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Invalid Simulator UUID format', + text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorId: Invalid Simulator UUID format', }, ], isError: true, @@ -563,7 +563,7 @@ describe('Tap Plugin', () => { it('should return Zod validation error for non-integer x coordinate', async () => { const result = await tapPlugin.handler({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 3.14, y: 200, }); @@ -581,7 +581,7 @@ describe('Tap Plugin', () => { it('should return Zod validation error for non-integer y coordinate', async () => { const result = await tapPlugin.handler({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 3.14, }); @@ -599,7 +599,7 @@ describe('Tap Plugin', () => { it('should return Zod validation error for negative preDelay', async () => { const result = await tapPlugin.handler({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, preDelay: -1, @@ -618,7 +618,7 @@ describe('Tap Plugin', () => { it('should return Zod validation error for negative postDelay', async () => { const result = await tapPlugin.handler({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, postDelay: -1, @@ -648,7 +648,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, preDelay: 0.5, @@ -680,7 +680,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, }, @@ -710,7 +710,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, }, @@ -738,7 +738,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, }, @@ -766,7 +766,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, }, @@ -794,7 +794,7 @@ describe('Tap Plugin', () => { const result = await tapLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, }, diff --git a/src/mcp/tools/ui-testing/__tests__/touch.test.ts b/src/mcp/tools/ui-testing/__tests__/touch.test.ts index 8efda141..a1dbef00 100644 --- a/src/mcp/tools/ui-testing/__tests__/touch.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/touch.test.ts @@ -30,7 +30,7 @@ describe('Touch Plugin', () => { // Valid case with down expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -40,17 +40,17 @@ describe('Touch Plugin', () => { // Valid case with up expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, up: true, }).success, ).toBe(true); - // Invalid simulatorUuid + // Invalid simulatorId expect( schema.safeParse({ - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', x: 100, y: 200, down: true, @@ -60,7 +60,7 @@ describe('Touch Plugin', () => { // Invalid x (not integer) expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100.5, y: 200, down: true, @@ -70,7 +70,7 @@ describe('Touch Plugin', () => { // Invalid y (not integer) expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200.5, down: true, @@ -80,7 +80,7 @@ describe('Touch Plugin', () => { // Valid with delay expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -91,7 +91,7 @@ describe('Touch Plugin', () => { // Invalid delay (negative) expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -130,7 +130,7 @@ describe('Touch Plugin', () => { await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -180,7 +180,7 @@ describe('Touch Plugin', () => { await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 150, y: 250, up: true, @@ -230,7 +230,7 @@ describe('Touch Plugin', () => { await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 300, y: 400, down: true, @@ -282,7 +282,7 @@ describe('Touch Plugin', () => { await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 50, y: 75, down: true, @@ -328,7 +328,7 @@ describe('Touch Plugin', () => { await touchLogic( { - simulatorUuid: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF', + simulatorId: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF', x: 0, y: 0, up: true, @@ -373,7 +373,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -411,7 +411,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -449,7 +449,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, up: true, @@ -474,7 +474,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, }, @@ -510,7 +510,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -553,7 +553,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, up: true, @@ -596,7 +596,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -636,7 +636,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -679,7 +679,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -720,7 +720,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -763,7 +763,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, @@ -806,7 +806,7 @@ describe('Touch Plugin', () => { const result = await touchLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', x: 100, y: 200, down: true, diff --git a/src/mcp/tools/ui-testing/__tests__/type_text.test.ts b/src/mcp/tools/ui-testing/__tests__/type_text.test.ts index 859f9466..6fcf1ecd 100644 --- a/src/mcp/tools/ui-testing/__tests__/type_text.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/type_text.test.ts @@ -57,15 +57,15 @@ describe('Type Text Plugin', () => { // Valid case expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Hello World', }).success, ).toBe(true); - // Invalid simulatorUuid + // Invalid simulatorId expect( schema.safeParse({ - simulatorUuid: 'invalid-uuid', + simulatorId: 'invalid-uuid', text: 'Hello World', }).success, ).toBe(false); @@ -73,7 +73,7 @@ describe('Type Text Plugin', () => { // Invalid text - empty string expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: '', }).success, ).toBe(false); @@ -81,7 +81,7 @@ describe('Type Text Plugin', () => { // Invalid text - non-string expect( schema.safeParse({ - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 123, }).success, ).toBe(false); @@ -111,7 +111,7 @@ describe('Type Text Plugin', () => { await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Hello World', }, trackingExecutor, @@ -146,7 +146,7 @@ describe('Type Text Plugin', () => { await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'user@example.com', }, trackingExecutor, @@ -181,7 +181,7 @@ describe('Type Text Plugin', () => { await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Password123!@#', }, trackingExecutor, @@ -219,7 +219,7 @@ describe('Type Text Plugin', () => { await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: longText, }, trackingExecutor, @@ -254,7 +254,7 @@ describe('Type Text Plugin', () => { await type_textLogic( { - simulatorUuid: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF', + simulatorId: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF', text: 'Test message', }, trackingExecutor, @@ -279,7 +279,7 @@ describe('Type Text Plugin', () => { const result = await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Hello World', }, createNoopExecutor(), @@ -310,7 +310,7 @@ describe('Type Text Plugin', () => { const result = await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Hello World', }, mockExecutor, @@ -337,7 +337,7 @@ describe('Type Text Plugin', () => { const result = await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Hello World', }, mockExecutor, @@ -357,7 +357,7 @@ describe('Type Text Plugin', () => { const result = await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Hello World', }, createNoopExecutor(), @@ -389,7 +389,7 @@ describe('Type Text Plugin', () => { const result = await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Hello World', }, mockExecutor, @@ -417,7 +417,7 @@ describe('Type Text Plugin', () => { const result = await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Hello World', }, mockExecutor, @@ -447,7 +447,7 @@ describe('Type Text Plugin', () => { const result = await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Hello World', }, mockExecutor, @@ -477,7 +477,7 @@ describe('Type Text Plugin', () => { const result = await type_textLogic( { - simulatorUuid: '12345678-1234-1234-1234-123456789012', + simulatorId: '12345678-1234-1234-1234-123456789012', text: 'Hello World', }, mockExecutor, diff --git a/src/mcp/tools/ui-testing/button.ts b/src/mcp/tools/ui-testing/button.ts index acc1ccb9..b9bdbf32 100644 --- a/src/mcp/tools/ui-testing/button.ts +++ b/src/mcp/tools/ui-testing/button.ts @@ -10,11 +10,11 @@ import { getBundledAxeEnvironment, } from '../../../utils/axe-helpers.ts'; import { DependencyError, AxeError, SystemError } from '../../../utils/errors.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const buttonSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), buttonType: z.enum(['apple-pay', 'home', 'lock', 'side-button', 'siri']), duration: z.number().min(0, 'Duration must be non-negative').optional(), }); @@ -40,17 +40,17 @@ export async function buttonLogic( }, ): Promise { const toolName = 'button'; - const { simulatorUuid, buttonType, duration } = params; + const { simulatorId, buttonType, duration } = params; const commandArgs = ['button', buttonType]; if (duration !== undefined) { commandArgs.push('--duration', String(duration)); } - log('info', `${LOG_PREFIX}/${toolName}: Starting ${buttonType} button press on ${simulatorUuid}`); + log('info', `${LOG_PREFIX}/${toolName}: Starting ${buttonType} button press on ${simulatorId}`); try { - await executeAxeCommand(commandArgs, simulatorUuid, 'button', executor, axeHelpers); - log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); + await executeAxeCommand(commandArgs, simulatorId, 'button', executor, axeHelpers); + log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); return createTextResponse(`Hardware button '${buttonType}' pressed successfully.`); } catch (error) { log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`); @@ -73,28 +73,30 @@ export async function buttonLogic( } } +const publicSchemaObject = buttonSchema.omit({ simulatorId: true } as const).strict(); + export default { name: 'button', description: 'Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri', - schema: buttonSchema.shape, // MCP SDK compatibility - handler: createTypedTool( - buttonSchema, - (params: ButtonParams, executor: CommandExecutor) => { - return buttonLogic(params, executor, { + schema: publicSchemaObject.shape, // MCP SDK compatibility + handler: createSessionAwareTool({ + internalSchema: buttonSchema as unknown as z.ZodType, + logicFunction: (params: ButtonParams, executor: CommandExecutor) => + buttonLogic(params, executor, { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse, - }); - }, - getDefaultCommandExecutor, - ), + }), + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], - simulatorUuid: string, + simulatorId: string, commandName: string, executor: CommandExecutor = getDefaultCommandExecutor(), axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse }, @@ -106,7 +108,7 @@ async function executeAxeCommand( } // Add --udid parameter to all commands - const fullArgs = [...commandArgs, '--udid', simulatorUuid]; + const fullArgs = [...commandArgs, '--udid', simulatorId]; // Construct the full command array with the axe binary as the first element const fullCommand = [axeBinary, ...fullArgs]; @@ -122,7 +124,7 @@ async function executeAxeCommand( `axe command '${commandName}' failed.`, commandName, result.error ?? result.output, - simulatorUuid, + simulatorId, ); } diff --git a/src/mcp/tools/ui-testing/describe_ui.ts b/src/mcp/tools/ui-testing/describe_ui.ts index c384e887..feb0f1f8 100644 --- a/src/mcp/tools/ui-testing/describe_ui.ts +++ b/src/mcp/tools/ui-testing/describe_ui.ts @@ -10,11 +10,11 @@ import { getAxePath, getBundledAxeEnvironment, } from '../../../utils/axe-helpers.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const describeUiSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), }); // Use z.infer for type safety @@ -31,10 +31,10 @@ const LOG_PREFIX = '[AXe]'; // Session tracking for describe_ui warnings (shared across UI tools) const describeUITimestamps = new Map(); -function recordDescribeUICall(simulatorUuid: string): void { - describeUITimestamps.set(simulatorUuid, { +function recordDescribeUICall(simulatorId: string): void { + describeUITimestamps.set(simulatorId, { timestamp: Date.now(), - simulatorUuid, + simulatorId, }); } @@ -51,24 +51,24 @@ export async function describe_uiLogic( }, ): Promise { const toolName = 'describe_ui'; - const { simulatorUuid } = params; + const { simulatorId } = params; const commandArgs = ['describe-ui']; - log('info', `${LOG_PREFIX}/${toolName}: Starting for ${simulatorUuid}`); + log('info', `${LOG_PREFIX}/${toolName}: Starting for ${simulatorId}`); try { const responseText = await executeAxeCommand( commandArgs, - simulatorUuid, + simulatorId, 'describe-ui', executor, axeHelpers, ); // Record the describe_ui call for warning system - recordDescribeUICall(simulatorUuid); + recordDescribeUICall(simulatorId); - log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); + log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); return { content: [ { @@ -106,28 +106,30 @@ export async function describe_uiLogic( } } +const publicSchemaObject = describeUiSchema.omit({ simulatorId: true } as const).strict(); + export default { name: 'describe_ui', description: 'Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation.', - schema: describeUiSchema.shape, // MCP SDK compatibility - handler: createTypedTool( - describeUiSchema, - (params: DescribeUiParams, executor: CommandExecutor) => { - return describe_uiLogic(params, executor, { + schema: publicSchemaObject.shape, // MCP SDK compatibility + handler: createSessionAwareTool({ + internalSchema: describeUiSchema as unknown as z.ZodType, + logicFunction: (params: DescribeUiParams, executor: CommandExecutor) => + describe_uiLogic(params, executor, { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse, - }); - }, - getDefaultCommandExecutor, - ), + }), + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], - simulatorUuid: string, + simulatorId: string, commandName: string, executor: CommandExecutor = getDefaultCommandExecutor(), axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse }, @@ -139,7 +141,7 @@ async function executeAxeCommand( } // Add --udid parameter to all commands - const fullArgs = [...commandArgs, '--udid', simulatorUuid]; + const fullArgs = [...commandArgs, '--udid', simulatorId]; // Construct the full command array with the axe binary as the first element const fullCommand = [axeBinary, ...fullArgs]; @@ -155,7 +157,7 @@ async function executeAxeCommand( `axe command '${commandName}' failed.`, commandName, result.error ?? result.output, - simulatorUuid, + simulatorId, ); } diff --git a/src/mcp/tools/ui-testing/gesture.ts b/src/mcp/tools/ui-testing/gesture.ts index fface159..5cb1f1d2 100644 --- a/src/mcp/tools/ui-testing/gesture.ts +++ b/src/mcp/tools/ui-testing/gesture.ts @@ -22,11 +22,11 @@ import { getAxePath, getBundledAxeEnvironment, } from '../../../utils/axe/index.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const gestureSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), preset: z .enum([ 'scroll-up', @@ -100,7 +100,7 @@ export async function gestureLogic( }, ): Promise { const toolName = 'gesture'; - const { simulatorUuid, preset, screenWidth, screenHeight, duration, delta, preDelay, postDelay } = + const { simulatorId, preset, screenWidth, screenHeight, duration, delta, preDelay, postDelay } = params; const commandArgs = ['gesture', preset]; @@ -123,11 +123,11 @@ export async function gestureLogic( commandArgs.push('--post-delay', String(postDelay)); } - log('info', `${LOG_PREFIX}/${toolName}: Starting gesture '${preset}' on ${simulatorUuid}`); + log('info', `${LOG_PREFIX}/${toolName}: Starting gesture '${preset}' on ${simulatorId}`); try { - await executeAxeCommand(commandArgs, simulatorUuid, 'gesture', executor, axeHelpers); - log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); + await executeAxeCommand(commandArgs, simulatorId, 'gesture', executor, axeHelpers); + log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); return createTextResponse(`Gesture '${preset}' executed successfully.`); } catch (error) { log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`); @@ -150,28 +150,30 @@ export async function gestureLogic( } } +const publicSchemaObject = gestureSchema.omit({ simulatorId: true } as const).strict(); + export default { name: 'gesture', description: 'Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge', - schema: gestureSchema.shape, // MCP SDK compatibility - handler: createTypedTool( - gestureSchema, - (params: GestureParams, executor: CommandExecutor) => { - return gestureLogic(params, executor, { + schema: publicSchemaObject.shape, // MCP SDK compatibility + handler: createSessionAwareTool({ + internalSchema: gestureSchema as unknown as z.ZodType, + logicFunction: (params: GestureParams, executor: CommandExecutor) => + gestureLogic(params, executor, { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse, - }); - }, - getDefaultCommandExecutor, - ), + }), + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], - simulatorUuid: string, + simulatorId: string, commandName: string, executor: CommandExecutor = getDefaultCommandExecutor(), axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse }, @@ -183,7 +185,7 @@ async function executeAxeCommand( } // Add --udid parameter to all commands - const fullArgs = [...commandArgs, '--udid', simulatorUuid]; + const fullArgs = [...commandArgs, '--udid', simulatorId]; // Construct the full command array with the axe binary as the first element const fullCommand = [axeBinary, ...fullArgs]; @@ -199,7 +201,7 @@ async function executeAxeCommand( `axe command '${commandName}' failed.`, commandName, result.error ?? result.output, - simulatorUuid, + simulatorId, ); } diff --git a/src/mcp/tools/ui-testing/key_press.ts b/src/mcp/tools/ui-testing/key_press.ts index c386283d..363dd511 100644 --- a/src/mcp/tools/ui-testing/key_press.ts +++ b/src/mcp/tools/ui-testing/key_press.ts @@ -15,11 +15,11 @@ import { getAxePath, getBundledAxeEnvironment, } from '../../../utils/axe/index.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const keyPressSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), keyCode: z.number().int('HID keycode to press (0-255)').min(0).max(255), duration: z.number().min(0, 'Duration must be non-negative').optional(), }); @@ -45,17 +45,17 @@ export async function key_pressLogic( }, ): Promise { const toolName = 'key_press'; - const { simulatorUuid, keyCode, duration } = params; + const { simulatorId, keyCode, duration } = params; const commandArgs = ['key', String(keyCode)]; if (duration !== undefined) { commandArgs.push('--duration', String(duration)); } - log('info', `${LOG_PREFIX}/${toolName}: Starting key press ${keyCode} on ${simulatorUuid}`); + log('info', `${LOG_PREFIX}/${toolName}: Starting key press ${keyCode} on ${simulatorId}`); try { - await executeAxeCommand(commandArgs, simulatorUuid, 'key', executor, axeHelpers); - log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); + await executeAxeCommand(commandArgs, simulatorId, 'key', executor, axeHelpers); + log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); return createTextResponse(`Key press (code: ${keyCode}) simulated successfully.`); } catch (error) { log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`); @@ -78,28 +78,30 @@ export async function key_pressLogic( } } +const publicSchemaObject = keyPressSchema.omit({ simulatorId: true } as const).strict(); + export default { name: 'key_press', description: 'Press a single key by keycode on the simulator. Common keycodes: 40=Return, 42=Backspace, 43=Tab, 44=Space, 58-67=F1-F10.', - schema: keyPressSchema.shape, // MCP SDK compatibility - handler: createTypedTool( - keyPressSchema, - (params: KeyPressParams, executor: CommandExecutor) => { - return key_pressLogic(params, executor, { + schema: publicSchemaObject.shape, // MCP SDK compatibility + handler: createSessionAwareTool({ + internalSchema: keyPressSchema as unknown as z.ZodType, + logicFunction: (params: KeyPressParams, executor: CommandExecutor) => + key_pressLogic(params, executor, { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse, - }); - }, - getDefaultCommandExecutor, - ), + }), + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], - simulatorUuid: string, + simulatorId: string, commandName: string, executor: CommandExecutor = getDefaultCommandExecutor(), axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse }, @@ -111,7 +113,7 @@ async function executeAxeCommand( } // Add --udid parameter to all commands - const fullArgs = [...commandArgs, '--udid', simulatorUuid]; + const fullArgs = [...commandArgs, '--udid', simulatorId]; // Construct the full command array with the axe binary as the first element const fullCommand = [axeBinary, ...fullArgs]; @@ -127,7 +129,7 @@ async function executeAxeCommand( `axe command '${commandName}' failed.`, commandName, result.error ?? result.output, - simulatorUuid, + simulatorId, ); } diff --git a/src/mcp/tools/ui-testing/key_sequence.ts b/src/mcp/tools/ui-testing/key_sequence.ts index f2904373..13da624f 100644 --- a/src/mcp/tools/ui-testing/key_sequence.ts +++ b/src/mcp/tools/ui-testing/key_sequence.ts @@ -21,11 +21,11 @@ import { getAxePath, getBundledAxeEnvironment, } from '../../../utils/axe/index.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const keySequenceSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), keyCodes: z.array(z.number().int().min(0).max(255)).min(1, 'At least one key code required'), delay: z.number().min(0, 'Delay must be non-negative').optional(), }); @@ -51,7 +51,7 @@ export async function key_sequenceLogic( }, ): Promise { const toolName = 'key_sequence'; - const { simulatorUuid, keyCodes, delay } = params; + const { simulatorId, keyCodes, delay } = params; const commandArgs = ['key-sequence', '--keycodes', keyCodes.join(',')]; if (delay !== undefined) { commandArgs.push('--delay', String(delay)); @@ -59,12 +59,12 @@ export async function key_sequenceLogic( log( 'info', - `${LOG_PREFIX}/${toolName}: Starting key sequence [${keyCodes.join(',')}] on ${simulatorUuid}`, + `${LOG_PREFIX}/${toolName}: Starting key sequence [${keyCodes.join(',')}] on ${simulatorId}`, ); try { - await executeAxeCommand(commandArgs, simulatorUuid, 'key-sequence', executor, axeHelpers); - log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); + await executeAxeCommand(commandArgs, simulatorId, 'key-sequence', executor, axeHelpers); + log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); return createTextResponse(`Key sequence [${keyCodes.join(',')}] executed successfully.`); } catch (error) { log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`); @@ -87,27 +87,29 @@ export async function key_sequenceLogic( } } +const publicSchemaObject = keySequenceSchema.omit({ simulatorId: true } as const).strict(); + export default { name: 'key_sequence', description: 'Press key sequence using HID keycodes on iOS simulator with configurable delay', - schema: keySequenceSchema.shape, // MCP SDK compatibility - handler: createTypedTool( - keySequenceSchema, - (params: KeySequenceParams, executor: CommandExecutor) => { - return key_sequenceLogic(params, executor, { + schema: publicSchemaObject.shape, // MCP SDK compatibility + handler: createSessionAwareTool({ + internalSchema: keySequenceSchema as unknown as z.ZodType, + logicFunction: (params: KeySequenceParams, executor: CommandExecutor) => + key_sequenceLogic(params, executor, { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse, - }); - }, - getDefaultCommandExecutor, - ), + }), + getExecutor: getDefaultCommandExecutor, + requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + }), }; // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], - simulatorUuid: string, + simulatorId: string, commandName: string, executor: CommandExecutor = getDefaultCommandExecutor(), axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse }, @@ -119,7 +121,7 @@ async function executeAxeCommand( } // Add --udid parameter to all commands - const fullArgs = [...commandArgs, '--udid', simulatorUuid]; + const fullArgs = [...commandArgs, '--udid', simulatorId]; // Construct the full command array with the axe binary as the first element const fullCommand = [axeBinary, ...fullArgs]; @@ -135,7 +137,7 @@ async function executeAxeCommand( `axe command '${commandName}' failed.`, commandName, result.error ?? result.output, - simulatorUuid, + simulatorId, ); } diff --git a/src/mcp/tools/ui-testing/long_press.ts b/src/mcp/tools/ui-testing/long_press.ts index fbd7ce0c..4f26f994 100644 --- a/src/mcp/tools/ui-testing/long_press.ts +++ b/src/mcp/tools/ui-testing/long_press.ts @@ -26,7 +26,7 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const longPressSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), x: z.number().int('X coordinate for the long press'), y: z.number().int('Y coordinate for the long press'), duration: z.number().positive('Duration of the long press in milliseconds'), @@ -53,7 +53,7 @@ export async function long_pressLogic( }, ): Promise { const toolName = 'long_press'; - const { simulatorUuid, x, y, duration } = params; + const { simulatorId, x, y, duration } = params; // AXe uses touch command with --down, --up, and --delay for long press const delayInSeconds = Number(duration) / 1000; // Convert ms to seconds const commandArgs = [ @@ -70,14 +70,14 @@ export async function long_pressLogic( log( 'info', - `${LOG_PREFIX}/${toolName}: Starting for (${x}, ${y}), ${duration}ms on ${simulatorUuid}`, + `${LOG_PREFIX}/${toolName}: Starting for (${x}, ${y}), ${duration}ms on ${simulatorId}`, ); try { - await executeAxeCommand(commandArgs, simulatorUuid, 'touch', executor, axeHelpers); - log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); + await executeAxeCommand(commandArgs, simulatorId, 'touch', executor, axeHelpers); + log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); - const warning = getCoordinateWarning(simulatorUuid); + const warning = getCoordinateWarning(simulatorId); const message = `Long press at (${x}, ${y}) for ${duration}ms simulated successfully.`; if (warning) { @@ -127,14 +127,14 @@ export default { // Session tracking for describe_ui warnings interface DescribeUISession { timestamp: number; - simulatorUuid: string; + simulatorId: string; } const describeUITimestamps = new Map(); const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds -function getCoordinateWarning(simulatorUuid: string): string | null { - const session = describeUITimestamps.get(simulatorUuid); +function getCoordinateWarning(simulatorId: string): string | null { + const session = describeUITimestamps.get(simulatorId); if (!session) { return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.'; } @@ -151,7 +151,7 @@ function getCoordinateWarning(simulatorUuid: string): string | null { // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], - simulatorUuid: string, + simulatorId: string, commandName: string, executor: CommandExecutor = getDefaultCommandExecutor(), axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse }, @@ -163,7 +163,7 @@ async function executeAxeCommand( } // Add --udid parameter to all commands - const fullArgs = [...commandArgs, '--udid', simulatorUuid]; + const fullArgs = [...commandArgs, '--udid', simulatorId]; // Construct the full command array with the axe binary as the first element const fullCommand = [axeBinary, ...fullArgs]; @@ -179,7 +179,7 @@ async function executeAxeCommand( `axe command '${commandName}' failed.`, commandName, result.error ?? result.output, - simulatorUuid, + simulatorId, ); } diff --git a/src/mcp/tools/ui-testing/screenshot.ts b/src/mcp/tools/ui-testing/screenshot.ts index adcb52eb..9cf80510 100644 --- a/src/mcp/tools/ui-testing/screenshot.ts +++ b/src/mcp/tools/ui-testing/screenshot.ts @@ -19,7 +19,7 @@ const LOG_PREFIX = '[Screenshot]'; // Define schema as ZodObject const screenshotSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), }); // Use z.infer for type safety @@ -32,7 +32,7 @@ export async function screenshotLogic( pathUtils: { tmpdir: () => string; join: (...paths: string[]) => string } = { ...path, tmpdir }, uuidUtils: { v4: () => string } = { v4: uuidv4 }, ): Promise { - const { simulatorUuid } = params; + const { simulatorId } = params; const tempDir = pathUtils.tmpdir(); const screenshotFilename = `screenshot_${uuidUtils.v4()}.png`; const screenshotPath = pathUtils.join(tempDir, screenshotFilename); @@ -43,15 +43,12 @@ export async function screenshotLogic( 'xcrun', 'simctl', 'io', - simulatorUuid, + simulatorId, 'screenshot', screenshotPath, ]; - log( - 'info', - `${LOG_PREFIX}/screenshot: Starting capture to ${screenshotPath} on ${simulatorUuid}`, - ); + log('info', `${LOG_PREFIX}/screenshot: Starting capture to ${screenshotPath} on ${simulatorId}`); try { // Execute the screenshot command @@ -61,7 +58,7 @@ export async function screenshotLogic( throw new SystemError(`Failed to capture screenshot: ${result.error ?? result.output}`); } - log('info', `${LOG_PREFIX}/screenshot: Success for ${simulatorUuid}`); + log('info', `${LOG_PREFIX}/screenshot: Success for ${simulatorId}`); try { // Optimize the image for LLM consumption: resize to max 800px width and convert to JPEG diff --git a/src/mcp/tools/ui-testing/swipe.ts b/src/mcp/tools/ui-testing/swipe.ts index 2b99635b..8892d751 100644 --- a/src/mcp/tools/ui-testing/swipe.ts +++ b/src/mcp/tools/ui-testing/swipe.ts @@ -20,7 +20,7 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const swipeSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), x1: z.number().int('Start X coordinate'), y1: z.number().int('Start Y coordinate'), x2: z.number().int('End X coordinate'), @@ -56,7 +56,7 @@ export async function swipeLogic( ): Promise { const toolName = 'swipe'; - const { simulatorUuid, x1, y1, x2, y2, duration, delta, preDelay, postDelay } = params; + const { simulatorId, x1, y1, x2, y2, duration, delta, preDelay, postDelay } = params; const commandArgs = [ 'swipe', '--start-x', @@ -84,14 +84,14 @@ export async function swipeLogic( const optionsText = duration ? ` duration=${duration}s` : ''; log( 'info', - `${LOG_PREFIX}/${toolName}: Starting swipe (${x1},${y1})->(${x2},${y2})${optionsText} on ${simulatorUuid}`, + `${LOG_PREFIX}/${toolName}: Starting swipe (${x1},${y1})->(${x2},${y2})${optionsText} on ${simulatorId}`, ); try { - await executeAxeCommand(commandArgs, simulatorUuid, 'swipe', executor, axeHelpers); - log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); + await executeAxeCommand(commandArgs, simulatorId, 'swipe', executor, axeHelpers); + log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); - const warning = getCoordinateWarning(simulatorUuid); + const warning = getCoordinateWarning(simulatorId); const message = `Swipe from (${x1}, ${y1}) to (${x2}, ${y2})${optionsText} simulated successfully.`; if (warning) { @@ -138,14 +138,14 @@ export default { // Session tracking for describe_ui warnings interface DescribeUISession { timestamp: number; - simulatorUuid: string; + simulatorId: string; } const describeUITimestamps = new Map(); const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds -function getCoordinateWarning(simulatorUuid: string): string | null { - const session = describeUITimestamps.get(simulatorUuid); +function getCoordinateWarning(simulatorId: string): string | null { + const session = describeUITimestamps.get(simulatorId); if (!session) { return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.'; } @@ -162,7 +162,7 @@ function getCoordinateWarning(simulatorUuid: string): string | null { // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], - simulatorUuid: string, + simulatorId: string, commandName: string, executor: CommandExecutor = getDefaultCommandExecutor(), axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse }, @@ -174,7 +174,7 @@ async function executeAxeCommand( } // Add --udid parameter to all commands - const fullArgs = [...commandArgs, '--udid', simulatorUuid]; + const fullArgs = [...commandArgs, '--udid', simulatorId]; // Construct the full command array with the axe binary as the first element const fullCommand = [axeBinary, ...fullArgs]; @@ -190,7 +190,7 @@ async function executeAxeCommand( `axe command '${commandName}' failed.`, commandName, result.error ?? result.output, - simulatorUuid, + simulatorId, ); } diff --git a/src/mcp/tools/ui-testing/tap.ts b/src/mcp/tools/ui-testing/tap.ts index 15f7aaf6..aabcfdb8 100644 --- a/src/mcp/tools/ui-testing/tap.ts +++ b/src/mcp/tools/ui-testing/tap.ts @@ -20,7 +20,7 @@ export interface AxeHelpers { // Define schema as ZodObject const tapSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), x: z.number().int('X coordinate must be an integer'), y: z.number().int('Y coordinate must be an integer'), preDelay: z.number().min(0, 'Pre-delay must be non-negative').optional(), @@ -36,8 +36,8 @@ const LOG_PREFIX = '[AXe]'; const describeUITimestamps = new Map(); const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds -function getCoordinateWarning(simulatorUuid: string): string | null { - const session = describeUITimestamps.get(simulatorUuid); +function getCoordinateWarning(simulatorId: string): string | null { + const session = describeUITimestamps.get(simulatorId); if (!session) { return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.'; } @@ -61,7 +61,7 @@ export async function tapLogic( }, ): Promise { const toolName = 'tap'; - const { simulatorUuid, x, y, preDelay, postDelay } = params; + const { simulatorId, x, y, preDelay, postDelay } = params; const commandArgs = ['tap', '-x', String(x), '-y', String(y)]; if (preDelay !== undefined) { commandArgs.push('--pre-delay', String(preDelay)); @@ -70,13 +70,13 @@ export async function tapLogic( commandArgs.push('--post-delay', String(postDelay)); } - log('info', `${LOG_PREFIX}/${toolName}: Starting for (${x}, ${y}) on ${simulatorUuid}`); + log('info', `${LOG_PREFIX}/${toolName}: Starting for (${x}, ${y}) on ${simulatorId}`); try { - await executeAxeCommand(commandArgs, simulatorUuid, 'tap', executor, axeHelpers); - log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); + await executeAxeCommand(commandArgs, simulatorId, 'tap', executor, axeHelpers); + log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); - const warning = getCoordinateWarning(simulatorUuid); + const warning = getCoordinateWarning(simulatorId); const message = `Tap at (${x}, ${y}) simulated successfully.`; if (warning) { @@ -126,7 +126,7 @@ export default { // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], - simulatorUuid: string, + simulatorId: string, commandName: string, executor: CommandExecutor = getDefaultCommandExecutor(), axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse }, @@ -138,7 +138,7 @@ async function executeAxeCommand( } // Add --udid parameter to all commands - const fullArgs = [...commandArgs, '--udid', simulatorUuid]; + const fullArgs = [...commandArgs, '--udid', simulatorId]; // Construct the full command array with the axe binary as the first element const fullCommand = [axeBinary, ...fullArgs]; @@ -154,7 +154,7 @@ async function executeAxeCommand( `axe command '${commandName}' failed.`, commandName, result.error ?? result.output, - simulatorUuid, + simulatorId, ); } diff --git a/src/mcp/tools/ui-testing/touch.ts b/src/mcp/tools/ui-testing/touch.ts index 410812f4..582f93f7 100644 --- a/src/mcp/tools/ui-testing/touch.ts +++ b/src/mcp/tools/ui-testing/touch.ts @@ -21,7 +21,7 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const touchSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), x: z.number().int('X coordinate must be an integer'), y: z.number().int('Y coordinate must be an integer'), down: z.boolean().optional(), @@ -47,7 +47,7 @@ export async function touchLogic( const toolName = 'touch'; // Params are already validated by createTypedTool - use directly - const { simulatorUuid, x, y, down, up, delay } = params; + const { simulatorId, x, y, down, up, delay } = params; // Validate that at least one of down or up is specified if (!down && !up) { @@ -68,14 +68,14 @@ export async function touchLogic( const actionText = down && up ? 'touch down+up' : down ? 'touch down' : 'touch up'; log( 'info', - `${LOG_PREFIX}/${toolName}: Starting ${actionText} at (${x}, ${y}) on ${simulatorUuid}`, + `${LOG_PREFIX}/${toolName}: Starting ${actionText} at (${x}, ${y}) on ${simulatorId}`, ); try { - await executeAxeCommand(commandArgs, simulatorUuid, 'touch', executor, axeHelpers); - log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); + await executeAxeCommand(commandArgs, simulatorId, 'touch', executor, axeHelpers); + log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); - const warning = getCoordinateWarning(simulatorUuid); + const warning = getCoordinateWarning(simulatorId); const message = `Touch event (${actionText}) at (${x}, ${y}) executed successfully.`; if (warning) { @@ -118,14 +118,14 @@ export default { // Session tracking for describe_ui warnings interface DescribeUISession { timestamp: number; - simulatorUuid: string; + simulatorId: string; } const describeUITimestamps = new Map(); const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds -function getCoordinateWarning(simulatorUuid: string): string | null { - const session = describeUITimestamps.get(simulatorUuid); +function getCoordinateWarning(simulatorId: string): string | null { + const session = describeUITimestamps.get(simulatorId); if (!session) { return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.'; } @@ -142,7 +142,7 @@ function getCoordinateWarning(simulatorUuid: string): string | null { // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], - simulatorUuid: string, + simulatorId: string, commandName: string, executor: CommandExecutor = getDefaultCommandExecutor(), axeHelpers?: AxeHelpers, @@ -157,7 +157,7 @@ async function executeAxeCommand( } // Add --udid parameter to all commands - const fullArgs = [...commandArgs, '--udid', simulatorUuid]; + const fullArgs = [...commandArgs, '--udid', simulatorId]; // Construct the full command array with the axe binary as the first element const fullCommand = [axeBinary, ...fullArgs]; @@ -173,7 +173,7 @@ async function executeAxeCommand( `axe command '${commandName}' failed.`, commandName, result.error ?? result.output, - simulatorUuid, + simulatorId, ); } diff --git a/src/mcp/tools/ui-testing/type_text.ts b/src/mcp/tools/ui-testing/type_text.ts index 45562d9a..3396c004 100644 --- a/src/mcp/tools/ui-testing/type_text.ts +++ b/src/mcp/tools/ui-testing/type_text.ts @@ -23,7 +23,7 @@ const LOG_PREFIX = '[AXe]'; // Define schema as ZodObject const typeTextSchema = z.object({ - simulatorUuid: z.string().uuid('Invalid Simulator UUID format'), + simulatorId: z.string().uuid('Invalid Simulator UUID format'), text: z.string().min(1, 'Text cannot be empty'), }); @@ -43,17 +43,17 @@ export async function type_textLogic( const toolName = 'type_text'; // Params are already validated by the factory, use directly - const { simulatorUuid, text } = params; + const { simulatorId, text } = params; const commandArgs = ['type', text]; log( 'info', - `${LOG_PREFIX}/${toolName}: Starting type "${text.substring(0, 20)}..." on ${simulatorUuid}`, + `${LOG_PREFIX}/${toolName}: Starting type "${text.substring(0, 20)}..." on ${simulatorId}`, ); try { - await executeAxeCommand(commandArgs, simulatorUuid, 'type', executor, axeHelpers); - log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorUuid}`); + await executeAxeCommand(commandArgs, simulatorId, 'type', executor, axeHelpers); + log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); return createTextResponse('Text typing simulated successfully.'); } catch (error) { log( @@ -90,7 +90,7 @@ export default { // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], - simulatorUuid: string, + simulatorId: string, commandName: string, executor: CommandExecutor = getDefaultCommandExecutor(), axeHelpers?: AxeHelpers, @@ -105,7 +105,7 @@ async function executeAxeCommand( } // Add --udid parameter to all commands - const fullArgs = [...commandArgs, '--udid', simulatorUuid]; + const fullArgs = [...commandArgs, '--udid', simulatorId]; // Construct the full command array with the axe binary as the first element const fullCommand = [axeBinary, ...fullArgs]; @@ -121,7 +121,7 @@ async function executeAxeCommand( `axe command '${commandName}' failed.`, commandName, result.error ?? result.output, - simulatorUuid, + simulatorId, ); } diff --git a/src/utils/simulator-utils.ts b/src/utils/simulator-utils.ts index f595c49f..b07d7a9a 100644 --- a/src/utils/simulator-utils.ts +++ b/src/utils/simulator-utils.ts @@ -25,13 +25,15 @@ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12 * @returns Object with uuid, optional warning, or error */ export async function determineSimulatorUuid( - params: { simulatorUuid?: string; simulatorName?: string }, + params: { simulatorUuid?: string; simulatorId?: string; simulatorName?: string }, executor: CommandExecutor, ): Promise<{ uuid?: string; warning?: string; error?: ToolResponse }> { + const directUuid = params.simulatorUuid ?? params.simulatorId; + // If UUID is provided directly, use it - if (params.simulatorUuid) { - log('info', `Using provided simulator UUID: ${params.simulatorUuid}`); - return { uuid: params.simulatorUuid }; + if (directUuid) { + log('info', `Using provided simulator UUID: ${directUuid}`); + return { uuid: directUuid }; } // If name is provided, check if it's actually a UUID