From 64de6e78b94436052ea106452761ef37aacc71f7 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 8 Jan 2026 08:40:13 +0000 Subject: [PATCH 1/4] test: align mocks with CommandExecutor options --- .../resources/__tests__/simulators.test.ts | 15 +- .../device/__tests__/build_device.test.ts | 51 ++--- .../__tests__/get_device_app_path.test.ts | 182 ++++++++++-------- .../__tests__/install_app_device.test.ts | 30 +-- .../__tests__/launch_app_device.test.ts | 38 ++-- .../device/__tests__/list_devices.test.ts | 88 +++++---- .../device/__tests__/stop_app_device.test.ts | 30 +-- .../device/__tests__/test_device.test.ts | 32 ++- .../__tests__/start_device_log_cap.test.ts | 91 ++++----- .../__tests__/stop_sim_log_cap.test.ts | 112 ++++------- .../macos/__tests__/build_run_macos.test.ts | 74 ++++--- .../macos/__tests__/get_mac_app_path.test.ts | 42 ++-- .../macos/__tests__/launch_mac_app.test.ts | 19 +- .../tools/macos/__tests__/test_macos.test.ts | 141 ++++++-------- .../__tests__/list_schemes.test.ts | 35 ++-- .../__tests__/show_build_settings.test.ts | 6 +- .../__tests__/scaffold_ios_project.test.ts | 18 +- .../__tests__/scaffold_macos_project.test.ts | 19 +- .../__tests__/session_show_defaults.test.ts | 8 +- .../__tests__/set_sim_appearance.test.ts | 18 +- .../__tests__/set_sim_location.test.ts | 26 +-- .../__tests__/sim_statusbar.test.ts | 44 +++-- .../simulator/__tests__/boot_sim.test.ts | 28 +-- .../simulator/__tests__/build_run_sim.test.ts | 129 +++++-------- .../simulator/__tests__/build_sim.test.ts | 86 +++------ .../__tests__/install_app_sim.test.ts | 107 +++++----- .../simulator/__tests__/list_sims.test.ts | 48 ++--- .../simulator/__tests__/open_sim.test.ts | 25 ++- .../__tests__/record_sim_video.test.ts | 6 +- .../simulator/__tests__/screenshot.test.ts | 3 +- .../simulator/__tests__/stop_app_sim.test.ts | 38 ++-- .../__tests__/swift_package_build.test.ts | 29 ++- .../__tests__/swift_package_clean.test.ts | 26 ++- .../__tests__/swift_package_list.test.ts | 14 +- .../__tests__/swift_package_run.test.ts | 151 +++++++-------- .../__tests__/swift_package_test.test.ts | 48 ++--- .../tools/ui-testing/__tests__/button.test.ts | 71 ++++--- .../ui-testing/__tests__/describe_ui.test.ts | 41 +++- .../ui-testing/__tests__/gesture.test.ts | 69 +++++-- .../ui-testing/__tests__/key_press.test.ts | 41 ++-- .../ui-testing/__tests__/key_sequence.test.ts | 48 +++-- .../ui-testing/__tests__/long_press.test.ts | 46 ++--- .../ui-testing/__tests__/screenshot.test.ts | 26 ++- .../tools/ui-testing/__tests__/swipe.test.ts | 30 +-- .../tools/ui-testing/__tests__/tap.test.ts | 88 ++++----- .../tools/ui-testing/__tests__/touch.test.ts | 68 +++---- .../ui-testing/__tests__/type_text.test.ts | 43 +++-- src/mcp/tools/ui-testing/button.ts | 7 +- src/mcp/tools/ui-testing/describe_ui.ts | 7 +- src/mcp/tools/ui-testing/gesture.ts | 7 +- src/mcp/tools/ui-testing/key_press.ts | 7 +- src/mcp/tools/ui-testing/key_sequence.ts | 7 +- src/mcp/tools/ui-testing/long_press.ts | 7 +- src/mcp/tools/ui-testing/swipe.ts | 9 +- src/mcp/tools/ui-testing/tap.ts | 7 +- src/mcp/tools/ui-testing/touch.ts | 7 +- src/mcp/tools/ui-testing/type_text.ts | 7 +- .../tools/utilities/__tests__/clean.test.ts | 11 +- src/test-utils/mock-executors.ts | 18 +- 59 files changed, 1339 insertions(+), 1190 deletions(-) diff --git a/src/mcp/resources/__tests__/simulators.test.ts b/src/mcp/resources/__tests__/simulators.test.ts index 9cc5707b..ad5c3599 100644 --- a/src/mcp/resources/__tests__/simulators.test.ts +++ b/src/mcp/resources/__tests__/simulators.test.ts @@ -2,7 +2,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; import simulatorsResource, { simulatorsResourceLogic } from '../simulators.ts'; -import { createMockExecutor } from '../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, +} from '../../../test-utils/mock-executors.ts'; describe('simulators resource', () => { describe('Export Field Validation', () => { @@ -73,21 +76,19 @@ describe('simulators resource', () => { const mockExecutor = async (command: string[]) => { // JSON command returns invalid JSON if (command.includes('--json')) { - return { + return createMockCommandResponse({ success: true, output: 'invalid json', error: undefined, - process: { pid: 12345 }, - }; + }); } // Text command returns valid text output - return { + return createMockCommandResponse({ success: true, output: mockTextOutput, error: undefined, - process: { pid: 12345 }, - }; + }); }; const result = await simulatorsResourceLogic(mockExecutor); diff --git a/src/mcp/tools/device/__tests__/build_device.test.ts b/src/mcp/tools/device/__tests__/build_device.test.ts index 4d70cada..1caaad94 100644 --- a/src/mcp/tools/device/__tests__/build_device.test.ts +++ b/src/mcp/tools/device/__tests__/build_device.test.ts @@ -6,7 +6,11 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, + createNoopExecutor, +} from '../../../../test-utils/mock-executors.ts'; import buildDevice, { buildDeviceLogic } from '../build_device.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; @@ -130,24 +134,25 @@ describe('build_device plugin', () => { it('should verify workspace command generation with mock executor', async () => { const commandCalls: Array<{ args: string[]; - logPrefix: string; - silent: boolean; + logPrefix?: string; + silent?: boolean; opts: { cwd?: string } | undefined; }> = []; const stubExecutor = async ( args: string[], - logPrefix: string, - silent: boolean, + logPrefix?: string, + silent?: boolean, opts?: { cwd?: string }, + detached?: boolean, ) => { commandCalls.push({ args, logPrefix, silent, opts }); - return { + void detached; + return createMockCommandResponse({ success: true, output: 'Build succeeded', error: undefined, - process: { pid: 12345 }, - }; + }); }; await buildDeviceLogic( @@ -182,24 +187,25 @@ describe('build_device plugin', () => { it('should verify command generation with mock executor', async () => { const commandCalls: Array<{ args: string[]; - logPrefix: string; - silent: boolean; + logPrefix?: string; + silent?: boolean; opts: { cwd?: string } | undefined; }> = []; const stubExecutor = async ( args: string[], - logPrefix: string, - silent: boolean, + logPrefix?: string, + silent?: boolean, opts?: { cwd?: string }, + detached?: boolean, ) => { commandCalls.push({ args, logPrefix, silent, opts }); - return { + void detached; + return createMockCommandResponse({ success: true, output: 'Build succeeded', error: undefined, - process: { pid: 12345 }, - }; + }); }; await buildDeviceLogic( @@ -291,24 +297,25 @@ describe('build_device plugin', () => { it('should include optional parameters in command', async () => { const commandCalls: Array<{ args: string[]; - logPrefix: string; - silent: boolean; + logPrefix?: string; + silent?: boolean; opts: { cwd?: string } | undefined; }> = []; const stubExecutor = async ( args: string[], - logPrefix: string, - silent: boolean, + logPrefix?: string, + silent?: boolean, opts?: { cwd?: string }, + detached?: boolean, ) => { commandCalls.push({ args, logPrefix, silent, opts }); - return { + void detached; + return createMockCommandResponse({ success: true, output: 'Build succeeded', error: undefined, - process: { pid: 12345 }, - }; + }); }; await buildDeviceLogic( diff --git a/src/mcp/tools/device/__tests__/get_device_app_path.test.ts b/src/mcp/tools/device/__tests__/get_device_app_path.test.ts index 4557850f..5e4a1730 100644 --- a/src/mcp/tools/device/__tests__/get_device_app_path.test.ts +++ b/src/mcp/tools/device/__tests__/get_device_app_path.test.ts @@ -6,7 +6,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, +} from '../../../../test-utils/mock-executors.ts'; import getDeviceAppPath, { get_device_app_pathLogic } from '../get_device_app_path.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; @@ -88,26 +91,29 @@ describe('get_device_app_path plugin', () => { it('should generate correct xcodebuild command for iOS', async () => { const calls: Array<{ - args: any[]; - description: string; - suppressErrors: boolean; - workingDirectory: string | undefined; + args: string[]; + logPrefix?: string; + useShell?: boolean; + opts?: { cwd?: string }; }> = []; const mockExecutor = ( - args: any[], - description: string, - suppressErrors: boolean, - workingDirectory: string | undefined, + args: string[], + logPrefix?: string, + useShell?: boolean, + opts?: { cwd?: string }, + detached?: boolean, ) => { - calls.push({ args, description, suppressErrors, workingDirectory }); - return Promise.resolve({ - success: true, - output: - 'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n', - error: undefined, - process: { pid: 12345 }, - }); + calls.push({ args, logPrefix, useShell, opts }); + void detached; + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: + 'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n', + error: undefined, + }), + ); }; await get_device_app_pathLogic( @@ -132,34 +138,37 @@ describe('get_device_app_path plugin', () => { '-destination', 'generic/platform=iOS', ], - description: 'Get App Path', - suppressErrors: true, - workingDirectory: undefined, + logPrefix: 'Get App Path', + useShell: true, + opts: undefined, }); }); it('should generate correct xcodebuild command for watchOS', async () => { const calls: Array<{ - args: any[]; - description: string; - suppressErrors: boolean; - workingDirectory: string | undefined; + args: string[]; + logPrefix?: string; + useShell?: boolean; + opts?: { cwd?: string }; }> = []; const mockExecutor = ( - args: any[], - description: string, - suppressErrors: boolean, - workingDirectory: string | undefined, + args: string[], + logPrefix?: string, + useShell?: boolean, + opts?: { cwd?: string }, + detached?: boolean, ) => { - calls.push({ args, description, suppressErrors, workingDirectory }); - return Promise.resolve({ - success: true, - output: - 'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-watchos\nFULL_PRODUCT_NAME = MyApp.app\n', - error: undefined, - process: { pid: 12345 }, - }); + calls.push({ args, logPrefix, useShell, opts }); + void detached; + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: + 'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-watchos\nFULL_PRODUCT_NAME = MyApp.app\n', + error: undefined, + }), + ); }; await get_device_app_pathLogic( @@ -185,34 +194,37 @@ describe('get_device_app_path plugin', () => { '-destination', 'generic/platform=watchOS', ], - description: 'Get App Path', - suppressErrors: true, - workingDirectory: undefined, + logPrefix: 'Get App Path', + useShell: true, + opts: undefined, }); }); it('should generate correct xcodebuild command for workspace with iOS', async () => { const calls: Array<{ - args: any[]; - description: string; - suppressErrors: boolean; - workingDirectory: string | undefined; + args: string[]; + logPrefix?: string; + useShell?: boolean; + opts?: { cwd?: string }; }> = []; const mockExecutor = ( - args: any[], - description: string, - suppressErrors: boolean, - workingDirectory: string | undefined, + args: string[], + logPrefix?: string, + useShell?: boolean, + opts?: { cwd?: string }, + detached?: boolean, ) => { - calls.push({ args, description, suppressErrors, workingDirectory }); - return Promise.resolve({ - success: true, - output: - 'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n', - error: undefined, - process: { pid: 12345 }, - }); + calls.push({ args, logPrefix, useShell, opts }); + void detached; + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: + 'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n', + error: undefined, + }), + ); }; await get_device_app_pathLogic( @@ -237,9 +249,9 @@ describe('get_device_app_path plugin', () => { '-destination', 'generic/platform=iOS', ], - description: 'Get App Path', - suppressErrors: true, - workingDirectory: undefined, + logPrefix: 'Get App Path', + useShell: true, + opts: undefined, }); }); @@ -324,26 +336,29 @@ describe('get_device_app_path plugin', () => { it('should include optional configuration parameter in command', async () => { const calls: Array<{ - args: any[]; - description: string; - suppressErrors: boolean; - workingDirectory: string | undefined; + args: string[]; + logPrefix?: string; + useShell?: boolean; + opts?: { cwd?: string }; }> = []; const mockExecutor = ( - args: any[], - description: string, - suppressErrors: boolean, - workingDirectory: string | undefined, + args: string[], + logPrefix?: string, + useShell?: boolean, + opts?: { cwd?: string }, + detached?: boolean, ) => { - calls.push({ args, description, suppressErrors, workingDirectory }); - return Promise.resolve({ - success: true, - output: - 'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Release-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n', - error: undefined, - process: { pid: 12345 }, - }); + calls.push({ args, logPrefix, useShell, opts }); + void detached; + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: + 'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Release-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n', + error: undefined, + }), + ); }; await get_device_app_pathLogic( @@ -369,14 +384,25 @@ describe('get_device_app_path plugin', () => { '-destination', 'generic/platform=iOS', ], - description: 'Get App Path', - suppressErrors: true, - workingDirectory: undefined, + logPrefix: 'Get App Path', + useShell: true, + opts: undefined, }); }); it('should return exact exception handling response', async () => { - const mockExecutor = () => { + const mockExecutor = ( + args: string[], + logPrefix?: string, + useShell?: boolean, + opts?: { cwd?: string }, + detached?: boolean, + ) => { + void args; + void logPrefix; + void useShell; + void opts; + void detached; return Promise.reject(new Error('Network error')); }; diff --git a/src/mcp/tools/device/__tests__/install_app_device.test.ts b/src/mcp/tools/device/__tests__/install_app_device.test.ts index 224ecd0d..ba20a9e5 100644 --- a/src/mcp/tools/device/__tests__/install_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/install_app_device.test.ts @@ -51,10 +51,10 @@ describe('install_app_device plugin', () => { describe('Command Generation', () => { it('should generate correct devicectl command with basic parameters', async () => { - let capturedCommand: unknown[] = []; + let capturedCommand: string[] = []; let capturedDescription: string = ''; let capturedUseShell: boolean = false; - let capturedEnv: unknown = undefined; + let capturedEnv: Record | undefined = undefined; const mockExecutor = createMockExecutor({ success: true, @@ -63,16 +63,18 @@ describe('install_app_device plugin', () => { }); const trackingExecutor = async ( - command: unknown[], - description: string, - useShell: boolean, - env: unknown, + command: string[], + description?: string, + useShell?: boolean, + opts?: { env?: Record }, + detached?: boolean, ) => { capturedCommand = command; - capturedDescription = description; - capturedUseShell = useShell; - capturedEnv = env; - return mockExecutor(command, description, useShell, env); + capturedDescription = description ?? ''; + capturedUseShell = !!useShell; + capturedEnv = opts?.env; + void detached; + return mockExecutor(command, description, useShell, opts, detached); }; await install_app_deviceLogic( @@ -99,7 +101,7 @@ describe('install_app_device plugin', () => { }); it('should generate correct command with different device ID', async () => { - let capturedCommand: unknown[] = []; + let capturedCommand: string[] = []; const mockExecutor = createMockExecutor({ success: true, @@ -107,7 +109,7 @@ describe('install_app_device plugin', () => { process: { pid: 12345 }, }); - const trackingExecutor = async (command: unknown[]) => { + const trackingExecutor = async (command: string[]) => { capturedCommand = command; return mockExecutor(command); }; @@ -133,7 +135,7 @@ describe('install_app_device plugin', () => { }); it('should generate correct command with paths containing spaces', async () => { - let capturedCommand: unknown[] = []; + let capturedCommand: string[] = []; const mockExecutor = createMockExecutor({ success: true, @@ -141,7 +143,7 @@ describe('install_app_device plugin', () => { process: { pid: 12345 }, }); - const trackingExecutor = async (command: unknown[]) => { + const trackingExecutor = async (command: string[]) => { capturedCommand = command; return mockExecutor(command); }; diff --git a/src/mcp/tools/device/__tests__/launch_app_device.test.ts b/src/mcp/tools/device/__tests__/launch_app_device.test.ts index b0008996..7344d19a 100644 --- a/src/mcp/tools/device/__tests__/launch_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/launch_app_device.test.ts @@ -67,10 +67,12 @@ describe('launch_app_device plugin (device-shared)', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record }, + detached?: boolean, ) => { - calls.push({ command, logPrefix, useShell, env }); - return mockExecutor(command, logPrefix, useShell, env); + calls.push({ command, logPrefix, useShell, env: opts?.env }); + void detached; + return mockExecutor(command, logPrefix, useShell, opts, detached); }; await launch_app_deviceLogic( @@ -192,20 +194,26 @@ describe('launch_app_device plugin (device-shared)', () => { const originalReadFile = fs.promises.readFile; const originalUnlink = fs.promises.unlink; - const mockReadFile = (path: string) => { - if (path.includes('launch-')) { - return Promise.resolve( - JSON.stringify({ - result: { - process: { - processIdentifier: 12345, - }, + const mockReadFile = (async (path, options) => { + const pathString = String(path); + if (pathString.includes('launch-')) { + const json = JSON.stringify({ + result: { + process: { + processIdentifier: 12345, }, - }), - ); + }, + }); + if (typeof options === 'string') { + return json; + } + if (options && typeof options === 'object' && 'encoding' in options && options.encoding) { + return json; + } + return Buffer.from(json); } - return originalReadFile(path); - }; + return originalReadFile(path, options as BufferEncoding); + }) as typeof fs.promises.readFile; const mockUnlink = () => Promise.resolve(); diff --git a/src/mcp/tools/device/__tests__/list_devices.test.ts b/src/mcp/tools/device/__tests__/list_devices.test.ts index 588cb4f7..160cf8a1 100644 --- a/src/mcp/tools/device/__tests__/list_devices.test.ts +++ b/src/mcp/tools/device/__tests__/list_devices.test.ts @@ -8,8 +8,8 @@ import { describe, it, expect } from 'vitest'; import { + createMockCommandResponse, createMockExecutor, - createMockFileSystemExecutor, } from '../../../../test-utils/mock-executors.ts'; // Import the logic function and re-export @@ -85,10 +85,12 @@ describe('list_devices plugin (device-shared)', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record }, + detached?: boolean, ) => { - commandCalls.push({ command, logPrefix, useShell, env }); - return mockExecutor(command, logPrefix, useShell, env); + commandCalls.push({ command, logPrefix, useShell, env: opts?.env }); + void detached; + return mockExecutor(command, logPrefix, useShell, opts, detached); }; // Create mock path dependencies @@ -98,10 +100,10 @@ describe('list_devices plugin (device-shared)', () => { }; // Create mock filesystem with specific behavior - const mockFsDeps = createMockFileSystemExecutor({ - readFile: async () => JSON.stringify(devicectlJson), + const mockFsDeps = { + readFile: async (_path: string, _encoding?: string) => JSON.stringify(devicectlJson), unlink: async () => {}, - }); + }; await list_devicesLogic({}, trackingExecutor, mockPathDeps, mockFsDeps); @@ -134,27 +136,27 @@ describe('list_devices plugin (device-shared)', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record }, + detached?: boolean, ) => { callCount++; - commandCalls.push({ command, logPrefix, useShell, env }); + commandCalls.push({ command, logPrefix, useShell, env: opts?.env }); + void detached; if (callCount === 1) { // First call fails (devicectl) - return { + return createMockCommandResponse({ success: false, output: '', error: 'devicectl failed', - process: { pid: 12345 }, - }; + }); } else { // Second call succeeds (xctrace) - return { + return createMockCommandResponse({ success: true, output: 'iPhone 15 (12345678-1234-1234-1234-123456789012)', error: undefined, - process: { pid: 12345 }, - }; + }); } }; @@ -165,12 +167,12 @@ describe('list_devices plugin (device-shared)', () => { }; // Create mock filesystem that throws for readFile - const mockFsDeps = createMockFileSystemExecutor({ + const mockFsDeps = { readFile: async () => { throw new Error('File not found'); }, unlink: async () => {}, - }); + }; await list_devicesLogic({}, trackingExecutor, mockPathDeps, mockFsDeps); @@ -220,10 +222,10 @@ describe('list_devices plugin (device-shared)', () => { }; // Create mock filesystem with specific behavior - const mockFsDeps = createMockFileSystemExecutor({ - readFile: async () => JSON.stringify(devicectlJson), + const mockFsDeps = { + readFile: async (_path: string, _encoding?: string) => JSON.stringify(devicectlJson), unlink: async () => {}, - }); + }; const result = await list_devicesLogic({}, mockExecutor, mockPathDeps, mockFsDeps); @@ -244,25 +246,29 @@ describe('list_devices plugin (device-shared)', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record }, + detached?: boolean, ) => { callCount++; + void command; + void logPrefix; + void useShell; + void opts; + void detached; if (callCount === 1) { // First call fails (devicectl) - return { + return createMockCommandResponse({ success: false, output: '', error: 'devicectl failed', - process: { pid: 12345 }, - }; + }); } else { // Second call succeeds (xctrace) - return { + return createMockCommandResponse({ success: true, output: 'iPhone 15 (12345678-1234-1234-1234-123456789012)', error: undefined, - process: { pid: 12345 }, - }; + }); } }; @@ -273,12 +279,12 @@ describe('list_devices plugin (device-shared)', () => { }; // Create mock filesystem that throws for readFile - const mockFsDeps = createMockFileSystemExecutor({ + const mockFsDeps = { readFile: async () => { throw new Error('File not found'); }, unlink: async () => {}, - }); + }; const result = await list_devicesLogic({}, mockExecutor, mockPathDeps, mockFsDeps); @@ -305,25 +311,29 @@ describe('list_devices plugin (device-shared)', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record }, + detached?: boolean, ) => { callCount++; + void command; + void logPrefix; + void useShell; + void opts; + void detached; if (callCount === 1) { // First call succeeds (devicectl) - return { + return createMockCommandResponse({ success: true, output: '', error: undefined, - process: { pid: 12345 }, - }; + }); } else { // Second call succeeds (xctrace) with empty output - return { + return createMockCommandResponse({ success: true, output: '', error: undefined, - process: { pid: 12345 }, - }; + }); } }; @@ -334,10 +344,10 @@ describe('list_devices plugin (device-shared)', () => { }; // Create mock filesystem with empty devices response - const mockFsDeps = createMockFileSystemExecutor({ - readFile: async () => JSON.stringify(devicectlJson), + const mockFsDeps = { + readFile: async (_path: string, _encoding?: string) => JSON.stringify(devicectlJson), unlink: async () => {}, - }); + }; const result = await list_devicesLogic({}, mockExecutor, mockPathDeps, mockFsDeps); diff --git a/src/mcp/tools/device/__tests__/stop_app_device.test.ts b/src/mcp/tools/device/__tests__/stop_app_device.test.ts index 8fcd0f76..71774795 100644 --- a/src/mcp/tools/device/__tests__/stop_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/stop_app_device.test.ts @@ -49,10 +49,10 @@ describe('stop_app_device plugin', () => { describe('Command Generation', () => { it('should generate correct devicectl command with basic parameters', async () => { - let capturedCommand: unknown[] = []; + let capturedCommand: string[] = []; let capturedDescription: string = ''; let capturedUseShell: boolean = false; - let capturedEnv: unknown = undefined; + let capturedEnv: Record | undefined = undefined; const mockExecutor = createMockExecutor({ success: true, @@ -61,16 +61,18 @@ describe('stop_app_device plugin', () => { }); const trackingExecutor = async ( - command: unknown[], - description: string, - useShell: boolean, - env: unknown, + command: string[], + description?: string, + useShell?: boolean, + opts?: { env?: Record }, + detached?: boolean, ) => { capturedCommand = command; - capturedDescription = description; - capturedUseShell = useShell; - capturedEnv = env; - return mockExecutor(command, description, useShell, env); + capturedDescription = description ?? ''; + capturedUseShell = !!useShell; + capturedEnv = opts?.env; + void detached; + return mockExecutor(command, description, useShell, opts, detached); }; await stop_app_deviceLogic( @@ -98,7 +100,7 @@ describe('stop_app_device plugin', () => { }); it('should generate correct command with different device ID and process ID', async () => { - let capturedCommand: unknown[] = []; + let capturedCommand: string[] = []; const mockExecutor = createMockExecutor({ success: true, @@ -106,7 +108,7 @@ describe('stop_app_device plugin', () => { process: { pid: 12345 }, }); - const trackingExecutor = async (command: unknown[]) => { + const trackingExecutor = async (command: string[]) => { capturedCommand = command; return mockExecutor(command); }; @@ -133,7 +135,7 @@ describe('stop_app_device plugin', () => { }); it('should generate correct command with large process ID', async () => { - let capturedCommand: unknown[] = []; + let capturedCommand: string[] = []; const mockExecutor = createMockExecutor({ success: true, @@ -141,7 +143,7 @@ describe('stop_app_device plugin', () => { process: { pid: 12345 }, }); - const trackingExecutor = async (command: unknown[]) => { + const trackingExecutor = async (command: string[]) => { capturedCommand = command; return mockExecutor(command); }; diff --git a/src/mcp/tools/device/__tests__/test_device.test.ts b/src/mcp/tools/device/__tests__/test_device.test.ts index 17f206d5..4ac6667f 100644 --- a/src/mcp/tools/device/__tests__/test_device.test.ts +++ b/src/mcp/tools/device/__tests__/test_device.test.ts @@ -8,6 +8,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; import { + createMockCommandResponse, createMockExecutor, createMockFileSystemExecutor, } from '../../../../test-utils/mock-executors.ts'; @@ -83,7 +84,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-123', tmpdir: () => '/tmp', - stat: async () => ({ isFile: () => true }), + stat: async () => ({ isDirectory: () => false }), rm: async () => {}, }), ); @@ -100,7 +101,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-456', tmpdir: () => '/tmp', - stat: async () => ({ isFile: () => true }), + stat: async () => ({ isDirectory: () => false }), rm: async () => {}, }), ); @@ -173,7 +174,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-123456', tmpdir: () => '/tmp', - stat: async () => ({ isFile: () => true }), + stat: async () => ({ isDirectory: () => false }), rm: async () => {}, }), ); @@ -219,7 +220,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-123456', tmpdir: () => '/tmp', - stat: async () => ({ isFile: () => true }), + stat: async () => ({ isDirectory: () => false }), rm: async () => {}, }), ); @@ -232,16 +233,27 @@ describe('test_device plugin', () => { it('should handle xcresult parsing failures gracefully', async () => { // Create a multi-call mock that handles different commands let callCount = 0; - const mockExecutor = async (args: string[], description: string) => { + const mockExecutor = async ( + args: string[], + description?: string, + useShell?: boolean, + opts?: { cwd?: string }, + detached?: boolean, + ) => { callCount++; + void args; + void description; + void useShell; + void opts; + void detached; // First call is for xcodebuild test (successful) if (callCount === 1) { - return { success: true, output: 'BUILD SUCCEEDED' }; + return createMockCommandResponse({ success: true, output: 'BUILD SUCCEEDED' }); } // Second call is for xcresulttool (fails) - return { success: false, error: 'xcresulttool failed' }; + return createMockCommandResponse({ success: false, error: 'xcresulttool failed' }); }; const result = await testDeviceLogic( @@ -297,7 +309,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-123456', tmpdir: () => '/tmp', - stat: async () => ({ isFile: () => true }), + stat: async () => ({ isDirectory: () => false }), rm: async () => {}, }), ); @@ -336,7 +348,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-123456', tmpdir: () => '/tmp', - stat: async () => ({ isFile: () => true }), + stat: async () => ({ isDirectory: () => false }), rm: async () => {}, }), ); @@ -373,7 +385,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-workspace-123', tmpdir: () => '/tmp', - stat: async () => ({ isFile: () => true }), + stat: async () => ({ isDirectory: () => false }), rm: async () => {}, }), ); diff --git a/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts b/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts index 237f1650..580360cf 100644 --- a/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts +++ b/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts @@ -4,6 +4,7 @@ */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { EventEmitter } from 'events'; +import { Readable } from 'stream'; import type { ChildProcess } from 'child_process'; import * as z from 'zod'; import { @@ -150,30 +151,22 @@ describe('start_device_log_cap plugin', () => { }); it('should surface early launch failures when process exits immediately', async () => { - const failingProcess = new EventEmitter() as unknown as ChildProcess & { - exitCode: number | null; - killed: boolean; - kill(signal?: string): boolean; - stdout: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void }; - stderr: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void }; - }; + const failingProcess = new EventEmitter() as unknown as ChildProcess; - const stubOutput = new EventEmitter() as NodeJS.ReadableStream & { - setEncoding?: (encoding: string) => void; - }; - stubOutput.setEncoding = () => {}; - const stubError = new EventEmitter() as NodeJS.ReadableStream & { - setEncoding?: (encoding: string) => void; - }; - stubError.setEncoding = () => {}; + const stubOutput = new Readable({ + read() {}, + }); + const stubError = new Readable({ + read() {}, + }); failingProcess.stdout = stubOutput; failingProcess.stderr = stubError; - failingProcess.exitCode = null; - failingProcess.killed = false; + (failingProcess as any).exitCode = null; + (failingProcess as any).killed = false; failingProcess.kill = () => { - failingProcess.killed = true; - failingProcess.exitCode = 0; + (failingProcess as any).killed = true; + (failingProcess as any).exitCode = 0; failingProcess.emit('close', 0, null); return true; }; @@ -207,7 +200,7 @@ describe('start_device_log_cap plugin', () => { 'data', 'ERROR: The application failed to launch. (com.apple.dt.CoreDeviceError error 10002)\nNSLocalizedRecoverySuggestion = Provide a valid bundle identifier.\n', ); - failingProcess.exitCode = 70; + (failingProcess as any).exitCode = 70; failingProcess.emit('close', 70, null); }, 10); @@ -233,29 +226,21 @@ describe('start_device_log_cap plugin', () => { }, }; - const failingProcess = new EventEmitter() as unknown as ChildProcess & { - exitCode: number | null; - killed: boolean; - kill(signal?: string): boolean; - stdout: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void }; - stderr: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void }; - }; + const failingProcess = new EventEmitter() as unknown as ChildProcess; - const stubOutput = new EventEmitter() as NodeJS.ReadableStream & { - setEncoding?: (encoding: string) => void; - }; - stubOutput.setEncoding = () => {}; - const stubError = new EventEmitter() as NodeJS.ReadableStream & { - setEncoding?: (encoding: string) => void; - }; - stubError.setEncoding = () => {}; + const stubOutput = new Readable({ + read() {}, + }); + const stubError = new Readable({ + read() {}, + }); failingProcess.stdout = stubOutput; failingProcess.stderr = stubError; - failingProcess.exitCode = null; - failingProcess.killed = false; + (failingProcess as any).exitCode = null; + (failingProcess as any).killed = false; failingProcess.kill = () => { - failingProcess.killed = true; + (failingProcess as any).killed = true; return true; }; @@ -293,7 +278,7 @@ describe('start_device_log_cap plugin', () => { }); setTimeout(() => { - failingProcess.exitCode = 0; + (failingProcess as any).exitCode = 0; failingProcess.emit('close', 0, null); }, 5); @@ -323,29 +308,21 @@ describe('start_device_log_cap plugin', () => { }, }; - const runningProcess = new EventEmitter() as unknown as ChildProcess & { - exitCode: number | null; - killed: boolean; - kill(signal?: string): boolean; - stdout: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void }; - stderr: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void }; - }; + const runningProcess = new EventEmitter() as unknown as ChildProcess; - const stubOutput = new EventEmitter() as NodeJS.ReadableStream & { - setEncoding?: (encoding: string) => void; - }; - stubOutput.setEncoding = () => {}; - const stubError = new EventEmitter() as NodeJS.ReadableStream & { - setEncoding?: (encoding: string) => void; - }; - stubError.setEncoding = () => {}; + const stubOutput = new Readable({ + read() {}, + }); + const stubError = new Readable({ + read() {}, + }); runningProcess.stdout = stubOutput; runningProcess.stderr = stubError; - runningProcess.exitCode = null; - runningProcess.killed = false; + (runningProcess as any).exitCode = null; + (runningProcess as any).killed = false; runningProcess.kill = () => { - runningProcess.killed = true; + (runningProcess as any).killed = true; runningProcess.emit('close', 0, null); return true; }; diff --git a/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts b/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts index 56f95e46..0c58f2bd 100644 --- a/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts +++ b/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts @@ -14,14 +14,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; import stopSimLogCap, { stop_sim_log_capLogic } from '../stop_sim_log_cap.ts'; -import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts'; import { activeLogSessions } from '../../../../utils/log_capture.ts'; describe('stop_sim_log_cap plugin', () => { - let mockFileSystem: any; - beforeEach(() => { - mockFileSystem = createMockFileSystemExecutor(); // Clear any active sessions before each test activeLogSessions.clear(); }); @@ -93,12 +89,9 @@ describe('stop_sim_log_cap plugin', () => { // This test now validates that the logic function works with valid empty strings await createTestLogSession('', 'Log content for empty session'); - const result = await stop_sim_log_capLogic( - { - logSessionId: '', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: '', + }); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -111,12 +104,9 @@ describe('stop_sim_log_cap plugin', () => { // This test now validates that the logic function works with valid empty strings await createTestLogSession('', 'Log content for empty session'); - const result = await stop_sim_log_capLogic( - { - logSessionId: '', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: '', + }); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -127,12 +117,9 @@ describe('stop_sim_log_cap plugin', () => { it('should handle empty string logSessionId', async () => { await createTestLogSession('', 'Log content for empty session'); - const result = await stop_sim_log_capLogic( - { - logSessionId: '', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: '', + }); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -145,12 +132,9 @@ describe('stop_sim_log_cap plugin', () => { it('should call stopLogCapture with correct parameters', async () => { await createTestLogSession('test-session-id', 'Mock log content from file'); - const result = await stop_sim_log_capLogic( - { - logSessionId: 'test-session-id', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: 'test-session-id', + }); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -161,12 +145,9 @@ describe('stop_sim_log_cap plugin', () => { it('should call stopLogCapture with different session ID', async () => { await createTestLogSession('different-session-id', 'Different log content'); - const result = await stop_sim_log_capLogic( - { - logSessionId: 'different-session-id', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: 'different-session-id', + }); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -179,12 +160,9 @@ describe('stop_sim_log_cap plugin', () => { it('should handle successful log capture stop', async () => { await createTestLogSession('test-session-id', 'Mock log content from file'); - const result = await stop_sim_log_capLogic( - { - logSessionId: 'test-session-id', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: 'test-session-id', + }); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -195,12 +173,9 @@ describe('stop_sim_log_cap plugin', () => { it('should handle empty log content', async () => { await createTestLogSession('test-session-id', ''); - const result = await stop_sim_log_capLogic( - { - logSessionId: 'test-session-id', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: 'test-session-id', + }); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -211,12 +186,9 @@ describe('stop_sim_log_cap plugin', () => { it('should handle multiline log content', async () => { await createTestLogSession('test-session-id', 'Line 1\nLine 2\nLine 3'); - const result = await stop_sim_log_capLogic( - { - logSessionId: 'test-session-id', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: 'test-session-id', + }); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -225,12 +197,9 @@ describe('stop_sim_log_cap plugin', () => { }); it('should handle log capture stop errors for non-existent session', async () => { - const result = await stop_sim_log_capLogic( - { - logSessionId: 'non-existent-session', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: 'non-existent-session', + }); expect(result.isError).toBe(true); expect(result.content[0].text).toBe( @@ -254,12 +223,9 @@ describe('stop_sim_log_cap plugin', () => { bundleId: 'com.example.TestApp', }); - const result = await stop_sim_log_capLogic( - { - logSessionId: 'test-session-id', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: 'test-session-id', + }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain( @@ -283,12 +249,9 @@ describe('stop_sim_log_cap plugin', () => { bundleId: 'com.example.TestApp', }); - const result = await stop_sim_log_capLogic( - { - logSessionId: 'test-session-id', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: 'test-session-id', + }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain( @@ -312,12 +275,9 @@ describe('stop_sim_log_cap plugin', () => { bundleId: 'com.example.TestApp', }); - const result = await stop_sim_log_capLogic( - { - logSessionId: 'test-session-id', - }, - mockFileSystem, - ); + const result = await stop_sim_log_capLogic({ + logSessionId: 'test-session-id', + }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain( diff --git a/src/mcp/tools/macos/__tests__/build_run_macos.test.ts b/src/mcp/tools/macos/__tests__/build_run_macos.test.ts index 529079c0..ccee27a4 100644 --- a/src/mcp/tools/macos/__tests__/build_run_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/build_run_macos.test.ts @@ -1,9 +1,12 @@ import { describe, it, expect, beforeEach } from 'vitest'; +import type { ChildProcess } from 'child_process'; import * as z from 'zod'; import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import tool, { buildRunMacOSLogic } from '../build_run_macos.ts'; +const mockProcess = { pid: 12345 } as unknown as ChildProcess; + describe('build_run_macos', () => { beforeEach(() => { sessionStore.clear(); @@ -80,12 +83,14 @@ describe('build_run_macos', () => { const executorCalls: any[] = []; const mockExecutor = ( command: string[], - description: string, - logOutput: boolean, + description?: string, + logOutput?: boolean, opts?: { cwd?: string }, + detached?: boolean, ) => { callCount++; executorCalls.push({ command, description, logOutput, opts }); + void detached; if (callCount === 1) { // First call for build @@ -93,6 +98,7 @@ describe('build_run_macos', () => { success: true, output: 'BUILD SUCCEEDED', error: '', + process: mockProcess, }); } else if (callCount === 2) { // Second call for build settings @@ -100,9 +106,10 @@ describe('build_run_macos', () => { success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: '', + process: mockProcess, }); } - return Promise.resolve({ success: true, output: '', error: '' }); + return Promise.resolve({ success: true, output: '', error: '', process: mockProcess }); }; const args = { @@ -148,7 +155,7 @@ describe('build_run_macos', () => { ], description: 'Get Build Settings for Launch', logOutput: true, - timeout: undefined, + opts: undefined, }); expect(result).toEqual({ @@ -176,12 +183,14 @@ describe('build_run_macos', () => { const executorCalls: any[] = []; const mockExecutor = ( command: string[], - description: string, - logOutput: boolean, + description?: string, + logOutput?: boolean, opts?: { cwd?: string }, + detached?: boolean, ) => { callCount++; executorCalls.push({ command, description, logOutput, opts }); + void detached; if (callCount === 1) { // First call for build @@ -189,6 +198,7 @@ describe('build_run_macos', () => { success: true, output: 'BUILD SUCCEEDED', error: '', + process: mockProcess, }); } else if (callCount === 2) { // Second call for build settings @@ -196,9 +206,10 @@ describe('build_run_macos', () => { success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: '', + process: mockProcess, }); } - return Promise.resolve({ success: true, output: '', error: '' }); + return Promise.resolve({ success: true, output: '', error: '', process: mockProcess }); }; const args = { @@ -244,7 +255,7 @@ describe('build_run_macos', () => { ], description: 'Get Build Settings for Launch', logOutput: true, - timeout: undefined, + opts: undefined, }); expect(result).toEqual({ @@ -296,17 +307,20 @@ describe('build_run_macos', () => { let callCount = 0; const mockExecutor = ( command: string[], - description: string, - logOutput: boolean, - timeout?: number, + description?: string, + logOutput?: boolean, + opts?: { cwd?: string }, + detached?: boolean, ) => { callCount++; + void detached; if (callCount === 1) { // First call for build succeeds return Promise.resolve({ success: true, output: 'BUILD SUCCEEDED', error: '', + process: mockProcess, }); } else if (callCount === 2) { // Second call for build settings fails @@ -314,9 +328,10 @@ describe('build_run_macos', () => { success: false, output: '', error: 'error: Failed to get settings', + process: mockProcess, }); } - return Promise.resolve({ success: true, output: '', error: '' }); + return Promise.resolve({ success: true, output: '', error: '', process: mockProcess }); }; const args = { @@ -352,17 +367,20 @@ describe('build_run_macos', () => { let callCount = 0; const mockExecutor = ( command: string[], - description: string, - logOutput: boolean, - timeout?: number, + description?: string, + logOutput?: boolean, + opts?: { cwd?: string }, + detached?: boolean, ) => { callCount++; + void detached; if (callCount === 1) { // First call for build succeeds return Promise.resolve({ success: true, output: 'BUILD SUCCEEDED', error: '', + process: mockProcess, }); } else if (callCount === 2) { // Second call for build settings succeeds @@ -370,6 +388,7 @@ describe('build_run_macos', () => { success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: '', + process: mockProcess, }); } else if (callCount === 3) { // Third call for open command fails @@ -377,9 +396,10 @@ describe('build_run_macos', () => { success: false, output: '', error: 'Failed to launch', + process: mockProcess, }); } - return Promise.resolve({ success: true, output: '', error: '' }); + return Promise.resolve({ success: true, output: '', error: '', process: mockProcess }); }; const args = { @@ -413,10 +433,16 @@ describe('build_run_macos', () => { it('should handle spawn error', async () => { const mockExecutor = ( command: string[], - description: string, - logOutput: boolean, - timeout?: number, + description?: string, + logOutput?: boolean, + opts?: { cwd?: string }, + detached?: boolean, ) => { + void command; + void description; + void logOutput; + void opts; + void detached; return Promise.reject(new Error('spawn xcodebuild ENOENT')); }; @@ -443,12 +469,14 @@ describe('build_run_macos', () => { const executorCalls: any[] = []; const mockExecutor = ( command: string[], - description: string, - logOutput: boolean, + description?: string, + logOutput?: boolean, opts?: { cwd?: string }, + detached?: boolean, ) => { callCount++; executorCalls.push({ command, description, logOutput, opts }); + void detached; if (callCount === 1) { // First call for build @@ -456,6 +484,7 @@ describe('build_run_macos', () => { success: true, output: 'BUILD SUCCEEDED', error: '', + process: mockProcess, }); } else if (callCount === 2) { // Second call for build settings @@ -463,9 +492,10 @@ describe('build_run_macos', () => { success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: '', + process: mockProcess, }); } - return Promise.resolve({ success: true, output: '', error: '' }); + return Promise.resolve({ success: true, output: '', error: '', process: mockProcess }); }; const args = { diff --git a/src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts b/src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts index cbb7e43a..1b992a54 100644 --- a/src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts +++ b/src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts @@ -5,7 +5,11 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor, type CommandExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, + type CommandExecutor, +} from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import getMacAppPath, { get_mac_app_pathLogic } from '../get_mac_app_path.ts'; @@ -104,12 +108,11 @@ describe('get_mac_app_path plugin', () => { const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); - return { + return createMockCommandResponse({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, - process: { pid: 12345 }, - }; + }); }; const args = { @@ -143,12 +146,11 @@ describe('get_mac_app_path plugin', () => { const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); - return { + return createMockCommandResponse({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, - process: { pid: 12345 }, - }; + }); }; const args = { @@ -182,19 +184,18 @@ describe('get_mac_app_path plugin', () => { const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); - return { + return createMockCommandResponse({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, - process: { pid: 12345 }, - }; + }); }; const args = { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', configuration: 'Release', - arch: 'arm64', + arch: 'arm64' as const, }; await get_mac_app_pathLogic(args, mockExecutor); @@ -225,19 +226,18 @@ describe('get_mac_app_path plugin', () => { const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); - return { + return createMockCommandResponse({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, - process: { pid: 12345 }, - }; + }); }; const args = { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', configuration: 'Debug', - arch: 'x86_64', + arch: 'x86_64' as const, }; await get_mac_app_pathLogic(args, mockExecutor); @@ -268,12 +268,11 @@ describe('get_mac_app_path plugin', () => { const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); - return { + return createMockCommandResponse({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, - process: { pid: 12345 }, - }; + }); }; const args = { @@ -313,18 +312,17 @@ describe('get_mac_app_path plugin', () => { const calls: any[] = []; const mockExecutor: CommandExecutor = async (...args) => { calls.push(args); - return { + return createMockCommandResponse({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', error: undefined, - process: { pid: 12345 }, - }; + }); }; const args = { workspacePath: '/path/to/MyProject.xcworkspace', scheme: 'MyScheme', - arch: 'arm64', + arch: 'arm64' as const, }; await get_mac_app_pathLogic(args, mockExecutor); diff --git a/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts b/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts index b133842f..b6eeedc5 100644 --- a/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts +++ b/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts @@ -9,7 +9,10 @@ import { describe, it, expect } from 'vitest'; import * as z from 'zod'; -import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockFileSystemExecutor, +} from '../../../../test-utils/mock-executors.ts'; import launchMacApp, { launch_mac_appLogic } from '../launch_mac_app.ts'; describe('launch_mac_app plugin', () => { @@ -62,7 +65,7 @@ describe('launch_mac_app plugin', () => { describe('Input Validation', () => { it('should handle non-existent app path', async () => { - const mockExecutor = async () => Promise.resolve({ stdout: '', stderr: '' }); + const mockExecutor = async () => Promise.resolve(createMockCommandResponse()); const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => false, }); @@ -92,7 +95,7 @@ describe('launch_mac_app plugin', () => { const calls: any[] = []; const mockExecutor = async (command: string[]) => { calls.push({ command }); - return { stdout: '', stderr: '' }; + return createMockCommandResponse(); }; const mockFileSystem = createMockFileSystemExecutor({ @@ -115,7 +118,7 @@ describe('launch_mac_app plugin', () => { const calls: any[] = []; const mockExecutor = async (command: string[]) => { calls.push({ command }); - return { stdout: '', stderr: '' }; + return createMockCommandResponse(); }; const mockFileSystem = createMockFileSystemExecutor({ @@ -145,7 +148,7 @@ describe('launch_mac_app plugin', () => { const calls: any[] = []; const mockExecutor = async (command: string[]) => { calls.push({ command }); - return { stdout: '', stderr: '' }; + return createMockCommandResponse(); }; const mockFileSystem = createMockFileSystemExecutor({ @@ -169,7 +172,7 @@ describe('launch_mac_app plugin', () => { const calls: any[] = []; const mockExecutor = async (command: string[]) => { calls.push({ command }); - return { stdout: '', stderr: '' }; + return createMockCommandResponse(); }; const mockFileSystem = createMockFileSystemExecutor({ @@ -191,7 +194,7 @@ describe('launch_mac_app plugin', () => { describe('Response Processing', () => { it('should return successful launch response', async () => { - const mockExecutor = async () => Promise.resolve({ stdout: '', stderr: '' }); + const mockExecutor = async () => Promise.resolve(createMockCommandResponse()); const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, @@ -216,7 +219,7 @@ describe('launch_mac_app plugin', () => { }); it('should return successful launch response with args', async () => { - const mockExecutor = async () => Promise.resolve({ stdout: '', stderr: '' }); + const mockExecutor = async () => Promise.resolve(createMockCommandResponse()); const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, diff --git a/src/mcp/tools/macos/__tests__/test_macos.test.ts b/src/mcp/tools/macos/__tests__/test_macos.test.ts index 57eb7a3e..7fb1c8c7 100644 --- a/src/mcp/tools/macos/__tests__/test_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/test_macos.test.ts @@ -5,10 +5,24 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, + createMockFileSystemExecutor, + type FileSystemExecutor, +} from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import testMacos, { testMacosLogic } from '../test_macos.ts'; +const createTestFileSystemExecutor = (overrides: Partial = {}) => + createMockFileSystemExecutor({ + mkdtemp: async () => '/tmp/test-123', + rm: async () => {}, + tmpdir: () => '/tmp', + stat: async () => ({ isDirectory: () => true }), + ...overrides, + }); + describe('test_macos plugin (unified)', () => { beforeEach(() => { sessionStore.clear(); @@ -111,12 +125,7 @@ describe('test_macos plugin (unified)', () => { output: 'Test Suite All Tests passed', }); - const mockFileSystemExecutor = { - mkdtemp: async () => '/tmp/test-123', - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + const mockFileSystemExecutor = createTestFileSystemExecutor(); const result = await testMacosLogic( { @@ -138,12 +147,7 @@ describe('test_macos plugin (unified)', () => { output: 'Test Suite All Tests passed', }); - const mockFileSystemExecutor = { - mkdtemp: async () => '/tmp/test-123', - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + const mockFileSystemExecutor = createTestFileSystemExecutor(); const result = await testMacosLogic( { @@ -168,12 +172,7 @@ describe('test_macos plugin (unified)', () => { }); // Mock file system dependencies - const mockFileSystemExecutor = { - mkdtemp: async () => '/tmp/test-123', - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + const mockFileSystemExecutor = createTestFileSystemExecutor(); const result = await testMacosLogic( { @@ -197,12 +196,7 @@ describe('test_macos plugin (unified)', () => { }); // Mock file system dependencies - const mockFileSystemExecutor = { - mkdtemp: async () => '/tmp/test-123', - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + const mockFileSystemExecutor = createTestFileSystemExecutor(); const result = await testMacosLogic( { @@ -226,12 +220,7 @@ describe('test_macos plugin (unified)', () => { }); // Mock file system dependencies - const mockFileSystemExecutor = { - mkdtemp: async () => '/tmp/test-123', - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + const mockFileSystemExecutor = createTestFileSystemExecutor(); const result = await testMacosLogic( { @@ -254,12 +243,7 @@ describe('test_macos plugin (unified)', () => { }); // Mock file system dependencies - const mockFileSystemExecutor = { - mkdtemp: async () => '/tmp/test-123', - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + const mockFileSystemExecutor = createTestFileSystemExecutor(); const result = await testMacosLogic( { @@ -286,12 +270,7 @@ describe('test_macos plugin (unified)', () => { }); // Mock file system dependencies - const mockFileSystemExecutor = { - mkdtemp: async () => '/tmp/test-123', - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + const mockFileSystemExecutor = createTestFileSystemExecutor(); const result = await testMacosLogic( { @@ -316,13 +295,15 @@ describe('test_macos plugin (unified)', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record }, + detached?: boolean, ) => { - commandCalls.push({ command, logPrefix, useShell, env }); + commandCalls.push({ command, logPrefix, useShell, env: opts?.env }); + void detached; // Handle xcresulttool command if (command.includes('xcresulttool')) { - return { + return createMockCommandResponse({ success: true, output: JSON.stringify({ title: 'Test Results', @@ -334,24 +315,20 @@ describe('test_macos plugin (unified)', () => { expectedFailures: 0, }), error: undefined, - }; + }); } - return { + return createMockCommandResponse({ success: true, output: 'Test Succeeded', error: undefined, - process: { pid: 12345 }, - }; + }); }; // Mock file system dependencies using approved utility - const mockFileSystemExecutor = { + const mockFileSystemExecutor = createTestFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-abc123', - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + }); const result = await testMacosLogic( { @@ -411,23 +388,27 @@ describe('test_macos plugin (unified)', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record }, + detached?: boolean, ) => { callCount++; + void logPrefix; + void useShell; + void opts; + void detached; // First call is xcodebuild test - fails if (callCount === 1) { - return { + return createMockCommandResponse({ success: false, output: '', error: 'error: Test failed', - process: { pid: 12345 }, - }; + }); } // Second call is xcresulttool if (command.includes('xcresulttool')) { - return { + return createMockCommandResponse({ success: true, output: JSON.stringify({ title: 'Test Results', @@ -439,19 +420,16 @@ describe('test_macos plugin (unified)', () => { expectedFailures: 0, }), error: undefined, - }; + }); } - return { success: true, output: '', error: undefined }; + return createMockCommandResponse({ success: true, output: '', error: undefined }); }; // Mock file system dependencies - const mockFileSystemExecutor = { + const mockFileSystemExecutor = createTestFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-abc123', - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + }); const result = await testMacosLogic( { @@ -482,13 +460,15 @@ describe('test_macos plugin (unified)', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record }, + detached?: boolean, ) => { - commandCalls.push({ command, logPrefix, useShell, env }); + commandCalls.push({ command, logPrefix, useShell, env: opts?.env }); + void detached; // Handle xcresulttool command if (command.includes('xcresulttool')) { - return { + return createMockCommandResponse({ success: true, output: JSON.stringify({ title: 'Test Results', @@ -500,24 +480,20 @@ describe('test_macos plugin (unified)', () => { expectedFailures: 0, }), error: undefined, - }; + }); } - return { + return createMockCommandResponse({ success: true, output: 'Test Succeeded', error: undefined, - process: { pid: 12345 }, - }; + }); }; // Mock file system dependencies - const mockFileSystemExecutor = { + const mockFileSystemExecutor = createTestFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-abc123', - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + }); const result = await testMacosLogic( { @@ -550,14 +526,11 @@ describe('test_macos plugin (unified)', () => { }); // Mock file system dependencies - mkdtemp fails - const mockFileSystemExecutor = { + const mockFileSystemExecutor = createTestFileSystemExecutor({ mkdtemp: async () => { throw new Error('Network error'); }, - rm: async () => {}, - tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), - }; + }); const result = await testMacosLogic( { diff --git a/src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts b/src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts index 924a03c8..45717b1d 100644 --- a/src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts +++ b/src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts @@ -6,7 +6,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, +} from '../../../../test-utils/mock-executors.ts'; import plugin, { listSchemesLogic } from '../list_schemes.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; @@ -191,12 +194,14 @@ describe('list_schemes plugin', () => { const calls: any[] = []; const mockExecutor = async ( command: string[], - action: string, - showOutput: boolean, - workingDir?: string, + action?: string, + showOutput?: boolean, + opts?: { cwd?: string }, + detached?: boolean, ) => { - calls.push([command, action, showOutput, workingDir]); - return { + calls.push([command, action, showOutput, opts?.cwd]); + void detached; + return createMockCommandResponse({ success: true, output: `Information about project "MyProject": Targets: @@ -209,8 +214,7 @@ describe('list_schemes plugin', () => { Schemes: MyProject`, error: undefined, - process: { pid: 12345 }, - }; + }); }; await listSchemesLogic({ projectPath: '/path/to/MyProject.xcodeproj' }, mockExecutor); @@ -304,19 +308,20 @@ describe('list_schemes plugin', () => { const calls: any[] = []; const mockExecutor = async ( command: string[], - action: string, - showOutput: boolean, - workingDir?: string, + action?: string, + showOutput?: boolean, + opts?: { cwd?: string }, + detached?: boolean, ) => { - calls.push([command, action, showOutput, workingDir]); - return { + calls.push([command, action, showOutput, opts?.cwd]); + void detached; + return createMockCommandResponse({ success: true, output: `Information about workspace "MyWorkspace": Schemes: MyApp`, error: undefined, - process: { pid: 12345 }, - }; + }); }; await listSchemesLogic({ workspacePath: '/path/to/MyProject.xcworkspace' }, mockExecutor); diff --git a/src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts b/src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts index 3f492494..0e4a55ac 100644 --- a/src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts +++ b/src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { createMockExecutor, type CommandExecutor } from '../../../../test-utils/mock-executors.ts'; import plugin, { showBuildSettingsLogic } from '../show_build_settings.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; @@ -76,7 +76,7 @@ describe('show_build_settings plugin', () => { }); // Wrap mockExecutor to track calls - const wrappedExecutor = (...args: any[]) => { + const wrappedExecutor: CommandExecutor = (...args) => { calls.push(args); return mockExecutor(...args); }; @@ -260,7 +260,7 @@ describe('show_build_settings plugin', () => { }); // Wrap mockExecutor to track calls - const wrappedExecutor = (...args: any[]) => { + const wrappedExecutor: CommandExecutor = (...args) => { calls.push(args); return mockExecutor(...args); }; diff --git a/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts b/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts index 08033bd1..92503a00 100644 --- a/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts +++ b/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts @@ -172,6 +172,7 @@ describe('scaffold_ios_project plugin', () => { await scaffold_ios_projectLogic( { projectName: 'TestIOSApp', + customizeNames: true, outputPath: '/tmp/test-projects', }, capturingExecutor, @@ -237,6 +238,7 @@ describe('scaffold_ios_project plugin', () => { await scaffold_ios_projectLogic( { projectName: 'TestIOSApp', + customizeNames: true, outputPath: '/tmp/test-projects', }, capturingExecutor, @@ -275,6 +277,7 @@ describe('scaffold_ios_project plugin', () => { await scaffold_ios_projectLogic( { projectName: 'TestIOSApp', + customizeNames: true, outputPath: '/tmp/test-projects', }, capturingExecutor, @@ -345,6 +348,7 @@ describe('scaffold_ios_project plugin', () => { await scaffold_ios_projectLogic( { projectName: 'TestIOSApp', + customizeNames: true, outputPath: '/tmp/test-projects', }, capturingExecutor, @@ -359,6 +363,9 @@ describe('scaffold_ios_project plugin', () => { expect(curlCommand).toBeDefined(); expect(unzipCommand).toBeDefined(); + if (!curlCommand || !unzipCommand) { + throw new Error('Expected curl and unzip commands to be captured'); + } expect(curlCommand[0]).toBe('curl'); expect(unzipCommand[0]).toBe('unzip'); @@ -372,6 +379,7 @@ describe('scaffold_ios_project plugin', () => { const result = await scaffold_ios_projectLogic( { projectName: 'TestIOSApp', + customizeNames: true, outputPath: '/tmp/test-projects', bundleIdentifier: 'com.test.iosapp', }, @@ -391,8 +399,8 @@ describe('scaffold_ios_project plugin', () => { message: 'Successfully scaffolded iOS project "TestIOSApp" in /tmp/test-projects', nextSteps: [ 'Important: Before working on the project make sure to read the README.md file in the workspace root directory.', - 'Build for simulator: build_sim({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject", simulatorName: "iPhone 16" })', - 'Build and run on simulator: build_run_sim({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject", simulatorName: "iPhone 16" })', + 'Build for simulator: build_sim({ workspacePath: "/tmp/test-projects/TestIOSApp.xcworkspace", scheme: "TestIOSApp", simulatorName: "iPhone 16" })', + 'Build and run on simulator: build_run_sim({ workspacePath: "/tmp/test-projects/TestIOSApp.xcworkspace", scheme: "TestIOSApp", simulatorName: "iPhone 16" })', ], }, null, @@ -407,12 +415,12 @@ describe('scaffold_ios_project plugin', () => { const result = await scaffold_ios_projectLogic( { projectName: 'TestIOSApp', + customizeNames: true, outputPath: '/tmp/test-projects', bundleIdentifier: 'com.test.iosapp', displayName: 'Test iOS App', marketingVersion: '2.0', currentProjectVersion: '5', - customizeNames: true, deploymentTarget: '17.0', targetedDeviceFamily: ['iphone'], supportedOrientations: ['portrait'], @@ -485,6 +493,7 @@ describe('scaffold_ios_project plugin', () => { const result = await scaffold_ios_projectLogic( { projectName: '123InvalidName', + customizeNames: true, outputPath: '/tmp/test-projects', }, mockCommandExecutor, @@ -524,6 +533,7 @@ describe('scaffold_ios_project plugin', () => { const result = await scaffold_ios_projectLogic( { projectName: 'TestIOSApp', + customizeNames: true, outputPath: '/tmp/test-projects', }, mockCommandExecutor, @@ -562,6 +572,7 @@ describe('scaffold_ios_project plugin', () => { const result = await scaffold_ios_projectLogic( { projectName: 'TestIOSApp', + customizeNames: true, outputPath: '/tmp/test-projects', }, failingMockCommandExecutor, @@ -626,6 +637,7 @@ describe('scaffold_ios_project plugin', () => { const result = await scaffold_ios_projectLogic( { projectName: 'TestIOSApp', + customizeNames: true, outputPath: '/tmp/test-projects', }, failingMockCommandExecutor, diff --git a/src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts b/src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts index 69acc1ba..65451c6d 100644 --- a/src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts +++ b/src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts @@ -14,6 +14,7 @@ import { createMockFileSystemExecutor, createNoopExecutor, createMockExecutor, + createMockCommandResponse, } from '../../../../test-utils/mock-executors.ts'; import plugin, { scaffold_macos_projectLogic } from '../scaffold_macos_project.ts'; import { TemplateManager } from '../../../../utils/template/index.ts'; @@ -171,12 +172,10 @@ describe('scaffold_macos_project plugin', () => { let capturedCommands: string[][] = []; const trackingExecutor = async (command: string[]) => { capturedCommands.push(command); - return { + return createMockCommandResponse({ success: true, output: 'Command successful', - error: undefined, - process: { pid: 12345 }, - }; + }); }; // Store original environment variable @@ -200,6 +199,7 @@ describe('scaffold_macos_project plugin', () => { await scaffold_macos_projectLogic( { projectName: 'TestMacApp', + customizeNames: true, outputPath: '/tmp/test-projects', }, trackingExecutor, @@ -228,9 +228,9 @@ describe('scaffold_macos_project plugin', () => { const result = await scaffold_macos_projectLogic( { projectName: 'TestMacApp', + customizeNames: true, outputPath: '/tmp/test-projects', bundleIdentifier: 'com.test.macapp', - customizeNames: false, }, createNoopExecutor(), mockFileSystemExecutor, @@ -248,8 +248,8 @@ describe('scaffold_macos_project plugin', () => { message: 'Successfully scaffolded macOS project "TestMacApp" in /tmp/test-projects', nextSteps: [ 'Important: Before working on the project make sure to read the README.md file in the workspace root directory.', - 'Build for macOS: build_macos({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject" })', - 'Build & Run on macOS: build_run_macos({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject" })', + 'Build for macOS: build_macos({ workspacePath: "/tmp/test-projects/TestMacApp.xcworkspace", scheme: "TestMacApp" })', + 'Build & Run on macOS: build_run_macos({ workspacePath: "/tmp/test-projects/TestMacApp.xcworkspace", scheme: "TestMacApp" })', ], }, null, @@ -304,6 +304,7 @@ describe('scaffold_macos_project plugin', () => { const result = await scaffold_macos_projectLogic( { projectName: '123InvalidName', + customizeNames: true, outputPath: '/tmp/test-projects', }, createNoopExecutor(), @@ -336,6 +337,7 @@ describe('scaffold_macos_project plugin', () => { const result = await scaffold_macos_projectLogic( { projectName: 'TestMacApp', + customizeNames: true, outputPath: '/tmp/test-projects', }, createNoopExecutor(), @@ -366,6 +368,7 @@ describe('scaffold_macos_project plugin', () => { const result = await scaffold_macos_projectLogic( { projectName: 'TestMacApp', + customizeNames: true, outputPath: '/tmp/test-projects', }, createNoopExecutor(), @@ -396,8 +399,8 @@ describe('scaffold_macos_project plugin', () => { await scaffold_macos_projectLogic( { projectName: 'TestApp', - outputPath: '/tmp/test', customizeNames: true, + outputPath: '/tmp/test', }, createNoopExecutor(), mockFileSystemExecutor, diff --git a/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts b/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts index e4162556..379ba4eb 100644 --- a/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts +++ b/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts @@ -31,17 +31,17 @@ describe('session-show-defaults tool', () => { describe('Handler Behavior', () => { it('should return empty defaults when none set', async () => { - const result = await plugin.handler({}); + const result = await plugin.handler(); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(result.content[0].text as string); expect(parsed).toEqual({}); }); it('should return current defaults when set', async () => { sessionStore.setDefaults({ scheme: 'MyScheme', simulatorId: 'SIM-123' }); - const result = await plugin.handler({}); + const result = await plugin.handler(); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(result.content[0].text as string); expect(parsed.scheme).toBe('MyScheme'); expect(parsed.simulatorId).toBe('SIM-123'); }); 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 4fbd2805..d102730d 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 @@ -1,7 +1,10 @@ import { describe, it, expect } from 'vitest'; import * as z from 'zod'; import setSimAppearancePlugin, { set_sim_appearanceLogic } from '../set_sim_appearance.ts'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, +} from '../../../../test-utils/mock-executors.ts'; describe('set_sim_appearance plugin', () => { describe('Export Field Validation (Literal)', () => { @@ -116,12 +119,13 @@ describe('set_sim_appearance plugin', () => { const commandCalls: any[] = []; const mockExecutor = (...args: any[]) => { commandCalls.push(args); - return Promise.resolve({ - success: true, - output: '', - error: '', - process: { pid: 12345 }, - }); + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: '', + error: '', + }), + ); }; await set_sim_appearanceLogic( 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 4179565f..14953edd 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 @@ -6,7 +6,11 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, + createNoopExecutor, +} from '../../../../test-utils/mock-executors.ts'; import setSimLocation, { set_sim_locationLogic } from '../set_sim_location.ts'; describe('set_sim_location tool', () => { @@ -48,12 +52,11 @@ describe('set_sim_location tool', () => { const mockExecutor = async (command: string[]) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'Location set successfully', error: undefined, - process: { pid: 12345 }, - }; + }); }; await set_sim_locationLogic( @@ -80,12 +83,11 @@ describe('set_sim_location tool', () => { const mockExecutor = async (command: string[]) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'Location set successfully', error: undefined, - process: { pid: 12345 }, - }; + }); }; await set_sim_locationLogic( @@ -112,12 +114,11 @@ describe('set_sim_location tool', () => { const mockExecutor = async (command: string[]) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'Location set successfully', error: undefined, - process: { pid: 12345 }, - }; + }); }; await set_sim_locationLogic( @@ -360,12 +361,11 @@ describe('set_sim_location tool', () => { const mockExecutor = async (...args: any[]) => { capturedArgs = args; - return { + return createMockCommandResponse({ success: true, output: 'Location set successfully', error: undefined, - process: { pid: 12345 }, - }; + }); }; await set_sim_locationLogic( 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 b4795c7e..55bc073d 100644 --- a/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts @@ -6,7 +6,11 @@ import { describe, it, expect } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor, type CommandExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, + type CommandExecutor, +} from '../../../../test-utils/mock-executors.ts'; import simStatusbar, { sim_statusbarLogic } from '../sim_statusbar.ts'; describe('sim_statusbar tool', () => { @@ -161,24 +165,25 @@ describe('sim_statusbar tool', () => { it('should verify command generation with mock executor for override', async () => { const calls: Array<{ command: string[]; - operationDescription: string; - keepAlive: boolean; - timeout: number | undefined; + operationDescription?: string; + keepAlive?: boolean; + opts?: { cwd?: string }; }> = []; const mockExecutor: CommandExecutor = async ( command, operationDescription, keepAlive, - timeout, + opts, + detached, ) => { - calls.push({ command, operationDescription, keepAlive, timeout }); - return { + calls.push({ command, operationDescription, keepAlive, opts }); + void detached; + return createMockCommandResponse({ success: true, output: 'Status bar set successfully', error: undefined, - process: { pid: 12345 }, - }; + }); }; await sim_statusbarLogic( @@ -202,31 +207,32 @@ describe('sim_statusbar tool', () => { ], operationDescription: 'Set Status Bar', keepAlive: true, - timeout: undefined, + opts: undefined, }); }); it('should verify command generation for clear operation', async () => { const calls: Array<{ command: string[]; - operationDescription: string; - keepAlive: boolean; - timeout: number | undefined; + operationDescription?: string; + keepAlive?: boolean; + opts?: { cwd?: string }; }> = []; const mockExecutor: CommandExecutor = async ( command, operationDescription, keepAlive, - timeout, + opts, + detached, ) => { - calls.push({ command, operationDescription, keepAlive, timeout }); - return { + calls.push({ command, operationDescription, keepAlive, opts }); + void detached; + return createMockCommandResponse({ success: true, output: 'Status bar cleared successfully', error: undefined, - process: { pid: 12345 }, - }; + }); }; await sim_statusbarLogic( @@ -242,7 +248,7 @@ describe('sim_statusbar tool', () => { command: ['xcrun', 'simctl', 'status_bar', 'test-uuid-123', 'clear'], operationDescription: 'Set Status Bar', keepAlive: true, - timeout: undefined, + opts: undefined, }); }); diff --git a/src/mcp/tools/simulator/__tests__/boot_sim.test.ts b/src/mcp/tools/simulator/__tests__/boot_sim.test.ts index 939eccec..070890e6 100644 --- a/src/mcp/tools/simulator/__tests__/boot_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/boot_sim.test.ts @@ -5,7 +5,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, +} from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import bootSim, { boot_simLogic } from '../boot_sim.ts'; @@ -120,23 +123,24 @@ describe('boot_sim tool', () => { it('should verify command generation with mock executor', async () => { const calls: Array<{ command: string[]; - description: string; - allowStderr: boolean; - timeout?: number; + description?: string; + allowStderr?: boolean; + opts?: { cwd?: string }; }> = []; const mockExecutor = async ( command: string[], - description: string, - allowStderr: boolean, - timeout?: number, + description?: string, + allowStderr?: boolean, + opts?: { cwd?: string }, + detached?: boolean, ) => { - calls.push({ command, description, allowStderr, timeout }); - return { + calls.push({ command, description, allowStderr, opts }); + void detached; + return createMockCommandResponse({ success: true, output: 'Simulator booted successfully', error: undefined, - process: { pid: 12345 }, - }; + }); }; await boot_simLogic({ simulatorId: 'test-uuid-123' }, mockExecutor); @@ -146,7 +150,7 @@ describe('boot_sim tool', () => { command: ['xcrun', 'simctl', 'boot', 'test-uuid-123'], description: 'Boot Simulator', allowStderr: true, - timeout: undefined, + opts: undefined, }); }); }); diff --git a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts index 30f37000..57e9bbc7 100644 --- a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts @@ -5,7 +5,11 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockExecutor, + createMockCommandResponse, +} from '../../../../test-utils/mock-executors.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import buildRunSim, { build_run_simLogic } from '../build_run_sim.ts'; @@ -58,28 +62,25 @@ describe('build_run_sim tool', () => { it('should handle simulator not found', async () => { let callCount = 0; - const mockExecutor = async (command: string[]) => { + const mockExecutor: CommandExecutor = async (command) => { callCount++; if (callCount === 1) { // First call: build succeeds - return { + return createMockCommandResponse({ success: true, output: 'BUILD SUCCEEDED', - process: { pid: 12345 }, - }; + }); } else if (callCount === 2) { // Second call: showBuildSettings fails to get app path - return { + return createMockCommandResponse({ success: false, error: 'Could not get build settings', - process: { pid: 12345 }, - }; + }); } - return { + return createMockCommandResponse({ success: false, error: 'Unexpected call', - process: { pid: 12345 }, - }; + }); }; const result = await build_run_simLogic( @@ -125,26 +126,24 @@ describe('build_run_sim tool', () => { it('should handle successful build and run', async () => { // Create a mock executor that simulates full successful flow let callCount = 0; - const mockExecutor = async (command: string[], logPrefix?: string) => { + const mockExecutor: CommandExecutor = async (command) => { callCount++; if (command.includes('xcodebuild') && command.includes('build')) { // First call: build succeeds - return { + return createMockCommandResponse({ success: true, output: 'BUILD SUCCEEDED', - process: { pid: 12345 }, - }; + }); } else if (command.includes('xcodebuild') && command.includes('-showBuildSettings')) { // Second call: build settings to get app path - return { + return createMockCommandResponse({ success: true, output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app\n', - process: { pid: 12345 }, - }; + }); } else if (command.includes('simctl') && command.includes('list')) { // Find simulator calls - return { + return createMockCommandResponse({ success: true, output: JSON.stringify({ devices: { @@ -158,26 +157,23 @@ describe('build_run_sim tool', () => { ], }, }), - process: { pid: 12345 }, - }; + }); } else if ( command.includes('plutil') || command.includes('PlistBuddy') || command.includes('defaults') ) { // Bundle ID extraction - return { + return createMockCommandResponse({ success: true, output: 'com.example.MyApp', - process: { pid: 12345 }, - }; + }); } else { // All other commands (boot, open, install, launch) succeed - return { + return createMockCommandResponse({ success: true, output: 'Success', - process: { pid: 12345 }, - }; + }); } }; @@ -242,23 +238,17 @@ describe('build_run_sim tool', () => { command: string[]; logPrefix?: string; useShell?: boolean; - env?: any; + opts?: { env?: Record; cwd?: string }; }> = []; // Create tracking executor - const trackingExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: Record, - ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return { + const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { + callHistory.push({ command, logPrefix, useShell, opts }); + return createMockCommandResponse({ success: false, output: '', error: 'Test error to stop execution early', - process: { pid: 12345 }, - }; + }); }; const result = await build_run_simLogic( @@ -293,23 +283,18 @@ describe('build_run_sim tool', () => { command: string[]; logPrefix?: string; useShell?: boolean; - env?: any; + opts?: { env?: Record; cwd?: string }; }> = []; let callCount = 0; // Create tracking executor that succeeds on first call (list) and fails on second - const trackingExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: Record, - ) => { - callHistory.push({ command, logPrefix, useShell, env }); + const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { + callHistory.push({ command, logPrefix, useShell, opts }); callCount++; if (callCount === 1) { // First call: simulator list succeeds - return { + return createMockCommandResponse({ success: true, output: JSON.stringify({ devices: { @@ -323,16 +308,14 @@ describe('build_run_sim tool', () => { }, }), error: undefined, - process: { pid: 12345 }, - }; + }); } else { // Second call: build command fails to stop execution - return { + return createMockCommandResponse({ success: false, output: '', error: 'Test error to stop execution', - process: { pid: 12345 }, - }; + }); } }; @@ -385,23 +368,18 @@ describe('build_run_sim tool', () => { command: string[]; logPrefix?: string; useShell?: boolean; - env?: any; + opts?: { env?: Record; cwd?: string }; }> = []; let callCount = 0; // Create tracking executor that succeeds on first two calls and fails on third - const trackingExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: Record, - ) => { - callHistory.push({ command, logPrefix, useShell, env }); + const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { + callHistory.push({ command, logPrefix, useShell, opts }); callCount++; if (callCount === 1) { // First call: simulator list succeeds - return { + return createMockCommandResponse({ success: true, output: JSON.stringify({ devices: { @@ -415,24 +393,21 @@ describe('build_run_sim tool', () => { }, }), error: undefined, - process: { pid: 12345 }, - }; + }); } else if (callCount === 2) { // Second call: build command succeeds - return { + return createMockCommandResponse({ success: true, output: 'BUILD SUCCEEDED', error: undefined, - process: { pid: 12345 }, - }; + }); } else { // Third call: build settings command fails to stop execution - return { + return createMockCommandResponse({ success: false, output: '', error: 'Test error to stop execution', - process: { pid: 12345 }, - }; + }); } }; @@ -487,23 +462,17 @@ describe('build_run_sim tool', () => { command: string[]; logPrefix?: string; useShell?: boolean; - env?: any; + opts?: { env?: Record; cwd?: string }; }> = []; // Create tracking executor - const trackingExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: Record, - ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return { + const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { + callHistory.push({ command, logPrefix, useShell, opts }); + return createMockCommandResponse({ success: false, output: '', error: 'Test error to stop execution early', - process: { pid: 12345 }, - }; + }); }; const result = await build_run_simLogic( diff --git a/src/mcp/tools/simulator/__tests__/build_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_sim.test.ts index 24b6f32e..5418ac0b 100644 --- a/src/mcp/tools/simulator/__tests__/build_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_sim.test.ts @@ -1,6 +1,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockExecutor, + createMockCommandResponse, +} from '../../../../test-utils/mock-executors.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; // Import the plugin and logic function @@ -193,23 +197,17 @@ describe('build_sim tool', () => { command: string[]; logPrefix?: string; useShell?: boolean; - env?: any; + opts?: { env?: Record; cwd?: string }; }> = []; // Create tracking executor - const trackingExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: Record, - ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return { + const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { + callHistory.push({ command, logPrefix, useShell, opts }); + return createMockCommandResponse({ success: false, output: '', error: 'Test error to stop execution early', - process: { pid: 12345 }, - }; + }); }; const result = await build_simLogic( @@ -244,23 +242,17 @@ describe('build_sim tool', () => { command: string[]; logPrefix?: string; useShell?: boolean; - env?: any; + opts?: { env?: Record; cwd?: string }; }> = []; // Create tracking executor - const trackingExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: Record, - ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return { + const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { + callHistory.push({ command, logPrefix, useShell, opts }); + return createMockCommandResponse({ success: false, output: '', error: 'Test error to stop execution early', - process: { pid: 12345 }, - }; + }); }; const result = await build_simLogic( @@ -295,23 +287,17 @@ describe('build_sim tool', () => { command: string[]; logPrefix?: string; useShell?: boolean; - env?: any; + opts?: { env?: Record; cwd?: string }; }> = []; // Create tracking executor - const trackingExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: Record, - ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return { + const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { + callHistory.push({ command, logPrefix, useShell, opts }); + return createMockCommandResponse({ success: false, output: '', error: 'Test error to stop execution early', - process: { pid: 12345 }, - }; + }); }; const result = await build_simLogic( @@ -353,23 +339,17 @@ describe('build_sim tool', () => { command: string[]; logPrefix?: string; useShell?: boolean; - env?: any; + opts?: { env?: Record; cwd?: string }; }> = []; // Create tracking executor - const trackingExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: Record, - ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return { + const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { + callHistory.push({ command, logPrefix, useShell, opts }); + return createMockCommandResponse({ success: false, output: '', error: 'Test error to stop execution early', - process: { pid: 12345 }, - }; + }); }; const result = await build_simLogic( @@ -404,23 +384,17 @@ describe('build_sim tool', () => { command: string[]; logPrefix?: string; useShell?: boolean; - env?: any; + opts?: { env?: Record; cwd?: string }; }> = []; // Create tracking executor - const trackingExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: Record, - ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return { + const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { + callHistory.push({ command, logPrefix, useShell, opts }); + return createMockCommandResponse({ success: false, output: '', error: 'Test error to stop execution early', - process: { pid: 12345 }, - }; + }); }; const result = await build_simLogic( 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 5989b0a1..bd5ca9b7 100644 --- a/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts @@ -4,8 +4,10 @@ import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, + createMockCommandResponse, } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; import installAppSim, { install_app_simLogic } from '../install_app_sim.ts'; describe('install_app_sim tool', () => { @@ -65,15 +67,15 @@ describe('install_app_sim tool', () => { describe('Command Generation', () => { it('should generate correct simctl install command', async () => { - const executorCalls: unknown[] = []; - const mockExecutor = (...args: unknown[]) => { + const executorCalls: Array> = []; + const mockExecutor: CommandExecutor = (...args) => { executorCalls.push(args); - return Promise.resolve({ - success: true, - output: 'App installed', - error: undefined, - process: { pid: 12345 }, - }); + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: 'App installed', + }), + ); }; const mockFileSystem = createMockFileSystemExecutor({ @@ -106,15 +108,15 @@ describe('install_app_sim tool', () => { }); it('should generate command with different simulator identifier', async () => { - const executorCalls: unknown[] = []; - const mockExecutor = (...args: unknown[]) => { + const executorCalls: Array> = []; + const mockExecutor: CommandExecutor = (...args) => { executorCalls.push(args); - return Promise.resolve({ - success: true, - output: 'App installed', - error: undefined, - process: { pid: 12345 }, - }); + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: 'App installed', + }), + ); }; const mockFileSystem = createMockFileSystemExecutor({ @@ -174,27 +176,29 @@ describe('install_app_sim tool', () => { }); it('should handle bundle id extraction failure gracefully', async () => { - const bundleIdCalls: unknown[] = []; - const mockExecutor = (...args: unknown[]) => { + const bundleIdCalls: Array> = []; + const mockExecutor: CommandExecutor = (...args) => { 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( + createMockCommandResponse({ + success: true, + output: 'App installed', + error: undefined, + }), + ); } - return Promise.resolve({ - success: false, - output: '', - error: 'Failed to read bundle ID', - process: { pid: 12345 }, - }); + return Promise.resolve( + createMockCommandResponse({ + success: false, + output: '', + error: 'Failed to read bundle ID', + }), + ); }; const mockFileSystem = createMockFileSystemExecutor({ @@ -228,27 +232,29 @@ describe('install_app_sim tool', () => { }); it('should include bundle id when extraction succeeds', async () => { - const bundleIdCalls: unknown[] = []; - const mockExecutor = (...args: unknown[]) => { + const bundleIdCalls: Array> = []; + const mockExecutor: CommandExecutor = (...args) => { bundleIdCalls.push(args); if ( Array.isArray(args[0]) && (args[0] as string[])[0] === 'xcrun' && (args[0] as string[])[1] === 'simctl' ) { - return Promise.resolve({ + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: 'App installed', + error: undefined, + }), + ); + } + return Promise.resolve( + createMockCommandResponse({ success: true, - output: 'App installed', + output: 'com.example.myapp', error: undefined, - process: { pid: 12345 }, - }); - } - return Promise.resolve({ - success: true, - output: 'com.example.myapp', - error: undefined, - process: { pid: 12345 }, - }); + }), + ); }; const mockFileSystem = createMockFileSystemExecutor({ @@ -282,13 +288,14 @@ describe('install_app_sim tool', () => { }); it('should handle command failure', async () => { - const mockExecutor = () => - Promise.resolve({ - success: false, - output: '', - error: 'Install failed', - process: { pid: 12345 }, - }); + const mockExecutor: CommandExecutor = () => + Promise.resolve( + createMockCommandResponse({ + success: false, + output: '', + error: 'Install failed', + }), + ); const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, diff --git a/src/mcp/tools/simulator/__tests__/list_sims.test.ts b/src/mcp/tools/simulator/__tests__/list_sims.test.ts index c0846482..cebcf4aa 100644 --- a/src/mcp/tools/simulator/__tests__/list_sims.test.ts +++ b/src/mcp/tools/simulator/__tests__/list_sims.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; import { + createMockCommandResponse, createMockExecutor, - createMockFileSystemExecutor, } from '../../../../test-utils/mock-executors.ts'; // Import the plugin and logic function @@ -71,27 +71,27 @@ describe('list_sims tool', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record }, + detached?: boolean, ) => { - callHistory.push({ command, logPrefix, useShell, env }); + callHistory.push({ command, logPrefix, useShell, env: opts?.env }); + void detached; // Return JSON output for JSON command if (command.includes('--json')) { - return { + return createMockCommandResponse({ success: true, output: mockJsonOutput, error: undefined, - process: { pid: 12345 }, - }; + }); } // Return text output for text command - return { + return createMockCommandResponse({ success: true, output: mockTextOutput, error: undefined, - process: { pid: 12345 }, - }; + }); }; const result = await list_simsLogic({ enabled: true }, mockExecutor); @@ -150,19 +150,17 @@ Next Steps: const mockExecutor = async (command: string[]) => { if (command.includes('--json')) { - return { + return createMockCommandResponse({ success: true, output: mockJsonOutput, error: undefined, - process: { pid: 12345 }, - }; + }); } - return { + return createMockCommandResponse({ success: true, output: mockTextOutput, error: undefined, - process: { pid: 12345 }, - }; + }); }; const result = await list_simsLogic({ enabled: true }, mockExecutor); @@ -208,19 +206,17 @@ Next Steps: const mockExecutor = async (command: string[]) => { if (command.includes('--json')) { - return { + return createMockCommandResponse({ success: true, output: mockJsonOutput, error: undefined, - process: { pid: 12345 }, - }; + }); } - return { + return createMockCommandResponse({ success: true, output: mockTextOutput, error: undefined, - process: { pid: 12345 }, - }; + }); }; const result = await list_simsLogic({ enabled: true }, mockExecutor); @@ -276,21 +272,19 @@ Next Steps: const mockExecutor = async (command: string[]) => { // JSON command returns invalid JSON if (command.includes('--json')) { - return { + return createMockCommandResponse({ success: true, output: 'invalid json', error: undefined, - process: { pid: 12345 }, - }; + }); } // Text command returns valid text output - return { + return createMockCommandResponse({ success: true, output: mockTextOutput, error: undefined, - process: { pid: 12345 }, - }; + }); }; const result = await list_simsLogic({ enabled: true }, mockExecutor); diff --git a/src/mcp/tools/simulator/__tests__/open_sim.test.ts b/src/mcp/tools/simulator/__tests__/open_sim.test.ts index 624221de..22e25df5 100644 --- a/src/mcp/tools/simulator/__tests__/open_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/open_sim.test.ts @@ -6,7 +6,11 @@ import { describe, it, expect } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor, type CommandExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockCommandResponse, + createMockExecutor, + type CommandExecutor, +} from '../../../../test-utils/mock-executors.ts'; import openSim, { open_simLogic } from '../open_sim.ts'; describe('open_sim tool', () => { @@ -131,24 +135,25 @@ describe('open_sim tool', () => { it('should verify command generation with mock executor', async () => { const calls: Array<{ command: string[]; - description: string; - hideOutput: boolean; - workingDirectory: string | undefined; + description?: string; + hideOutput?: boolean; + opts?: { cwd?: string }; }> = []; const mockExecutor: CommandExecutor = async ( command, description, hideOutput, - workingDirectory, + opts, + detached, ) => { - calls.push({ command, description, hideOutput, workingDirectory }); - return { + calls.push({ command, description, hideOutput, opts }); + void detached; + return createMockCommandResponse({ success: true, output: '', error: undefined, - process: { pid: 12345 }, - }; + }); }; await open_simLogic({}, mockExecutor); @@ -158,7 +163,7 @@ describe('open_sim tool', () => { command: ['open', '-a', 'Simulator'], description: 'Open Simulator', hideOutput: true, - workingDirectory: undefined, + opts: undefined, }); }); }); diff --git a/src/mcp/tools/simulator/__tests__/record_sim_video.test.ts b/src/mcp/tools/simulator/__tests__/record_sim_video.test.ts index fbd8d65e..2556172b 100644 --- a/src/mcp/tools/simulator/__tests__/record_sim_video.test.ts +++ b/src/mcp/tools/simulator/__tests__/record_sim_video.test.ts @@ -53,7 +53,7 @@ describe('record_sim_video logic - start behavior', () => { areAxeToolsAvailable: () => true, isAxeAtLeastVersion: async () => true, createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'AXe not available' }], + content: [{ type: 'text' as const, text: 'AXe not available' }], isError: true, }), }; @@ -105,7 +105,7 @@ describe('record_sim_video logic - end-to-end stop with rename', () => { areAxeToolsAvailable: () => true, isAxeAtLeastVersion: async () => true, createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'AXe not available' }], + content: [{ type: 'text' as const, text: 'AXe not available' }], isError: true, }), }; @@ -153,7 +153,7 @@ describe('record_sim_video logic - version gate', () => { areAxeToolsAvailable: () => true, isAxeAtLeastVersion: async () => false, createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'AXe not available' }], + content: [{ type: 'text' as const, text: 'AXe not available' }], isError: true, }), }; diff --git a/src/mcp/tools/simulator/__tests__/screenshot.test.ts b/src/mcp/tools/simulator/__tests__/screenshot.test.ts index 3c10dfc3..5562f38d 100644 --- a/src/mcp/tools/simulator/__tests__/screenshot.test.ts +++ b/src/mcp/tools/simulator/__tests__/screenshot.test.ts @@ -11,6 +11,7 @@ import { createMockFileSystemExecutor, createCommandMatchingMockExecutor, } from '../../../../test-utils/mock-executors.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; import { SystemError } from '../../../../utils/responses/index.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import screenshotPlugin, { screenshotLogic } from '../../ui-testing/screenshot.ts'; @@ -377,7 +378,7 @@ describe('screenshot plugin', () => { }); // Wrap to capture both command executions - const capturingExecutor = async (...args: any[]) => { + const capturingExecutor: CommandExecutor = async (...args) => { capturedArgs.push(args); return mockExecutor(...args); }; 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 c8572670..6b174976 100644 --- a/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts @@ -1,6 +1,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockExecutor, + createMockCommandResponse, +} from '../../../../test-utils/mock-executors.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import plugin, { stop_app_simLogic } from '../stop_app_sim.ts'; @@ -241,24 +245,25 @@ describe('stop_app_sim tool', () => { it('should call correct terminate command', async () => { const calls: Array<{ command: string[]; - description: string; - suppressErrorLogging: boolean; - timeout?: number; + logPrefix?: string; + useShell?: boolean; + opts?: { env?: Record; cwd?: string }; + detached?: boolean; }> = []; - const trackingExecutor = async ( - command: string[], - description: string, - suppressErrorLogging: boolean, - timeout?: number, + const trackingExecutor: CommandExecutor = async ( + command, + logPrefix, + useShell, + opts, + detached, ) => { - calls.push({ command, description, suppressErrorLogging, timeout }); - return { + calls.push({ command, logPrefix, useShell, opts, detached }); + return createMockCommandResponse({ success: true, output: '', error: undefined, - process: { pid: 12345 }, - }; + }); }; await stop_app_simLogic( @@ -272,9 +277,10 @@ describe('stop_app_sim tool', () => { expect(calls).toEqual([ { command: ['xcrun', 'simctl', 'terminate', 'test-uuid', 'com.example.App'], - description: 'Stop App in Simulator', - suppressErrorLogging: true, - timeout: undefined, + logPrefix: 'Stop App in Simulator', + useShell: true, + opts: undefined, + detached: undefined, }, ]); }); diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts index 180d4baa..3fc12358 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts @@ -9,8 +9,10 @@ import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, + createMockCommandResponse, } from '../../../../test-utils/mock-executors.ts'; import swiftPackageBuild, { swift_package_buildLogic } from '../swift_package_build.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; describe('swift_package_build plugin', () => { describe('Export Field Validation (Literal)', () => { @@ -58,14 +60,13 @@ describe('swift_package_build plugin', () => { describe('Command Generation Testing', () => { it('should build correct command for basic build', async () => { - const executor = async (args: any, description: any, useShell: any, cwd: any) => { - executorCalls.push({ args, description, useShell, cwd }); - return { + const executor: CommandExecutor = async (args, description, useShell, opts) => { + executorCalls.push({ args, description, useShell, cwd: opts?.cwd }); + return createMockCommandResponse({ success: true, output: 'Build succeeded', error: undefined, - process: { pid: 12345 }, - }; + }); }; await swift_package_buildLogic( @@ -86,14 +87,13 @@ describe('swift_package_build plugin', () => { }); it('should build correct command with release configuration', async () => { - const executor = async (args: any, description: any, useShell: any, cwd: any) => { - executorCalls.push({ args, description, useShell, cwd }); - return { + const executor: CommandExecutor = async (args, description, useShell, opts) => { + executorCalls.push({ args, description, useShell, cwd: opts?.cwd }); + return createMockCommandResponse({ success: true, output: 'Build succeeded', error: undefined, - process: { pid: 12345 }, - }; + }); }; await swift_package_buildLogic( @@ -115,14 +115,13 @@ describe('swift_package_build plugin', () => { }); it('should build correct command with all parameters', async () => { - const executor = async (args: any, description: any, useShell: any, cwd: any) => { - executorCalls.push({ args, description, useShell, cwd }); - return { + const executor: CommandExecutor = async (args, description, useShell, opts) => { + executorCalls.push({ args, description, useShell, cwd: opts?.cwd }); + return createMockCommandResponse({ success: true, output: 'Build succeeded', error: undefined, - process: { pid: 12345 }, - }; + }); }; await swift_package_buildLogic( diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts index d443a1b1..328cadbf 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts @@ -9,8 +9,10 @@ import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, + createMockCommandResponse, } from '../../../../test-utils/mock-executors.ts'; import swiftPackageClean, { swift_package_cleanLogic } from '../swift_package_clean.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; describe('swift_package_clean plugin', () => { describe('Export Field Validation (Literal)', () => { @@ -43,24 +45,18 @@ describe('swift_package_clean plugin', () => { it('should build correct command for clean', async () => { const calls: Array<{ command: string[]; - description: string; - showOutput: boolean; - workingDirectory: string | undefined; + description?: string; + useShell?: boolean; + opts?: { env?: Record; cwd?: string }; }> = []; - const mockExecutor = async ( - command: string[], - description: string, - showOutput: boolean, - workingDirectory?: string, - ) => { - calls.push({ command, description, showOutput, workingDirectory }); - return { + const mockExecutor: CommandExecutor = async (command, description, useShell, opts) => { + calls.push({ command, description, useShell, opts }); + return createMockCommandResponse({ success: true, output: 'Clean succeeded', error: undefined, - process: { pid: 12345 }, - }; + }); }; await swift_package_cleanLogic( @@ -74,8 +70,8 @@ describe('swift_package_clean plugin', () => { expect(calls[0]).toEqual({ command: ['swift', 'package', '--package-path', '/test/package', 'clean'], description: 'Swift Package Clean', - showOutput: true, - workingDirectory: undefined, + useShell: true, + opts: undefined, }); }); }); diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_list.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_list.test.ts index b4f1d4be..0af914b3 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_list.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_list.test.ts @@ -203,7 +203,10 @@ describe('swift_package_list plugin', () => { }; // Create mock process map with multiple processes - const mockProcessMap = new Map([ + const mockProcessMap = new Map< + number, + { executableName?: string; packagePath: string; startedAt: Date } + >([ [12345, mockProcess1], [12346, mockProcess2], ]); @@ -231,16 +234,19 @@ describe('swift_package_list plugin', () => { }); }); - it('should handle process with null executableName', async () => { + it('should handle process with missing executableName', async () => { const startedAt = new Date('2023-01-01T10:00:00.000Z'); const mockProcess = { - executableName: null, // Test null executable name + executableName: undefined, // Test missing executable name packagePath: '/test/package', startedAt: startedAt, }; // Create mock process map with one process - const mockProcessMap = new Map([[12345, mockProcess]]); + const mockProcessMap = new Map< + number, + { executableName?: string; packagePath: string; startedAt: Date } + >([[12345, mockProcess]]); // Use pure dependency injection with stub functions const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries); diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts index 3c0af421..15d4211d 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts @@ -6,8 +6,13 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockExecutor, + createNoopExecutor, + createMockCommandResponse, +} from '../../../../test-utils/mock-executors.ts'; import swiftPackageRun, { swift_package_runLogic } from '../swift_package_run.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; describe('swift_package_run plugin', () => { describe('Export Field Validation (Literal)', () => { @@ -73,19 +78,15 @@ describe('swift_package_run plugin', () => { describe('Command Generation Testing', () => { it('should build correct command for basic run (foreground mode)', async () => { - const mockExecutor = ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: any, - ) => { - executorCalls.push({ command, logPrefix, useShell, env }); - return Promise.resolve({ - success: true, - output: 'Process completed', - error: undefined, - process: { pid: 12345 }, - }); + const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => { + executorCalls.push({ command, logPrefix, useShell, opts }); + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: 'Process completed', + error: undefined, + }), + ); }; await swift_package_runLogic( @@ -99,24 +100,20 @@ describe('swift_package_run plugin', () => { command: ['swift', 'run', '--package-path', '/test/package'], logPrefix: 'Swift Package Run', useShell: true, - env: undefined, + opts: undefined, }); }); it('should build correct command with release configuration', async () => { - const mockExecutor = ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: any, - ) => { - executorCalls.push({ command, logPrefix, useShell, env }); - return Promise.resolve({ - success: true, - output: 'Process completed', - error: undefined, - process: { pid: 12345 }, - }); + const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => { + executorCalls.push({ command, logPrefix, useShell, opts }); + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: 'Process completed', + error: undefined, + }), + ); }; await swift_package_runLogic( @@ -131,24 +128,20 @@ describe('swift_package_run plugin', () => { command: ['swift', 'run', '--package-path', '/test/package', '-c', 'release'], logPrefix: 'Swift Package Run', useShell: true, - env: undefined, + opts: undefined, }); }); it('should build correct command with executable name', async () => { - const mockExecutor = ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: any, - ) => { - executorCalls.push({ command, logPrefix, useShell, env }); - return Promise.resolve({ - success: true, - output: 'Process completed', - error: undefined, - process: { pid: 12345 }, - }); + const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => { + executorCalls.push({ command, logPrefix, useShell, opts }); + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: 'Process completed', + error: undefined, + }), + ); }; await swift_package_runLogic( @@ -163,24 +156,20 @@ describe('swift_package_run plugin', () => { command: ['swift', 'run', '--package-path', '/test/package', 'MyApp'], logPrefix: 'Swift Package Run', useShell: true, - env: undefined, + opts: undefined, }); }); it('should build correct command with arguments', async () => { - const mockExecutor = ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: any, - ) => { - executorCalls.push({ command, logPrefix, useShell, env }); - return Promise.resolve({ - success: true, - output: 'Process completed', - error: undefined, - process: { pid: 12345 }, - }); + const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => { + executorCalls.push({ command, logPrefix, useShell, opts }); + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: 'Process completed', + error: undefined, + }), + ); }; await swift_package_runLogic( @@ -195,24 +184,20 @@ describe('swift_package_run plugin', () => { command: ['swift', 'run', '--package-path', '/test/package', '--', 'arg1', 'arg2'], logPrefix: 'Swift Package Run', useShell: true, - env: undefined, + opts: undefined, }); }); it('should build correct command with parseAsLibrary flag', async () => { - const mockExecutor = ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: any, - ) => { - executorCalls.push({ command, logPrefix, useShell, env }); - return Promise.resolve({ - success: true, - output: 'Process completed', - error: undefined, - process: { pid: 12345 }, - }); + const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => { + executorCalls.push({ command, logPrefix, useShell, opts }); + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: 'Process completed', + error: undefined, + }), + ); }; await swift_package_runLogic( @@ -234,24 +219,20 @@ describe('swift_package_run plugin', () => { ], logPrefix: 'Swift Package Run', useShell: true, - env: undefined, + opts: undefined, }); }); it('should build correct command with all parameters', async () => { - const mockExecutor = ( - command: string[], - logPrefix?: string, - useShell?: boolean, - env?: any, - ) => { - executorCalls.push({ command, logPrefix, useShell, env }); - return Promise.resolve({ - success: true, - output: 'Process completed', - error: undefined, - process: { pid: 12345 }, - }); + const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => { + executorCalls.push({ command, logPrefix, useShell, opts }); + return Promise.resolve( + createMockCommandResponse({ + success: true, + output: 'Process completed', + error: undefined, + }), + ); }; await swift_package_runLogic( @@ -281,7 +262,7 @@ describe('swift_package_run plugin', () => { ], logPrefix: 'Swift Package Run', useShell: true, - env: undefined, + opts: undefined, }); }); diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts index 4ad8c6c3..0112e2df 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts @@ -9,8 +9,10 @@ import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, + createMockCommandResponse, } from '../../../../test-utils/mock-executors.ts'; import swiftPackageTest, { swift_package_testLogic } from '../swift_package_test.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; describe('swift_package_test plugin', () => { describe('Export Field Validation (Literal)', () => { @@ -57,20 +59,19 @@ describe('swift_package_test plugin', () => { describe('Command Generation Testing', () => { it('should build correct command for basic test', async () => { - const calls: any[] = []; - const mockExecutor = async ( - args: string[], - name: string, - hideOutput: boolean, - workingDir: string | undefined, - ) => { - calls.push({ args, name, hideOutput, workingDir }); - return { + const calls: Array<{ + args: string[]; + name?: string; + hideOutput?: boolean; + opts?: { env?: Record; cwd?: string }; + }> = []; + const mockExecutor: CommandExecutor = async (args, name, hideOutput, opts) => { + calls.push({ args, name, hideOutput, opts }); + return createMockCommandResponse({ success: true, output: 'Test Passed', error: undefined, - process: { pid: 12345 }, - }; + }); }; await swift_package_testLogic( @@ -85,25 +86,24 @@ describe('swift_package_test plugin', () => { args: ['swift', 'test', '--package-path', '/test/package'], name: 'Swift Package Test', hideOutput: true, - workingDir: undefined, + opts: undefined, }); }); it('should build correct command with all parameters', async () => { - const calls: any[] = []; - const mockExecutor = async ( - args: string[], - name: string, - hideOutput: boolean, - workingDir: string | undefined, - ) => { - calls.push({ args, name, hideOutput, workingDir }); - return { + const calls: Array<{ + args: string[]; + name?: string; + hideOutput?: boolean; + opts?: { env?: Record; cwd?: string }; + }> = []; + const mockExecutor: CommandExecutor = async (args, name, hideOutput, opts) => { + calls.push({ args, name, hideOutput, opts }); + return createMockCommandResponse({ success: true, output: 'Tests completed', error: undefined, - process: { pid: 12345 }, - }; + }); }; await swift_package_testLogic( @@ -139,7 +139,7 @@ describe('swift_package_test plugin', () => { ], name: 'Swift Package Test', hideOutput: true, - workingDir: undefined, + opts: undefined, }); }); }); diff --git a/src/mcp/tools/ui-testing/__tests__/button.test.ts b/src/mcp/tools/ui-testing/__tests__/button.test.ts index 30c02cb1..bb646102 100644 --- a/src/mcp/tools/ui-testing/__tests__/button.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/button.test.ts @@ -4,8 +4,13 @@ import { describe, it, expect } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockExecutor, + createNoopExecutor, + createMockCommandResponse, +} from '../../../../test-utils/mock-executors.ts'; import buttonPlugin, { buttonLogic } from '../button.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; describe('Button Plugin', () => { describe('Export Field Validation (Literal)', () => { @@ -45,21 +50,20 @@ describe('Button Plugin', () => { describe('Command Generation', () => { it('should generate correct axe command for basic button press', async () => { let capturedCommand: string[] = []; - const trackingExecutor = async (command: string[]) => { + const trackingExecutor: CommandExecutor = async (command) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'button press completed', error: undefined, - process: { pid: 12345 }, - }; + }); }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'axe not available' }], + content: [{ type: 'text' as const, text: 'axe not available' }], isError: true, }), }; @@ -84,21 +88,20 @@ describe('Button Plugin', () => { it('should generate correct axe command for button press with duration', async () => { let capturedCommand: string[] = []; - const trackingExecutor = async (command: string[]) => { + const trackingExecutor: CommandExecutor = async (command) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'button press completed', error: undefined, - process: { pid: 12345 }, - }; + }); }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'axe not available' }], + content: [{ type: 'text' as const, text: 'axe not available' }], isError: true, }), }; @@ -126,21 +129,20 @@ describe('Button Plugin', () => { it('should generate correct axe command for different button types', async () => { let capturedCommand: string[] = []; - const trackingExecutor = async (command: string[]) => { + const trackingExecutor: CommandExecutor = async (command) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'button press completed', error: undefined, - process: { pid: 12345 }, - }; + }); }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'axe not available' }], + content: [{ type: 'text' as const, text: 'axe not available' }], isError: true, }), }; @@ -165,19 +167,22 @@ describe('Button Plugin', () => { it('should generate correct axe command with bundled axe path', async () => { let capturedCommand: string[] = []; - const trackingExecutor = async (command: string[]) => { + const trackingExecutor: CommandExecutor = async (command) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'button press completed', error: undefined, - process: { pid: 12345 }, - }; + }); }; const mockAxeHelpers = { getAxePath: () => '/path/to/bundled/axe', getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'axe not available' }], + isError: true, + }), }; await buttonLogic( @@ -265,7 +270,7 @@ describe('Button Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'axe not available' }], + content: [{ type: 'text' as const, text: 'axe not available' }], isError: true, }), }; @@ -280,7 +285,7 @@ describe('Button Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: "Hardware button 'home' pressed successfully." }], + content: [{ type: 'text' as const, text: "Hardware button 'home' pressed successfully." }], isError: false, }); }); @@ -297,7 +302,7 @@ describe('Button Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'axe not available' }], + content: [{ type: 'text' as const, text: 'axe not available' }], isError: true, }), }; @@ -313,7 +318,9 @@ describe('Button Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: "Hardware button 'side-button' pressed successfully." }], + content: [ + { type: 'text' as const, text: "Hardware button 'side-button' pressed successfully." }, + ], isError: false, }); }); @@ -325,7 +332,7 @@ describe('Button Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -345,7 +352,7 @@ describe('Button Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -365,7 +372,7 @@ describe('Button Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'axe not available' }], + content: [{ type: 'text' as const, text: 'axe not available' }], isError: true, }), }; @@ -382,7 +389,7 @@ describe('Button Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: "Error: Failed to press button 'home': axe command 'button' failed.\nDetails: axe command failed", }, ], @@ -399,7 +406,7 @@ describe('Button Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'axe not available' }], + content: [{ type: 'text' as const, text: 'axe not available' }], isError: true, }), }; @@ -428,7 +435,7 @@ describe('Button Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'axe not available' }], + content: [{ type: 'text' as const, text: 'axe not available' }], isError: true, }), }; @@ -457,7 +464,7 @@ describe('Button Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'axe not available' }], + content: [{ type: 'text' as const, text: 'axe not available' }], isError: true, }), }; @@ -474,7 +481,7 @@ describe('Button Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], 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 ab14394a..d3b974ea 100644 --- a/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts @@ -5,6 +5,7 @@ import { describe, it, expect } from 'vitest'; import * as z from 'zod'; import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; +import type { CommandExecutor } from '../../../../utils/execution/index.ts'; import describeUIPlugin, { describe_uiLogic } from '../describe_ui.ts'; describe('Describe UI Plugin', () => { @@ -73,11 +74,15 @@ describe('Describe UI Plugin', () => { const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'axe not available' }], + isError: true, + }), }; // Wrap executor to track calls const executorCalls: any[] = []; - const trackingExecutor = async (...args: any[]) => { + const trackingExecutor: CommandExecutor = async (...args) => { executorCalls.push(args); return mockExecutor(...args); }; @@ -94,17 +99,17 @@ describe('Describe UI Plugin', () => { ['/usr/local/bin/axe', 'describe-ui', '--udid', '12345678-1234-4234-8234-123456789012'], '[AXe]: describe-ui', false, - {}, + { env: {} }, ]); expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Accessibility hierarchy retrieved successfully:\n```json\n{"elements": [{"type": "Button", "frame": {"x": 100, "y": 200, "width": 50, "height": 30}}]}\n```', }, { - type: 'text', + type: 'text' as const, text: `Next Steps: - Use frame coordinates for tap/swipe (center: x+width/2, y+height/2) - Re-run describe_ui after layout changes @@ -122,7 +127,7 @@ describe('Describe UI Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -141,7 +146,7 @@ describe('Describe UI Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -161,6 +166,10 @@ describe('Describe UI Plugin', () => { const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'axe not available' }], + isError: true, + }), }; const result = await describe_uiLogic( @@ -174,7 +183,7 @@ describe('Describe UI Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: "Error: Failed to get accessibility hierarchy: axe command 'describe-ui' failed.\nDetails: axe command failed", }, ], @@ -189,6 +198,10 @@ describe('Describe UI Plugin', () => { const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'axe not available' }], + isError: true, + }), }; const result = await describe_uiLogic( @@ -202,7 +215,7 @@ describe('Describe UI Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory', ), @@ -219,6 +232,10 @@ describe('Describe UI Plugin', () => { const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'axe not available' }], + isError: true, + }), }; const result = await describe_uiLogic( @@ -232,7 +249,7 @@ describe('Describe UI Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: Unexpected error', ), @@ -249,6 +266,10 @@ describe('Describe UI Plugin', () => { const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'axe not available' }], + isError: true, + }), }; const result = await describe_uiLogic( @@ -262,7 +283,7 @@ describe('Describe UI Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], diff --git a/src/mcp/tools/ui-testing/__tests__/gesture.test.ts b/src/mcp/tools/ui-testing/__tests__/gesture.test.ts index a39c0c79..4a15baa7 100644 --- a/src/mcp/tools/ui-testing/__tests__/gesture.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/gesture.test.ts @@ -8,6 +8,7 @@ import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, + mockProcess, } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import gesturePlugin, { gestureLogic } from '../gesture.ts'; @@ -94,13 +95,17 @@ describe('Gesture Plugin', () => { success: true, output: 'gesture completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'AXe CLI is not available.' }], + isError: true, + }), }; await gestureLogic( @@ -129,13 +134,17 @@ describe('Gesture Plugin', () => { success: true, output: 'gesture completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'AXe CLI is not available.' }], + isError: true, + }), }; await gestureLogic( @@ -170,13 +179,17 @@ describe('Gesture Plugin', () => { success: true, output: 'gesture completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'AXe CLI is not available.' }], + isError: true, + }), }; await gestureLogic( @@ -223,13 +236,17 @@ describe('Gesture Plugin', () => { success: true, output: 'gesture completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'AXe CLI is not available.' }], + isError: true, + }), }; await gestureLogic( @@ -261,12 +278,16 @@ describe('Gesture Plugin', () => { success: true, output: 'gesture completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'AXe CLI is not available.' }], + isError: true, + }), }; const result = await gestureLogic( @@ -279,7 +300,7 @@ describe('Gesture Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: "Gesture 'scroll-up' executed successfully." }], + content: [{ type: 'text' as const, text: "Gesture 'scroll-up' executed successfully." }], isError: false, }); }); @@ -289,12 +310,16 @@ describe('Gesture Plugin', () => { success: true, output: 'gesture completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'AXe CLI is not available.' }], + isError: true, + }), }; const result = await gestureLogic( @@ -313,7 +338,9 @@ describe('Gesture Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: "Gesture 'swipe-from-left-edge' executed successfully." }], + content: [ + { type: 'text' as const, text: "Gesture 'swipe-from-left-edge' executed successfully." }, + ], isError: false, }); }); @@ -325,7 +352,7 @@ describe('Gesture Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -345,7 +372,7 @@ describe('Gesture Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -358,12 +385,16 @@ describe('Gesture Plugin', () => { success: false, output: '', error: 'axe command failed', - process: { pid: 12345 }, + process: mockProcess, }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'AXe CLI is not available.' }], + isError: true, + }), }; const result = await gestureLogic( @@ -378,7 +409,7 @@ describe('Gesture Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: "Error: Failed to execute gesture 'scroll-up': axe command 'gesture' failed.\nDetails: axe command failed", }, ], @@ -392,6 +423,10 @@ describe('Gesture Plugin', () => { const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'AXe CLI is not available.' }], + isError: true, + }), }; const result = await gestureLogic( @@ -415,6 +450,10 @@ describe('Gesture Plugin', () => { const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'AXe CLI is not available.' }], + isError: true, + }), }; const result = await gestureLogic( @@ -438,6 +477,10 @@ describe('Gesture Plugin', () => { const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), + createAxeNotAvailableResponse: () => ({ + content: [{ type: 'text' as const, text: 'AXe CLI is not available.' }], + isError: true, + }), }; const result = await gestureLogic( @@ -452,7 +495,7 @@ describe('Gesture Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], 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 eac916a5..c6bcc7ba 100644 --- a/src/mcp/tools/ui-testing/__tests__/key_press.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/key_press.test.ts @@ -8,6 +8,7 @@ import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, + mockProcess, } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import keyPressPlugin, { key_pressLogic } from '../key_press.ts'; @@ -84,7 +85,7 @@ describe('Key Press Plugin', () => { success: true, output: 'key press completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -94,7 +95,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -128,7 +129,7 @@ describe('Key Press Plugin', () => { success: true, output: 'key press completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -138,7 +139,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -175,7 +176,7 @@ describe('Key Press Plugin', () => { success: true, output: 'key press completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -185,7 +186,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -219,7 +220,7 @@ describe('Key Press Plugin', () => { success: true, output: 'key press completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -229,7 +230,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -273,7 +274,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -291,7 +292,7 @@ describe('Key Press Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: 'Key press (code: 40) simulated successfully.' }], + content: [{ type: 'text' as const, text: 'Key press (code: 40) simulated successfully.' }], isError: false, }); }); @@ -309,7 +310,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -328,7 +329,7 @@ describe('Key Press Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: 'Key press (code: 42) simulated successfully.' }], + content: [{ type: 'text' as const, text: 'Key press (code: 42) simulated successfully.' }], isError: false, }); }); @@ -340,7 +341,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -360,7 +361,7 @@ describe('Key Press Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -381,7 +382,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -401,7 +402,7 @@ describe('Key Press Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: "Error: Failed to simulate key press (code: 40): axe command 'key' failed.\nDetails: axe command failed", }, ], @@ -420,7 +421,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -454,7 +455,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -488,7 +489,7 @@ describe('Key Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -508,7 +509,7 @@ describe('Key Press Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], 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 abb6c821..3885f016 100644 --- a/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts @@ -4,7 +4,11 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockExecutor, + createNoopExecutor, + mockProcess, +} from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import keySequencePlugin, { key_sequenceLogic } from '../key_sequence.ts'; @@ -81,7 +85,7 @@ describe('Key Sequence Plugin', () => { success: true, output: 'key sequence completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -91,7 +95,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -126,7 +130,7 @@ describe('Key Sequence Plugin', () => { success: true, output: 'key sequence completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -136,7 +140,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -174,7 +178,7 @@ describe('Key Sequence Plugin', () => { success: true, output: 'key sequence completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -184,7 +188,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -219,7 +223,7 @@ describe('Key Sequence Plugin', () => { success: true, output: 'key sequence completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -229,7 +233,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -282,7 +286,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -301,7 +305,9 @@ describe('Key Sequence Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: 'Key sequence [40,42,44] executed successfully.' }], + content: [ + { type: 'text' as const, text: 'Key sequence [40,42,44] executed successfully.' }, + ], isError: false, }); }); @@ -319,7 +325,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -337,7 +343,7 @@ describe('Key Sequence Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: 'Key sequence [40] executed successfully.' }], + content: [{ type: 'text' as const, text: 'Key sequence [40] executed successfully.' }], isError: false, }); }); @@ -349,7 +355,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -369,7 +375,7 @@ describe('Key Sequence Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -390,7 +396,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -410,7 +416,7 @@ describe('Key Sequence Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: "Error: Failed to execute key sequence: axe command 'key-sequence' failed.\nDetails: Simulator not found", }, ], @@ -429,7 +435,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -463,7 +469,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -497,7 +503,7 @@ describe('Key Sequence Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -517,7 +523,7 @@ describe('Key Sequence Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], 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 34c5c7ab..9fc97b4d 100644 --- a/src/mcp/tools/ui-testing/__tests__/long_press.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/long_press.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { createMockExecutor, mockProcess } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import longPressPlugin, { long_pressLogic } from '../long_press.ts'; @@ -114,7 +114,7 @@ describe('Long Press Plugin', () => { success: true, output: 'long press completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -122,7 +122,7 @@ describe('Long Press Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'Mock axe not available' }], + content: [{ type: 'text' as const, text: 'Mock axe not available' }], isError: true, }), }; @@ -162,7 +162,7 @@ describe('Long Press Plugin', () => { success: true, output: 'long press completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -170,7 +170,7 @@ describe('Long Press Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'Mock axe not available' }], + content: [{ type: 'text' as const, text: 'Mock axe not available' }], isError: true, }), }; @@ -210,7 +210,7 @@ describe('Long Press Plugin', () => { success: true, output: 'long press completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -218,7 +218,7 @@ describe('Long Press Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'Mock axe not available' }], + content: [{ type: 'text' as const, text: 'Mock axe not available' }], isError: true, }), }; @@ -258,7 +258,7 @@ describe('Long Press Plugin', () => { success: true, output: 'long press completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -266,7 +266,7 @@ describe('Long Press Plugin', () => { getAxePath: () => '/path/to/bundled/axe', getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'Mock axe not available' }], + content: [{ type: 'text' as const, text: 'Mock axe not available' }], isError: true, }), }; @@ -311,7 +311,7 @@ describe('Long Press Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'Mock axe not available' }], + content: [{ type: 'text' as const, text: 'Mock axe not available' }], isError: true, }), }; @@ -330,7 +330,7 @@ describe('Long Press Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Long press at (100, 200) for 1500ms simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -343,7 +343,7 @@ describe('Long Press Plugin', () => { success: true, output: '', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }); const mockAxeHelpers = { @@ -352,7 +352,7 @@ describe('Long Press Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -374,7 +374,7 @@ describe('Long Press Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -387,14 +387,14 @@ describe('Long Press Plugin', () => { success: false, output: '', error: 'axe command failed', - process: { pid: 12345 }, + process: mockProcess, }); const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'Mock axe not available' }], + content: [{ type: 'text' as const, text: 'Mock axe not available' }], isError: true, }), }; @@ -413,7 +413,7 @@ describe('Long Press Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: "Error: Failed to simulate long press at (100, 200): axe command 'touch' failed.\nDetails: axe command failed", }, ], @@ -430,7 +430,7 @@ describe('Long Press Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'Mock axe not available' }], + content: [{ type: 'text' as const, text: 'Mock axe not available' }], isError: true, }), }; @@ -449,7 +449,7 @@ describe('Long Press Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory', ), @@ -468,7 +468,7 @@ describe('Long Press Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'Mock axe not available' }], + content: [{ type: 'text' as const, text: 'Mock axe not available' }], isError: true, }), }; @@ -487,7 +487,7 @@ describe('Long Press Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: Unexpected error', ), @@ -506,7 +506,7 @@ describe('Long Press Plugin', () => { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'Mock axe not available' }], + content: [{ type: 'text' as const, text: 'Mock axe not available' }], isError: true, }), }; @@ -525,7 +525,7 @@ describe('Long Press Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], diff --git a/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts b/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts index a670d937..58c46e51 100644 --- a/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts @@ -8,6 +8,7 @@ import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, + mockProcess, } from '../../../../test-utils/mock-executors.ts'; import { SystemError } from '../../../../utils/responses/index.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; @@ -79,7 +80,7 @@ describe('Screenshot Plugin', () => { success: true, output: 'Screenshot saved', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -117,7 +118,7 @@ describe('Screenshot Plugin', () => { success: true, output: 'Screenshot saved', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -154,7 +155,7 @@ describe('Screenshot Plugin', () => { success: true, output: 'Screenshot saved', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -194,7 +195,7 @@ describe('Screenshot Plugin', () => { success: true, output: 'Screenshot saved', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -299,7 +300,7 @@ describe('Screenshot Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: System error executing screenshot: Failed to capture screenshot: Simulator not found', }, ], @@ -331,7 +332,7 @@ describe('Screenshot Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: Screenshot captured but failed to process image file: File not found', }, ], @@ -390,7 +391,10 @@ describe('Screenshot Plugin', () => { expect(result).toEqual({ content: [ - { type: 'text', text: 'Error: System error executing screenshot: System error occurred' }, + { + type: 'text' as const, + text: 'Error: System error executing screenshot: System error occurred', + }, ], isError: true, }); @@ -410,7 +414,9 @@ describe('Screenshot Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: 'Error: An unexpected error occurred: Unexpected error' }], + content: [ + { type: 'text' as const, text: 'Error: An unexpected error occurred: Unexpected error' }, + ], isError: true, }); }); @@ -429,7 +435,9 @@ describe('Screenshot Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: 'Error: An unexpected error occurred: String error' }], + content: [ + { type: 'text' as const, text: 'Error: An unexpected error occurred: String error' }, + ], isError: true, }); }); diff --git a/src/mcp/tools/ui-testing/__tests__/swipe.test.ts b/src/mcp/tools/ui-testing/__tests__/swipe.test.ts index a6eab257..041d3628 100644 --- a/src/mcp/tools/ui-testing/__tests__/swipe.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/swipe.test.ts @@ -4,7 +4,11 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockExecutor, + createNoopExecutor, + mockProcess, +} from '../../../../test-utils/mock-executors.ts'; import { SystemError, DependencyError } from '../../../../utils/responses/index.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; @@ -19,7 +23,7 @@ function createMockAxeHelpers(): AxeHelpers { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -36,7 +40,7 @@ function createMockAxeHelpersWithNullPath(): AxeHelpers { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -129,7 +133,7 @@ describe('Swipe Plugin', () => { success: true, output: 'swipe completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -171,7 +175,7 @@ describe('Swipe Plugin', () => { success: true, output: 'swipe completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -216,7 +220,7 @@ describe('Swipe Plugin', () => { success: true, output: 'swipe completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -270,7 +274,7 @@ describe('Swipe Plugin', () => { success: true, output: 'swipe completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -278,7 +282,7 @@ describe('Swipe Plugin', () => { getAxePath: () => '/path/to/bundled/axe', getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }), createAxeNotAvailableResponse: () => ({ - content: [{ type: 'text', text: 'AXe tools not available' }], + content: [{ type: 'text' as const, text: 'AXe tools not available' }], isError: true, }), }; @@ -367,7 +371,7 @@ describe('Swipe Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Swipe from (100, 200) to (300, 400) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -400,7 +404,7 @@ describe('Swipe Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Swipe from (100, 200) to (300, 400) duration=1.5s simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -432,7 +436,7 @@ describe('Swipe Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -464,7 +468,7 @@ describe('Swipe Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: "Error: Failed to simulate swipe: axe command 'swipe' failed.\nDetails: axe command failed", }, ], @@ -551,7 +555,7 @@ describe('Swipe Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], diff --git a/src/mcp/tools/ui-testing/__tests__/tap.test.ts b/src/mcp/tools/ui-testing/__tests__/tap.test.ts index c4c96ab4..e60b6979 100644 --- a/src/mcp/tools/ui-testing/__tests__/tap.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/tap.test.ts @@ -17,7 +17,7 @@ function createMockAxeHelpers(): AxeHelpers { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -34,7 +34,7 @@ function createMockAxeHelpersWithNullPath(): AxeHelpers { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -132,7 +132,7 @@ describe('Tap Plugin', () => { command: string[]; logPrefix?: string; useShell?: boolean; - env?: Record; + opts?: { env?: Record; cwd?: string }; }>; beforeEach(() => { @@ -149,10 +149,10 @@ describe('Tap Plugin', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record; cwd?: string }, ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return mockExecutor(command, logPrefix, useShell, env); + callHistory.push({ command, logPrefix, useShell, opts }); + return mockExecutor(command, logPrefix, useShell, opts); }; const mockAxeHelpers = createMockAxeHelpers(); @@ -181,7 +181,7 @@ describe('Tap Plugin', () => { ], logPrefix: '[AXe]: tap', useShell: false, - env: { SOME_ENV: 'value' }, + opts: { env: { SOME_ENV: 'value' } }, }); }); @@ -195,10 +195,10 @@ describe('Tap Plugin', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record; cwd?: string }, ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return mockExecutor(command, logPrefix, useShell, env); + callHistory.push({ command, logPrefix, useShell, opts }); + return mockExecutor(command, logPrefix, useShell, opts); }; const mockAxeHelpers = createMockAxeHelpers(); @@ -224,7 +224,7 @@ describe('Tap Plugin', () => { ], logPrefix: '[AXe]: tap', useShell: false, - env: { SOME_ENV: 'value' }, + opts: { env: { SOME_ENV: 'value' } }, }); }); @@ -238,10 +238,10 @@ describe('Tap Plugin', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record; cwd?: string }, ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return mockExecutor(command, logPrefix, useShell, env); + callHistory.push({ command, logPrefix, useShell, opts }); + return mockExecutor(command, logPrefix, useShell, opts); }; const mockAxeHelpers = createMockAxeHelpers(); @@ -267,7 +267,7 @@ describe('Tap Plugin', () => { ], logPrefix: '[AXe]: tap', useShell: false, - env: { SOME_ENV: 'value' }, + opts: { env: { SOME_ENV: 'value' } }, }); }); @@ -281,10 +281,10 @@ describe('Tap Plugin', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record; cwd?: string }, ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return mockExecutor(command, logPrefix, useShell, env); + callHistory.push({ command, logPrefix, useShell, opts }); + return mockExecutor(command, logPrefix, useShell, opts); }; const mockAxeHelpers = createMockAxeHelpers(); @@ -314,7 +314,7 @@ describe('Tap Plugin', () => { ], logPrefix: '[AXe]: tap', useShell: false, - env: { SOME_ENV: 'value' }, + opts: { env: { SOME_ENV: 'value' } }, }); }); @@ -328,10 +328,10 @@ describe('Tap Plugin', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record; cwd?: string }, ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return mockExecutor(command, logPrefix, useShell, env); + callHistory.push({ command, logPrefix, useShell, opts }); + return mockExecutor(command, logPrefix, useShell, opts); }; const mockAxeHelpers = createMockAxeHelpers(); @@ -363,7 +363,7 @@ describe('Tap Plugin', () => { ], logPrefix: '[AXe]: tap', useShell: false, - env: { SOME_ENV: 'value' }, + opts: { env: { SOME_ENV: 'value' } }, }); }); @@ -377,10 +377,10 @@ describe('Tap Plugin', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record; cwd?: string }, ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return mockExecutor(command, logPrefix, useShell, env); + callHistory.push({ command, logPrefix, useShell, opts }); + return mockExecutor(command, logPrefix, useShell, opts); }; const mockAxeHelpers = createMockAxeHelpers(); @@ -412,7 +412,7 @@ describe('Tap Plugin', () => { ], logPrefix: '[AXe]: tap', useShell: false, - env: { SOME_ENV: 'value' }, + opts: { env: { SOME_ENV: 'value' } }, }); }); @@ -426,10 +426,10 @@ describe('Tap Plugin', () => { command: string[], logPrefix?: string, useShell?: boolean, - env?: Record, + opts?: { env?: Record; cwd?: string }, ) => { - callHistory.push({ command, logPrefix, useShell, env }); - return mockExecutor(command, logPrefix, useShell, env); + callHistory.push({ command, logPrefix, useShell, opts }); + return mockExecutor(command, logPrefix, useShell, opts); }; const mockAxeHelpers = createMockAxeHelpers(); @@ -464,7 +464,7 @@ describe('Tap Plugin', () => { ], logPrefix: '[AXe]: tap', useShell: false, - env: { SOME_ENV: 'value' }, + opts: { env: { SOME_ENV: 'value' } }, }); }); }); @@ -491,7 +491,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Tap at (100, 200) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -520,7 +520,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Tap at (150, 300) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -551,7 +551,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Tap at (250, 400) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -580,7 +580,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Tap at (0, 0) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -609,7 +609,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Tap at (1920, 1080) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -637,7 +637,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Tap on element id "loginButton" simulated successfully.', }, ], @@ -665,7 +665,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Tap on element label "Log in" simulated successfully.', }, ], @@ -812,7 +812,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -842,7 +842,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -872,7 +872,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -900,7 +900,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -928,7 +928,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -956,7 +956,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], diff --git a/src/mcp/tools/ui-testing/__tests__/touch.test.ts b/src/mcp/tools/ui-testing/__tests__/touch.test.ts index 8734ff09..8629093b 100644 --- a/src/mcp/tools/ui-testing/__tests__/touch.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/touch.test.ts @@ -5,7 +5,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { createMockExecutor, mockProcess } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import touchPlugin, { touchLogic } from '../touch.ts'; @@ -123,7 +123,7 @@ describe('Touch Plugin', () => { success: true, output: 'touch completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -133,7 +133,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -173,7 +173,7 @@ describe('Touch Plugin', () => { success: true, output: 'touch completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -183,7 +183,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -223,7 +223,7 @@ describe('Touch Plugin', () => { success: true, output: 'touch completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -233,7 +233,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -275,7 +275,7 @@ describe('Touch Plugin', () => { success: true, output: 'touch completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -285,7 +285,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -330,7 +330,7 @@ describe('Touch Plugin', () => { success: true, output: 'touch completed', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -376,7 +376,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -398,7 +398,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -414,7 +414,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -436,7 +436,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -452,7 +452,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -474,7 +474,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -495,7 +495,9 @@ describe('Touch Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: 'Error: At least one of "down" or "up" must be true' }], + content: [ + { type: 'text' as const, text: 'Error: At least one of "down" or "up" must be true' }, + ], isError: true, }); }); @@ -513,7 +515,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -535,7 +537,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -556,7 +558,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -578,7 +580,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -599,7 +601,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -622,7 +624,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Touch event (touch down+up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -639,7 +641,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -661,7 +663,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -682,7 +684,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -704,7 +706,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: "Error: Failed to execute touch event: axe command 'touch' failed.\nDetails: axe command failed", }, ], @@ -723,7 +725,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -745,7 +747,7 @@ describe('Touch Plugin', () => { expect(result).toMatchObject({ content: [ { - type: 'text', + type: 'text' as const, text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: System error occurred', ), @@ -766,7 +768,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -788,7 +790,7 @@ describe('Touch Plugin', () => { expect(result).toMatchObject({ content: [ { - type: 'text', + type: 'text' as const, text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: Unexpected error', ), @@ -809,7 +811,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -831,7 +833,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], 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 a78c2696..ba4ce27e 100644 --- a/src/mcp/tools/ui-testing/__tests__/type_text.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/type_text.test.ts @@ -8,6 +8,7 @@ import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, + mockProcess, } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import typeTextPlugin, { type_textLogic } from '../type_text.ts'; @@ -20,12 +21,18 @@ function createMockAxeHelpers( } = {}, ) { return { - getAxePath: () => { - return Object.prototype.hasOwnProperty.call(overrides, 'getAxePathReturn') - ? overrides.getAxePathReturn - : '/usr/local/bin/axe'; - }, + getAxePath: () => + overrides.getAxePathReturn !== undefined ? overrides.getAxePathReturn : '/usr/local/bin/axe', getBundledAxeEnvironment: () => overrides.getBundledAxeEnvironmentReturn ?? {}, + createAxeNotAvailableResponse: () => ({ + content: [ + { + type: 'text' as const, + text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', + }, + ], + isError: true, + }), }; } @@ -120,7 +127,7 @@ describe('Type Text Plugin', () => { success: true, output: 'Text typed successfully', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -155,7 +162,7 @@ describe('Type Text Plugin', () => { success: true, output: 'Text typed successfully', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -190,7 +197,7 @@ describe('Type Text Plugin', () => { success: true, output: 'Text typed successfully', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -225,7 +232,7 @@ describe('Type Text Plugin', () => { success: true, output: 'Text typed successfully', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -263,7 +270,7 @@ describe('Type Text Plugin', () => { success: true, output: 'Text typed successfully', error: undefined, - process: { pid: 12345 }, + process: mockProcess, }; }; @@ -309,7 +316,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -338,7 +345,7 @@ describe('Type Text Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: 'Text typing simulated successfully.' }], + content: [{ type: 'text' as const, text: 'Text typing simulated successfully.' }], isError: false, }); }); @@ -365,7 +372,7 @@ describe('Type Text Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text', text: 'Text typing simulated successfully.' }], + content: [{ type: 'text' as const, text: 'Text typing simulated successfully.' }], isError: false, }); }); @@ -387,7 +394,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -419,7 +426,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: "Error: Failed to simulate text typing: axe command 'type' failed.\nDetails: Text field not found", }, ], @@ -447,7 +454,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory', ), @@ -477,7 +484,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: Unexpected error', ), @@ -507,7 +514,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text', + type: 'text' as const, text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], diff --git a/src/mcp/tools/ui-testing/button.ts b/src/mcp/tools/ui-testing/button.ts index 3449cec8..1889b048 100644 --- a/src/mcp/tools/ui-testing/button.ts +++ b/src/mcp/tools/ui-testing/button.ts @@ -127,7 +127,12 @@ async function executeAxeCommand( // Determine environment variables for bundled AXe const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined; - const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv); + const result = await executor( + fullCommand, + `${LOG_PREFIX}: ${commandName}`, + false, + axeEnv ? { env: axeEnv } : undefined, + ); if (!result.success) { throw new AxeError( diff --git a/src/mcp/tools/ui-testing/describe_ui.ts b/src/mcp/tools/ui-testing/describe_ui.ts index 0ce9df52..30a6b491 100644 --- a/src/mcp/tools/ui-testing/describe_ui.ts +++ b/src/mcp/tools/ui-testing/describe_ui.ts @@ -162,7 +162,12 @@ async function executeAxeCommand( // Determine environment variables for bundled AXe const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined; - const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv); + const result = await executor( + fullCommand, + `${LOG_PREFIX}: ${commandName}`, + false, + axeEnv ? { env: axeEnv } : undefined, + ); if (!result.success) { throw new AxeError( diff --git a/src/mcp/tools/ui-testing/gesture.ts b/src/mcp/tools/ui-testing/gesture.ts index 137d6a93..0b744599 100644 --- a/src/mcp/tools/ui-testing/gesture.ts +++ b/src/mcp/tools/ui-testing/gesture.ts @@ -204,7 +204,12 @@ async function executeAxeCommand( // Determine environment variables for bundled AXe const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined; - const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv); + const result = await executor( + fullCommand, + `${LOG_PREFIX}: ${commandName}`, + false, + axeEnv ? { env: axeEnv } : undefined, + ); if (!result.success) { throw new AxeError( diff --git a/src/mcp/tools/ui-testing/key_press.ts b/src/mcp/tools/ui-testing/key_press.ts index f8fd4b80..48d483b3 100644 --- a/src/mcp/tools/ui-testing/key_press.ts +++ b/src/mcp/tools/ui-testing/key_press.ts @@ -134,7 +134,12 @@ async function executeAxeCommand( // Determine environment variables for bundled AXe const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined; - const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv); + const result = await executor( + fullCommand, + `${LOG_PREFIX}: ${commandName}`, + false, + axeEnv ? { env: axeEnv } : undefined, + ); if (!result.success) { throw new AxeError( diff --git a/src/mcp/tools/ui-testing/key_sequence.ts b/src/mcp/tools/ui-testing/key_sequence.ts index 55fe7a1e..f54886f3 100644 --- a/src/mcp/tools/ui-testing/key_sequence.ts +++ b/src/mcp/tools/ui-testing/key_sequence.ts @@ -144,7 +144,12 @@ async function executeAxeCommand( // Determine environment variables for bundled AXe const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined; - const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv); + const result = await executor( + fullCommand, + `${LOG_PREFIX}: ${commandName}`, + false, + axeEnv ? { env: axeEnv } : undefined, + ); if (!result.success) { throw new AxeError( diff --git a/src/mcp/tools/ui-testing/long_press.ts b/src/mcp/tools/ui-testing/long_press.ts index e823c544..677b6550 100644 --- a/src/mcp/tools/ui-testing/long_press.ts +++ b/src/mcp/tools/ui-testing/long_press.ts @@ -186,7 +186,12 @@ async function executeAxeCommand( // Determine environment variables for bundled AXe const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined; - const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv); + const result = await executor( + fullCommand, + `${LOG_PREFIX}: ${commandName}`, + false, + axeEnv ? { env: axeEnv } : undefined, + ); if (!result.success) { throw new AxeError( diff --git a/src/mcp/tools/ui-testing/swipe.ts b/src/mcp/tools/ui-testing/swipe.ts index fea104fa..57898ff3 100644 --- a/src/mcp/tools/ui-testing/swipe.ts +++ b/src/mcp/tools/ui-testing/swipe.ts @@ -35,7 +35,7 @@ const swipeSchema = z.object({ }); // Use z.infer for type safety -type SwipeParams = z.infer; +export type SwipeParams = z.infer; const publicSchemaObject = z.strictObject(swipeSchema.omit({ simulatorId: true } as const).shape); @@ -195,7 +195,12 @@ async function executeAxeCommand( // Determine environment variables for bundled AXe const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined; - const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv); + const result = await executor( + fullCommand, + `${LOG_PREFIX}: ${commandName}`, + false, + axeEnv ? { env: axeEnv } : undefined, + ); if (!result.success) { throw new AxeError( diff --git a/src/mcp/tools/ui-testing/tap.ts b/src/mcp/tools/ui-testing/tap.ts index d53c3b7d..e55f6acd 100644 --- a/src/mcp/tools/ui-testing/tap.ts +++ b/src/mcp/tools/ui-testing/tap.ts @@ -227,7 +227,12 @@ async function executeAxeCommand( // Determine environment variables for bundled AXe const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined; - const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv); + const result = await executor( + fullCommand, + `${LOG_PREFIX}: ${commandName}`, + false, + axeEnv ? { env: axeEnv } : undefined, + ); if (!result.success) { throw new AxeError( diff --git a/src/mcp/tools/ui-testing/touch.ts b/src/mcp/tools/ui-testing/touch.ts index e425a9ec..a0334141 100644 --- a/src/mcp/tools/ui-testing/touch.ts +++ b/src/mcp/tools/ui-testing/touch.ts @@ -183,7 +183,12 @@ async function executeAxeCommand( // Determine environment variables for bundled AXe const axeEnv = axeBinary !== 'axe' ? helpers.getBundledAxeEnvironment() : undefined; - const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv); + const result = await executor( + fullCommand, + `${LOG_PREFIX}: ${commandName}`, + false, + axeEnv ? { env: axeEnv } : undefined, + ); if (!result.success) { throw new AxeError( diff --git a/src/mcp/tools/ui-testing/type_text.ts b/src/mcp/tools/ui-testing/type_text.ts index f0152189..7ac8727e 100644 --- a/src/mcp/tools/ui-testing/type_text.ts +++ b/src/mcp/tools/ui-testing/type_text.ts @@ -134,7 +134,12 @@ async function executeAxeCommand( // Determine environment variables for bundled AXe const axeEnv = axeBinary !== 'axe' ? helpers.getBundledAxeEnvironment() : undefined; - const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv); + const result = await executor( + fullCommand, + `${LOG_PREFIX}: ${commandName}`, + false, + axeEnv ? { env: axeEnv } : undefined, + ); if (!result.success) { throw new AxeError( diff --git a/src/mcp/tools/utilities/__tests__/clean.test.ts b/src/mcp/tools/utilities/__tests__/clean.test.ts index 48d934f1..f406ccb1 100644 --- a/src/mcp/tools/utilities/__tests__/clean.test.ts +++ b/src/mcp/tools/utilities/__tests__/clean.test.ts @@ -1,7 +1,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; import tool, { cleanLogic } from '../clean.ts'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockExecutor, + createMockCommandResponse, +} from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; describe('clean (unified) tool', () => { @@ -77,7 +80,7 @@ describe('clean (unified) tool', () => { let capturedCommand: string[] = []; const mockExecutor = async (command: string[]) => { capturedCommand = command; - return { success: true, output: 'clean success' }; + return createMockCommandResponse({ success: true, output: 'clean success' }); }; const result = await cleanLogic( @@ -96,7 +99,7 @@ describe('clean (unified) tool', () => { let capturedCommand: string[] = []; const mockExecutor = async (command: string[]) => { capturedCommand = command; - return { success: true, output: 'clean success' }; + return createMockCommandResponse({ success: true, output: 'clean success' }); }; const result = await cleanLogic( @@ -119,7 +122,7 @@ describe('clean (unified) tool', () => { let capturedCommand: string[] = []; const mockExecutor = async (command: string[]) => { capturedCommand = command; - return { success: true, output: 'clean success' }; + return createMockCommandResponse({ success: true, output: 'clean success' }); }; const result = await cleanLogic( diff --git a/src/test-utils/mock-executors.ts b/src/test-utils/mock-executors.ts index eb3ad02e..c0419ee0 100644 --- a/src/test-utils/mock-executors.ts +++ b/src/test-utils/mock-executors.ts @@ -16,9 +16,25 @@ */ import { ChildProcess } from 'child_process'; -import { CommandExecutor } from '../utils/CommandExecutor.ts'; +import { CommandExecutor, type CommandResponse } from '../utils/CommandExecutor.ts'; import { FileSystemExecutor } from '../utils/FileSystemExecutor.ts'; +export type { CommandExecutor, FileSystemExecutor }; + +export const mockProcess = { pid: 12345 } as unknown as ChildProcess; + +export function createMockCommandResponse( + overrides: Partial = {}, +): CommandResponse { + return { + success: overrides.success ?? true, + output: overrides.output ?? '', + error: overrides.error, + process: overrides.process ?? mockProcess, + exitCode: overrides.exitCode ?? (overrides.success === false ? 1 : 0), + }; +} + /** * Create a mock executor for testing * @param result Mock command result or error to throw From 2730155de91a3cade29e954d18dd9b0e7bcaefaf Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 8 Jan 2026 09:40:48 +0000 Subject: [PATCH 2/4] Expand filesystem DI and typecheck tests --- package.json | 5 +- .../__tests__/launch_app_device.test.ts | 51 +++--- .../device/__tests__/test_device.test.ts | 14 +- src/mcp/tools/device/launch_app_device.ts | 19 ++- .../__tests__/stop_sim_log_cap.test.ts | 158 ++++++++++++------ src/mcp/tools/logging/start_device_log_cap.ts | 73 ++++---- src/mcp/tools/logging/stop_device_log_cap.ts | 21 ++- src/mcp/tools/logging/stop_sim_log_cap.ts | 16 +- .../tools/macos/__tests__/test_macos.test.ts | 2 +- .../__tests__/discover_projs.test.ts | 20 +-- .../__tests__/scaffold_ios_project.test.ts | 8 +- src/test-utils/mock-executors.ts | 31 +++- src/utils/FileSystemExecutor.ts | 5 +- src/utils/command.ts | 8 +- src/utils/log_capture.ts | 43 +++-- 15 files changed, 292 insertions(+), 182 deletions(-) diff --git a/package.json b/package.json index 7d1b7105..62068c26 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,8 @@ "lint:fix": "eslint 'src/**/*.{js,ts}' --fix", "format": "prettier --write 'src/**/*.{js,ts}'", "format:check": "prettier --check 'src/**/*.{js,ts}'", - "typecheck": "npx tsc --noEmit", - "typecheck:tests": "npx tsc -p tsconfig.tests.json --noEmit", - "typecheck:all": "npm run typecheck && npm run typecheck:tests", + "typecheck": "npx tsc --noEmit && npx tsc -p tsconfig.test.json", + "typecheck:tests": "npx tsc -p tsconfig.test.json", "verify:smithery-bundle": "bash scripts/verify-smithery-bundle.sh", "inspect": "npx @modelcontextprotocol/inspector node build/index.js", "doctor": "node build/doctor-cli.js", diff --git a/src/mcp/tools/device/__tests__/launch_app_device.test.ts b/src/mcp/tools/device/__tests__/launch_app_device.test.ts index 7344d19a..020870db 100644 --- a/src/mcp/tools/device/__tests__/launch_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/launch_app_device.test.ts @@ -9,7 +9,10 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { + createMockExecutor, + createMockFileSystemExecutor, +} from '../../../../test-utils/mock-executors.ts'; import launchAppDevice, { launch_app_deviceLogic } from '../launch_app_device.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; @@ -81,6 +84,7 @@ describe('launch_app_device plugin (device-shared)', () => { bundleId: 'com.example.app', }, trackingExecutor, + createMockFileSystemExecutor(), ); expect(calls).toHaveLength(1); @@ -121,6 +125,7 @@ describe('launch_app_device plugin (device-shared)', () => { bundleId: 'com.apple.mobilesafari', }, trackingExecutor, + createMockFileSystemExecutor(), ); expect(calls[0].command).toEqual([ @@ -152,6 +157,7 @@ describe('launch_app_device plugin (device-shared)', () => { bundleId: 'com.example.app', }, mockExecutor, + createMockFileSystemExecutor(), ); expect(result).toEqual({ @@ -176,6 +182,7 @@ describe('launch_app_device plugin (device-shared)', () => { bundleId: 'com.example.app', }, mockExecutor, + createMockFileSystemExecutor(), ); expect(result).toEqual({ @@ -189,37 +196,17 @@ describe('launch_app_device plugin (device-shared)', () => { }); it('should handle successful launch with process ID information', async () => { - // Mock fs operations for JSON parsing - const fs = await import('fs'); - const originalReadFile = fs.promises.readFile; - const originalUnlink = fs.promises.unlink; - - const mockReadFile = (async (path, options) => { - const pathString = String(path); - if (pathString.includes('launch-')) { - const json = JSON.stringify({ + const mockFileSystem = createMockFileSystemExecutor({ + readFile: async () => + JSON.stringify({ result: { process: { processIdentifier: 12345, }, }, - }); - if (typeof options === 'string') { - return json; - } - if (options && typeof options === 'object' && 'encoding' in options && options.encoding) { - return json; - } - return Buffer.from(json); - } - return originalReadFile(path, options as BufferEncoding); - }) as typeof fs.promises.readFile; - - const mockUnlink = () => Promise.resolve(); - - // Replace fs methods - fs.promises.readFile = mockReadFile; - fs.promises.unlink = mockUnlink; + }), + rm: async () => {}, + }); const mockExecutor = createMockExecutor({ success: true, @@ -232,12 +219,9 @@ describe('launch_app_device plugin (device-shared)', () => { bundleId: 'com.example.app', }, mockExecutor, + mockFileSystem, ); - // Restore fs methods - fs.promises.readFile = originalReadFile; - fs.promises.unlink = originalUnlink; - expect(result).toEqual({ content: [ { @@ -260,6 +244,7 @@ describe('launch_app_device plugin (device-shared)', () => { bundleId: 'com.example.app', }, mockExecutor, + createMockFileSystemExecutor(), ); expect(result).toEqual({ @@ -286,6 +271,7 @@ describe('launch_app_device plugin (device-shared)', () => { bundleId: 'com.nonexistent.app', }, mockExecutor, + createMockFileSystemExecutor(), ); expect(result).toEqual({ @@ -311,6 +297,7 @@ describe('launch_app_device plugin (device-shared)', () => { bundleId: 'com.example.app', }, mockExecutor, + createMockFileSystemExecutor(), ); expect(result).toEqual({ @@ -333,6 +320,7 @@ describe('launch_app_device plugin (device-shared)', () => { bundleId: 'com.example.app', }, mockExecutor, + createMockFileSystemExecutor(), ); expect(result).toEqual({ @@ -355,6 +343,7 @@ describe('launch_app_device plugin (device-shared)', () => { bundleId: 'com.example.app', }, mockExecutor, + createMockFileSystemExecutor(), ); expect(result).toEqual({ diff --git a/src/mcp/tools/device/__tests__/test_device.test.ts b/src/mcp/tools/device/__tests__/test_device.test.ts index 4ac6667f..2ce5a76d 100644 --- a/src/mcp/tools/device/__tests__/test_device.test.ts +++ b/src/mcp/tools/device/__tests__/test_device.test.ts @@ -84,7 +84,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-123', tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => false }), + stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }), rm: async () => {}, }), ); @@ -101,7 +101,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-456', tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => false }), + stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }), rm: async () => {}, }), ); @@ -174,7 +174,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-123456', tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => false }), + stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }), rm: async () => {}, }), ); @@ -220,7 +220,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-123456', tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => false }), + stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }), rm: async () => {}, }), ); @@ -309,7 +309,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-123456', tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => false }), + stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }), rm: async () => {}, }), ); @@ -348,7 +348,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-123456', tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => false }), + stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }), rm: async () => {}, }), ); @@ -385,7 +385,7 @@ describe('test_device plugin', () => { createMockFileSystemExecutor({ mkdtemp: async () => '/tmp/xcodebuild-test-workspace-123', tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => false }), + stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }), rm: async () => {}, }), ); diff --git a/src/mcp/tools/device/launch_app_device.ts b/src/mcp/tools/device/launch_app_device.ts index 299db503..df55fc6c 100644 --- a/src/mcp/tools/device/launch_app_device.ts +++ b/src/mcp/tools/device/launch_app_device.ts @@ -8,14 +8,15 @@ import * as z from 'zod'; import { ToolResponse } from '../../../types/common.ts'; import { log } from '../../../utils/logging/index.ts'; -import type { CommandExecutor } from '../../../utils/execution/index.ts'; -import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; +import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts'; +import { + getDefaultCommandExecutor, + getDefaultFileSystemExecutor, +} from '../../../utils/execution/index.ts'; import { createSessionAwareTool, getSessionAwareToolSchemaShape, } from '../../../utils/typed-tool-factory.ts'; -import { promises as fs } from 'fs'; -import { tmpdir } from 'os'; import { join } from 'path'; // Type for the launch JSON response @@ -43,6 +44,7 @@ type LaunchAppDeviceParams = z.infer; export async function launch_app_deviceLogic( params: LaunchAppDeviceParams, executor: CommandExecutor, + fileSystem: FileSystemExecutor, ): Promise { const { deviceId, bundleId } = params; @@ -50,7 +52,7 @@ export async function launch_app_deviceLogic( try { // Use JSON output to capture process ID - const tempJsonPath = join(tmpdir(), `launch-${Date.now()}.json`); + const tempJsonPath = join(fileSystem.tmpdir(), `launch-${Date.now()}.json`); const result = await executor( [ @@ -86,7 +88,7 @@ export async function launch_app_deviceLogic( // Parse JSON to extract process ID let processId: number | undefined; try { - const jsonContent = await fs.readFile(tempJsonPath, 'utf8'); + const jsonContent = await fileSystem.readFile(tempJsonPath, 'utf8'); const parsedData: unknown = JSON.parse(jsonContent); // Type guard to validate the parsed data structure @@ -107,7 +109,7 @@ export async function launch_app_deviceLogic( } // Clean up temp file - await fs.unlink(tempJsonPath).catch(() => {}); + await fileSystem.rm(tempJsonPath, { force: true }).catch(() => {}); } catch (error) { log('warn', `Failed to parse launch JSON output: ${error}`); } @@ -157,7 +159,8 @@ export default { }, handler: createSessionAwareTool({ internalSchema: launchAppDeviceSchema as unknown as z.ZodType, - logicFunction: launch_app_deviceLogic, + logicFunction: (params, executor) => + launch_app_deviceLogic(params, executor, getDefaultFileSystemExecutor()), getExecutor: getDefaultCommandExecutor, requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }], }), diff --git a/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts b/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts index 0c58f2bd..252d1410 100644 --- a/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts +++ b/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts @@ -15,6 +15,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; import stopSimLogCap, { stop_sim_log_capLogic } from '../stop_sim_log_cap.ts'; import { activeLogSessions } from '../../../../utils/log_capture.ts'; +import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts'; describe('stop_sim_log_cap plugin', () => { beforeEach(() => { @@ -32,10 +33,15 @@ describe('stop_sim_log_cap plugin', () => { }; const logFilePath = `/tmp/xcodemcp_sim_log_test_${sessionId}.log`; - - // Create actual file for the test - const fs = await import('fs/promises'); - await fs.writeFile(logFilePath, logContent, 'utf-8'); + const fileSystem = createMockFileSystemExecutor({ + existsSync: (path) => path === logFilePath, + readFile: async (path, _encoding) => { + if (path !== logFilePath) { + throw new Error(`ENOENT: no such file or directory, open '${path}'`); + } + return logContent; + }, + }); activeLogSessions.set(sessionId, { processes: [mockProcess as any], @@ -43,6 +49,8 @@ describe('stop_sim_log_cap plugin', () => { simulatorUuid: 'test-simulator-uuid', bundleId: 'com.example.TestApp', }); + + return { fileSystem, logFilePath }; } describe('Export Field Validation (Literal)', () => { @@ -87,11 +95,14 @@ describe('stop_sim_log_cap plugin', () => { it('should handle null logSessionId (validation handled by framework)', async () => { // With typed tool factory, invalid params won't reach the logic function // This test now validates that the logic function works with valid empty strings - await createTestLogSession('', 'Log content for empty session'); + const { fileSystem } = await createTestLogSession('', 'Log content for empty session'); - const result = await stop_sim_log_capLogic({ - logSessionId: '', - }); + const result = await stop_sim_log_capLogic( + { + logSessionId: '', + }, + fileSystem, + ); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -102,11 +113,14 @@ describe('stop_sim_log_cap plugin', () => { it('should handle undefined logSessionId (validation handled by framework)', async () => { // With typed tool factory, invalid params won't reach the logic function // This test now validates that the logic function works with valid empty strings - await createTestLogSession('', 'Log content for empty session'); + const { fileSystem } = await createTestLogSession('', 'Log content for empty session'); - const result = await stop_sim_log_capLogic({ - logSessionId: '', - }); + const result = await stop_sim_log_capLogic( + { + logSessionId: '', + }, + fileSystem, + ); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -115,11 +129,14 @@ describe('stop_sim_log_cap plugin', () => { }); it('should handle empty string logSessionId', async () => { - await createTestLogSession('', 'Log content for empty session'); + const { fileSystem } = await createTestLogSession('', 'Log content for empty session'); - const result = await stop_sim_log_capLogic({ - logSessionId: '', - }); + const result = await stop_sim_log_capLogic( + { + logSessionId: '', + }, + fileSystem, + ); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -130,11 +147,17 @@ describe('stop_sim_log_cap plugin', () => { describe('Function Call Generation', () => { it('should call stopLogCapture with correct parameters', async () => { - await createTestLogSession('test-session-id', 'Mock log content from file'); + const { fileSystem } = await createTestLogSession( + 'test-session-id', + 'Mock log content from file', + ); - const result = await stop_sim_log_capLogic({ - logSessionId: 'test-session-id', - }); + const result = await stop_sim_log_capLogic( + { + logSessionId: 'test-session-id', + }, + fileSystem, + ); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -143,11 +166,17 @@ describe('stop_sim_log_cap plugin', () => { }); it('should call stopLogCapture with different session ID', async () => { - await createTestLogSession('different-session-id', 'Different log content'); + const { fileSystem } = await createTestLogSession( + 'different-session-id', + 'Different log content', + ); - const result = await stop_sim_log_capLogic({ - logSessionId: 'different-session-id', - }); + const result = await stop_sim_log_capLogic( + { + logSessionId: 'different-session-id', + }, + fileSystem, + ); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -158,11 +187,17 @@ describe('stop_sim_log_cap plugin', () => { describe('Response Processing', () => { it('should handle successful log capture stop', async () => { - await createTestLogSession('test-session-id', 'Mock log content from file'); + const { fileSystem } = await createTestLogSession( + 'test-session-id', + 'Mock log content from file', + ); - const result = await stop_sim_log_capLogic({ - logSessionId: 'test-session-id', - }); + const result = await stop_sim_log_capLogic( + { + logSessionId: 'test-session-id', + }, + fileSystem, + ); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -171,11 +206,14 @@ describe('stop_sim_log_cap plugin', () => { }); it('should handle empty log content', async () => { - await createTestLogSession('test-session-id', ''); + const { fileSystem } = await createTestLogSession('test-session-id', ''); - const result = await stop_sim_log_capLogic({ - logSessionId: 'test-session-id', - }); + const result = await stop_sim_log_capLogic( + { + logSessionId: 'test-session-id', + }, + fileSystem, + ); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -184,11 +222,17 @@ describe('stop_sim_log_cap plugin', () => { }); it('should handle multiline log content', async () => { - await createTestLogSession('test-session-id', 'Line 1\nLine 2\nLine 3'); + const { fileSystem } = await createTestLogSession( + 'test-session-id', + 'Line 1\nLine 2\nLine 3', + ); - const result = await stop_sim_log_capLogic({ - logSessionId: 'test-session-id', - }); + const result = await stop_sim_log_capLogic( + { + logSessionId: 'test-session-id', + }, + fileSystem, + ); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( @@ -197,9 +241,12 @@ describe('stop_sim_log_cap plugin', () => { }); it('should handle log capture stop errors for non-existent session', async () => { - const result = await stop_sim_log_capLogic({ - logSessionId: 'non-existent-session', - }); + const result = await stop_sim_log_capLogic( + { + logSessionId: 'non-existent-session', + }, + createMockFileSystemExecutor(), + ); expect(result.isError).toBe(true); expect(result.content[0].text).toBe( @@ -223,9 +270,12 @@ describe('stop_sim_log_cap plugin', () => { bundleId: 'com.example.TestApp', }); - const result = await stop_sim_log_capLogic({ - logSessionId: 'test-session-id', - }); + const result = await stop_sim_log_capLogic( + { logSessionId: 'test-session-id' }, + createMockFileSystemExecutor({ + existsSync: () => false, + }), + ); expect(result.isError).toBe(true); expect(result.content[0].text).toContain( @@ -249,9 +299,15 @@ describe('stop_sim_log_cap plugin', () => { bundleId: 'com.example.TestApp', }); - const result = await stop_sim_log_capLogic({ - logSessionId: 'test-session-id', - }); + const result = await stop_sim_log_capLogic( + { logSessionId: 'test-session-id' }, + createMockFileSystemExecutor({ + existsSync: () => true, + readFile: async () => { + throw new Error('Permission denied'); + }, + }), + ); expect(result.isError).toBe(true); expect(result.content[0].text).toContain( @@ -275,9 +331,15 @@ describe('stop_sim_log_cap plugin', () => { bundleId: 'com.example.TestApp', }); - const result = await stop_sim_log_capLogic({ - logSessionId: 'test-session-id', - }); + const result = await stop_sim_log_capLogic( + { logSessionId: 'test-session-id' }, + createMockFileSystemExecutor({ + existsSync: () => true, + readFile: async () => { + throw new Error('Something went wrong'); + }, + }), + ); expect(result.isError).toBe(true); expect(result.content[0].text).toContain( diff --git a/src/mcp/tools/logging/start_device_log_cap.ts b/src/mcp/tools/logging/start_device_log_cap.ts index c2085a67..39e27fe0 100644 --- a/src/mcp/tools/logging/start_device_log_cap.ts +++ b/src/mcp/tools/logging/start_device_log_cap.ts @@ -4,20 +4,22 @@ * Starts capturing logs from a specified Apple device by launching the app with console output. */ -import * as fs from 'fs'; import * as path from 'path'; -import * as os from 'os'; import type { ChildProcess } from 'child_process'; import { v4 as uuidv4 } from 'uuid'; import * as z from 'zod'; import { log } from '../../../utils/logging/index.ts'; import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts'; -import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; +import { + getDefaultCommandExecutor, + getDefaultFileSystemExecutor, +} from '../../../utils/execution/index.ts'; import { ToolResponse } from '../../../types/common.ts'; import { createSessionAwareTool, getSessionAwareToolSchemaShape, } from '../../../utils/typed-tool-factory.ts'; +import type { WriteStream } from 'fs'; /** * Log file retention policy for device logs: @@ -37,7 +39,7 @@ export interface DeviceLogSession { logFilePath: string; deviceUuid: string; bundleId: string; - logStream?: fs.WriteStream; + logStream?: WriteStream; hasEnded: boolean; } @@ -158,18 +160,11 @@ function extractJsonOutcome(json: DevicectlLaunchJson | null): JsonOutcome | nul async function removeFileIfExists( targetPath: string, - fileExecutor?: FileSystemExecutor, + fileExecutor: FileSystemExecutor, ): Promise { try { - if (fileExecutor) { - if (fileExecutor.existsSync(targetPath)) { - await fileExecutor.rm(targetPath, { force: true }); - } - return; - } - - if (fs.existsSync(targetPath)) { - await fs.promises.rm(targetPath, { force: true }); + if (fileExecutor.existsSync(targetPath)) { + await fileExecutor.rm(targetPath, { force: true }); } } catch { // Best-effort cleanup only @@ -178,22 +173,20 @@ async function removeFileIfExists( async function pollJsonOutcome( jsonPath: string, - fileExecutor: FileSystemExecutor | undefined, + fileExecutor: FileSystemExecutor, timeoutMs: number, ): Promise { const start = Date.now(); const readOnce = async (): Promise => { try { - const exists = fileExecutor?.existsSync(jsonPath) ?? fs.existsSync(jsonPath); + const exists = fileExecutor.existsSync(jsonPath); if (!exists) { return null; } - const content = fileExecutor - ? await fileExecutor.readFile(jsonPath, 'utf8') - : await fs.promises.readFile(jsonPath, 'utf8'); + const content = await fileExecutor.readFile(jsonPath, 'utf8'); const outcome = extractJsonOutcome(safeParseJson(content)); if (outcome) { @@ -230,7 +223,7 @@ async function pollJsonOutcome( return null; } -type WriteStreamWithClosed = fs.WriteStream & { closed?: boolean }; +type WriteStreamWithClosed = WriteStream & { closed?: boolean }; /** * Start a log capture session for an iOS device by launching the app with console output. @@ -243,31 +236,25 @@ export async function startDeviceLogCapture( bundleId: string; }, executor: CommandExecutor = getDefaultCommandExecutor(), - fileSystemExecutor?: FileSystemExecutor, + fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(), ): Promise<{ sessionId: string; error?: string }> { // Clean up old logs before starting a new session - await cleanOldDeviceLogs(); + await cleanOldDeviceLogs(fileSystemExecutor); const { deviceUuid, bundleId } = params; const logSessionId = uuidv4(); const logFileName = `${DEVICE_LOG_FILE_PREFIX}${logSessionId}.log`; - const tempDir = fileSystemExecutor ? fileSystemExecutor.tmpdir() : os.tmpdir(); + const tempDir = fileSystemExecutor.tmpdir(); const logFilePath = path.join(tempDir, logFileName); const launchJsonPath = path.join(tempDir, `devicectl-launch-${logSessionId}.json`); - let logStream: fs.WriteStream | undefined; + let logStream: WriteStream | undefined; try { - // Use injected file system executor or default - if (fileSystemExecutor) { - await fileSystemExecutor.mkdir(tempDir, { recursive: true }); - await fileSystemExecutor.writeFile(logFilePath, ''); - } else { - await fs.promises.mkdir(tempDir, { recursive: true }); - await fs.promises.writeFile(logFilePath, ''); - } + await fileSystemExecutor.mkdir(tempDir, { recursive: true }); + await fileSystemExecutor.writeFile(logFilePath, ''); - logStream = fs.createWriteStream(logFilePath, { flags: 'a' }); + logStream = fileSystemExecutor.createWriteStream(logFilePath, { flags: 'a' }); logStream.write( `\n--- Device log capture for bundle ID: ${bundleId} on device: ${deviceUuid} ---\n`, @@ -594,11 +581,11 @@ function extractFailureMessage(output?: string): string | undefined { */ // Device logs follow the same retention policy as simulator logs but use a different prefix // to avoid conflicts. Both clean up logs older than LOG_RETENTION_DAYS automatically. -async function cleanOldDeviceLogs(): Promise { - const tempDir = os.tmpdir(); - let files; +async function cleanOldDeviceLogs(fileSystemExecutor: FileSystemExecutor): Promise { + const tempDir = fileSystemExecutor.tmpdir(); + let files: unknown[]; try { - files = await fs.promises.readdir(tempDir); + files = await fileSystemExecutor.readdir(tempDir); } catch (err) { log( 'warn', @@ -608,15 +595,17 @@ async function cleanOldDeviceLogs(): Promise { } const now = Date.now(); const retentionMs = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000; + const fileNames = files.filter((file): file is string => typeof file === 'string'); + await Promise.all( - files + fileNames .filter((f) => f.startsWith(DEVICE_LOG_FILE_PREFIX) && f.endsWith('.log')) .map(async (f) => { const filePath = path.join(tempDir, f); try { - const stat = await fs.promises.stat(filePath); + const stat = await fileSystemExecutor.stat(filePath); if (now - stat.mtimeMs > retentionMs) { - await fs.promises.unlink(filePath); + await fileSystemExecutor.rm(filePath, { force: true }); log('info', `Deleted old device log file: ${filePath}`); } } catch (err) { @@ -650,13 +639,15 @@ export async function start_device_log_capLogic( ): Promise { const { deviceId, bundleId } = params; + const resolvedFileSystemExecutor = fileSystemExecutor ?? getDefaultFileSystemExecutor(); + const { sessionId, error } = await startDeviceLogCapture( { deviceUuid: deviceId, bundleId: bundleId, }, executor, - fileSystemExecutor, + resolvedFileSystemExecutor, ); if (error) { diff --git a/src/mcp/tools/logging/stop_device_log_cap.ts b/src/mcp/tools/logging/stop_device_log_cap.ts index 25c50469..8bc716b9 100644 --- a/src/mcp/tools/logging/stop_device_log_cap.ts +++ b/src/mcp/tools/logging/stop_device_log_cap.ts @@ -176,6 +176,15 @@ function hasExistsSyncMethod(obj: unknown): obj is { existsSync: typeof fs.exist return typeof obj === 'object' && obj !== null && 'existsSync' in obj; } +/** + * Type guard to check if an object has createWriteStream method + */ +function hasCreateWriteStreamMethod( + obj: unknown, +): obj is { createWriteStream: typeof fs.createWriteStream } { + return typeof obj === 'object' && obj !== null && 'createWriteStream' in obj; +} + /** * Legacy support for backward compatibility */ @@ -213,6 +222,12 @@ export async function stopDeviceLogCapture( await fs.promises.writeFile(path, content, encoding); } }, + createWriteStream(path: string, options?: { flags?: string }) { + if (hasCreateWriteStreamMethod(fsToUse)) { + return fsToUse.createWriteStream(path, options); + } + return fs.createWriteStream(path, options); + }, async cp( source: string, destination: string, @@ -257,13 +272,13 @@ export async function stopDeviceLogCapture( return fs.existsSync(path); } }, - async stat(path: string): Promise<{ isDirectory(): boolean }> { + async stat(path: string): Promise<{ isDirectory(): boolean; mtimeMs: number }> { if (hasPromisesInterface(fsToUse)) { const result = await fsToUse.promises.stat(path); - return result as { isDirectory(): boolean }; + return result as { isDirectory(): boolean; mtimeMs: number }; } else { const result = await fs.promises.stat(path); - return result as { isDirectory(): boolean }; + return result as { isDirectory(): boolean; mtimeMs: number }; } }, async mkdtemp(prefix: string): Promise { diff --git a/src/mcp/tools/logging/stop_sim_log_cap.ts b/src/mcp/tools/logging/stop_sim_log_cap.ts index 64523550..9c662e0d 100644 --- a/src/mcp/tools/logging/stop_sim_log_cap.ts +++ b/src/mcp/tools/logging/stop_sim_log_cap.ts @@ -8,7 +8,8 @@ import * as z from 'zod'; import { stopLogCapture as _stopLogCapture } from '../../../utils/log-capture/index.ts'; import { ToolResponse, createTextContent } from '../../../types/common.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; -import { getDefaultCommandExecutor } from '../../../utils/command.ts'; +import { getDefaultCommandExecutor, getDefaultFileSystemExecutor } from '../../../utils/command.ts'; +import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts'; // Define schema as ZodObject const stopSimLogCapSchema = z.object({ @@ -21,8 +22,11 @@ type StopSimLogCapParams = z.infer; /** * Business logic for stopping simulator log capture session */ -export async function stop_sim_log_capLogic(params: StopSimLogCapParams): Promise { - const { logContent, error } = await _stopLogCapture(params.logSessionId); +export async function stop_sim_log_capLogic( + params: StopSimLogCapParams, + fileSystem: FileSystemExecutor, +): Promise { + const { logContent, error } = await _stopLogCapture(params.logSessionId, fileSystem); if (error) { return { content: [ @@ -48,5 +52,9 @@ export default { title: 'Stop Simulator Log Capture', destructiveHint: true, }, - handler: createTypedTool(stopSimLogCapSchema, stop_sim_log_capLogic, getDefaultCommandExecutor), + handler: createTypedTool( + stopSimLogCapSchema, + (params: StopSimLogCapParams) => stop_sim_log_capLogic(params, getDefaultFileSystemExecutor()), + getDefaultCommandExecutor, + ), }; diff --git a/src/mcp/tools/macos/__tests__/test_macos.test.ts b/src/mcp/tools/macos/__tests__/test_macos.test.ts index 7fb1c8c7..1758e50b 100644 --- a/src/mcp/tools/macos/__tests__/test_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/test_macos.test.ts @@ -19,7 +19,7 @@ const createTestFileSystemExecutor = (overrides: Partial = { mkdtemp: async () => '/tmp/test-123', rm: async () => {}, tmpdir: () => '/tmp', - stat: async () => ({ isDirectory: () => true }), + stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }), ...overrides, }); diff --git a/src/mcp/tools/project-discovery/__tests__/discover_projs.test.ts b/src/mcp/tools/project-discovery/__tests__/discover_projs.test.ts index f534ab9c..82926ed5 100644 --- a/src/mcp/tools/project-discovery/__tests__/discover_projs.test.ts +++ b/src/mcp/tools/project-discovery/__tests__/discover_projs.test.ts @@ -17,7 +17,7 @@ describe('discover_projs plugin', () => { // Create mock file system executor mockFileSystemExecutor = createMockFileSystemExecutor({ - stat: async () => ({ isDirectory: () => true }), + stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }), readdir: async () => [], }); @@ -67,7 +67,7 @@ describe('discover_projs plugin', () => { describe('Handler Behavior (Complete Literal Returns)', () => { it('should handle workspaceRoot parameter correctly when provided', async () => { - mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true }); + mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 }); mockFileSystemExecutor.readdir = async () => []; const result = await discover_projsLogic( @@ -107,7 +107,7 @@ describe('discover_projs plugin', () => { }); it('should return error when scan path is not a directory', async () => { - mockFileSystemExecutor.stat = async () => ({ isDirectory: () => false }); + mockFileSystemExecutor.stat = async () => ({ isDirectory: () => false, mtimeMs: 0 }); const result = await discover_projsLogic( { @@ -125,7 +125,7 @@ describe('discover_projs plugin', () => { }); it('should return success with no projects found', async () => { - mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true }); + mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 }); mockFileSystemExecutor.readdir = async () => []; const result = await discover_projsLogic( @@ -144,7 +144,7 @@ describe('discover_projs plugin', () => { }); it('should return success with projects found', async () => { - mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true }); + mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 }); mockFileSystemExecutor.readdir = async () => [ { name: 'MyApp.xcodeproj', isDirectory: () => true, isSymbolicLink: () => false }, { name: 'MyWorkspace.xcworkspace', isDirectory: () => true, isSymbolicLink: () => false }, @@ -219,7 +219,7 @@ describe('discover_projs plugin', () => { }); it('should handle workspaceRoot parameter correctly', async () => { - mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true }); + mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 }); mockFileSystemExecutor.readdir = async () => []; const result = await discover_projsLogic( @@ -237,7 +237,7 @@ describe('discover_projs plugin', () => { it('should handle scan path outside workspace root', async () => { // Mock path normalization to simulate path outside workspace root - mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true }); + mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 }); mockFileSystemExecutor.readdir = async () => []; const result = await discover_projsLogic( @@ -284,7 +284,7 @@ describe('discover_projs plugin', () => { it('should handle max depth reached during recursive scan', async () => { let readdirCallCount = 0; - mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true }); + mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 }); mockFileSystemExecutor.readdir = async () => { readdirCallCount++; if (readdirCallCount <= 3) { @@ -315,7 +315,7 @@ describe('discover_projs plugin', () => { }); it('should handle skipped directory types during scan', async () => { - mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true }); + mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 }); mockFileSystemExecutor.readdir = async () => [ { name: 'build', isDirectory: () => true, isSymbolicLink: () => false }, { name: 'DerivedData', isDirectory: () => true, isSymbolicLink: () => false }, @@ -340,7 +340,7 @@ describe('discover_projs plugin', () => { }); it('should handle error during recursive directory reading', async () => { - mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true }); + mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 }); mockFileSystemExecutor.readdir = async () => { const readError = new Error('Permission denied'); (readError as any).code = 'EACCES'; diff --git a/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts b/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts index 92503a00..28866c9a 100644 --- a/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts +++ b/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts @@ -48,7 +48,7 @@ describe('scaffold_ios_project plugin', () => { rm: async () => {}, cp: async () => {}, writeFile: async () => {}, - stat: async () => ({ isDirectory: () => true }), + stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }), }); // Store original environment for cleanup @@ -220,7 +220,7 @@ describe('scaffold_ios_project plugin', () => { rm: async () => {}, cp: async () => {}, writeFile: async () => {}, - stat: async () => ({ isDirectory: () => true }), + stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }), }); // Track commands executed @@ -330,7 +330,7 @@ describe('scaffold_ios_project plugin', () => { rm: async () => {}, cp: async () => {}, writeFile: async () => {}, - stat: async () => ({ isDirectory: () => true }), + stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }), }); // Track commands executed - using default executor path @@ -624,7 +624,7 @@ describe('scaffold_ios_project plugin', () => { rm: async () => {}, cp: async () => {}, writeFile: async () => {}, - stat: async () => ({ isDirectory: () => true }), + stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }), }); // Mock command executor to fail for unzip commands diff --git a/src/test-utils/mock-executors.ts b/src/test-utils/mock-executors.ts index c0419ee0..6b53e4d1 100644 --- a/src/test-utils/mock-executors.ts +++ b/src/test-utils/mock-executors.ts @@ -16,6 +16,8 @@ */ import { ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; +import type { WriteStream } from 'fs'; import { CommandExecutor, type CommandResponse } from '../utils/CommandExecutor.ts'; import { FileSystemExecutor } from '../utils/FileSystemExecutor.ts'; @@ -195,15 +197,32 @@ export function createCommandMatchingMockExecutor( export function createMockFileSystemExecutor( overrides?: Partial, ): FileSystemExecutor { + const mockWriteStream = ((): WriteStream => { + const emitter = new EventEmitter(); + const stream = Object.assign(emitter, { + destroyed: false, + write: () => true, + end: () => { + stream.destroyed = true; + emitter.emit('close'); + }, + }) as unknown as WriteStream; + return stream; + })(); + return { mkdir: async (): Promise => {}, readFile: async (): Promise => 'mock file content', writeFile: async (): Promise => {}, + createWriteStream: () => mockWriteStream, cp: async (): Promise => {}, readdir: async (): Promise => [], rm: async (): Promise => {}, existsSync: (): boolean => false, - stat: async (): Promise<{ isDirectory(): boolean }> => ({ isDirectory: (): boolean => true }), + stat: async (): Promise<{ isDirectory(): boolean; mtimeMs: number }> => ({ + isDirectory: (): boolean => true, + mtimeMs: Date.now(), + }), mkdtemp: async (): Promise => '/tmp/mock-temp-123456', tmpdir: (): string => '/tmp', ...overrides, @@ -241,6 +260,14 @@ export function createNoopFileSystemExecutor(): FileSystemExecutor { `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, + createWriteStream: (): WriteStream => { + throw new Error( + `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + + `This executor should never be called in this test context.\n` + + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + + `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, + ); + }, cp: async (): Promise => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + @@ -273,7 +300,7 @@ export function createNoopFileSystemExecutor(): FileSystemExecutor { `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, - stat: async (): Promise<{ isDirectory(): boolean }> => { + stat: async (): Promise<{ isDirectory(): boolean; mtimeMs: number }> => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + `This executor should never be called in this test context.\n` + diff --git a/src/utils/FileSystemExecutor.ts b/src/utils/FileSystemExecutor.ts index 5c91258c..4453e29d 100644 --- a/src/utils/FileSystemExecutor.ts +++ b/src/utils/FileSystemExecutor.ts @@ -2,15 +2,18 @@ * File system executor interface for dependency injection */ +import type { WriteStream } from 'fs'; + export interface FileSystemExecutor { mkdir(path: string, options?: { recursive?: boolean }): Promise; readFile(path: string, encoding?: BufferEncoding): Promise; writeFile(path: string, content: string, encoding?: BufferEncoding): Promise; + createWriteStream(path: string, options?: { flags?: string }): WriteStream; cp(source: string, destination: string, options?: { recursive?: boolean }): Promise; readdir(path: string, options?: { withFileTypes?: boolean }): Promise; rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise; existsSync(path: string): boolean; - stat(path: string): Promise<{ isDirectory(): boolean }>; + stat(path: string): Promise<{ isDirectory(): boolean; mtimeMs: number }>; mkdtemp(prefix: string): Promise; tmpdir(): string; } diff --git a/src/utils/command.ts b/src/utils/command.ts index 2e35f4f5..ca80b7ff 100644 --- a/src/utils/command.ts +++ b/src/utils/command.ts @@ -10,7 +10,7 @@ */ import { spawn } from 'child_process'; -import { existsSync } from 'fs'; +import { createWriteStream, existsSync } from 'fs'; import { tmpdir as osTmpdir } from 'os'; import { log } from './logger.ts'; import { FileSystemExecutor } from './FileSystemExecutor.ts'; @@ -158,6 +158,10 @@ const defaultFileSystemExecutor: FileSystemExecutor = { await fs.writeFile(path, content, encoding); }, + createWriteStream(path: string, options?: { flags?: string }) { + return createWriteStream(path, options); + }, + async cp(source: string, destination: string, options?: { recursive?: boolean }): Promise { const fs = await import('fs/promises'); await fs.cp(source, destination, options); @@ -177,7 +181,7 @@ const defaultFileSystemExecutor: FileSystemExecutor = { return existsSync(path); }, - async stat(path: string): Promise<{ isDirectory(): boolean }> { + async stat(path: string): Promise<{ isDirectory(): boolean; mtimeMs: number }> { const fs = await import('fs/promises'); return await fs.stat(path); }, diff --git a/src/utils/log_capture.ts b/src/utils/log_capture.ts index 6588aa42..dfc602df 100644 --- a/src/utils/log_capture.ts +++ b/src/utils/log_capture.ts @@ -1,10 +1,13 @@ -import * as fs from 'fs'; import * as path from 'path'; -import * as os from 'os'; import type { ChildProcess } from 'child_process'; import { v4 as uuidv4 } from 'uuid'; import { log } from '../utils/logger.ts'; -import { CommandExecutor, getDefaultCommandExecutor } from './command.ts'; +import { + CommandExecutor, + getDefaultCommandExecutor, + getDefaultFileSystemExecutor, +} from './command.ts'; +import { FileSystemExecutor } from './FileSystemExecutor.ts'; /** * Log file retention policy: @@ -35,19 +38,20 @@ export async function startLogCapture( args?: string[]; }, executor: CommandExecutor = getDefaultCommandExecutor(), + fileSystem: FileSystemExecutor = getDefaultFileSystemExecutor(), ): Promise<{ sessionId: string; logFilePath: string; processes: ChildProcess[]; error?: string }> { // Clean up old logs before starting a new session - await cleanOldLogs(); + await cleanOldLogs(fileSystem); const { simulatorUuid, bundleId, captureConsole = false, args = [] } = params; const logSessionId = uuidv4(); const logFileName = `${LOG_FILE_PREFIX}${logSessionId}.log`; - const logFilePath = path.join(os.tmpdir(), logFileName); + const logFilePath = path.join(fileSystem.tmpdir(), logFileName); try { - await fs.promises.mkdir(os.tmpdir(), { recursive: true }); - await fs.promises.writeFile(logFilePath, ''); - const logStream = fs.createWriteStream(logFilePath, { flags: 'a' }); + await fileSystem.mkdir(fileSystem.tmpdir(), { recursive: true }); + await fileSystem.writeFile(logFilePath, ''); + const logStream = fileSystem.createWriteStream(logFilePath, { flags: 'a' }); const processes: ChildProcess[] = []; logStream.write('\n--- Log capture for bundle ID: ' + bundleId + ' ---\n'); @@ -145,6 +149,7 @@ export async function startLogCapture( */ export async function stopLogCapture( logSessionId: string, + fileSystem: FileSystemExecutor = getDefaultFileSystemExecutor(), ): Promise<{ logContent: string; error?: string }> { const session = activeLogSessions.get(logSessionId); if (!session) { @@ -165,8 +170,10 @@ export async function stopLogCapture( 'info', `Log capture session ${logSessionId} stopped. Log file retained at: ${logFilePath}`, ); - await fs.promises.access(logFilePath, fs.constants.R_OK); - const fileContent = await fs.promises.readFile(logFilePath, 'utf-8'); + if (!fileSystem.existsSync(logFilePath)) { + throw new Error(`Log file not found: ${logFilePath}`); + } + const fileContent = await fileSystem.readFile(logFilePath, 'utf-8'); log('info', `Successfully read log content from ${logFilePath}`); return { logContent: fileContent }; } catch (error) { @@ -180,11 +187,11 @@ export async function stopLogCapture( * Deletes log files older than LOG_RETENTION_DAYS from the temp directory. * Runs quietly; errors are logged but do not throw. */ -async function cleanOldLogs(): Promise { - const tempDir = os.tmpdir(); - let files: string[]; +async function cleanOldLogs(fileSystem: FileSystemExecutor): Promise { + const tempDir = fileSystem.tmpdir(); + let files: unknown[]; try { - files = await fs.promises.readdir(tempDir); + files = await fileSystem.readdir(tempDir); } catch (err) { log( 'warn', @@ -194,15 +201,17 @@ async function cleanOldLogs(): Promise { } const now = Date.now(); const retentionMs = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000; + const fileNames = files.filter((file): file is string => typeof file === 'string'); + await Promise.all( - files + fileNames .filter((f) => f.startsWith(LOG_FILE_PREFIX) && f.endsWith('.log')) .map(async (f) => { const filePath = path.join(tempDir, f); try { - const stat = await fs.promises.stat(filePath); + const stat = await fileSystem.stat(filePath); if (now - stat.mtimeMs > retentionMs) { - await fs.promises.unlink(filePath); + await fileSystem.rm(filePath, { force: true }); log('info', `Deleted old log file: ${filePath}`); } } catch (err) { From 0591063a2fd3a7f03132a5e978a35db2eb23bb4d Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 8 Jan 2026 09:54:21 +0000 Subject: [PATCH 3/4] test: deslop mocks and assertions --- .../device/__tests__/build_device.test.ts | 9 +-- .../__tests__/get_device_app_path.test.ts | 27 +++------ .../__tests__/install_app_device.test.ts | 5 +- .../__tests__/launch_app_device.test.ts | 5 +- .../device/__tests__/list_devices.test.ts | 38 +++++-------- .../device/__tests__/stop_app_device.test.ts | 5 +- .../device/__tests__/test_device.test.ts | 15 ++--- .../__tests__/start_device_log_cap.test.ts | 39 ++++++++----- .../tools/ui-testing/__tests__/tap.test.ts | 30 +++++----- .../tools/ui-testing/__tests__/touch.test.ts | 56 +++++++++---------- .../ui-testing/__tests__/type_text.test.ts | 18 +++--- 11 files changed, 111 insertions(+), 136 deletions(-) diff --git a/src/mcp/tools/device/__tests__/build_device.test.ts b/src/mcp/tools/device/__tests__/build_device.test.ts index 1caaad94..254ca35b 100644 --- a/src/mcp/tools/device/__tests__/build_device.test.ts +++ b/src/mcp/tools/device/__tests__/build_device.test.ts @@ -144,10 +144,9 @@ describe('build_device plugin', () => { logPrefix?: string, silent?: boolean, opts?: { cwd?: string }, - detached?: boolean, + _detached?: boolean, ) => { commandCalls.push({ args, logPrefix, silent, opts }); - void detached; return createMockCommandResponse({ success: true, output: 'Build succeeded', @@ -197,10 +196,9 @@ describe('build_device plugin', () => { logPrefix?: string, silent?: boolean, opts?: { cwd?: string }, - detached?: boolean, + _detached?: boolean, ) => { commandCalls.push({ args, logPrefix, silent, opts }); - void detached; return createMockCommandResponse({ success: true, output: 'Build succeeded', @@ -307,10 +305,9 @@ describe('build_device plugin', () => { logPrefix?: string, silent?: boolean, opts?: { cwd?: string }, - detached?: boolean, + _detached?: boolean, ) => { commandCalls.push({ args, logPrefix, silent, opts }); - void detached; return createMockCommandResponse({ success: true, output: 'Build succeeded', diff --git a/src/mcp/tools/device/__tests__/get_device_app_path.test.ts b/src/mcp/tools/device/__tests__/get_device_app_path.test.ts index 5e4a1730..c394f800 100644 --- a/src/mcp/tools/device/__tests__/get_device_app_path.test.ts +++ b/src/mcp/tools/device/__tests__/get_device_app_path.test.ts @@ -102,10 +102,9 @@ describe('get_device_app_path plugin', () => { logPrefix?: string, useShell?: boolean, opts?: { cwd?: string }, - detached?: boolean, + _detached?: boolean, ) => { calls.push({ args, logPrefix, useShell, opts }); - void detached; return Promise.resolve( createMockCommandResponse({ success: true, @@ -157,10 +156,9 @@ describe('get_device_app_path plugin', () => { logPrefix?: string, useShell?: boolean, opts?: { cwd?: string }, - detached?: boolean, + _detached?: boolean, ) => { calls.push({ args, logPrefix, useShell, opts }); - void detached; return Promise.resolve( createMockCommandResponse({ success: true, @@ -213,10 +211,9 @@ describe('get_device_app_path plugin', () => { logPrefix?: string, useShell?: boolean, opts?: { cwd?: string }, - detached?: boolean, + _detached?: boolean, ) => { calls.push({ args, logPrefix, useShell, opts }); - void detached; return Promise.resolve( createMockCommandResponse({ success: true, @@ -347,10 +344,9 @@ describe('get_device_app_path plugin', () => { logPrefix?: string, useShell?: boolean, opts?: { cwd?: string }, - detached?: boolean, + _detached?: boolean, ) => { calls.push({ args, logPrefix, useShell, opts }); - void detached; return Promise.resolve( createMockCommandResponse({ success: true, @@ -392,17 +388,12 @@ describe('get_device_app_path plugin', () => { it('should return exact exception handling response', async () => { const mockExecutor = ( - args: string[], - logPrefix?: string, - useShell?: boolean, - opts?: { cwd?: string }, - detached?: boolean, + _args: string[], + _logPrefix?: string, + _useShell?: boolean, + _opts?: { cwd?: string }, + _detached?: boolean, ) => { - void args; - void logPrefix; - void useShell; - void opts; - void detached; return Promise.reject(new Error('Network error')); }; diff --git a/src/mcp/tools/device/__tests__/install_app_device.test.ts b/src/mcp/tools/device/__tests__/install_app_device.test.ts index ba20a9e5..856fd770 100644 --- a/src/mcp/tools/device/__tests__/install_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/install_app_device.test.ts @@ -67,14 +67,13 @@ describe('install_app_device plugin', () => { description?: string, useShell?: boolean, opts?: { env?: Record }, - detached?: boolean, + _detached?: boolean, ) => { capturedCommand = command; capturedDescription = description ?? ''; capturedUseShell = !!useShell; capturedEnv = opts?.env; - void detached; - return mockExecutor(command, description, useShell, opts, detached); + return mockExecutor(command, description, useShell, opts, _detached); }; await install_app_deviceLogic( diff --git a/src/mcp/tools/device/__tests__/launch_app_device.test.ts b/src/mcp/tools/device/__tests__/launch_app_device.test.ts index 020870db..bbb52b4c 100644 --- a/src/mcp/tools/device/__tests__/launch_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/launch_app_device.test.ts @@ -71,11 +71,10 @@ describe('launch_app_device plugin (device-shared)', () => { logPrefix?: string, useShell?: boolean, opts?: { env?: Record }, - detached?: boolean, + _detached?: boolean, ) => { calls.push({ command, logPrefix, useShell, env: opts?.env }); - void detached; - return mockExecutor(command, logPrefix, useShell, opts, detached); + return mockExecutor(command, logPrefix, useShell, opts, _detached); }; await launch_app_deviceLogic( diff --git a/src/mcp/tools/device/__tests__/list_devices.test.ts b/src/mcp/tools/device/__tests__/list_devices.test.ts index 160cf8a1..991afb3c 100644 --- a/src/mcp/tools/device/__tests__/list_devices.test.ts +++ b/src/mcp/tools/device/__tests__/list_devices.test.ts @@ -86,11 +86,10 @@ describe('list_devices plugin (device-shared)', () => { logPrefix?: string, useShell?: boolean, opts?: { env?: Record }, - detached?: boolean, + _detached?: boolean, ) => { commandCalls.push({ command, logPrefix, useShell, env: opts?.env }); - void detached; - return mockExecutor(command, logPrefix, useShell, opts, detached); + return mockExecutor(command, logPrefix, useShell, opts, _detached); }; // Create mock path dependencies @@ -137,11 +136,10 @@ describe('list_devices plugin (device-shared)', () => { logPrefix?: string, useShell?: boolean, opts?: { env?: Record }, - detached?: boolean, + _detached?: boolean, ) => { callCount++; commandCalls.push({ command, logPrefix, useShell, env: opts?.env }); - void detached; if (callCount === 1) { // First call fails (devicectl) @@ -243,18 +241,13 @@ describe('list_devices plugin (device-shared)', () => { // Create executor with call count behavior let callCount = 0; const mockExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - opts?: { env?: Record }, - detached?: boolean, + _command: string[], + _logPrefix?: string, + _useShell?: boolean, + _opts?: { env?: Record }, + _detached?: boolean, ) => { callCount++; - void command; - void logPrefix; - void useShell; - void opts; - void detached; if (callCount === 1) { // First call fails (devicectl) return createMockCommandResponse({ @@ -308,18 +301,13 @@ describe('list_devices plugin (device-shared)', () => { // Create executor with call count behavior let callCount = 0; const mockExecutor = async ( - command: string[], - logPrefix?: string, - useShell?: boolean, - opts?: { env?: Record }, - detached?: boolean, + _command: string[], + _logPrefix?: string, + _useShell?: boolean, + _opts?: { env?: Record }, + _detached?: boolean, ) => { callCount++; - void command; - void logPrefix; - void useShell; - void opts; - void detached; if (callCount === 1) { // First call succeeds (devicectl) return createMockCommandResponse({ diff --git a/src/mcp/tools/device/__tests__/stop_app_device.test.ts b/src/mcp/tools/device/__tests__/stop_app_device.test.ts index 71774795..32d9ed96 100644 --- a/src/mcp/tools/device/__tests__/stop_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/stop_app_device.test.ts @@ -65,14 +65,13 @@ describe('stop_app_device plugin', () => { description?: string, useShell?: boolean, opts?: { env?: Record }, - detached?: boolean, + _detached?: boolean, ) => { capturedCommand = command; capturedDescription = description ?? ''; capturedUseShell = !!useShell; capturedEnv = opts?.env; - void detached; - return mockExecutor(command, description, useShell, opts, detached); + return mockExecutor(command, description, useShell, opts, _detached); }; await stop_app_deviceLogic( diff --git a/src/mcp/tools/device/__tests__/test_device.test.ts b/src/mcp/tools/device/__tests__/test_device.test.ts index 2ce5a76d..d5da3c9d 100644 --- a/src/mcp/tools/device/__tests__/test_device.test.ts +++ b/src/mcp/tools/device/__tests__/test_device.test.ts @@ -234,18 +234,13 @@ describe('test_device plugin', () => { // Create a multi-call mock that handles different commands let callCount = 0; const mockExecutor = async ( - args: string[], - description?: string, - useShell?: boolean, - opts?: { cwd?: string }, - detached?: boolean, + _args: string[], + _description?: string, + _useShell?: boolean, + _opts?: { cwd?: string }, + _detached?: boolean, ) => { callCount++; - void args; - void description; - void useShell; - void opts; - void detached; // First call is for xcodebuild test (successful) if (callCount === 1) { diff --git a/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts b/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts index 580360cf..133062ec 100644 --- a/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts +++ b/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts @@ -17,6 +17,15 @@ import plugin, { } from '../start_device_log_cap.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; +type Mutable = { + -readonly [K in keyof T]: T[K]; +}; + +type MockChildProcess = Mutable & { + stdout: Readable; + stderr: Readable; +}; + describe('start_device_log_cap plugin', () => { // Mock state tracking let commandCalls: Array<{ @@ -151,7 +160,7 @@ describe('start_device_log_cap plugin', () => { }); it('should surface early launch failures when process exits immediately', async () => { - const failingProcess = new EventEmitter() as unknown as ChildProcess; + const failingProcess = new EventEmitter() as MockChildProcess; const stubOutput = new Readable({ read() {}, @@ -162,11 +171,11 @@ describe('start_device_log_cap plugin', () => { failingProcess.stdout = stubOutput; failingProcess.stderr = stubError; - (failingProcess as any).exitCode = null; - (failingProcess as any).killed = false; + failingProcess.exitCode = null; + failingProcess.killed = false; failingProcess.kill = () => { - (failingProcess as any).killed = true; - (failingProcess as any).exitCode = 0; + failingProcess.killed = true; + failingProcess.exitCode = 0; failingProcess.emit('close', 0, null); return true; }; @@ -200,7 +209,7 @@ describe('start_device_log_cap plugin', () => { 'data', 'ERROR: The application failed to launch. (com.apple.dt.CoreDeviceError error 10002)\nNSLocalizedRecoverySuggestion = Provide a valid bundle identifier.\n', ); - (failingProcess as any).exitCode = 70; + failingProcess.exitCode = 70; failingProcess.emit('close', 70, null); }, 10); @@ -226,7 +235,7 @@ describe('start_device_log_cap plugin', () => { }, }; - const failingProcess = new EventEmitter() as unknown as ChildProcess; + const failingProcess = new EventEmitter() as MockChildProcess; const stubOutput = new Readable({ read() {}, @@ -237,10 +246,10 @@ describe('start_device_log_cap plugin', () => { failingProcess.stdout = stubOutput; failingProcess.stderr = stubError; - (failingProcess as any).exitCode = null; - (failingProcess as any).killed = false; + failingProcess.exitCode = null; + failingProcess.killed = false; failingProcess.kill = () => { - (failingProcess as any).killed = true; + failingProcess.killed = true; return true; }; @@ -278,7 +287,7 @@ describe('start_device_log_cap plugin', () => { }); setTimeout(() => { - (failingProcess as any).exitCode = 0; + failingProcess.exitCode = 0; failingProcess.emit('close', 0, null); }, 5); @@ -308,7 +317,7 @@ describe('start_device_log_cap plugin', () => { }, }; - const runningProcess = new EventEmitter() as unknown as ChildProcess; + const runningProcess = new EventEmitter() as MockChildProcess; const stubOutput = new Readable({ read() {}, @@ -319,10 +328,10 @@ describe('start_device_log_cap plugin', () => { runningProcess.stdout = stubOutput; runningProcess.stderr = stubError; - (runningProcess as any).exitCode = null; - (runningProcess as any).killed = false; + runningProcess.exitCode = null; + runningProcess.killed = false; runningProcess.kill = () => { - (runningProcess as any).killed = true; + runningProcess.killed = true; runningProcess.emit('close', 0, null); return true; }; diff --git a/src/mcp/tools/ui-testing/__tests__/tap.test.ts b/src/mcp/tools/ui-testing/__tests__/tap.test.ts index e60b6979..d956edf5 100644 --- a/src/mcp/tools/ui-testing/__tests__/tap.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/tap.test.ts @@ -17,7 +17,7 @@ function createMockAxeHelpers(): AxeHelpers { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -34,7 +34,7 @@ function createMockAxeHelpersWithNullPath(): AxeHelpers { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -491,7 +491,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Tap at (100, 200) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -520,7 +520,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Tap at (150, 300) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -551,7 +551,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Tap at (250, 400) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -580,7 +580,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Tap at (0, 0) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -609,7 +609,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Tap at (1920, 1080) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -637,7 +637,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Tap on element id "loginButton" simulated successfully.', }, ], @@ -665,7 +665,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Tap on element label "Log in" simulated successfully.', }, ], @@ -812,7 +812,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -842,7 +842,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -872,7 +872,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -900,7 +900,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -928,7 +928,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -956,7 +956,7 @@ describe('Tap Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], diff --git a/src/mcp/tools/ui-testing/__tests__/touch.test.ts b/src/mcp/tools/ui-testing/__tests__/touch.test.ts index 8629093b..e6545ec8 100644 --- a/src/mcp/tools/ui-testing/__tests__/touch.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/touch.test.ts @@ -133,7 +133,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -183,7 +183,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -233,7 +233,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -285,7 +285,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -376,7 +376,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -398,7 +398,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -414,7 +414,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -436,7 +436,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -452,7 +452,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -474,7 +474,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -495,9 +495,7 @@ describe('Touch Plugin', () => { ); expect(result).toEqual({ - content: [ - { type: 'text' as const, text: 'Error: At least one of "down" or "up" must be true' }, - ], + content: [{ type: 'text', text: 'Error: At least one of "down" or "up" must be true' }], isError: true, }); }); @@ -515,7 +513,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -537,7 +535,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -558,7 +556,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -580,7 +578,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -601,7 +599,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -624,7 +622,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Touch event (touch down+up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', }, ], @@ -641,7 +639,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -663,7 +661,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -684,7 +682,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -706,7 +704,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: "Error: Failed to execute touch event: axe command 'touch' failed.\nDetails: axe command failed", }, ], @@ -725,7 +723,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -747,7 +745,7 @@ describe('Touch Plugin', () => { expect(result).toMatchObject({ content: [ { - type: 'text' as const, + type: 'text', text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: System error occurred', ), @@ -768,7 +766,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -790,7 +788,7 @@ describe('Touch Plugin', () => { expect(result).toMatchObject({ content: [ { - type: 'text' as const, + type: 'text', text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: Unexpected error', ), @@ -811,7 +809,7 @@ describe('Touch Plugin', () => { createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -833,7 +831,7 @@ describe('Touch Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], 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 ba4ce27e..53c48d19 100644 --- a/src/mcp/tools/ui-testing/__tests__/type_text.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/type_text.test.ts @@ -27,7 +27,7 @@ function createMockAxeHelpers( createAxeNotAvailableResponse: () => ({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -316,7 +316,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -345,7 +345,7 @@ describe('Type Text Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text' as const, text: 'Text typing simulated successfully.' }], + content: [{ type: 'text', text: 'Text typing simulated successfully.' }], isError: false, }); }); @@ -372,7 +372,7 @@ describe('Type Text Plugin', () => { ); expect(result).toEqual({ - content: [{ type: 'text' as const, text: 'Text typing simulated successfully.' }], + content: [{ type: 'text', text: 'Text typing simulated successfully.' }], isError: false, }); }); @@ -394,7 +394,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.', }, ], @@ -426,7 +426,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: "Error: Failed to simulate text typing: axe command 'type' failed.\nDetails: Text field not found", }, ], @@ -454,7 +454,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory', ), @@ -484,7 +484,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: Unexpected error', ), @@ -514,7 +514,7 @@ describe('Type Text Plugin', () => { expect(result).toEqual({ content: [ { - type: 'text' as const, + type: 'text', text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], From 7c7a7fdd330a23ec000758faf3da81403ed0f679 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 8 Jan 2026 10:13:33 +0000 Subject: [PATCH 4/4] test: align nitpick test conventions --- .../macos/__tests__/build_run_macos.test.ts | 5 +---- .../__tests__/session_show_defaults.test.ts | 4 ++++ .../ui-testing/__tests__/key_press.test.ts | 17 +++++++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/mcp/tools/macos/__tests__/build_run_macos.test.ts b/src/mcp/tools/macos/__tests__/build_run_macos.test.ts index ccee27a4..bad1a519 100644 --- a/src/mcp/tools/macos/__tests__/build_run_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/build_run_macos.test.ts @@ -1,12 +1,9 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import type { ChildProcess } from 'child_process'; import * as z from 'zod'; -import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { createMockExecutor, mockProcess } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import tool, { buildRunMacOSLogic } from '../build_run_macos.ts'; -const mockProcess = { pid: 12345 } as unknown as ChildProcess; - describe('build_run_macos', () => { beforeEach(() => { sessionStore.clear(); diff --git a/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts b/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts index 379ba4eb..6096f4d6 100644 --- a/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts +++ b/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts @@ -33,6 +33,8 @@ describe('session-show-defaults tool', () => { it('should return empty defaults when none set', async () => { const result = await plugin.handler(); expect(result.isError).toBe(false); + expect(result.content).toHaveLength(1); + expect(typeof result.content[0].text).toBe('string'); const parsed = JSON.parse(result.content[0].text as string); expect(parsed).toEqual({}); }); @@ -41,6 +43,8 @@ describe('session-show-defaults tool', () => { sessionStore.setDefaults({ scheme: 'MyScheme', simulatorId: 'SIM-123' }); const result = await plugin.handler(); expect(result.isError).toBe(false); + expect(result.content).toHaveLength(1); + expect(typeof result.content[0].text).toBe('string'); const parsed = JSON.parse(result.content[0].text as string); expect(parsed.scheme).toBe('MyScheme'); expect(parsed.simulatorId).toBe('SIM-123'); 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 c6bcc7ba..0af1f5c4 100644 --- a/src/mcp/tools/ui-testing/__tests__/key_press.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/key_press.test.ts @@ -5,6 +5,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import * as z from 'zod'; import { + createMockCommandResponse, createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, @@ -81,12 +82,12 @@ describe('Key Press Plugin', () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'key press completed', error: undefined, process: mockProcess, - }; + }); }; const mockAxeHelpers = { @@ -125,12 +126,12 @@ describe('Key Press Plugin', () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'key press completed', error: undefined, process: mockProcess, - }; + }); }; const mockAxeHelpers = { @@ -172,12 +173,12 @@ describe('Key Press Plugin', () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'key press completed', error: undefined, process: mockProcess, - }; + }); }; const mockAxeHelpers = { @@ -216,12 +217,12 @@ describe('Key Press Plugin', () => { let capturedCommand: string[] = []; const trackingExecutor = async (command: string[]) => { capturedCommand = command; - return { + return createMockCommandResponse({ success: true, output: 'key press completed', error: undefined, process: mockProcess, - }; + }); }; const mockAxeHelpers = {