#
tokens: 46673/50000 10/337 files (page 9/14)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 9 of 14. Use http://codebase.md/cameroncooke/xcodebuildmcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .axe-version
├── .claude
│   └── agents
│       └── xcodebuild-mcp-qa-tester.md
├── .cursor
│   ├── BUGBOT.md
│   └── environment.json
├── .cursorrules
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   └── workflows
│       ├── ci.yml
│       ├── claude-code-review.yml
│       ├── claude-dispatch.yml
│       ├── claude.yml
│       ├── droid-code-review.yml
│       ├── README.md
│       ├── release.yml
│       └── sentry.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── .vscode
│   ├── extensions.json
│   ├── launch.json
│   ├── mcp.json
│   ├── settings.json
│   └── tasks.json
├── AGENTS.md
├── banner.png
├── build-plugins
│   ├── plugin-discovery.js
│   ├── plugin-discovery.ts
│   └── tsconfig.json
├── CHANGELOG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── docs
│   ├── ARCHITECTURE.md
│   ├── CODE_QUALITY.md
│   ├── CONTRIBUTING.md
│   ├── ESLINT_TYPE_SAFETY.md
│   ├── MANUAL_TESTING.md
│   ├── NODEJS_2025.md
│   ├── PLUGIN_DEVELOPMENT.md
│   ├── RELEASE_PROCESS.md
│   ├── RELOADEROO_FOR_XCODEBUILDMCP.md
│   ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md
│   ├── RELOADEROO.md
│   ├── session_management_plan.md
│   ├── session-aware-migration-todo.md
│   ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md
│   ├── TESTING.md
│   └── TOOLS.md
├── eslint.config.js
├── example_projects
│   ├── .vscode
│   │   └── launch.json
│   ├── iOS
│   │   ├── .cursor
│   │   │   └── rules
│   │   │       └── errors.mdc
│   │   ├── .vscode
│   │   │   └── settings.json
│   │   ├── Makefile
│   │   ├── MCPTest
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── MCPTestApp.swift
│   │   │   └── Preview Content
│   │   │       └── Preview Assets.xcassets
│   │   │           └── Contents.json
│   │   ├── MCPTest.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── xcshareddata
│   │   │       └── xcschemes
│   │   │           └── MCPTest.xcscheme
│   │   └── MCPTestUITests
│   │       └── MCPTestUITests.swift
│   ├── iOS_Calculator
│   │   ├── CalculatorApp
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── CalculatorApp.swift
│   │   │   └── CalculatorApp.xctestplan
│   │   ├── CalculatorApp.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── xcshareddata
│   │   │       └── xcschemes
│   │   │           └── CalculatorApp.xcscheme
│   │   ├── CalculatorApp.xcworkspace
│   │   │   └── contents.xcworkspacedata
│   │   ├── CalculatorAppPackage
│   │   │   ├── .gitignore
│   │   │   ├── Package.swift
│   │   │   ├── Sources
│   │   │   │   └── CalculatorAppFeature
│   │   │   │       ├── BackgroundEffect.swift
│   │   │   │       ├── CalculatorButton.swift
│   │   │   │       ├── CalculatorDisplay.swift
│   │   │   │       ├── CalculatorInputHandler.swift
│   │   │   │       ├── CalculatorService.swift
│   │   │   │       └── ContentView.swift
│   │   │   └── Tests
│   │   │       └── CalculatorAppFeatureTests
│   │   │           └── CalculatorServiceTests.swift
│   │   ├── CalculatorAppTests
│   │   │   └── CalculatorAppTests.swift
│   │   └── Config
│   │       ├── Debug.xcconfig
│   │       ├── Release.xcconfig
│   │       ├── Shared.xcconfig
│   │       └── Tests.xcconfig
│   ├── macOS
│   │   ├── MCPTest
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── MCPTest.entitlements
│   │   │   ├── MCPTestApp.swift
│   │   │   └── Preview Content
│   │   │       └── Preview Assets.xcassets
│   │   │           └── Contents.json
│   │   └── MCPTest.xcodeproj
│   │       ├── project.pbxproj
│   │       └── xcshareddata
│   │           └── xcschemes
│   │               └── MCPTest.xcscheme
│   └── spm
│       ├── .gitignore
│       ├── Package.resolved
│       ├── Package.swift
│       ├── Sources
│       │   ├── long-server
│       │   │   └── main.swift
│       │   ├── quick-task
│       │   │   └── main.swift
│       │   ├── spm
│       │   │   └── main.swift
│       │   └── TestLib
│       │       └── TaskManager.swift
│       └── Tests
│           └── TestLibTests
│               └── SimpleTests.swift
├── LICENSE
├── mcp-install-dark.png
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── analysis
│   │   └── tools-analysis.ts
│   ├── bundle-axe.sh
│   ├── check-code-patterns.js
│   ├── release.sh
│   ├── tools-cli.ts
│   └── update-tools-docs.ts
├── server.json
├── smithery.yaml
├── src
│   ├── core
│   │   ├── __tests__
│   │   │   └── resources.test.ts
│   │   ├── dynamic-tools.ts
│   │   ├── plugin-registry.ts
│   │   ├── plugin-types.ts
│   │   └── resources.ts
│   ├── doctor-cli.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── resources
│   │   │   ├── __tests__
│   │   │   │   ├── devices.test.ts
│   │   │   │   ├── doctor.test.ts
│   │   │   │   └── simulators.test.ts
│   │   │   ├── devices.ts
│   │   │   ├── doctor.ts
│   │   │   └── simulators.ts
│   │   └── tools
│   │       ├── device
│   │       │   ├── __tests__
│   │       │   │   ├── build_device.test.ts
│   │       │   │   ├── get_device_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── install_app_device.test.ts
│   │       │   │   ├── launch_app_device.test.ts
│   │       │   │   ├── list_devices.test.ts
│   │       │   │   ├── re-exports.test.ts
│   │       │   │   ├── stop_app_device.test.ts
│   │       │   │   └── test_device.test.ts
│   │       │   ├── build_device.ts
│   │       │   ├── clean.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_device_app_path.ts
│   │       │   ├── index.ts
│   │       │   ├── install_app_device.ts
│   │       │   ├── launch_app_device.ts
│   │       │   ├── list_devices.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── start_device_log_cap.ts
│   │       │   ├── stop_app_device.ts
│   │       │   ├── stop_device_log_cap.ts
│   │       │   └── test_device.ts
│   │       ├── discovery
│   │       │   ├── __tests__
│   │       │   │   └── discover_tools.test.ts
│   │       │   ├── discover_tools.ts
│   │       │   └── index.ts
│   │       ├── doctor
│   │       │   ├── __tests__
│   │       │   │   ├── doctor.test.ts
│   │       │   │   └── index.test.ts
│   │       │   ├── doctor.ts
│   │       │   ├── index.ts
│   │       │   └── lib
│   │       │       └── doctor.deps.ts
│   │       ├── logging
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── start_device_log_cap.test.ts
│   │       │   │   ├── start_sim_log_cap.test.ts
│   │       │   │   ├── stop_device_log_cap.test.ts
│   │       │   │   └── stop_sim_log_cap.test.ts
│   │       │   ├── index.ts
│   │       │   ├── start_device_log_cap.ts
│   │       │   ├── start_sim_log_cap.ts
│   │       │   ├── stop_device_log_cap.ts
│   │       │   └── stop_sim_log_cap.ts
│   │       ├── macos
│   │       │   ├── __tests__
│   │       │   │   ├── build_macos.test.ts
│   │       │   │   ├── build_run_macos.test.ts
│   │       │   │   ├── get_mac_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── launch_mac_app.test.ts
│   │       │   │   ├── re-exports.test.ts
│   │       │   │   ├── stop_mac_app.test.ts
│   │       │   │   └── test_macos.test.ts
│   │       │   ├── build_macos.ts
│   │       │   ├── build_run_macos.ts
│   │       │   ├── clean.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_mac_app_path.ts
│   │       │   ├── get_mac_bundle_id.ts
│   │       │   ├── index.ts
│   │       │   ├── launch_mac_app.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── stop_mac_app.ts
│   │       │   └── test_macos.ts
│   │       ├── project-discovery
│   │       │   ├── __tests__
│   │       │   │   ├── discover_projs.test.ts
│   │       │   │   ├── get_app_bundle_id.test.ts
│   │       │   │   ├── get_mac_bundle_id.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── list_schemes.test.ts
│   │       │   │   └── show_build_settings.test.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_mac_bundle_id.ts
│   │       │   ├── index.ts
│   │       │   ├── list_schemes.ts
│   │       │   └── show_build_settings.ts
│   │       ├── project-scaffolding
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── scaffold_ios_project.test.ts
│   │       │   │   └── scaffold_macos_project.test.ts
│   │       │   ├── index.ts
│   │       │   ├── scaffold_ios_project.ts
│   │       │   └── scaffold_macos_project.ts
│   │       ├── session-management
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── session_clear_defaults.test.ts
│   │       │   │   ├── session_set_defaults.test.ts
│   │       │   │   └── session_show_defaults.test.ts
│   │       │   ├── index.ts
│   │       │   ├── session_clear_defaults.ts
│   │       │   ├── session_set_defaults.ts
│   │       │   └── session_show_defaults.ts
│   │       ├── simulator
│   │       │   ├── __tests__
│   │       │   │   ├── boot_sim.test.ts
│   │       │   │   ├── build_run_sim.test.ts
│   │       │   │   ├── build_sim.test.ts
│   │       │   │   ├── get_sim_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── install_app_sim.test.ts
│   │       │   │   ├── launch_app_logs_sim.test.ts
│   │       │   │   ├── launch_app_sim.test.ts
│   │       │   │   ├── list_sims.test.ts
│   │       │   │   ├── open_sim.test.ts
│   │       │   │   ├── record_sim_video.test.ts
│   │       │   │   ├── screenshot.test.ts
│   │       │   │   ├── stop_app_sim.test.ts
│   │       │   │   └── test_sim.test.ts
│   │       │   ├── boot_sim.ts
│   │       │   ├── build_run_sim.ts
│   │       │   ├── build_sim.ts
│   │       │   ├── clean.ts
│   │       │   ├── describe_ui.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_sim_app_path.ts
│   │       │   ├── index.ts
│   │       │   ├── install_app_sim.ts
│   │       │   ├── launch_app_logs_sim.ts
│   │       │   ├── launch_app_sim.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── list_sims.ts
│   │       │   ├── open_sim.ts
│   │       │   ├── record_sim_video.ts
│   │       │   ├── screenshot.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── stop_app_sim.ts
│   │       │   └── test_sim.ts
│   │       ├── simulator-management
│   │       │   ├── __tests__
│   │       │   │   ├── erase_sims.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── reset_sim_location.test.ts
│   │       │   │   ├── set_sim_appearance.test.ts
│   │       │   │   ├── set_sim_location.test.ts
│   │       │   │   └── sim_statusbar.test.ts
│   │       │   ├── boot_sim.ts
│   │       │   ├── erase_sims.ts
│   │       │   ├── index.ts
│   │       │   ├── list_sims.ts
│   │       │   ├── open_sim.ts
│   │       │   ├── reset_sim_location.ts
│   │       │   ├── set_sim_appearance.ts
│   │       │   ├── set_sim_location.ts
│   │       │   └── sim_statusbar.ts
│   │       ├── swift-package
│   │       │   ├── __tests__
│   │       │   │   ├── active-processes.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── swift_package_build.test.ts
│   │       │   │   ├── swift_package_clean.test.ts
│   │       │   │   ├── swift_package_list.test.ts
│   │       │   │   ├── swift_package_run.test.ts
│   │       │   │   ├── swift_package_stop.test.ts
│   │       │   │   └── swift_package_test.test.ts
│   │       │   ├── active-processes.ts
│   │       │   ├── index.ts
│   │       │   ├── swift_package_build.ts
│   │       │   ├── swift_package_clean.ts
│   │       │   ├── swift_package_list.ts
│   │       │   ├── swift_package_run.ts
│   │       │   ├── swift_package_stop.ts
│   │       │   └── swift_package_test.ts
│   │       ├── ui-testing
│   │       │   ├── __tests__
│   │       │   │   ├── button.test.ts
│   │       │   │   ├── describe_ui.test.ts
│   │       │   │   ├── gesture.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── key_press.test.ts
│   │       │   │   ├── key_sequence.test.ts
│   │       │   │   ├── long_press.test.ts
│   │       │   │   ├── screenshot.test.ts
│   │       │   │   ├── swipe.test.ts
│   │       │   │   ├── tap.test.ts
│   │       │   │   ├── touch.test.ts
│   │       │   │   └── type_text.test.ts
│   │       │   ├── button.ts
│   │       │   ├── describe_ui.ts
│   │       │   ├── gesture.ts
│   │       │   ├── index.ts
│   │       │   ├── key_press.ts
│   │       │   ├── key_sequence.ts
│   │       │   ├── long_press.ts
│   │       │   ├── screenshot.ts
│   │       │   ├── swipe.ts
│   │       │   ├── tap.ts
│   │       │   ├── touch.ts
│   │       │   └── type_text.ts
│   │       └── utilities
│   │           ├── __tests__
│   │           │   ├── clean.test.ts
│   │           │   └── index.test.ts
│   │           ├── clean.ts
│   │           └── index.ts
│   ├── server
│   │   └── server.ts
│   ├── test-utils
│   │   └── mock-executors.ts
│   ├── types
│   │   └── common.ts
│   └── utils
│       ├── __tests__
│       │   ├── build-utils.test.ts
│       │   ├── environment.test.ts
│       │   ├── session-aware-tool-factory.test.ts
│       │   ├── session-store.test.ts
│       │   ├── simulator-utils.test.ts
│       │   ├── test-runner-env-integration.test.ts
│       │   └── typed-tool-factory.test.ts
│       ├── axe
│       │   └── index.ts
│       ├── axe-helpers.ts
│       ├── build
│       │   └── index.ts
│       ├── build-utils.ts
│       ├── capabilities.ts
│       ├── command.ts
│       ├── CommandExecutor.ts
│       ├── environment.ts
│       ├── errors.ts
│       ├── execution
│       │   └── index.ts
│       ├── FileSystemExecutor.ts
│       ├── log_capture.ts
│       ├── log-capture
│       │   └── index.ts
│       ├── logger.ts
│       ├── logging
│       │   └── index.ts
│       ├── plugin-registry
│       │   └── index.ts
│       ├── responses
│       │   └── index.ts
│       ├── schema-helpers.ts
│       ├── sentry.ts
│       ├── session-store.ts
│       ├── simulator-utils.ts
│       ├── template
│       │   └── index.ts
│       ├── template-manager.ts
│       ├── test
│       │   └── index.ts
│       ├── test-common.ts
│       ├── tool-registry.ts
│       ├── typed-tool-factory.ts
│       ├── validation
│       │   └── index.ts
│       ├── validation.ts
│       ├── version
│       │   └── index.ts
│       ├── video_capture.ts
│       ├── video-capture
│       │   └── index.ts
│       ├── xcode.ts
│       ├── xcodemake
│       │   └── index.ts
│       └── xcodemake.ts
├── tsconfig.json
├── tsconfig.test.json
├── tsup.config.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/__tests__/gesture.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for gesture tool plugin
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach } from 'vitest';
  6 | import { z } from 'zod';
  7 | import {
  8 |   createMockExecutor,
  9 |   createMockFileSystemExecutor,
 10 |   createNoopExecutor,
 11 | } from '../../../../test-utils/mock-executors.ts';
 12 | import gesturePlugin, { gestureLogic } from '../gesture.ts';
 13 | 
 14 | describe('Gesture Plugin', () => {
 15 |   describe('Export Field Validation (Literal)', () => {
 16 |     it('should have correct name', () => {
 17 |       expect(gesturePlugin.name).toBe('gesture');
 18 |     });
 19 | 
 20 |     it('should have correct description', () => {
 21 |       expect(gesturePlugin.description).toBe(
 22 |         'Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge',
 23 |       );
 24 |     });
 25 | 
 26 |     it('should have handler function', () => {
 27 |       expect(typeof gesturePlugin.handler).toBe('function');
 28 |     });
 29 | 
 30 |     it('should validate schema fields with safeParse', () => {
 31 |       const schema = z.object(gesturePlugin.schema);
 32 | 
 33 |       // Valid case
 34 |       expect(
 35 |         schema.safeParse({
 36 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 37 |           preset: 'scroll-up',
 38 |         }).success,
 39 |       ).toBe(true);
 40 | 
 41 |       // Invalid simulatorUuid
 42 |       expect(
 43 |         schema.safeParse({
 44 |           simulatorUuid: 'invalid-uuid',
 45 |           preset: 'scroll-up',
 46 |         }).success,
 47 |       ).toBe(false);
 48 | 
 49 |       // Invalid preset
 50 |       expect(
 51 |         schema.safeParse({
 52 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 53 |           preset: 'invalid-preset',
 54 |         }).success,
 55 |       ).toBe(false);
 56 | 
 57 |       // Valid optional parameters
 58 |       expect(
 59 |         schema.safeParse({
 60 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 61 |           preset: 'scroll-up',
 62 |           screenWidth: 375,
 63 |           screenHeight: 667,
 64 |           duration: 1.5,
 65 |           delta: 100,
 66 |           preDelay: 0.5,
 67 |           postDelay: 0.2,
 68 |         }).success,
 69 |       ).toBe(true);
 70 | 
 71 |       // Invalid optional parameters
 72 |       expect(
 73 |         schema.safeParse({
 74 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 75 |           preset: 'scroll-up',
 76 |           screenWidth: 0,
 77 |         }).success,
 78 |       ).toBe(false);
 79 | 
 80 |       expect(
 81 |         schema.safeParse({
 82 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 83 |           preset: 'scroll-up',
 84 |           duration: -1,
 85 |         }).success,
 86 |       ).toBe(false);
 87 |     });
 88 |   });
 89 | 
 90 |   describe('Command Generation', () => {
 91 |     it('should generate correct axe command for basic gesture', async () => {
 92 |       let capturedCommand: string[] = [];
 93 |       const trackingExecutor = async (command: string[]) => {
 94 |         capturedCommand = command;
 95 |         return {
 96 |           success: true,
 97 |           output: 'gesture completed',
 98 |           error: undefined,
 99 |           process: { pid: 12345 },
100 |         };
101 |       };
102 | 
103 |       const mockAxeHelpers = {
104 |         getAxePath: () => '/usr/local/bin/axe',
105 |         getBundledAxeEnvironment: () => ({}),
106 |       };
107 | 
108 |       await gestureLogic(
109 |         {
110 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
111 |           preset: 'scroll-up',
112 |         },
113 |         trackingExecutor,
114 |         mockAxeHelpers,
115 |       );
116 | 
117 |       expect(capturedCommand).toEqual([
118 |         '/usr/local/bin/axe',
119 |         'gesture',
120 |         'scroll-up',
121 |         '--udid',
122 |         '12345678-1234-1234-1234-123456789012',
123 |       ]);
124 |     });
125 | 
126 |     it('should generate correct axe command for gesture with screen dimensions', async () => {
127 |       let capturedCommand: string[] = [];
128 |       const trackingExecutor = async (command: string[]) => {
129 |         capturedCommand = command;
130 |         return {
131 |           success: true,
132 |           output: 'gesture completed',
133 |           error: undefined,
134 |           process: { pid: 12345 },
135 |         };
136 |       };
137 | 
138 |       const mockAxeHelpers = {
139 |         getAxePath: () => '/usr/local/bin/axe',
140 |         getBundledAxeEnvironment: () => ({}),
141 |       };
142 | 
143 |       await gestureLogic(
144 |         {
145 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
146 |           preset: 'swipe-from-left-edge',
147 |           screenWidth: 375,
148 |           screenHeight: 667,
149 |         },
150 |         trackingExecutor,
151 |         mockAxeHelpers,
152 |       );
153 | 
154 |       expect(capturedCommand).toEqual([
155 |         '/usr/local/bin/axe',
156 |         'gesture',
157 |         'swipe-from-left-edge',
158 |         '--screen-width',
159 |         '375',
160 |         '--screen-height',
161 |         '667',
162 |         '--udid',
163 |         '12345678-1234-1234-1234-123456789012',
164 |       ]);
165 |     });
166 | 
167 |     it('should generate correct axe command for gesture with all parameters', async () => {
168 |       let capturedCommand: string[] = [];
169 |       const trackingExecutor = async (command: string[]) => {
170 |         capturedCommand = command;
171 |         return {
172 |           success: true,
173 |           output: 'gesture completed',
174 |           error: undefined,
175 |           process: { pid: 12345 },
176 |         };
177 |       };
178 | 
179 |       const mockAxeHelpers = {
180 |         getAxePath: () => '/usr/local/bin/axe',
181 |         getBundledAxeEnvironment: () => ({}),
182 |       };
183 | 
184 |       await gestureLogic(
185 |         {
186 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
187 |           preset: 'scroll-down',
188 |           screenWidth: 414,
189 |           screenHeight: 896,
190 |           duration: 2.0,
191 |           delta: 150,
192 |           preDelay: 0.5,
193 |           postDelay: 0.3,
194 |         },
195 |         trackingExecutor,
196 |         mockAxeHelpers,
197 |       );
198 | 
199 |       expect(capturedCommand).toEqual([
200 |         '/usr/local/bin/axe',
201 |         'gesture',
202 |         'scroll-down',
203 |         '--screen-width',
204 |         '414',
205 |         '--screen-height',
206 |         '896',
207 |         '--duration',
208 |         '2',
209 |         '--delta',
210 |         '150',
211 |         '--pre-delay',
212 |         '0.5',
213 |         '--post-delay',
214 |         '0.3',
215 |         '--udid',
216 |         '12345678-1234-1234-1234-123456789012',
217 |       ]);
218 |     });
219 | 
220 |     it('should generate correct axe command with different gesture presets', async () => {
221 |       let capturedCommand: string[] = [];
222 |       const trackingExecutor = async (command: string[]) => {
223 |         capturedCommand = command;
224 |         return {
225 |           success: true,
226 |           output: 'gesture completed',
227 |           error: undefined,
228 |           process: { pid: 12345 },
229 |         };
230 |       };
231 | 
232 |       const mockAxeHelpers = {
233 |         getAxePath: () => '/usr/local/bin/axe',
234 |         getBundledAxeEnvironment: () => ({}),
235 |       };
236 | 
237 |       await gestureLogic(
238 |         {
239 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
240 |           preset: 'swipe-from-bottom-edge',
241 |         },
242 |         trackingExecutor,
243 |         mockAxeHelpers,
244 |       );
245 | 
246 |       expect(capturedCommand).toEqual([
247 |         '/usr/local/bin/axe',
248 |         'gesture',
249 |         'swipe-from-bottom-edge',
250 |         '--udid',
251 |         '12345678-1234-1234-1234-123456789012',
252 |       ]);
253 |     });
254 |   });
255 | 
256 |   describe('Handler Behavior (Complete Literal Returns)', () => {
257 |     // Note: Parameter validation is now handled by Zod schema validation in createTypedTool,
258 |     // so invalid parameters never reach gestureLogic. The schema validation tests above
259 |     // cover parameter validation scenarios.
260 | 
261 |     it('should return success for valid gesture execution', async () => {
262 |       const mockExecutor = createMockExecutor({
263 |         success: true,
264 |         output: 'gesture completed',
265 |         error: undefined,
266 |         process: { pid: 12345 },
267 |       });
268 | 
269 |       const mockAxeHelpers = {
270 |         getAxePath: () => '/usr/local/bin/axe',
271 |         getBundledAxeEnvironment: () => ({}),
272 |       };
273 | 
274 |       const result = await gestureLogic(
275 |         {
276 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
277 |           preset: 'scroll-up',
278 |         },
279 |         mockExecutor,
280 |         mockAxeHelpers,
281 |       );
282 | 
283 |       expect(result).toEqual({
284 |         content: [{ type: 'text', text: "Gesture 'scroll-up' executed successfully." }],
285 |         isError: false,
286 |       });
287 |     });
288 | 
289 |     it('should return success for gesture execution with all optional parameters', async () => {
290 |       const mockExecutor = createMockExecutor({
291 |         success: true,
292 |         output: 'gesture completed',
293 |         error: undefined,
294 |         process: { pid: 12345 },
295 |       });
296 | 
297 |       const mockAxeHelpers = {
298 |         getAxePath: () => '/usr/local/bin/axe',
299 |         getBundledAxeEnvironment: () => ({}),
300 |       };
301 | 
302 |       const result = await gestureLogic(
303 |         {
304 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
305 |           preset: 'swipe-from-left-edge',
306 |           screenWidth: 375,
307 |           screenHeight: 667,
308 |           duration: 1.0,
309 |           delta: 50,
310 |           preDelay: 0.1,
311 |           postDelay: 0.2,
312 |         },
313 |         mockExecutor,
314 |         mockAxeHelpers,
315 |       );
316 | 
317 |       expect(result).toEqual({
318 |         content: [{ type: 'text', text: "Gesture 'swipe-from-left-edge' executed successfully." }],
319 |         isError: false,
320 |       });
321 |     });
322 | 
323 |     it('should handle DependencyError when axe is not available', async () => {
324 |       const mockAxeHelpers = {
325 |         getAxePath: () => null,
326 |         getBundledAxeEnvironment: () => ({}),
327 |         createAxeNotAvailableResponse: () => ({
328 |           content: [
329 |             {
330 |               type: 'text',
331 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
332 |             },
333 |           ],
334 |           isError: true,
335 |         }),
336 |       };
337 | 
338 |       const result = await gestureLogic(
339 |         {
340 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
341 |           preset: 'scroll-up',
342 |         },
343 |         createNoopExecutor(),
344 |         mockAxeHelpers,
345 |       );
346 | 
347 |       expect(result).toEqual({
348 |         content: [
349 |           {
350 |             type: 'text',
351 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
352 |           },
353 |         ],
354 |         isError: true,
355 |       });
356 |     });
357 | 
358 |     it('should handle AxeError from failed command execution', async () => {
359 |       const mockExecutor = createMockExecutor({
360 |         success: false,
361 |         output: '',
362 |         error: 'axe command failed',
363 |         process: { pid: 12345 },
364 |       });
365 | 
366 |       const mockAxeHelpers = {
367 |         getAxePath: () => '/usr/local/bin/axe',
368 |         getBundledAxeEnvironment: () => ({}),
369 |       };
370 | 
371 |       const result = await gestureLogic(
372 |         {
373 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
374 |           preset: 'scroll-up',
375 |         },
376 |         mockExecutor,
377 |         mockAxeHelpers,
378 |       );
379 | 
380 |       expect(result).toEqual({
381 |         content: [
382 |           {
383 |             type: 'text',
384 |             text: "Error: Failed to execute gesture 'scroll-up': axe command 'gesture' failed.\nDetails: axe command failed",
385 |           },
386 |         ],
387 |         isError: true,
388 |       });
389 |     });
390 | 
391 |     it('should handle SystemError from command execution', async () => {
392 |       const mockExecutor = createMockExecutor(new Error('ENOENT: no such file or directory'));
393 | 
394 |       const mockAxeHelpers = {
395 |         getAxePath: () => '/usr/local/bin/axe',
396 |         getBundledAxeEnvironment: () => ({}),
397 |       };
398 | 
399 |       const result = await gestureLogic(
400 |         {
401 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
402 |           preset: 'scroll-up',
403 |         },
404 |         mockExecutor,
405 |         mockAxeHelpers,
406 |       );
407 | 
408 |       expect(result.content[0].text).toMatch(
409 |         /^Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory/,
410 |       );
411 |       expect(result.isError).toBe(true);
412 |     });
413 | 
414 |     it('should handle unexpected Error objects', async () => {
415 |       const mockExecutor = createMockExecutor(new Error('Unexpected error'));
416 | 
417 |       const mockAxeHelpers = {
418 |         getAxePath: () => '/usr/local/bin/axe',
419 |         getBundledAxeEnvironment: () => ({}),
420 |       };
421 | 
422 |       const result = await gestureLogic(
423 |         {
424 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
425 |           preset: 'scroll-up',
426 |         },
427 |         mockExecutor,
428 |         mockAxeHelpers,
429 |       );
430 | 
431 |       expect(result.content[0].text).toMatch(
432 |         /^Error: System error executing axe: Failed to execute axe command: Unexpected error/,
433 |       );
434 |       expect(result.isError).toBe(true);
435 |     });
436 | 
437 |     it('should handle unexpected string errors', async () => {
438 |       const mockExecutor = createMockExecutor('String error');
439 | 
440 |       const mockAxeHelpers = {
441 |         getAxePath: () => '/usr/local/bin/axe',
442 |         getBundledAxeEnvironment: () => ({}),
443 |       };
444 | 
445 |       const result = await gestureLogic(
446 |         {
447 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
448 |           preset: 'scroll-up',
449 |         },
450 |         mockExecutor,
451 |         mockAxeHelpers,
452 |       );
453 | 
454 |       expect(result).toEqual({
455 |         content: [
456 |           {
457 |             type: 'text',
458 |             text: 'Error: System error executing axe: Failed to execute axe command: String error',
459 |           },
460 |         ],
461 |         isError: true,
462 |       });
463 |     });
464 |   });
465 | });
466 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/device/__tests__/get_device_app_path.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for get_device_app_path plugin (unified)
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import { z } from 'zod';
  9 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
 10 | import getDeviceAppPath, { get_device_app_pathLogic } from '../get_device_app_path.ts';
 11 | import { sessionStore } from '../../../../utils/session-store.ts';
 12 | 
 13 | describe('get_device_app_path plugin', () => {
 14 |   beforeEach(() => {
 15 |     sessionStore.clear();
 16 |   });
 17 | 
 18 |   describe('Export Field Validation (Literal)', () => {
 19 |     it('should have correct name', () => {
 20 |       expect(getDeviceAppPath.name).toBe('get_device_app_path');
 21 |     });
 22 | 
 23 |     it('should have correct description', () => {
 24 |       expect(getDeviceAppPath.description).toBe(
 25 |         'Retrieves the built app path for a connected device.',
 26 |       );
 27 |     });
 28 | 
 29 |     it('should have handler function', () => {
 30 |       expect(typeof getDeviceAppPath.handler).toBe('function');
 31 |     });
 32 | 
 33 |     it('should expose only platform in public schema', () => {
 34 |       const schema = z.object(getDeviceAppPath.schema).strict();
 35 |       expect(schema.safeParse({}).success).toBe(true);
 36 |       expect(schema.safeParse({ platform: 'iOS' }).success).toBe(true);
 37 |       expect(schema.safeParse({ projectPath: '/path/to/project.xcodeproj' }).success).toBe(false);
 38 | 
 39 |       const schemaKeys = Object.keys(getDeviceAppPath.schema).sort();
 40 |       expect(schemaKeys).toEqual(['platform']);
 41 |     });
 42 |   });
 43 | 
 44 |   describe('XOR Validation', () => {
 45 |     it('should error when neither projectPath nor workspacePath provided', async () => {
 46 |       const result = await getDeviceAppPath.handler({
 47 |         scheme: 'MyScheme',
 48 |       });
 49 |       expect(result.isError).toBe(true);
 50 |       expect(result.content[0].text).toContain('Missing required session defaults');
 51 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 52 |     });
 53 | 
 54 |     it('should error when both projectPath and workspacePath provided', async () => {
 55 |       const result = await getDeviceAppPath.handler({
 56 |         projectPath: '/path/to/project.xcodeproj',
 57 |         workspacePath: '/path/to/workspace.xcworkspace',
 58 |         scheme: 'MyScheme',
 59 |       });
 60 |       expect(result.isError).toBe(true);
 61 |       expect(result.content[0].text).toContain('Parameter validation failed');
 62 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 63 |     });
 64 |   });
 65 | 
 66 |   describe('Handler Requirements', () => {
 67 |     it('should require scheme when missing', async () => {
 68 |       const result = await getDeviceAppPath.handler({
 69 |         projectPath: '/path/to/project.xcodeproj',
 70 |       });
 71 |       expect(result.isError).toBe(true);
 72 |       expect(result.content[0].text).toContain('Missing required session defaults');
 73 |       expect(result.content[0].text).toContain('scheme is required');
 74 |     });
 75 | 
 76 |     it('should require project or workspace when scheme default exists', async () => {
 77 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 78 | 
 79 |       const result = await getDeviceAppPath.handler({});
 80 |       expect(result.isError).toBe(true);
 81 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 82 |     });
 83 |   });
 84 | 
 85 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 86 |     // Note: Parameter validation is now handled by Zod schema validation in createTypedTool,
 87 |     // so invalid parameters never reach the logic function. Schema validation is tested above.
 88 | 
 89 |     it('should generate correct xcodebuild command for iOS', async () => {
 90 |       const calls: Array<{
 91 |         args: any[];
 92 |         description: string;
 93 |         suppressErrors: boolean;
 94 |         workingDirectory: string | undefined;
 95 |       }> = [];
 96 | 
 97 |       const mockExecutor = (
 98 |         args: any[],
 99 |         description: string,
100 |         suppressErrors: boolean,
101 |         workingDirectory: string | undefined,
102 |       ) => {
103 |         calls.push({ args, description, suppressErrors, workingDirectory });
104 |         return Promise.resolve({
105 |           success: true,
106 |           output:
107 |             'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n',
108 |           error: undefined,
109 |           process: { pid: 12345 },
110 |         });
111 |       };
112 | 
113 |       await get_device_app_pathLogic(
114 |         {
115 |           projectPath: '/path/to/project.xcodeproj',
116 |           scheme: 'MyScheme',
117 |         },
118 |         mockExecutor,
119 |       );
120 | 
121 |       expect(calls).toHaveLength(1);
122 |       expect(calls[0]).toEqual({
123 |         args: [
124 |           'xcodebuild',
125 |           '-showBuildSettings',
126 |           '-project',
127 |           '/path/to/project.xcodeproj',
128 |           '-scheme',
129 |           'MyScheme',
130 |           '-configuration',
131 |           'Debug',
132 |           '-destination',
133 |           'generic/platform=iOS',
134 |         ],
135 |         description: 'Get App Path',
136 |         suppressErrors: true,
137 |         workingDirectory: undefined,
138 |       });
139 |     });
140 | 
141 |     it('should generate correct xcodebuild command for watchOS', async () => {
142 |       const calls: Array<{
143 |         args: any[];
144 |         description: string;
145 |         suppressErrors: boolean;
146 |         workingDirectory: string | undefined;
147 |       }> = [];
148 | 
149 |       const mockExecutor = (
150 |         args: any[],
151 |         description: string,
152 |         suppressErrors: boolean,
153 |         workingDirectory: string | undefined,
154 |       ) => {
155 |         calls.push({ args, description, suppressErrors, workingDirectory });
156 |         return Promise.resolve({
157 |           success: true,
158 |           output:
159 |             'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-watchos\nFULL_PRODUCT_NAME = MyApp.app\n',
160 |           error: undefined,
161 |           process: { pid: 12345 },
162 |         });
163 |       };
164 | 
165 |       await get_device_app_pathLogic(
166 |         {
167 |           projectPath: '/path/to/project.xcodeproj',
168 |           scheme: 'MyScheme',
169 |           platform: 'watchOS',
170 |         },
171 |         mockExecutor,
172 |       );
173 | 
174 |       expect(calls).toHaveLength(1);
175 |       expect(calls[0]).toEqual({
176 |         args: [
177 |           'xcodebuild',
178 |           '-showBuildSettings',
179 |           '-project',
180 |           '/path/to/project.xcodeproj',
181 |           '-scheme',
182 |           'MyScheme',
183 |           '-configuration',
184 |           'Debug',
185 |           '-destination',
186 |           'generic/platform=watchOS',
187 |         ],
188 |         description: 'Get App Path',
189 |         suppressErrors: true,
190 |         workingDirectory: undefined,
191 |       });
192 |     });
193 | 
194 |     it('should generate correct xcodebuild command for workspace with iOS', async () => {
195 |       const calls: Array<{
196 |         args: any[];
197 |         description: string;
198 |         suppressErrors: boolean;
199 |         workingDirectory: string | undefined;
200 |       }> = [];
201 | 
202 |       const mockExecutor = (
203 |         args: any[],
204 |         description: string,
205 |         suppressErrors: boolean,
206 |         workingDirectory: string | undefined,
207 |       ) => {
208 |         calls.push({ args, description, suppressErrors, workingDirectory });
209 |         return Promise.resolve({
210 |           success: true,
211 |           output:
212 |             'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n',
213 |           error: undefined,
214 |           process: { pid: 12345 },
215 |         });
216 |       };
217 | 
218 |       await get_device_app_pathLogic(
219 |         {
220 |           workspacePath: '/path/to/workspace.xcworkspace',
221 |           scheme: 'MyScheme',
222 |         },
223 |         mockExecutor,
224 |       );
225 | 
226 |       expect(calls).toHaveLength(1);
227 |       expect(calls[0]).toEqual({
228 |         args: [
229 |           'xcodebuild',
230 |           '-showBuildSettings',
231 |           '-workspace',
232 |           '/path/to/workspace.xcworkspace',
233 |           '-scheme',
234 |           'MyScheme',
235 |           '-configuration',
236 |           'Debug',
237 |           '-destination',
238 |           'generic/platform=iOS',
239 |         ],
240 |         description: 'Get App Path',
241 |         suppressErrors: true,
242 |         workingDirectory: undefined,
243 |       });
244 |     });
245 | 
246 |     it('should return exact successful app path retrieval response', async () => {
247 |       const mockExecutor = createMockExecutor({
248 |         success: true,
249 |         output:
250 |           'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n',
251 |       });
252 | 
253 |       const result = await get_device_app_pathLogic(
254 |         {
255 |           projectPath: '/path/to/project.xcodeproj',
256 |           scheme: 'MyScheme',
257 |         },
258 |         mockExecutor,
259 |       );
260 | 
261 |       expect(result).toEqual({
262 |         content: [
263 |           {
264 |             type: 'text',
265 |             text: '✅ App path retrieved successfully: /path/to/build/Debug-iphoneos/MyApp.app',
266 |           },
267 |           {
268 |             type: 'text',
269 |             text: 'Next Steps:\n1. Get bundle ID: get_app_bundle_id({ appPath: "/path/to/build/Debug-iphoneos/MyApp.app" })\n2. Install app on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "/path/to/build/Debug-iphoneos/MyApp.app" })\n3. Launch app on device: launch_app_device({ deviceId: "DEVICE_UDID", bundleId: "BUNDLE_ID" })',
270 |           },
271 |         ],
272 |       });
273 |     });
274 | 
275 |     it('should return exact command failure response', async () => {
276 |       const mockExecutor = createMockExecutor({
277 |         success: false,
278 |         error: 'xcodebuild: error: The project does not exist.',
279 |       });
280 | 
281 |       const result = await get_device_app_pathLogic(
282 |         {
283 |           projectPath: '/path/to/nonexistent.xcodeproj',
284 |           scheme: 'MyScheme',
285 |         },
286 |         mockExecutor,
287 |       );
288 | 
289 |       expect(result).toEqual({
290 |         content: [
291 |           {
292 |             type: 'text',
293 |             text: 'Failed to get app path: xcodebuild: error: The project does not exist.',
294 |           },
295 |         ],
296 |         isError: true,
297 |       });
298 |     });
299 | 
300 |     it('should return exact parse failure response', async () => {
301 |       const mockExecutor = createMockExecutor({
302 |         success: true,
303 |         output: 'Build settings without required fields',
304 |       });
305 | 
306 |       const result = await get_device_app_pathLogic(
307 |         {
308 |           projectPath: '/path/to/project.xcodeproj',
309 |           scheme: 'MyScheme',
310 |         },
311 |         mockExecutor,
312 |       );
313 | 
314 |       expect(result).toEqual({
315 |         content: [
316 |           {
317 |             type: 'text',
318 |             text: 'Failed to extract app path from build settings. Make sure the app has been built first.',
319 |           },
320 |         ],
321 |         isError: true,
322 |       });
323 |     });
324 | 
325 |     it('should include optional configuration parameter in command', async () => {
326 |       const calls: Array<{
327 |         args: any[];
328 |         description: string;
329 |         suppressErrors: boolean;
330 |         workingDirectory: string | undefined;
331 |       }> = [];
332 | 
333 |       const mockExecutor = (
334 |         args: any[],
335 |         description: string,
336 |         suppressErrors: boolean,
337 |         workingDirectory: string | undefined,
338 |       ) => {
339 |         calls.push({ args, description, suppressErrors, workingDirectory });
340 |         return Promise.resolve({
341 |           success: true,
342 |           output:
343 |             'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Release-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n',
344 |           error: undefined,
345 |           process: { pid: 12345 },
346 |         });
347 |       };
348 | 
349 |       await get_device_app_pathLogic(
350 |         {
351 |           projectPath: '/path/to/project.xcodeproj',
352 |           scheme: 'MyScheme',
353 |           configuration: 'Release',
354 |         },
355 |         mockExecutor,
356 |       );
357 | 
358 |       expect(calls).toHaveLength(1);
359 |       expect(calls[0]).toEqual({
360 |         args: [
361 |           'xcodebuild',
362 |           '-showBuildSettings',
363 |           '-project',
364 |           '/path/to/project.xcodeproj',
365 |           '-scheme',
366 |           'MyScheme',
367 |           '-configuration',
368 |           'Release',
369 |           '-destination',
370 |           'generic/platform=iOS',
371 |         ],
372 |         description: 'Get App Path',
373 |         suppressErrors: true,
374 |         workingDirectory: undefined,
375 |       });
376 |     });
377 | 
378 |     it('should return exact exception handling response', async () => {
379 |       const mockExecutor = () => {
380 |         return Promise.reject(new Error('Network error'));
381 |       };
382 | 
383 |       const result = await get_device_app_pathLogic(
384 |         {
385 |           projectPath: '/path/to/project.xcodeproj',
386 |           scheme: 'MyScheme',
387 |         },
388 |         mockExecutor,
389 |       );
390 | 
391 |       expect(result).toEqual({
392 |         content: [
393 |           {
394 |             type: 'text',
395 |             text: 'Error retrieving app path: Network error',
396 |           },
397 |         ],
398 |         isError: true,
399 |       });
400 |     });
401 | 
402 |     it('should return exact string error handling response', async () => {
403 |       const mockExecutor = () => {
404 |         return Promise.reject('String error');
405 |       };
406 | 
407 |       const result = await get_device_app_pathLogic(
408 |         {
409 |           projectPath: '/path/to/project.xcodeproj',
410 |           scheme: 'MyScheme',
411 |         },
412 |         mockExecutor,
413 |       );
414 | 
415 |       expect(result).toEqual({
416 |         content: [
417 |           {
418 |             type: 'text',
419 |             text: 'Error retrieving app path: String error',
420 |           },
421 |         ],
422 |         isError: true,
423 |       });
424 |     });
425 |   });
426 | });
427 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/__tests__/swift_package_stop.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for swift_package_stop plugin
  3 |  * Following CLAUDE.md testing standards with pure dependency injection
  4 |  * No vitest mocking - using dependency injection pattern
  5 |  */
  6 | 
  7 | import { describe, it, expect } from 'vitest';
  8 | import { z } from 'zod';
  9 | import swiftPackageStop, {
 10 |   createMockProcessManager,
 11 |   swift_package_stopLogic,
 12 |   type ProcessManager,
 13 | } from '../swift_package_stop.ts';
 14 | 
 15 | /**
 16 |  * Mock process implementation for testing
 17 |  */
 18 | class MockProcess {
 19 |   public killed = false;
 20 |   public killSignal: string | undefined;
 21 |   public exitCallback: (() => void) | undefined;
 22 |   public shouldThrowOnKill = false;
 23 |   public killError: Error | string | undefined;
 24 |   public pid: number;
 25 | 
 26 |   constructor(pid: number) {
 27 |     this.pid = pid;
 28 |   }
 29 | 
 30 |   kill(signal?: string): void {
 31 |     if (this.shouldThrowOnKill) {
 32 |       throw this.killError ?? new Error('Process kill failed');
 33 |     }
 34 |     this.killed = true;
 35 |     this.killSignal = signal;
 36 |   }
 37 | 
 38 |   on(event: string, callback: () => void): void {
 39 |     if (event === 'exit') {
 40 |       this.exitCallback = callback;
 41 |     }
 42 |   }
 43 | 
 44 |   // Simulate immediate exit for test control
 45 |   simulateExit(): void {
 46 |     if (this.exitCallback) {
 47 |       this.exitCallback();
 48 |     }
 49 |   }
 50 | }
 51 | 
 52 | describe('swift_package_stop plugin', () => {
 53 |   describe('Export Field Validation (Literal)', () => {
 54 |     it('should have correct name', () => {
 55 |       expect(swiftPackageStop.name).toBe('swift_package_stop');
 56 |     });
 57 | 
 58 |     it('should have correct description', () => {
 59 |       expect(swiftPackageStop.description).toBe(
 60 |         'Stops a running Swift Package executable started with swift_package_run',
 61 |       );
 62 |     });
 63 | 
 64 |     it('should have handler function', () => {
 65 |       expect(typeof swiftPackageStop.handler).toBe('function');
 66 |     });
 67 | 
 68 |     it('should validate schema correctly', () => {
 69 |       // Test valid inputs
 70 |       expect(swiftPackageStop.schema.pid.safeParse(12345).success).toBe(true);
 71 |       expect(swiftPackageStop.schema.pid.safeParse(0).success).toBe(true);
 72 |       expect(swiftPackageStop.schema.pid.safeParse(-1).success).toBe(true);
 73 | 
 74 |       // Test invalid inputs
 75 |       expect(swiftPackageStop.schema.pid.safeParse('not-a-number').success).toBe(false);
 76 |       expect(swiftPackageStop.schema.pid.safeParse(null).success).toBe(false);
 77 |       expect(swiftPackageStop.schema.pid.safeParse(undefined).success).toBe(false);
 78 |       expect(swiftPackageStop.schema.pid.safeParse({}).success).toBe(false);
 79 |       expect(swiftPackageStop.schema.pid.safeParse([]).success).toBe(false);
 80 |     });
 81 |   });
 82 | 
 83 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 84 |     it('should return exact error for process not found', async () => {
 85 |       const mockProcessManager = createMockProcessManager({
 86 |         getProcess: () => undefined,
 87 |       });
 88 | 
 89 |       const result = await swift_package_stopLogic({ pid: 99999 }, mockProcessManager);
 90 | 
 91 |       expect(result).toEqual({
 92 |         content: [
 93 |           {
 94 |             type: 'text',
 95 |             text: '⚠️ No running process found with PID 99999. Use swift_package_run to check active processes.',
 96 |           },
 97 |         ],
 98 |         isError: true,
 99 |       });
100 |     });
101 | 
102 |     it('should successfully stop a process that exits gracefully', async () => {
103 |       const mockProcess = new MockProcess(12345);
104 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
105 | 
106 |       const mockProcessManager = createMockProcessManager({
107 |         getProcess: (pid: number) =>
108 |           pid === 12345
109 |             ? {
110 |                 process: mockProcess,
111 |                 startedAt: startedAt,
112 |               }
113 |             : undefined,
114 |         removeProcess: () => true,
115 |       });
116 | 
117 |       // Set up the process to exit immediately when exit handler is registered
118 |       const originalOn = mockProcess.on.bind(mockProcess);
119 |       mockProcess.on = (event: string, callback: () => void) => {
120 |         originalOn(event, callback);
121 |         if (event === 'exit') {
122 |           // Simulate immediate graceful exit
123 |           setImmediate(() => callback());
124 |         }
125 |       };
126 | 
127 |       const result = await swift_package_stopLogic(
128 |         { pid: 12345 },
129 |         mockProcessManager,
130 |         10, // Very short timeout for testing
131 |       );
132 | 
133 |       expect(mockProcess.killed).toBe(true);
134 |       expect(mockProcess.killSignal).toBe('SIGTERM');
135 |       expect(result).toEqual({
136 |         content: [
137 |           {
138 |             type: 'text',
139 |             text: '✅ Stopped executable (was running since 2023-01-01T10:00:00.000Z)',
140 |           },
141 |           {
142 |             type: 'text',
143 |             text: '💡 Process terminated. You can now run swift_package_run again if needed.',
144 |           },
145 |         ],
146 |       });
147 |     });
148 | 
149 |     it('should force kill process if graceful termination fails', async () => {
150 |       const mockProcess = new MockProcess(67890);
151 |       const startedAt = new Date('2023-02-15T14:30:00.000Z');
152 | 
153 |       const mockProcessManager = createMockProcessManager({
154 |         getProcess: (pid: number) =>
155 |           pid === 67890
156 |             ? {
157 |                 process: mockProcess,
158 |                 startedAt: startedAt,
159 |               }
160 |             : undefined,
161 |         removeProcess: () => true,
162 |       });
163 | 
164 |       // Mock the process to NOT exit gracefully (no callback invocation)
165 |       const killCalls: string[] = [];
166 |       const originalKill = mockProcess.kill.bind(mockProcess);
167 |       mockProcess.kill = (signal?: string) => {
168 |         killCalls.push(signal ?? 'default');
169 |         originalKill(signal);
170 |       };
171 | 
172 |       // Set up timeout to trigger SIGKILL after SIGTERM
173 |       const originalOn = mockProcess.on.bind(mockProcess);
174 |       mockProcess.on = (event: string, callback: () => void) => {
175 |         originalOn(event, callback);
176 |         // Do NOT call the callback to simulate hanging process
177 |       };
178 | 
179 |       const result = await swift_package_stopLogic(
180 |         { pid: 67890 },
181 |         mockProcessManager,
182 |         10, // Very short timeout for testing
183 |       );
184 | 
185 |       expect(killCalls).toEqual(['SIGTERM', 'SIGKILL']);
186 |       expect(result).toEqual({
187 |         content: [
188 |           {
189 |             type: 'text',
190 |             text: '✅ Stopped executable (was running since 2023-02-15T14:30:00.000Z)',
191 |           },
192 |           {
193 |             type: 'text',
194 |             text: '💡 Process terminated. You can now run swift_package_run again if needed.',
195 |           },
196 |         ],
197 |       });
198 |     });
199 | 
200 |     it('should handle process kill error and return error response', async () => {
201 |       const mockProcess = new MockProcess(54321);
202 |       const startedAt = new Date('2023-03-20T09:15:00.000Z');
203 | 
204 |       // Configure process to throw error on kill
205 |       mockProcess.shouldThrowOnKill = true;
206 |       mockProcess.killError = new Error('ESRCH: No such process');
207 | 
208 |       const mockProcessManager = createMockProcessManager({
209 |         getProcess: (pid: number) =>
210 |           pid === 54321
211 |             ? {
212 |                 process: mockProcess,
213 |                 startedAt: startedAt,
214 |               }
215 |             : undefined,
216 |       });
217 | 
218 |       const result = await swift_package_stopLogic({ pid: 54321 }, mockProcessManager);
219 | 
220 |       expect(result).toEqual({
221 |         content: [
222 |           {
223 |             type: 'text',
224 |             text: 'Error: Failed to stop process\nDetails: ESRCH: No such process',
225 |           },
226 |         ],
227 |         isError: true,
228 |       });
229 |     });
230 | 
231 |     it('should handle non-Error exception in catch block', async () => {
232 |       const mockProcess = new MockProcess(11111);
233 |       const startedAt = new Date('2023-04-10T16:45:00.000Z');
234 | 
235 |       // Configure process to throw non-Error object
236 |       mockProcess.shouldThrowOnKill = true;
237 |       mockProcess.killError = 'Process termination failed';
238 | 
239 |       const mockProcessManager = createMockProcessManager({
240 |         getProcess: (pid: number) =>
241 |           pid === 11111
242 |             ? {
243 |                 process: mockProcess,
244 |                 startedAt: startedAt,
245 |               }
246 |             : undefined,
247 |       });
248 | 
249 |       const result = await swift_package_stopLogic({ pid: 11111 }, mockProcessManager);
250 | 
251 |       expect(result).toEqual({
252 |         content: [
253 |           {
254 |             type: 'text',
255 |             text: 'Error: Failed to stop process\nDetails: Process termination failed',
256 |           },
257 |         ],
258 |         isError: true,
259 |       });
260 |     });
261 | 
262 |     it('should handle process found but exit event never fires and timeout occurs', async () => {
263 |       const mockProcess = new MockProcess(22222);
264 |       const startedAt = new Date('2023-05-05T12:00:00.000Z');
265 | 
266 |       const mockProcessManager = createMockProcessManager({
267 |         getProcess: (pid: number) =>
268 |           pid === 22222
269 |             ? {
270 |                 process: mockProcess,
271 |                 startedAt: startedAt,
272 |               }
273 |             : undefined,
274 |         removeProcess: () => true,
275 |       });
276 | 
277 |       const killCalls: string[] = [];
278 |       const originalKill = mockProcess.kill.bind(mockProcess);
279 |       mockProcess.kill = (signal?: string) => {
280 |         killCalls.push(signal ?? 'default');
281 |         originalKill(signal);
282 |       };
283 | 
284 |       // Mock process.on to register the exit handler but never call it (timeout scenario)
285 |       const originalOn = mockProcess.on.bind(mockProcess);
286 |       mockProcess.on = (event: string, callback: () => void) => {
287 |         originalOn(event, callback);
288 |         // Handler is registered but callback never called (simulates hanging process)
289 |       };
290 | 
291 |       const result = await swift_package_stopLogic(
292 |         { pid: 22222 },
293 |         mockProcessManager,
294 |         10, // Very short timeout for testing
295 |       );
296 | 
297 |       expect(killCalls).toEqual(['SIGTERM', 'SIGKILL']);
298 |       expect(result).toEqual({
299 |         content: [
300 |           {
301 |             type: 'text',
302 |             text: '✅ Stopped executable (was running since 2023-05-05T12:00:00.000Z)',
303 |           },
304 |           {
305 |             type: 'text',
306 |             text: '💡 Process terminated. You can now run swift_package_run again if needed.',
307 |           },
308 |         ],
309 |       });
310 |     });
311 | 
312 |     it('should handle edge case with pid 0', async () => {
313 |       const mockProcessManager = createMockProcessManager({
314 |         getProcess: () => undefined,
315 |       });
316 | 
317 |       const result = await swift_package_stopLogic({ pid: 0 }, mockProcessManager);
318 | 
319 |       expect(result).toEqual({
320 |         content: [
321 |           {
322 |             type: 'text',
323 |             text: '⚠️ No running process found with PID 0. Use swift_package_run to check active processes.',
324 |           },
325 |         ],
326 |         isError: true,
327 |       });
328 |     });
329 | 
330 |     it('should handle edge case with negative pid', async () => {
331 |       const mockProcessManager = createMockProcessManager({
332 |         getProcess: () => undefined,
333 |       });
334 | 
335 |       const result = await swift_package_stopLogic({ pid: -1 }, mockProcessManager);
336 | 
337 |       expect(result).toEqual({
338 |         content: [
339 |           {
340 |             type: 'text',
341 |             text: '⚠️ No running process found with PID -1. Use swift_package_run to check active processes.',
342 |           },
343 |         ],
344 |         isError: true,
345 |       });
346 |     });
347 | 
348 |     it('should handle process that exits after first SIGTERM call', async () => {
349 |       const mockProcess = new MockProcess(33333);
350 |       const startedAt = new Date('2023-06-01T08:30:00.000Z');
351 | 
352 |       const mockProcessManager = createMockProcessManager({
353 |         getProcess: (pid: number) =>
354 |           pid === 33333
355 |             ? {
356 |                 process: mockProcess,
357 |                 startedAt: startedAt,
358 |               }
359 |             : undefined,
360 |         removeProcess: () => true,
361 |       });
362 | 
363 |       const killCalls: string[] = [];
364 |       const originalKill = mockProcess.kill.bind(mockProcess);
365 |       mockProcess.kill = (signal?: string) => {
366 |         killCalls.push(signal ?? 'default');
367 |         originalKill(signal);
368 |       };
369 | 
370 |       // Set up the process to exit immediately when exit handler is registered
371 |       const originalOn = mockProcess.on.bind(mockProcess);
372 |       mockProcess.on = (event: string, callback: () => void) => {
373 |         originalOn(event, callback);
374 |         if (event === 'exit') {
375 |           // Simulate immediate graceful exit
376 |           setImmediate(() => callback());
377 |         }
378 |       };
379 | 
380 |       const result = await swift_package_stopLogic(
381 |         { pid: 33333 },
382 |         mockProcessManager,
383 |         10, // Very short timeout for testing
384 |       );
385 | 
386 |       expect(killCalls).toEqual(['SIGTERM']); // Should not call SIGKILL
387 |       expect(result).toEqual({
388 |         content: [
389 |           {
390 |             type: 'text',
391 |             text: '✅ Stopped executable (was running since 2023-06-01T08:30:00.000Z)',
392 |           },
393 |           {
394 |             type: 'text',
395 |             text: '💡 Process terminated. You can now run swift_package_run again if needed.',
396 |           },
397 |         ],
398 |       });
399 |     });
400 | 
401 |     it('should handle undefined pid parameter', async () => {
402 |       const mockProcessManager = createMockProcessManager({
403 |         getProcess: () => undefined,
404 |       });
405 | 
406 |       const result = await swift_package_stopLogic({} as any, mockProcessManager);
407 | 
408 |       expect(result).toEqual({
409 |         content: [
410 |           {
411 |             type: 'text',
412 |             text: '⚠️ No running process found with PID undefined. Use swift_package_run to check active processes.',
413 |           },
414 |         ],
415 |         isError: true,
416 |       });
417 |     });
418 |   });
419 | });
420 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for screenshot tool plugin
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach } from 'vitest';
  6 | import { z } from 'zod';
  7 | import {
  8 |   createMockExecutor,
  9 |   createMockFileSystemExecutor,
 10 |   createNoopExecutor,
 11 | } from '../../../../test-utils/mock-executors.ts';
 12 | import { SystemError } from '../../../../utils/responses/index.ts';
 13 | import screenshotPlugin, { screenshotLogic } from '../screenshot.ts';
 14 | 
 15 | describe('Screenshot Plugin', () => {
 16 |   describe('Export Field Validation (Literal)', () => {
 17 |     it('should have correct name', () => {
 18 |       expect(screenshotPlugin.name).toBe('screenshot');
 19 |     });
 20 | 
 21 |     it('should have correct description', () => {
 22 |       expect(screenshotPlugin.description).toBe(
 23 |         "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).",
 24 |       );
 25 |     });
 26 | 
 27 |     it('should have handler function', () => {
 28 |       expect(typeof screenshotPlugin.handler).toBe('function');
 29 |     });
 30 | 
 31 |     it('should validate schema fields with safeParse', () => {
 32 |       const schema = z.object(screenshotPlugin.schema);
 33 | 
 34 |       // Valid case
 35 |       expect(
 36 |         schema.safeParse({
 37 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 38 |         }).success,
 39 |       ).toBe(true);
 40 | 
 41 |       // Invalid simulatorUuid
 42 |       expect(
 43 |         schema.safeParse({
 44 |           simulatorUuid: 'invalid-uuid',
 45 |         }).success,
 46 |       ).toBe(false);
 47 | 
 48 |       // Missing simulatorUuid
 49 |       expect(schema.safeParse({}).success).toBe(false);
 50 |     });
 51 |   });
 52 | 
 53 |   describe('Plugin Handler Validation', () => {
 54 |     it('should return Zod validation error for missing simulatorUuid', async () => {
 55 |       const result = await screenshotPlugin.handler({});
 56 | 
 57 |       expect(result).toEqual({
 58 |         content: [
 59 |           {
 60 |             type: 'text',
 61 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Required',
 62 |           },
 63 |         ],
 64 |         isError: true,
 65 |       });
 66 |     });
 67 | 
 68 |     it('should return Zod validation error for invalid UUID format', async () => {
 69 |       const result = await screenshotPlugin.handler({
 70 |         simulatorUuid: 'invalid-uuid',
 71 |       });
 72 | 
 73 |       expect(result).toEqual({
 74 |         content: [
 75 |           {
 76 |             type: 'text',
 77 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Invalid Simulator UUID format',
 78 |           },
 79 |         ],
 80 |         isError: true,
 81 |       });
 82 |     });
 83 |   });
 84 | 
 85 |   describe('Command Generation', () => {
 86 |     it('should generate correct xcrun simctl command for basic screenshot', async () => {
 87 |       const capturedCommands: string[][] = [];
 88 |       const trackingExecutor = async (command: string[]) => {
 89 |         capturedCommands.push(command);
 90 |         return {
 91 |           success: true,
 92 |           output: 'Screenshot saved',
 93 |           error: undefined,
 94 |           process: { pid: 12345 },
 95 |         };
 96 |       };
 97 | 
 98 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
 99 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
100 |         readFile: async () => mockImageBuffer.toString('utf8'),
101 |       });
102 | 
103 |       await screenshotLogic(
104 |         {
105 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
106 |         },
107 |         trackingExecutor,
108 |         mockFileSystemExecutor,
109 |         { tmpdir: () => '/tmp', join: (...paths) => paths.join('/') },
110 |         { v4: () => 'test-uuid' },
111 |       );
112 | 
113 |       // Should capture the screenshot command first
114 |       expect(capturedCommands[0]).toEqual([
115 |         'xcrun',
116 |         'simctl',
117 |         'io',
118 |         '12345678-1234-1234-1234-123456789012',
119 |         'screenshot',
120 |         '/tmp/screenshot_test-uuid.png',
121 |       ]);
122 |     });
123 | 
124 |     it('should generate correct xcrun simctl command with different simulator UUID', async () => {
125 |       const capturedCommands: string[][] = [];
126 |       const trackingExecutor = async (command: string[]) => {
127 |         capturedCommands.push(command);
128 |         return {
129 |           success: true,
130 |           output: 'Screenshot saved',
131 |           error: undefined,
132 |           process: { pid: 12345 },
133 |         };
134 |       };
135 | 
136 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
137 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
138 |         readFile: async () => mockImageBuffer.toString('utf8'),
139 |       });
140 | 
141 |       await screenshotLogic(
142 |         {
143 |           simulatorUuid: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
144 |         },
145 |         trackingExecutor,
146 |         mockFileSystemExecutor,
147 |         { tmpdir: () => '/var/tmp', join: (...paths) => paths.join('/') },
148 |         { v4: () => 'another-uuid' },
149 |       );
150 | 
151 |       expect(capturedCommands[0]).toEqual([
152 |         'xcrun',
153 |         'simctl',
154 |         'io',
155 |         'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
156 |         'screenshot',
157 |         '/var/tmp/screenshot_another-uuid.png',
158 |       ]);
159 |     });
160 | 
161 |     it('should generate correct xcrun simctl command with custom path dependencies', async () => {
162 |       const capturedCommands: string[][] = [];
163 |       const trackingExecutor = async (command: string[]) => {
164 |         capturedCommands.push(command);
165 |         return {
166 |           success: true,
167 |           output: 'Screenshot saved',
168 |           error: undefined,
169 |           process: { pid: 12345 },
170 |         };
171 |       };
172 | 
173 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
174 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
175 |         readFile: async () => mockImageBuffer.toString('utf8'),
176 |       });
177 | 
178 |       await screenshotLogic(
179 |         {
180 |           simulatorUuid: '98765432-1098-7654-3210-987654321098',
181 |         },
182 |         trackingExecutor,
183 |         mockFileSystemExecutor,
184 |         {
185 |           tmpdir: () => '/custom/temp/dir',
186 |           join: (...paths) => paths.join('\\'), // Windows-style path joining
187 |         },
188 |         { v4: () => 'custom-uuid' },
189 |       );
190 | 
191 |       expect(capturedCommands[0]).toEqual([
192 |         'xcrun',
193 |         'simctl',
194 |         'io',
195 |         '98765432-1098-7654-3210-987654321098',
196 |         'screenshot',
197 |         '/custom/temp/dir\\screenshot_custom-uuid.png',
198 |       ]);
199 |     });
200 | 
201 |     it('should generate correct xcrun simctl command with generated UUID when no UUID deps provided', async () => {
202 |       const capturedCommands: string[][] = [];
203 |       const trackingExecutor = async (command: string[]) => {
204 |         capturedCommands.push(command);
205 |         return {
206 |           success: true,
207 |           output: 'Screenshot saved',
208 |           error: undefined,
209 |           process: { pid: 12345 },
210 |         };
211 |       };
212 | 
213 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
214 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
215 |         readFile: async () => mockImageBuffer.toString('utf8'),
216 |       });
217 | 
218 |       await screenshotLogic(
219 |         {
220 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
221 |         },
222 |         trackingExecutor,
223 |         mockFileSystemExecutor,
224 |         { tmpdir: () => '/tmp', join: (...paths) => paths.join('/') },
225 |         // No UUID deps provided - should use real uuidv4()
226 |       );
227 | 
228 |       // Verify the command structure but not the exact UUID since it's generated
229 |       expect(capturedCommands[0].slice(0, 5)).toEqual([
230 |         'xcrun',
231 |         'simctl',
232 |         'io',
233 |         '12345678-1234-1234-1234-123456789012',
234 |         'screenshot',
235 |       ]);
236 |       expect(capturedCommands[0][5]).toMatch(/^\/tmp\/screenshot_[a-f0-9-]+\.png$/);
237 |     });
238 |   });
239 | 
240 |   describe('Handler Behavior (Complete Literal Returns)', () => {
241 |     it('should handle parameter validation via plugin handler (not logic function)', async () => {
242 |       // Note: With Zod validation in createTypedTool, the screenshotLogic function
243 |       // will never receive invalid parameters - validation happens at the handler level.
244 |       // This test documents that screenshotLogic assumes valid parameters.
245 |       const result = await screenshotLogic(
246 |         {
247 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
248 |         },
249 |         createMockExecutor({
250 |           success: true,
251 |           output: 'Screenshot saved',
252 |           error: undefined,
253 |         }),
254 |         createMockFileSystemExecutor({
255 |           readFile: async () => Buffer.from('fake-image-data', 'utf8').toString('utf8'),
256 |         }),
257 |       );
258 | 
259 |       expect(result.isError).toBe(false);
260 |       expect(result.content[0].type).toBe('image');
261 |     });
262 | 
263 |     it('should return success for valid screenshot capture', async () => {
264 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
265 | 
266 |       const mockExecutor = createMockExecutor({
267 |         success: true,
268 |         output: 'Screenshot saved',
269 |         error: undefined,
270 |       });
271 | 
272 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
273 |         readFile: async () => mockImageBuffer.toString('utf8'),
274 |       });
275 | 
276 |       const result = await screenshotLogic(
277 |         {
278 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
279 |         },
280 |         mockExecutor,
281 |         mockFileSystemExecutor,
282 |       );
283 | 
284 |       expect(result).toEqual({
285 |         content: [
286 |           {
287 |             type: 'image',
288 |             data: 'fake-image-data',
289 |             mimeType: 'image/jpeg',
290 |           },
291 |         ],
292 |         isError: false,
293 |       });
294 |     });
295 | 
296 |     it('should handle command execution failure', async () => {
297 |       const mockExecutor = createMockExecutor({
298 |         success: false,
299 |         output: '',
300 |         error: 'Simulator not found',
301 |       });
302 | 
303 |       const result = await screenshotLogic(
304 |         {
305 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
306 |         },
307 |         mockExecutor,
308 |         createMockFileSystemExecutor(),
309 |       );
310 | 
311 |       expect(result).toEqual({
312 |         content: [
313 |           {
314 |             type: 'text',
315 |             text: 'Error: System error executing screenshot: Failed to capture screenshot: Simulator not found',
316 |           },
317 |         ],
318 |         isError: true,
319 |       });
320 |     });
321 | 
322 |     it('should handle file reading errors', async () => {
323 |       const mockExecutor = createMockExecutor({
324 |         success: true,
325 |         output: 'Screenshot saved',
326 |         error: undefined,
327 |       });
328 | 
329 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
330 |         readFile: async () => {
331 |           throw new Error('File not found');
332 |         },
333 |       });
334 | 
335 |       const result = await screenshotLogic(
336 |         {
337 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
338 |         },
339 |         mockExecutor,
340 |         mockFileSystemExecutor,
341 |       );
342 | 
343 |       expect(result).toEqual({
344 |         content: [
345 |           {
346 |             type: 'text',
347 |             text: 'Error: Screenshot captured but failed to process image file: File not found',
348 |           },
349 |         ],
350 |         isError: true,
351 |       });
352 |     });
353 | 
354 |     it('should handle file cleanup errors gracefully', async () => {
355 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
356 | 
357 |       const mockExecutor = createMockExecutor({
358 |         success: true,
359 |         output: 'Screenshot saved',
360 |         error: undefined,
361 |       });
362 | 
363 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
364 |         readFile: async () => mockImageBuffer.toString('utf8'),
365 |         // unlink method is not overridden, so it will use the default (no-op)
366 |         // which simulates the cleanup failure being caught and logged
367 |       });
368 | 
369 |       const result = await screenshotLogic(
370 |         {
371 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
372 |         },
373 |         mockExecutor,
374 |         mockFileSystemExecutor,
375 |       );
376 | 
377 |       // Should still return successful result despite cleanup failure
378 |       expect(result).toEqual({
379 |         content: [
380 |           {
381 |             type: 'image',
382 |             data: 'fake-image-data',
383 |             mimeType: 'image/jpeg',
384 |           },
385 |         ],
386 |         isError: false,
387 |       });
388 |     });
389 | 
390 |     it('should handle SystemError from command execution', async () => {
391 |       const mockExecutor = async () => {
392 |         throw new SystemError('System error occurred');
393 |       };
394 | 
395 |       const result = await screenshotLogic(
396 |         {
397 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
398 |         },
399 |         mockExecutor,
400 |         createMockFileSystemExecutor(),
401 |       );
402 | 
403 |       expect(result).toEqual({
404 |         content: [
405 |           { type: 'text', text: 'Error: System error executing screenshot: System error occurred' },
406 |         ],
407 |         isError: true,
408 |       });
409 |     });
410 | 
411 |     it('should handle unexpected Error objects', async () => {
412 |       const mockExecutor = async () => {
413 |         throw new Error('Unexpected error');
414 |       };
415 | 
416 |       const result = await screenshotLogic(
417 |         {
418 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
419 |         },
420 |         mockExecutor,
421 |         createMockFileSystemExecutor(),
422 |       );
423 | 
424 |       expect(result).toEqual({
425 |         content: [{ type: 'text', text: 'Error: An unexpected error occurred: Unexpected error' }],
426 |         isError: true,
427 |       });
428 |     });
429 | 
430 |     it('should handle unexpected string errors', async () => {
431 |       const mockExecutor = async () => {
432 |         throw 'String error';
433 |       };
434 | 
435 |       const result = await screenshotLogic(
436 |         {
437 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
438 |         },
439 |         mockExecutor,
440 |         createMockFileSystemExecutor(),
441 |       );
442 | 
443 |       expect(result).toEqual({
444 |         content: [{ type: 'text', text: 'Error: An unexpected error occurred: String error' }],
445 |         isError: true,
446 |       });
447 |     });
448 |   });
449 | });
450 | 
```

--------------------------------------------------------------------------------
/src/utils/build-utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Build Utilities - Higher-level abstractions for Xcode build operations
  3 |  *
  4 |  * This utility module provides specialized functions for build-related operations
  5 |  * across different platforms (macOS, iOS, watchOS, etc.). It serves as a higher-level
  6 |  * abstraction layer on top of the core Xcode utilities.
  7 |  *
  8 |  * Responsibilities:
  9 |  * - Providing a unified interface (executeXcodeBuild) for all build operations
 10 |  * - Handling build-specific parameter formatting and validation
 11 |  * - Standardizing response formatting for build results
 12 |  * - Managing build-specific error handling and reporting
 13 |  * - Supporting various build actions (build, clean, showBuildSettings, etc.)
 14 |  * - Supporting xcodemake as an alternative build strategy for faster incremental builds
 15 |  *
 16 |  * This file depends on the lower-level utilities in xcode.ts for command execution
 17 |  * while adding build-specific behavior, formatting, and error handling.
 18 |  */
 19 | 
 20 | import { log } from './logger.ts';
 21 | import { XcodePlatform, constructDestinationString } from './xcode.ts';
 22 | import { CommandExecutor, CommandExecOptions } from './command.ts';
 23 | import { ToolResponse, SharedBuildParams, PlatformBuildOptions } from '../types/common.ts';
 24 | import { createTextResponse, consolidateContentForClaudeCode } from './validation.ts';
 25 | import {
 26 |   isXcodemakeEnabled,
 27 |   isXcodemakeAvailable,
 28 |   executeXcodemakeCommand,
 29 |   executeMakeCommand,
 30 |   doesMakefileExist,
 31 |   doesMakeLogFileExist,
 32 | } from './xcodemake.ts';
 33 | import path from 'path';
 34 | 
 35 | /**
 36 |  * Common function to execute an Xcode build command across platforms
 37 |  * @param params Common build parameters
 38 |  * @param platformOptions Platform-specific options
 39 |  * @param preferXcodebuild Whether to prefer xcodebuild over xcodemake, useful for if xcodemake is failing
 40 |  * @param buildAction The xcodebuild action to perform (e.g., 'build', 'clean', 'test')
 41 |  * @param executor Optional command executor for dependency injection (used for testing)
 42 |  * @returns Promise resolving to tool response
 43 |  */
 44 | export async function executeXcodeBuildCommand(
 45 |   params: SharedBuildParams,
 46 |   platformOptions: PlatformBuildOptions,
 47 |   preferXcodebuild: boolean = false,
 48 |   buildAction: string = 'build',
 49 |   executor: CommandExecutor,
 50 |   execOpts?: CommandExecOptions,
 51 | ): Promise<ToolResponse> {
 52 |   // Collect warnings, errors, and stderr messages from the build output
 53 |   const buildMessages: { type: 'text'; text: string }[] = [];
 54 |   function grepWarningsAndErrors(text: string): { type: 'warning' | 'error'; content: string }[] {
 55 |     return text
 56 |       .split('\n')
 57 |       .map((content) => {
 58 |         if (/warning:/i.test(content)) return { type: 'warning', content };
 59 |         if (/error:/i.test(content)) return { type: 'error', content };
 60 |         return null;
 61 |       })
 62 |       .filter(Boolean) as { type: 'warning' | 'error'; content: string }[];
 63 |   }
 64 | 
 65 |   log('info', `Starting ${platformOptions.logPrefix} ${buildAction} for scheme ${params.scheme}`);
 66 | 
 67 |   // Check if xcodemake is enabled and available
 68 |   const isXcodemakeEnabledFlag = isXcodemakeEnabled();
 69 |   let xcodemakeAvailableFlag = false;
 70 | 
 71 |   if (isXcodemakeEnabledFlag && buildAction === 'build') {
 72 |     xcodemakeAvailableFlag = await isXcodemakeAvailable();
 73 | 
 74 |     if (xcodemakeAvailableFlag && preferXcodebuild) {
 75 |       log(
 76 |         'info',
 77 |         'xcodemake is enabled but preferXcodebuild is set to true. Falling back to xcodebuild.',
 78 |       );
 79 |       buildMessages.push({
 80 |         type: 'text',
 81 |         text: '⚠️ incremental build support is enabled but preferXcodebuild is set to true. Falling back to xcodebuild.',
 82 |       });
 83 |     } else if (!xcodemakeAvailableFlag) {
 84 |       buildMessages.push({
 85 |         type: 'text',
 86 |         text: '⚠️ xcodemake is enabled but not available. Falling back to xcodebuild.',
 87 |       });
 88 |       log('info', 'xcodemake is enabled but not available. Falling back to xcodebuild.');
 89 |     } else {
 90 |       log('info', 'xcodemake is enabled and available, using it for incremental builds.');
 91 |       buildMessages.push({
 92 |         type: 'text',
 93 |         text: 'ℹ️ xcodemake is enabled and available, using it for incremental builds.',
 94 |       });
 95 |     }
 96 |   }
 97 | 
 98 |   try {
 99 |     const command = ['xcodebuild'];
100 | 
101 |     let projectDir = '';
102 |     if (params.workspacePath) {
103 |       projectDir = path.dirname(params.workspacePath);
104 |       command.push('-workspace', params.workspacePath);
105 |     } else if (params.projectPath) {
106 |       projectDir = path.dirname(params.projectPath);
107 |       command.push('-project', params.projectPath);
108 |     }
109 | 
110 |     command.push('-scheme', params.scheme);
111 |     command.push('-configuration', params.configuration);
112 |     command.push('-skipMacroValidation');
113 | 
114 |     // Construct destination string based on platform
115 |     let destinationString: string;
116 |     const isSimulatorPlatform = [
117 |       XcodePlatform.iOSSimulator,
118 |       XcodePlatform.watchOSSimulator,
119 |       XcodePlatform.tvOSSimulator,
120 |       XcodePlatform.visionOSSimulator,
121 |     ].includes(platformOptions.platform);
122 | 
123 |     if (isSimulatorPlatform) {
124 |       if (platformOptions.simulatorId) {
125 |         destinationString = constructDestinationString(
126 |           platformOptions.platform,
127 |           undefined,
128 |           platformOptions.simulatorId,
129 |         );
130 |       } else if (platformOptions.simulatorName) {
131 |         destinationString = constructDestinationString(
132 |           platformOptions.platform,
133 |           platformOptions.simulatorName,
134 |           undefined,
135 |           platformOptions.useLatestOS,
136 |         );
137 |       } else {
138 |         return createTextResponse(
139 |           `For ${platformOptions.platform} platform, either simulatorId or simulatorName must be provided`,
140 |           true,
141 |         );
142 |       }
143 |     } else if (platformOptions.platform === XcodePlatform.macOS) {
144 |       destinationString = constructDestinationString(
145 |         platformOptions.platform,
146 |         undefined,
147 |         undefined,
148 |         false,
149 |         platformOptions.arch,
150 |       );
151 |     } else if (platformOptions.platform === XcodePlatform.iOS) {
152 |       if (platformOptions.deviceId) {
153 |         destinationString = `platform=iOS,id=${platformOptions.deviceId}`;
154 |       } else {
155 |         destinationString = 'generic/platform=iOS';
156 |       }
157 |     } else if (platformOptions.platform === XcodePlatform.watchOS) {
158 |       if (platformOptions.deviceId) {
159 |         destinationString = `platform=watchOS,id=${platformOptions.deviceId}`;
160 |       } else {
161 |         destinationString = 'generic/platform=watchOS';
162 |       }
163 |     } else if (platformOptions.platform === XcodePlatform.tvOS) {
164 |       if (platformOptions.deviceId) {
165 |         destinationString = `platform=tvOS,id=${platformOptions.deviceId}`;
166 |       } else {
167 |         destinationString = 'generic/platform=tvOS';
168 |       }
169 |     } else if (platformOptions.platform === XcodePlatform.visionOS) {
170 |       if (platformOptions.deviceId) {
171 |         destinationString = `platform=visionOS,id=${platformOptions.deviceId}`;
172 |       } else {
173 |         destinationString = 'generic/platform=visionOS';
174 |       }
175 |     } else {
176 |       return createTextResponse(`Unsupported platform: ${platformOptions.platform}`, true);
177 |     }
178 | 
179 |     command.push('-destination', destinationString);
180 | 
181 |     if (params.derivedDataPath) {
182 |       command.push('-derivedDataPath', params.derivedDataPath);
183 |     }
184 | 
185 |     if (params.extraArgs && params.extraArgs.length > 0) {
186 |       command.push(...params.extraArgs);
187 |     }
188 | 
189 |     command.push(buildAction);
190 | 
191 |     // Execute the command using xcodemake or xcodebuild
192 |     let result;
193 |     if (
194 |       isXcodemakeEnabledFlag &&
195 |       xcodemakeAvailableFlag &&
196 |       buildAction === 'build' &&
197 |       !preferXcodebuild
198 |     ) {
199 |       // Check if Makefile already exists
200 |       const makefileExists = doesMakefileExist(projectDir);
201 |       log('debug', 'Makefile exists: ' + makefileExists);
202 | 
203 |       // Check if Makefile log already exists
204 |       const makeLogFileExists = doesMakeLogFileExist(projectDir, command);
205 |       log('debug', 'Makefile log exists: ' + makeLogFileExists);
206 | 
207 |       if (makefileExists && makeLogFileExists) {
208 |         // Use make for incremental builds
209 |         buildMessages.push({
210 |           type: 'text',
211 |           text: 'ℹ️ Using make for incremental build',
212 |         });
213 |         result = await executeMakeCommand(projectDir, platformOptions.logPrefix);
214 |       } else {
215 |         // Generate Makefile using xcodemake
216 |         buildMessages.push({
217 |           type: 'text',
218 |           text: 'ℹ️ Generating Makefile with xcodemake (first build may take longer)',
219 |         });
220 |         // Remove 'xcodebuild' from the command array before passing to executeXcodemakeCommand
221 |         result = await executeXcodemakeCommand(
222 |           projectDir,
223 |           command.slice(1),
224 |           platformOptions.logPrefix,
225 |         );
226 |       }
227 |     } else {
228 |       // Use standard xcodebuild
229 |       result = await executor(command, platformOptions.logPrefix, true, execOpts);
230 |     }
231 | 
232 |     // Grep warnings and errors from stdout (build output)
233 |     const warningOrErrorLines = grepWarningsAndErrors(result.output);
234 |     warningOrErrorLines.forEach(({ type, content }) => {
235 |       buildMessages.push({
236 |         type: 'text',
237 |         text: type === 'warning' ? `⚠️ Warning: ${content}` : `❌ Error: ${content}`,
238 |       });
239 |     });
240 | 
241 |     // Include all stderr lines as errors
242 |     if (result.error) {
243 |       result.error.split('\n').forEach((content) => {
244 |         if (content.trim()) {
245 |           buildMessages.push({ type: 'text', text: `❌ [stderr] ${content}` });
246 |         }
247 |       });
248 |     }
249 | 
250 |     if (!result.success) {
251 |       const isMcpError = result.exitCode === 64;
252 | 
253 |       log(
254 |         isMcpError ? 'error' : 'warning',
255 |         `${platformOptions.logPrefix} ${buildAction} failed: ${result.error}`,
256 |         { sentry: isMcpError },
257 |       );
258 |       const errorResponse = createTextResponse(
259 |         `❌ ${platformOptions.logPrefix} ${buildAction} failed for scheme ${params.scheme}.`,
260 |         true,
261 |       );
262 | 
263 |       if (buildMessages.length > 0 && errorResponse.content) {
264 |         errorResponse.content.unshift(...buildMessages);
265 |       }
266 | 
267 |       // If using xcodemake and build failed but no compiling errors, suggest using xcodebuild
268 |       if (
269 |         warningOrErrorLines.length == 0 &&
270 |         isXcodemakeEnabledFlag &&
271 |         xcodemakeAvailableFlag &&
272 |         buildAction === 'build' &&
273 |         !preferXcodebuild
274 |       ) {
275 |         errorResponse.content.push({
276 |           type: 'text',
277 |           text: `💡 Incremental build using xcodemake failed, suggest using preferXcodebuild option to try build again using slower xcodebuild command.`,
278 |         });
279 |       }
280 | 
281 |       return consolidateContentForClaudeCode(errorResponse);
282 |     }
283 | 
284 |     log('info', `✅ ${platformOptions.logPrefix} ${buildAction} succeeded.`);
285 | 
286 |     // Create additional info based on platform and action
287 |     let additionalInfo = '';
288 | 
289 |     // Add xcodemake info if relevant
290 |     if (
291 |       isXcodemakeEnabledFlag &&
292 |       xcodemakeAvailableFlag &&
293 |       buildAction === 'build' &&
294 |       !preferXcodebuild
295 |     ) {
296 |       additionalInfo += `xcodemake: Using faster incremental builds with xcodemake. 
297 | Future builds will use the generated Makefile for improved performance.
298 | 
299 | `;
300 |     }
301 | 
302 |     // Only show next steps for 'build' action
303 |     if (buildAction === 'build') {
304 |       if (platformOptions.platform === XcodePlatform.macOS) {
305 |         additionalInfo = `Next Steps:
306 | 1. Get app path: get_mac_app_path({ scheme: '${params.scheme}' })
307 | 2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
308 | 3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })`;
309 |       } else if (platformOptions.platform === XcodePlatform.iOS) {
310 |         additionalInfo = `Next Steps:
311 | 1. Get app path: get_device_app_path({ scheme: '${params.scheme}' })
312 | 2. Get bundle ID: get_app_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
313 | 3. Launch: launch_app_device({ bundleId: 'BUNDLE_ID_FROM_STEP_2' })`;
314 |       } else if (isSimulatorPlatform) {
315 |         const simIdParam = platformOptions.simulatorId ? 'simulatorId' : 'simulatorName';
316 |         const simIdValue = platformOptions.simulatorId ?? platformOptions.simulatorName;
317 | 
318 |         additionalInfo = `Next Steps:
319 | 1. Get app path: get_sim_app_path({ ${simIdParam}: '${simIdValue}', scheme: '${params.scheme}', platform: 'iOS Simulator' })
320 | 2. Get bundle ID: get_app_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
321 | 3. Launch: launch_app_sim({ ${simIdParam}: '${simIdValue}', bundleId: 'BUNDLE_ID_FROM_STEP_2' })
322 |    Or with logs: launch_app_logs_sim({ ${simIdParam}: '${simIdValue}', bundleId: 'BUNDLE_ID_FROM_STEP_2' })`;
323 |       }
324 |     }
325 | 
326 |     const successResponse: ToolResponse = {
327 |       content: [
328 |         ...buildMessages,
329 |         {
330 |           type: 'text',
331 |           text: `✅ ${platformOptions.logPrefix} ${buildAction} succeeded for scheme ${params.scheme}.`,
332 |         },
333 |       ],
334 |     };
335 | 
336 |     // Only add additional info if we have any
337 |     if (additionalInfo) {
338 |       successResponse.content.push({
339 |         type: 'text',
340 |         text: additionalInfo,
341 |       });
342 |     }
343 | 
344 |     return consolidateContentForClaudeCode(successResponse);
345 |   } catch (error) {
346 |     const errorMessage = error instanceof Error ? error.message : String(error);
347 | 
348 |     const isSpawnError =
349 |       error instanceof Error &&
350 |       'code' in error &&
351 |       ['ENOENT', 'EACCES', 'EPERM'].includes((error as NodeJS.ErrnoException).code ?? '');
352 | 
353 |     log('error', `Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`, {
354 |       sentry: !isSpawnError,
355 |     });
356 | 
357 |     return consolidateContentForClaudeCode(
358 |       createTextResponse(
359 |         `Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`,
360 |         true,
361 |       ),
362 |     );
363 |   }
364 | }
365 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/swift-package/__tests__/swift_package_list.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for swift_package_list plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using pure dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import swiftPackageList, { swift_package_listLogic } from '../swift_package_list.ts';
  9 | 
 10 | describe('swift_package_list plugin', () => {
 11 |   // No mocks to clear with pure dependency injection
 12 | 
 13 |   describe('Export Field Validation (Literal)', () => {
 14 |     it('should have correct name', () => {
 15 |       expect(swiftPackageList.name).toBe('swift_package_list');
 16 |     });
 17 | 
 18 |     it('should have correct description', () => {
 19 |       expect(swiftPackageList.description).toBe('Lists currently running Swift Package processes');
 20 |     });
 21 | 
 22 |     it('should have handler function', () => {
 23 |       expect(typeof swiftPackageList.handler).toBe('function');
 24 |     });
 25 | 
 26 |     it('should validate schema correctly', () => {
 27 |       // The schema is an empty object, so any input should be valid
 28 |       expect(typeof swiftPackageList.schema).toBe('object');
 29 |       expect(Object.keys(swiftPackageList.schema)).toEqual([]);
 30 |     });
 31 |   });
 32 | 
 33 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 34 |     it('should return empty list when no processes are running', async () => {
 35 |       // Create empty mock process map
 36 |       const mockProcessMap = new Map();
 37 | 
 38 |       // Use pure dependency injection with stub functions
 39 |       const mockArrayFrom = () => [];
 40 |       const mockDateNow = () => Date.now();
 41 | 
 42 |       const result = await swift_package_listLogic(
 43 |         {},
 44 |         {
 45 |           processMap: mockProcessMap,
 46 |           arrayFrom: mockArrayFrom,
 47 |           dateNow: mockDateNow,
 48 |         },
 49 |       );
 50 | 
 51 |       expect(result).toEqual({
 52 |         content: [
 53 |           { type: 'text', text: 'ℹ️ No Swift Package processes currently running.' },
 54 |           { type: 'text', text: '💡 Use swift_package_run to start an executable.' },
 55 |         ],
 56 |       });
 57 |     });
 58 | 
 59 |     it('should handle empty args object', async () => {
 60 |       // Create empty mock process map
 61 |       const mockProcessMap = new Map();
 62 | 
 63 |       // Use pure dependency injection with stub functions
 64 |       const mockArrayFrom = () => [];
 65 |       const mockDateNow = () => Date.now();
 66 | 
 67 |       const result = await swift_package_listLogic(
 68 |         {},
 69 |         {
 70 |           processMap: mockProcessMap,
 71 |           arrayFrom: mockArrayFrom,
 72 |           dateNow: mockDateNow,
 73 |         },
 74 |       );
 75 | 
 76 |       expect(result).toEqual({
 77 |         content: [
 78 |           { type: 'text', text: 'ℹ️ No Swift Package processes currently running.' },
 79 |           { type: 'text', text: '💡 Use swift_package_run to start an executable.' },
 80 |         ],
 81 |       });
 82 |     });
 83 | 
 84 |     it('should handle null args', async () => {
 85 |       // Create empty mock process map
 86 |       const mockProcessMap = new Map();
 87 | 
 88 |       // Use pure dependency injection with stub functions
 89 |       const mockArrayFrom = () => [];
 90 |       const mockDateNow = () => Date.now();
 91 | 
 92 |       const result = await swift_package_listLogic(null, {
 93 |         processMap: mockProcessMap,
 94 |         arrayFrom: mockArrayFrom,
 95 |         dateNow: mockDateNow,
 96 |       });
 97 | 
 98 |       expect(result).toEqual({
 99 |         content: [
100 |           { type: 'text', text: 'ℹ️ No Swift Package processes currently running.' },
101 |           { type: 'text', text: '💡 Use swift_package_run to start an executable.' },
102 |         ],
103 |       });
104 |     });
105 | 
106 |     it('should handle undefined args', async () => {
107 |       // Create empty mock process map
108 |       const mockProcessMap = new Map();
109 | 
110 |       // Use pure dependency injection with stub functions
111 |       const mockArrayFrom = () => [];
112 |       const mockDateNow = () => Date.now();
113 | 
114 |       const result = await swift_package_listLogic(undefined, {
115 |         processMap: mockProcessMap,
116 |         arrayFrom: mockArrayFrom,
117 |         dateNow: mockDateNow,
118 |       });
119 | 
120 |       expect(result).toEqual({
121 |         content: [
122 |           { type: 'text', text: 'ℹ️ No Swift Package processes currently running.' },
123 |           { type: 'text', text: '💡 Use swift_package_run to start an executable.' },
124 |         ],
125 |       });
126 |     });
127 | 
128 |     it('should handle args with extra properties', async () => {
129 |       // Create empty mock process map
130 |       const mockProcessMap = new Map();
131 | 
132 |       // Use pure dependency injection with stub functions
133 |       const mockArrayFrom = () => [];
134 |       const mockDateNow = () => Date.now();
135 | 
136 |       const result = await swift_package_listLogic(
137 |         {
138 |           extraProperty: 'value',
139 |           anotherProperty: 123,
140 |         },
141 |         {
142 |           processMap: mockProcessMap,
143 |           arrayFrom: mockArrayFrom,
144 |           dateNow: mockDateNow,
145 |         },
146 |       );
147 | 
148 |       expect(result).toEqual({
149 |         content: [
150 |           { type: 'text', text: 'ℹ️ No Swift Package processes currently running.' },
151 |           { type: 'text', text: '💡 Use swift_package_run to start an executable.' },
152 |         ],
153 |       });
154 |     });
155 | 
156 |     it('should return single process when one process is running', async () => {
157 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
158 |       const mockProcess = {
159 |         executableName: 'MyApp',
160 |         packagePath: '/test/package',
161 |         startedAt: startedAt,
162 |       };
163 | 
164 |       // Create mock process map with one process
165 |       const mockProcessMap = new Map([[12345, mockProcess]]);
166 | 
167 |       // Use pure dependency injection with stub functions
168 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
169 |       const mockDateNow = () => startedAt.getTime() + 5000; // 5 seconds after start
170 | 
171 |       const result = await swift_package_listLogic(
172 |         {},
173 |         {
174 |           processMap: mockProcessMap,
175 |           arrayFrom: mockArrayFrom,
176 |           dateNow: mockDateNow,
177 |         },
178 |       );
179 | 
180 |       expect(result).toEqual({
181 |         content: [
182 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
183 |           { type: 'text', text: '  • PID 12345: MyApp (/test/package) - running 5s' },
184 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
185 |         ],
186 |       });
187 |     });
188 | 
189 |     it('should return multiple processes when several are running', async () => {
190 |       const startedAt1 = new Date('2023-01-01T10:00:00.000Z');
191 |       const startedAt2 = new Date('2023-01-01T10:00:07.000Z');
192 | 
193 |       const mockProcess1 = {
194 |         executableName: 'MyApp',
195 |         packagePath: '/test/package1',
196 |         startedAt: startedAt1,
197 |       };
198 | 
199 |       const mockProcess2 = {
200 |         executableName: undefined, // Test default executable name
201 |         packagePath: '/test/package2',
202 |         startedAt: startedAt2,
203 |       };
204 | 
205 |       // Create mock process map with multiple processes
206 |       const mockProcessMap = new Map([
207 |         [12345, mockProcess1],
208 |         [12346, mockProcess2],
209 |       ]);
210 | 
211 |       // Use pure dependency injection with stub functions
212 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
213 |       const mockDateNow = () => startedAt1.getTime() + 10000; // 10 seconds after first start
214 | 
215 |       const result = await swift_package_listLogic(
216 |         {},
217 |         {
218 |           processMap: mockProcessMap,
219 |           arrayFrom: mockArrayFrom,
220 |           dateNow: mockDateNow,
221 |         },
222 |       );
223 | 
224 |       expect(result).toEqual({
225 |         content: [
226 |           { type: 'text', text: '📋 Active Swift Package processes (2):' },
227 |           { type: 'text', text: '  • PID 12345: MyApp (/test/package1) - running 10s' },
228 |           { type: 'text', text: '  • PID 12346: default (/test/package2) - running 3s' },
229 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
230 |         ],
231 |       });
232 |     });
233 | 
234 |     it('should handle process with null executableName', async () => {
235 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
236 |       const mockProcess = {
237 |         executableName: null, // Test null executable name
238 |         packagePath: '/test/package',
239 |         startedAt: startedAt,
240 |       };
241 | 
242 |       // Create mock process map with one process
243 |       const mockProcessMap = new Map([[12345, mockProcess]]);
244 | 
245 |       // Use pure dependency injection with stub functions
246 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
247 |       const mockDateNow = () => startedAt.getTime() + 1000; // 1 second after start
248 | 
249 |       const result = await swift_package_listLogic(
250 |         {},
251 |         {
252 |           processMap: mockProcessMap,
253 |           arrayFrom: mockArrayFrom,
254 |           dateNow: mockDateNow,
255 |         },
256 |       );
257 | 
258 |       expect(result).toEqual({
259 |         content: [
260 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
261 |           { type: 'text', text: '  • PID 12345: default (/test/package) - running 1s' },
262 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
263 |         ],
264 |       });
265 |     });
266 | 
267 |     it('should handle process with empty string executableName', async () => {
268 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
269 |       const mockProcess = {
270 |         executableName: '', // Test empty string executable name
271 |         packagePath: '/test/package',
272 |         startedAt: startedAt,
273 |       };
274 | 
275 |       // Create mock process map with one process
276 |       const mockProcessMap = new Map([[12345, mockProcess]]);
277 | 
278 |       // Use pure dependency injection with stub functions
279 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
280 |       const mockDateNow = () => startedAt.getTime() + 2000; // 2 seconds after start
281 | 
282 |       const result = await swift_package_listLogic(
283 |         {},
284 |         {
285 |           processMap: mockProcessMap,
286 |           arrayFrom: mockArrayFrom,
287 |           dateNow: mockDateNow,
288 |         },
289 |       );
290 | 
291 |       expect(result).toEqual({
292 |         content: [
293 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
294 |           { type: 'text', text: '  • PID 12345: default (/test/package) - running 2s' },
295 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
296 |         ],
297 |       });
298 |     });
299 | 
300 |     it('should handle very recent process (less than 1 second)', async () => {
301 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
302 |       const mockProcess = {
303 |         executableName: 'FastApp',
304 |         packagePath: '/test/package',
305 |         startedAt: startedAt,
306 |       };
307 | 
308 |       // Create mock process map with one process
309 |       const mockProcessMap = new Map([[12345, mockProcess]]);
310 | 
311 |       // Use pure dependency injection with stub functions
312 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
313 |       const mockDateNow = () => startedAt.getTime() + 500; // 500ms after start
314 | 
315 |       const result = await swift_package_listLogic(
316 |         {},
317 |         {
318 |           processMap: mockProcessMap,
319 |           arrayFrom: mockArrayFrom,
320 |           dateNow: mockDateNow,
321 |         },
322 |       );
323 | 
324 |       expect(result).toEqual({
325 |         content: [
326 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
327 |           { type: 'text', text: '  • PID 12345: FastApp (/test/package) - running 1s' },
328 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
329 |         ],
330 |       });
331 |     });
332 | 
333 |     it('should handle process running for exactly 0 milliseconds', async () => {
334 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
335 |       const mockProcess = {
336 |         executableName: 'InstantApp',
337 |         packagePath: '/test/package',
338 |         startedAt: startedAt,
339 |       };
340 | 
341 |       // Create mock process map with one process
342 |       const mockProcessMap = new Map([[12345, mockProcess]]);
343 | 
344 |       // Use pure dependency injection with stub functions
345 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
346 |       const mockDateNow = () => startedAt.getTime(); // Same time as start
347 | 
348 |       const result = await swift_package_listLogic(
349 |         {},
350 |         {
351 |           processMap: mockProcessMap,
352 |           arrayFrom: mockArrayFrom,
353 |           dateNow: mockDateNow,
354 |         },
355 |       );
356 | 
357 |       expect(result).toEqual({
358 |         content: [
359 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
360 |           { type: 'text', text: '  • PID 12345: InstantApp (/test/package) - running 1s' },
361 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
362 |         ],
363 |       });
364 |     });
365 | 
366 |     it('should handle process running for a long time', async () => {
367 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
368 |       const mockProcess = {
369 |         executableName: 'LongRunningApp',
370 |         packagePath: '/test/package',
371 |         startedAt: startedAt,
372 |       };
373 | 
374 |       // Create mock process map with one process
375 |       const mockProcessMap = new Map([[12345, mockProcess]]);
376 | 
377 |       // Use pure dependency injection with stub functions
378 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
379 |       const mockDateNow = () => startedAt.getTime() + 7200000; // 2 hours later
380 | 
381 |       const result = await swift_package_listLogic(
382 |         {},
383 |         {
384 |           processMap: mockProcessMap,
385 |           arrayFrom: mockArrayFrom,
386 |           dateNow: mockDateNow,
387 |         },
388 |       );
389 | 
390 |       expect(result).toEqual({
391 |         content: [
392 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
393 |           { type: 'text', text: '  • PID 12345: LongRunningApp (/test/package) - running 7200s' },
394 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
395 |         ],
396 |       });
397 |     });
398 |   });
399 | });
400 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Utilities Plugin: Scaffold macOS Project
  3 |  *
  4 |  * Scaffold a new macOS project from templates.
  5 |  */
  6 | 
  7 | import { z } from 'zod';
  8 | import { join, dirname, basename } from 'path';
  9 | import { log } from '../../../utils/logging/index.ts';
 10 | import { ValidationError } from '../../../utils/responses/index.ts';
 11 | import { TemplateManager } from '../../../utils/template/index.ts';
 12 | import { ToolResponse } from '../../../types/common.ts';
 13 | import {
 14 |   CommandExecutor,
 15 |   getDefaultCommandExecutor,
 16 |   getDefaultFileSystemExecutor,
 17 | } from '../../../utils/command.ts';
 18 | import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
 19 | 
 20 | // Common base schema for both iOS and macOS
 21 | const BaseScaffoldSchema = z.object({
 22 |   projectName: z.string().min(1).describe('Name of the new project'),
 23 |   outputPath: z.string().describe('Path where the project should be created'),
 24 |   bundleIdentifier: z
 25 |     .string()
 26 |     .optional()
 27 |     .describe(
 28 |       'Bundle identifier (e.g., com.example.myapp). If not provided, will use com.example.projectname',
 29 |     ),
 30 |   displayName: z
 31 |     .string()
 32 |     .optional()
 33 |     .describe(
 34 |       'App display name (shown on home screen/dock). If not provided, will use projectName',
 35 |     ),
 36 |   marketingVersion: z
 37 |     .string()
 38 |     .optional()
 39 |     .describe('Marketing version (e.g., 1.0, 2.1.3). If not provided, will use 1.0'),
 40 |   currentProjectVersion: z
 41 |     .string()
 42 |     .optional()
 43 |     .describe('Build number (e.g., 1, 42, 100). If not provided, will use 1'),
 44 |   customizeNames: z
 45 |     .boolean()
 46 |     .default(true)
 47 |     .describe('Whether to customize project names and identifiers. Default is true.'),
 48 | });
 49 | 
 50 | // macOS-specific schema
 51 | const ScaffoldmacOSProjectSchema = BaseScaffoldSchema.extend({
 52 |   deploymentTarget: z
 53 |     .string()
 54 |     .optional()
 55 |     .describe('macOS deployment target (e.g., 15.4, 14.0). If not provided, will use 15.4'),
 56 | });
 57 | 
 58 | // Use z.infer for type safety
 59 | type ScaffoldMacOSProjectParams = z.infer<typeof ScaffoldmacOSProjectSchema>;
 60 | 
 61 | /**
 62 |  * Update Package.swift file with deployment target
 63 |  */
 64 | function updatePackageSwiftFile(
 65 |   content: string,
 66 |   params: ScaffoldMacOSProjectParams & { platform: string },
 67 | ): string {
 68 |   let result = content;
 69 | 
 70 |   // Update ALL target name references in Package.swift
 71 |   const featureName = `${params.projectName}Feature`;
 72 |   const testName = `${params.projectName}FeatureTests`;
 73 | 
 74 |   // Replace ALL occurrences of MyProjectFeatureTests first (more specific)
 75 |   result = result.replace(/MyProjectFeatureTests/g, testName);
 76 |   // Then replace ALL occurrences of MyProjectFeature (less specific, so comes after)
 77 |   result = result.replace(/MyProjectFeature/g, featureName);
 78 | 
 79 |   // Update deployment targets based on platform
 80 |   if (params.platform === 'macOS') {
 81 |     if (params.deploymentTarget) {
 82 |       // Extract major version (e.g., "14.0" -> "14")
 83 |       const majorVersion = params.deploymentTarget.split('.')[0];
 84 |       result = result.replace(/\.macOS\(\.v\d+\)/, `.macOS(.v${majorVersion})`);
 85 |     }
 86 |   }
 87 | 
 88 |   return result;
 89 | }
 90 | 
 91 | /**
 92 |  * Update XCConfig file with scaffold parameters
 93 |  */
 94 | function updateXCConfigFile(
 95 |   content: string,
 96 |   params: ScaffoldMacOSProjectParams & { platform: string },
 97 | ): string {
 98 |   let result = content;
 99 | 
100 |   // Update project identity settings
101 |   result = result.replace(/PRODUCT_NAME = .+/g, `PRODUCT_NAME = ${params.projectName}`);
102 |   result = result.replace(
103 |     /PRODUCT_DISPLAY_NAME = .+/g,
104 |     `PRODUCT_DISPLAY_NAME = ${params.displayName ?? params.projectName}`,
105 |   );
106 |   result = result.replace(
107 |     /PRODUCT_BUNDLE_IDENTIFIER = .+/g,
108 |     `PRODUCT_BUNDLE_IDENTIFIER = ${params.bundleIdentifier ?? `com.example.${params.projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`}`,
109 |   );
110 |   result = result.replace(
111 |     /MARKETING_VERSION = .+/g,
112 |     `MARKETING_VERSION = ${params.marketingVersion ?? '1.0'}`,
113 |   );
114 |   result = result.replace(
115 |     /CURRENT_PROJECT_VERSION = .+/g,
116 |     `CURRENT_PROJECT_VERSION = ${params.currentProjectVersion ?? '1'}`,
117 |   );
118 | 
119 |   // Platform-specific updates
120 |   if (params.platform === 'macOS') {
121 |     // macOS deployment target
122 |     if (params.deploymentTarget) {
123 |       result = result.replace(
124 |         /MACOSX_DEPLOYMENT_TARGET = .+/g,
125 |         `MACOSX_DEPLOYMENT_TARGET = ${params.deploymentTarget}`,
126 |       );
127 |     }
128 | 
129 |     // Update entitlements path for macOS
130 |     result = result.replace(
131 |       /CODE_SIGN_ENTITLEMENTS = .+/g,
132 |       `CODE_SIGN_ENTITLEMENTS = Config/${params.projectName}.entitlements`,
133 |     );
134 |   }
135 | 
136 |   // Update test bundle identifier and target name
137 |   result = result.replace(/TEST_TARGET_NAME = .+/g, `TEST_TARGET_NAME = ${params.projectName}`);
138 | 
139 |   // Update comments that reference MyProject in entitlements paths
140 |   result = result.replace(
141 |     /Config\/MyProject\.entitlements/g,
142 |     `Config/${params.projectName}.entitlements`,
143 |   );
144 | 
145 |   return result;
146 | }
147 | 
148 | /**
149 |  * Replace placeholders in a string (for non-XCConfig files)
150 |  */
151 | function replacePlaceholders(
152 |   content: string,
153 |   projectName: string,
154 |   bundleIdentifier: string,
155 | ): string {
156 |   let result = content;
157 | 
158 |   // Replace project name
159 |   result = result.replace(/MyProject/g, projectName);
160 | 
161 |   // Replace bundle identifier - check for both patterns used in templates
162 |   if (bundleIdentifier) {
163 |     result = result.replace(/com\.example\.MyProject/g, bundleIdentifier);
164 |     result = result.replace(/com\.mycompany\.MyProject/g, bundleIdentifier);
165 |   }
166 | 
167 |   return result;
168 | }
169 | 
170 | /**
171 |  * Process a single file, replacing placeholders if it's a text file
172 |  */
173 | async function processFile(
174 |   sourcePath: string,
175 |   destPath: string,
176 |   params: ScaffoldMacOSProjectParams & { platform: string },
177 |   fileSystemExecutor: FileSystemExecutor,
178 | ): Promise<void> {
179 |   // Determine the destination file path
180 |   let finalDestPath = destPath;
181 |   if (params.customizeNames) {
182 |     // Replace MyProject in file/directory names
183 |     const fileName = basename(destPath);
184 |     const dirName = dirname(destPath);
185 |     const newFileName = fileName.replace(/MyProject/g, params.projectName);
186 |     finalDestPath = join(dirName, newFileName);
187 |   }
188 | 
189 |   // Text file extensions that should be processed
190 |   const textExtensions = [
191 |     '.swift',
192 |     '.h',
193 |     '.m',
194 |     '.mm',
195 |     '.cpp',
196 |     '.c',
197 |     '.pbxproj',
198 |     '.plist',
199 |     '.xcscheme',
200 |     '.xctestplan',
201 |     '.xcworkspacedata',
202 |     '.xcconfig',
203 |     '.json',
204 |     '.xml',
205 |     '.entitlements',
206 |     '.storyboard',
207 |     '.xib',
208 |     '.md',
209 |   ];
210 | 
211 |   const ext = sourcePath.toLowerCase();
212 |   const isTextFile = textExtensions.some((textExt) => ext.endsWith(textExt));
213 |   const isXCConfig = sourcePath.endsWith('.xcconfig');
214 |   const isPackageSwift = sourcePath.endsWith('Package.swift');
215 | 
216 |   if (isTextFile && params.customizeNames) {
217 |     // Read the file content
218 |     const content = await fileSystemExecutor.readFile(sourcePath, 'utf-8');
219 | 
220 |     let processedContent;
221 | 
222 |     if (isXCConfig) {
223 |       // Use special XCConfig processing
224 |       processedContent = updateXCConfigFile(content, params);
225 |     } else if (isPackageSwift) {
226 |       // Use special Package.swift processing
227 |       processedContent = updatePackageSwiftFile(content, params);
228 |     } else {
229 |       // Use standard placeholder replacement
230 |       const bundleIdentifier =
231 |         params.bundleIdentifier ??
232 |         `com.example.${params.projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
233 |       processedContent = replacePlaceholders(content, params.projectName, bundleIdentifier);
234 |     }
235 | 
236 |     await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
237 |     await fileSystemExecutor.writeFile(finalDestPath, processedContent, 'utf-8');
238 |   } else {
239 |     // Copy binary files as-is
240 |     await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
241 |     await fileSystemExecutor.cp(sourcePath, finalDestPath, { recursive: true });
242 |   }
243 | }
244 | 
245 | /**
246 |  * Recursively process a directory
247 |  */
248 | async function processDirectory(
249 |   sourceDir: string,
250 |   destDir: string,
251 |   params: ScaffoldMacOSProjectParams & { platform: string },
252 |   fileSystemExecutor: FileSystemExecutor,
253 | ): Promise<void> {
254 |   const entries = await fileSystemExecutor.readdir(sourceDir, { withFileTypes: true });
255 | 
256 |   for (const entry of entries) {
257 |     const dirent = entry as { isDirectory(): boolean; isFile(): boolean; name: string };
258 |     const sourcePath = join(sourceDir, dirent.name);
259 |     let destName = dirent.name;
260 | 
261 |     if (params.customizeNames) {
262 |       // Replace MyProject in directory names
263 |       destName = destName.replace(/MyProject/g, params.projectName);
264 |     }
265 | 
266 |     const destPath = join(destDir, destName);
267 | 
268 |     if (dirent.isDirectory()) {
269 |       // Skip certain directories
270 |       if (dirent.name === '.git' || dirent.name === 'xcuserdata') {
271 |         continue;
272 |       }
273 |       await fileSystemExecutor.mkdir(destPath, { recursive: true });
274 |       await processDirectory(sourcePath, destPath, params, fileSystemExecutor);
275 |     } else if (dirent.isFile()) {
276 |       // Skip certain files
277 |       if (dirent.name === '.DS_Store' || dirent.name.endsWith('.xcuserstate')) {
278 |         continue;
279 |       }
280 |       await processFile(sourcePath, destPath, params, fileSystemExecutor);
281 |     }
282 |   }
283 | }
284 | 
285 | /**
286 |  * Scaffold a new iOS or macOS project
287 |  */
288 | async function scaffoldProject(
289 |   params: ScaffoldMacOSProjectParams & { platform: string },
290 |   commandExecutor: CommandExecutor,
291 |   fileSystemExecutor: FileSystemExecutor,
292 | ): Promise<string> {
293 |   const projectName = params.projectName;
294 |   const outputPath = params.outputPath;
295 |   const platform = params.platform;
296 |   const customizeNames = params.customizeNames ?? true;
297 | 
298 |   log('info', `Scaffolding project: ${projectName} (${platform}) at ${outputPath}`);
299 | 
300 |   // Validate project name
301 |   if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(projectName)) {
302 |     throw new ValidationError(
303 |       'Project name must start with a letter and contain only letters, numbers, and underscores',
304 |     );
305 |   }
306 | 
307 |   // Get template path from TemplateManager
308 |   let templatePath;
309 |   try {
310 |     templatePath = await TemplateManager.getTemplatePath(
311 |       platform as 'macOS' | 'iOS',
312 |       commandExecutor,
313 |       fileSystemExecutor,
314 |     );
315 |   } catch (error) {
316 |     throw new ValidationError(
317 |       `Failed to get template for ${platform}: ${error instanceof Error ? error.message : String(error)}`,
318 |     );
319 |   }
320 | 
321 |   // Use outputPath directly as the destination
322 |   const projectPath = outputPath;
323 | 
324 |   // Check if the output directory already has Xcode project files
325 |   const xcworkspaceExists = fileSystemExecutor.existsSync(
326 |     join(projectPath, `${customizeNames ? projectName : 'MyProject'}.xcworkspace`),
327 |   );
328 |   const xcodeprojExists = fileSystemExecutor.existsSync(
329 |     join(projectPath, `${customizeNames ? projectName : 'MyProject'}.xcodeproj`),
330 |   );
331 | 
332 |   if (xcworkspaceExists || xcodeprojExists) {
333 |     throw new ValidationError(`Xcode project files already exist in ${projectPath}`);
334 |   }
335 | 
336 |   try {
337 |     // Process the template directly into the output path
338 |     await processDirectory(templatePath, projectPath, params, fileSystemExecutor);
339 | 
340 |     return projectPath;
341 |   } finally {
342 |     // Clean up downloaded template if needed
343 |     await TemplateManager.cleanup(templatePath, fileSystemExecutor);
344 |   }
345 | }
346 | 
347 | /**
348 |  * Business logic for scaffolding macOS projects
349 |  * Extracted for testability and Separation of Concerns
350 |  */
351 | export async function scaffold_macos_projectLogic(
352 |   params: ScaffoldMacOSProjectParams,
353 |   commandExecutor: CommandExecutor,
354 |   fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
355 | ): Promise<ToolResponse> {
356 |   try {
357 |     const projectParams = { ...params, platform: 'macOS' as const };
358 |     const projectPath = await scaffoldProject(projectParams, commandExecutor, fileSystemExecutor);
359 | 
360 |     const response = {
361 |       success: true,
362 |       projectPath,
363 |       platform: 'macOS',
364 |       message: `Successfully scaffolded macOS project "${params.projectName}" in ${projectPath}`,
365 |       nextSteps: [
366 |         `Important: Before working on the project make sure to read the README.md file in the workspace root directory.`,
367 |         `Build for macOS: build_macos({ workspacePath: "${projectPath}/${params.customizeNames ? params.projectName : 'MyProject'}.xcworkspace", scheme: "${params.customizeNames ? params.projectName : 'MyProject'}" })`,
368 |         `Build & Run on macOS: build_run_macos({ workspacePath: "${projectPath}/${params.customizeNames ? params.projectName : 'MyProject'}.xcworkspace", scheme: "${params.customizeNames ? params.projectName : 'MyProject'}" })`,
369 |       ],
370 |     };
371 | 
372 |     return {
373 |       content: [
374 |         {
375 |           type: 'text',
376 |           text: JSON.stringify(response, null, 2),
377 |         },
378 |       ],
379 |     };
380 |   } catch (error) {
381 |     log(
382 |       'error',
383 |       `Failed to scaffold macOS project: ${error instanceof Error ? error.message : String(error)}`,
384 |     );
385 | 
386 |     return {
387 |       content: [
388 |         {
389 |           type: 'text',
390 |           text: JSON.stringify(
391 |             {
392 |               success: false,
393 |               error: error instanceof Error ? error.message : 'Unknown error occurred',
394 |             },
395 |             null,
396 |             2,
397 |           ),
398 |         },
399 |       ],
400 |       isError: true,
401 |     };
402 |   }
403 | }
404 | 
405 | export default {
406 |   name: 'scaffold_macos_project',
407 |   description:
408 |     'Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration.',
409 |   schema: ScaffoldmacOSProjectSchema.shape,
410 |   async handler(args: Record<string, unknown>): Promise<ToolResponse> {
411 |     // Validate the arguments against the schema before processing
412 |     const validatedArgs = ScaffoldmacOSProjectSchema.parse(args);
413 |     return scaffold_macos_projectLogic(
414 |       validatedArgs,
415 |       getDefaultCommandExecutor(),
416 |       getDefaultFileSystemExecutor(),
417 |     );
418 |   },
419 | };
420 | 
```

--------------------------------------------------------------------------------
/docs/NODEJS_2025.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Modern Node.js Development Guide
  2 | 
  3 | This guide provides actionable instructions for AI agents to apply modern Node.js patterns when the scenarios are applicable. Use these patterns when creating or modifying Node.js code that fits these use cases.
  4 | 
  5 | ## Core Principles
  6 | 
  7 | **WHEN APPLICABLE** apply these modern patterns:
  8 | 
  9 | 1. **Use ES Modules** with `node:` prefix for built-in modules
 10 | 2. **Leverage built-in APIs** over external dependencies when the functionality matches
 11 | 3. **Use top-level await** instead of IIFE patterns when initialization is needed
 12 | 4. **Implement structured error handling** with proper context when handling application errors
 13 | 5. **Use built-in testing** over external test frameworks when adding tests
 14 | 6. **Apply modern async patterns** for better performance when dealing with async operations
 15 | 
 16 | ## 1. Module System Patterns
 17 | 
 18 | ### WHEN USING MODULES: ES Modules with node: Prefix
 19 | 
 20 | **✅ DO THIS:**
 21 | ```javascript
 22 | // Use ES modules with node: prefix for built-ins
 23 | import { readFile } from 'node:fs/promises';
 24 | import { createServer } from 'node:http';
 25 | import { EventEmitter } from 'node:events';
 26 | 
 27 | export function myFunction() {
 28 |   return 'modern code';
 29 | }
 30 | ```
 31 | 
 32 | **❌ AVOID:**
 33 | ```javascript
 34 | // Don't use CommonJS or bare imports for built-ins
 35 | const fs = require('fs');
 36 | const { readFile } = require('fs/promises');
 37 | import { readFile } from 'fs/promises'; // Missing node: prefix
 38 | ```
 39 | 
 40 | ### WHEN INITIALIZING: Top-Level Await
 41 | 
 42 | **✅ DO THIS:**
 43 | ```javascript
 44 | // Use top-level await for initialization
 45 | import { readFile } from 'node:fs/promises';
 46 | 
 47 | const config = JSON.parse(await readFile('config.json', 'utf8'));
 48 | const server = createServer(/* ... */);
 49 | 
 50 | console.log('App started with config:', config.appName);
 51 | ```
 52 | 
 53 | **❌ AVOID:**
 54 | ```javascript
 55 | // Don't wrap in IIFE
 56 | (async () => {
 57 |   const config = JSON.parse(await readFile('config.json', 'utf8'));
 58 |   // ...
 59 | })();
 60 | ```
 61 | 
 62 | ### WHEN USING ES MODULES: Package.json Settings
 63 | 
 64 | **✅ ENSURE package.json includes:**
 65 | ```json
 66 | {
 67 |   "type": "module",
 68 |   "engines": {
 69 |     "node": ">=20.0.0"
 70 |   }
 71 | }
 72 | ```
 73 | 
 74 | ## 2. HTTP and Network Patterns
 75 | 
 76 | ### WHEN MAKING HTTP REQUESTS: Use Built-in fetch
 77 | 
 78 | **✅ DO THIS:**
 79 | ```javascript
 80 | // Use built-in fetch with AbortSignal.timeout
 81 | async function fetchData(url) {
 82 |   try {
 83 |     const response = await fetch(url, {
 84 |       signal: AbortSignal.timeout(5000)
 85 |     });
 86 | 
 87 |     if (!response.ok) {
 88 |       throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 89 |     }
 90 | 
 91 |     return await response.json();
 92 |   } catch (error) {
 93 |     if (error.name === 'TimeoutError') {
 94 |       throw new Error('Request timed out');
 95 |     }
 96 |     throw error;
 97 |   }
 98 | }
 99 | ```
100 | 
101 | **❌ AVOID:**
102 | ```javascript
103 | // Don't add axios, node-fetch, or similar dependencies
104 | const axios = require('axios');
105 | const response = await axios.get(url);
106 | ```
107 | 
108 | ### WHEN NEEDING CANCELLATION: AbortController Pattern
109 | 
110 | **✅ DO THIS:**
111 | ```javascript
112 | // Implement proper cancellation
113 | const controller = new AbortController();
114 | setTimeout(() => controller.abort(), 10000);
115 | 
116 | try {
117 |   const data = await fetch(url, { signal: controller.signal });
118 |   console.log('Data received:', data);
119 | } catch (error) {
120 |   if (error.name === 'AbortError') {
121 |     console.log('Request was cancelled');
122 |   } else {
123 |     console.error('Unexpected error:', error);
124 |   }
125 | }
126 | ```
127 | 
128 | ## 3. Testing Patterns
129 | 
130 | ### WHEN ADDING TESTS: Use Built-in Test Runner
131 | 
132 | **✅ DO THIS:**
133 | ```javascript
134 | // Use node:test instead of external frameworks
135 | import { test, describe } from 'node:test';
136 | import assert from 'node:assert';
137 | 
138 | describe('My Module', () => {
139 |   test('should work correctly', () => {
140 |     assert.strictEqual(myFunction(), 'expected');
141 |   });
142 | 
143 |   test('should handle async operations', async () => {
144 |     const result = await myAsyncFunction();
145 |     assert.strictEqual(result, 'expected');
146 |   });
147 | 
148 |   test('should throw on invalid input', () => {
149 |     assert.throws(() => myFunction('invalid'), /Expected error/);
150 |   });
151 | });
152 | ```
153 | 
154 | **✅ RECOMMENDED package.json scripts:**
155 | ```json
156 | {
157 |   "scripts": {
158 |     "test": "node --test",
159 |     "test:watch": "node --test --watch",
160 |     "test:coverage": "node --test --experimental-test-coverage"
161 |   }
162 | }
163 | ```
164 | 
165 | **❌ AVOID:**
166 | ```javascript
167 | // Don't add Jest, Mocha, or other test frameworks unless specifically required
168 | ```
169 | 
170 | ## 4. Async Pattern Recommendations
171 | 
172 | ### WHEN HANDLING MULTIPLE ASYNC OPERATIONS: Parallel Execution with Promise.all
173 | 
174 | **✅ DO THIS:**
175 | ```javascript
176 | // Execute independent operations in parallel
177 | async function processData() {
178 |   try {
179 |     const [config, userData] = await Promise.all([
180 |       readFile('config.json', 'utf8'),
181 |       fetch('/api/user').then(r => r.json())
182 |     ]);
183 | 
184 |     const processed = processUserData(userData, JSON.parse(config));
185 |     await writeFile('output.json', JSON.stringify(processed, null, 2));
186 | 
187 |     return processed;
188 |   } catch (error) {
189 |     console.error('Processing failed:', {
190 |       error: error.message,
191 |       stack: error.stack,
192 |       timestamp: new Date().toISOString()
193 |     });
194 |     throw error;
195 |   }
196 | }
197 | ```
198 | 
199 | ### WHEN PROCESSING EVENT STREAMS: AsyncIterators Pattern
200 | 
201 | **✅ DO THIS:**
202 | ```javascript
203 | // Use async iterators for event processing
204 | import { EventEmitter } from 'node:events';
205 | 
206 | class DataProcessor extends EventEmitter {
207 |   async *processStream() {
208 |     for (let i = 0; i < 10; i++) {
209 |       this.emit('data', `chunk-${i}`);
210 |       yield `processed-${i}`;
211 |       await new Promise(resolve => setTimeout(resolve, 100));
212 |     }
213 |     this.emit('end');
214 |   }
215 | }
216 | 
217 | // Consume with for-await-of
218 | const processor = new DataProcessor();
219 | for await (const result of processor.processStream()) {
220 |   console.log('Processed:', result);
221 | }
222 | ```
223 | 
224 | ## 5. Stream Processing Patterns
225 | 
226 | ### WHEN PROCESSING STREAMS: Use pipeline with Promises
227 | 
228 | **✅ DO THIS:**
229 | ```javascript
230 | import { pipeline } from 'node:stream/promises';
231 | import { createReadStream, createWriteStream } from 'node:fs';
232 | import { Transform } from 'node:stream';
233 | 
234 | // Always use pipeline for stream processing
235 | async function processFile(inputFile, outputFile) {
236 |   try {
237 |     await pipeline(
238 |       createReadStream(inputFile),
239 |       new Transform({
240 |         transform(chunk, encoding, callback) {
241 |           this.push(chunk.toString().toUpperCase());
242 |           callback();
243 |         }
244 |       }),
245 |       createWriteStream(outputFile)
246 |     );
247 |     console.log('File processed successfully');
248 |   } catch (error) {
249 |     console.error('Pipeline failed:', error);
250 |     throw error;
251 |   }
252 | }
253 | ```
254 | 
255 | ### WHEN NEEDING BROWSER COMPATIBILITY: Web Streams
256 | 
257 | **✅ DO THIS:**
258 | ```javascript
259 | import { Readable } from 'node:stream';
260 | 
261 | // Convert between Web Streams and Node streams when needed
262 | const webReadable = new ReadableStream({
263 |   start(controller) {
264 |     controller.enqueue('Hello ');
265 |     controller.enqueue('World!');
266 |     controller.close();
267 |   }
268 | });
269 | 
270 | const nodeStream = Readable.fromWeb(webReadable);
271 | ```
272 | 
273 | ## 6. CPU-Intensive Task Patterns
274 | 
275 | ### WHEN DOING HEAVY COMPUTATION: Worker Threads
276 | 
277 | **✅ DO THIS:**
278 | ```javascript
279 | // worker.js - Separate file for CPU-intensive tasks
280 | import { parentPort, workerData } from 'node:worker_threads';
281 | 
282 | function heavyComputation(data) {
283 |   // CPU-intensive work here
284 |   return processedData;
285 | }
286 | 
287 | const result = heavyComputation(workerData);
288 | parentPort.postMessage(result);
289 | ```
290 | 
291 | ```javascript
292 | // main.js - Delegate to worker
293 | import { Worker } from 'node:worker_threads';
294 | import { fileURLToPath } from 'node:url';
295 | 
296 | async function processHeavyTask(data) {
297 |   return new Promise((resolve, reject) => {
298 |     const worker = new Worker(
299 |       fileURLToPath(new URL('./worker.js', import.meta.url)),
300 |       { workerData: data }
301 |     );
302 | 
303 |     worker.on('message', resolve);
304 |     worker.on('error', reject);
305 |     worker.on('exit', (code) => {
306 |       if (code !== 0) {
307 |         reject(new Error(`Worker stopped with exit code ${code}`));
308 |       }
309 |     });
310 |   });
311 | }
312 | ```
313 | 
314 | ## 7. Development Configuration Patterns
315 | 
316 | ### FOR NEW PROJECTS: Modern package.json
317 | 
318 | **✅ RECOMMENDED for new projects:**
319 | ```json
320 | {
321 |   "name": "modern-node-app",
322 |   "type": "module",
323 |   "engines": {
324 |     "node": ">=20.0.0"
325 |   },
326 |   "scripts": {
327 |     "dev": "node --watch --env-file=.env app.js",
328 |     "test": "node --test --watch",
329 |     "start": "node app.js"
330 |   }
331 | }
332 | ```
333 | 
334 | ### WHEN LOADING ENVIRONMENT VARIABLES: Built-in Support
335 | 
336 | **✅ DO THIS:**
337 | ```javascript
338 | // Use --env-file flag instead of dotenv package
339 | // Environment variables are automatically available
340 | console.log('Database URL:', process.env.DATABASE_URL);
341 | console.log('API Key loaded:', process.env.API_KEY ? 'Yes' : 'No');
342 | ```
343 | 
344 | **❌ AVOID:**
345 | ```javascript
346 | // Don't add dotenv dependency
347 | require('dotenv').config();
348 | ```
349 | 
350 | ## 8. Error Handling Patterns
351 | 
352 | ### WHEN CREATING CUSTOM ERRORS: Structured Error Classes
353 | 
354 | **✅ DO THIS:**
355 | ```javascript
356 | class AppError extends Error {
357 |   constructor(message, code, statusCode = 500, context = {}) {
358 |     super(message);
359 |     this.name = 'AppError';
360 |     this.code = code;
361 |     this.statusCode = statusCode;
362 |     this.context = context;
363 |     this.timestamp = new Date().toISOString();
364 |   }
365 | 
366 |   toJSON() {
367 |     return {
368 |       name: this.name,
369 |       message: this.message,
370 |       code: this.code,
371 |       statusCode: this.statusCode,
372 |       context: this.context,
373 |       timestamp: this.timestamp,
374 |       stack: this.stack
375 |     };
376 |   }
377 | }
378 | 
379 | // Usage with rich context
380 | throw new AppError(
381 |   'Database connection failed',
382 |   'DB_CONNECTION_ERROR',
383 |   503,
384 |   { host: 'localhost', port: 5432, retryAttempt: 3 }
385 | );
386 | ```
387 | 
388 | ## 9. Performance Monitoring Patterns
389 | 
390 | ### WHEN MONITORING PERFORMANCE: Built-in Performance APIs
391 | 
392 | **✅ DO THIS:**
393 | ```javascript
394 | import { PerformanceObserver, performance } from 'node:perf_hooks';
395 | 
396 | // Set up performance monitoring
397 | const obs = new PerformanceObserver((list) => {
398 |   for (const entry of list.getEntries()) {
399 |     if (entry.duration > 100) {
400 |       console.log(`Slow operation: ${entry.name} took ${entry.duration}ms`);
401 |     }
402 |   }
403 | });
404 | obs.observe({ entryTypes: ['function', 'http', 'dns'] });
405 | 
406 | // Instrument operations
407 | async function processLargeDataset(data) {
408 |   performance.mark('processing-start');
409 |   
410 |   const result = await heavyProcessing(data);
411 |   
412 |   performance.mark('processing-end');
413 |   performance.measure('data-processing', 'processing-start', 'processing-end');
414 |   
415 |   return result;
416 | }
417 | ```
418 | 
419 | ## 10. Module Organization Patterns
420 | 
421 | ### WHEN ORGANIZING INTERNAL MODULES: Import Maps
422 | 
423 | **✅ DO THIS in package.json:**
424 | ```json
425 | {
426 |   "imports": {
427 |     "#config": "./src/config/index.js",
428 |     "#utils/*": "./src/utils/*.js",
429 |     "#db": "./src/database/connection.js"
430 |   }
431 | }
432 | ```
433 | 
434 | **✅ Use in code:**
435 | ```javascript
436 | // Clean internal imports
437 | import config from '#config';
438 | import { logger, validator } from '#utils/common';
439 | import db from '#db';
440 | ```
441 | 
442 | ### WHEN LOADING CONDITIONALLY: Dynamic Imports
443 | 
444 | **✅ DO THIS:**
445 | ```javascript
446 | // Load features based on environment
447 | async function loadDatabaseAdapter() {
448 |   const dbType = process.env.DATABASE_TYPE || 'sqlite';
449 |   
450 |   try {
451 |     const adapter = await import(`#db/adapters/${dbType}`);
452 |     return adapter.default;
453 |   } catch (error) {
454 |     console.warn(`Database adapter ${dbType} not available, falling back to sqlite`);
455 |     const fallback = await import('#db/adapters/sqlite');
456 |     return fallback.default;
457 |   }
458 | }
459 | ```
460 | 
461 | ## 11. Diagnostic Patterns
462 | 
463 | ### WHEN ADDING OBSERVABILITY: Diagnostic Channels
464 | 
465 | **✅ DO THIS:**
466 | ```javascript
467 | import diagnostics_channel from 'node:diagnostics_channel';
468 | 
469 | // Create diagnostic channels
470 | const dbChannel = diagnostics_channel.channel('app:database');
471 | 
472 | // Subscribe to events
473 | dbChannel.subscribe((message) => {
474 |   console.log('Database operation:', {
475 |     operation: message.operation,
476 |     duration: message.duration,
477 |     query: message.query
478 |   });
479 | });
480 | 
481 | // Publish diagnostic information
482 | async function queryDatabase(sql, params) {
483 |   const start = performance.now();
484 |   
485 |   try {
486 |     const result = await db.query(sql, params);
487 |     
488 |     dbChannel.publish({
489 |       operation: 'query',
490 |       sql,
491 |       params,
492 |       duration: performance.now() - start,
493 |       success: true
494 |     });
495 |     
496 |     return result;
497 |   } catch (error) {
498 |     dbChannel.publish({
499 |       operation: 'query',
500 |       sql,
501 |       params,
502 |       duration: performance.now() - start,
503 |       success: false,
504 |       error: error.message
505 |     });
506 |     throw error;
507 |   }
508 | }
509 | ```
510 | 
511 | ## Modernization Checklist
512 | 
513 | When working with Node.js code, consider applying these patterns where applicable:
514 | 
515 | - [ ] `"type": "module"` in package.json
516 | - [ ] `"engines": {"node": ">=20.0.0"}` specified
517 | - [ ] All built-in imports use `node:` prefix
518 | - [ ] Using `fetch()` instead of HTTP libraries
519 | - [ ] Using `node --test` instead of external test frameworks
520 | - [ ] Using `--watch` and `--env-file` flags
521 | - [ ] Implementing structured error handling
522 | - [ ] Using `Promise.all()` for parallel operations
523 | - [ ] Using `pipeline()` for stream processing
524 | - [ ] Implementing performance monitoring where appropriate
525 | - [ ] Using worker threads for CPU-intensive tasks
526 | - [ ] Using import maps for internal modules
527 | 
528 | ## Dependencies to Remove
529 | 
530 | When modernizing, remove these dependencies if present:
531 | 
532 | - `axios`, `node-fetch`, `got` → Use built-in `fetch()`
533 | - `jest`, `mocha`, `ava` → Use `node:test`
534 | - `nodemon` → Use `node --watch`
535 | - `dotenv` → Use `--env-file`
536 | - `cross-env` → Use native environment handling
537 | 
538 | ## Security Patterns
539 | 
540 | **WHEN SECURITY IS A CONCERN** apply these practices:
541 | 
542 | ```bash
543 | # Use permission model for enhanced security
544 | node --experimental-permission --allow-fs-read=./data --allow-fs-write=./logs app.js
545 | 
546 | # Network restrictions
547 | node --experimental-permission --allow-net=api.example.com app.js
548 | ```
549 | 
550 | This guide provides modern Node.js patterns to apply when the specific scenarios are encountered, ensuring code follows 2025 best practices for performance, security, and maintainability without forcing unnecessary changes.
```

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/__tests__/type_text.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for type_text plugin
  3 |  */
  4 | 
  5 | import { describe, it, expect } from 'vitest';
  6 | import { z } from 'zod';
  7 | import {
  8 |   createMockExecutor,
  9 |   createMockFileSystemExecutor,
 10 |   createNoopExecutor,
 11 | } from '../../../../test-utils/mock-executors.ts';
 12 | import typeTextPlugin, { type_textLogic } from '../type_text.ts';
 13 | 
 14 | // Mock axe helpers for dependency injection
 15 | function createMockAxeHelpers(
 16 |   overrides: {
 17 |     getAxePathReturn?: string | null;
 18 |     getBundledAxeEnvironmentReturn?: Record<string, string>;
 19 |   } = {},
 20 | ) {
 21 |   return {
 22 |     getAxePath: () => {
 23 |       return Object.prototype.hasOwnProperty.call(overrides, 'getAxePathReturn')
 24 |         ? overrides.getAxePathReturn
 25 |         : '/usr/local/bin/axe';
 26 |     },
 27 |     getBundledAxeEnvironment: () => overrides.getBundledAxeEnvironmentReturn ?? {},
 28 |   };
 29 | }
 30 | 
 31 | // Mock executor that tracks rejections for testing
 32 | function createRejectingExecutor(error: any) {
 33 |   return async () => {
 34 |     throw error;
 35 |   };
 36 | }
 37 | 
 38 | describe('Type Text Plugin', () => {
 39 |   describe('Export Field Validation (Literal)', () => {
 40 |     it('should have correct name', () => {
 41 |       expect(typeTextPlugin.name).toBe('type_text');
 42 |     });
 43 | 
 44 |     it('should have correct description', () => {
 45 |       expect(typeTextPlugin.description).toBe(
 46 |         'Type text (supports US keyboard characters). Use describe_ui to find text field, tap to focus, then type.',
 47 |       );
 48 |     });
 49 | 
 50 |     it('should have handler function', () => {
 51 |       expect(typeof typeTextPlugin.handler).toBe('function');
 52 |     });
 53 | 
 54 |     it('should validate schema fields with safeParse', () => {
 55 |       const schema = z.object(typeTextPlugin.schema);
 56 | 
 57 |       // Valid case
 58 |       expect(
 59 |         schema.safeParse({
 60 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 61 |           text: 'Hello World',
 62 |         }).success,
 63 |       ).toBe(true);
 64 | 
 65 |       // Invalid simulatorUuid
 66 |       expect(
 67 |         schema.safeParse({
 68 |           simulatorUuid: 'invalid-uuid',
 69 |           text: 'Hello World',
 70 |         }).success,
 71 |       ).toBe(false);
 72 | 
 73 |       // Invalid text - empty string
 74 |       expect(
 75 |         schema.safeParse({
 76 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 77 |           text: '',
 78 |         }).success,
 79 |       ).toBe(false);
 80 | 
 81 |       // Invalid text - non-string
 82 |       expect(
 83 |         schema.safeParse({
 84 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 85 |           text: 123,
 86 |         }).success,
 87 |       ).toBe(false);
 88 | 
 89 |       // Missing required fields
 90 |       expect(schema.safeParse({}).success).toBe(false);
 91 |     });
 92 |   });
 93 | 
 94 |   describe('Command Generation', () => {
 95 |     it('should generate correct axe command for basic text typing', async () => {
 96 |       let capturedCommand: string[] = [];
 97 |       const trackingExecutor = async (command: string[]) => {
 98 |         capturedCommand = command;
 99 |         return {
100 |           success: true,
101 |           output: 'Text typed successfully',
102 |           error: undefined,
103 |           process: { pid: 12345 },
104 |         };
105 |       };
106 | 
107 |       const mockAxeHelpers = createMockAxeHelpers({
108 |         getAxePathReturn: '/usr/local/bin/axe',
109 |         getBundledAxeEnvironmentReturn: {},
110 |       });
111 | 
112 |       await type_textLogic(
113 |         {
114 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
115 |           text: 'Hello World',
116 |         },
117 |         trackingExecutor,
118 |         mockAxeHelpers,
119 |       );
120 | 
121 |       expect(capturedCommand).toEqual([
122 |         '/usr/local/bin/axe',
123 |         'type',
124 |         'Hello World',
125 |         '--udid',
126 |         '12345678-1234-1234-1234-123456789012',
127 |       ]);
128 |     });
129 | 
130 |     it('should generate correct axe command for text with special characters', async () => {
131 |       let capturedCommand: string[] = [];
132 |       const trackingExecutor = async (command: string[]) => {
133 |         capturedCommand = command;
134 |         return {
135 |           success: true,
136 |           output: 'Text typed successfully',
137 |           error: undefined,
138 |           process: { pid: 12345 },
139 |         };
140 |       };
141 | 
142 |       const mockAxeHelpers = createMockAxeHelpers({
143 |         getAxePathReturn: '/usr/local/bin/axe',
144 |         getBundledAxeEnvironmentReturn: {},
145 |       });
146 | 
147 |       await type_textLogic(
148 |         {
149 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
150 |           text: '[email protected]',
151 |         },
152 |         trackingExecutor,
153 |         mockAxeHelpers,
154 |       );
155 | 
156 |       expect(capturedCommand).toEqual([
157 |         '/usr/local/bin/axe',
158 |         'type',
159 |         '[email protected]',
160 |         '--udid',
161 |         '12345678-1234-1234-1234-123456789012',
162 |       ]);
163 |     });
164 | 
165 |     it('should generate correct axe command for text with numbers and symbols', async () => {
166 |       let capturedCommand: string[] = [];
167 |       const trackingExecutor = async (command: string[]) => {
168 |         capturedCommand = command;
169 |         return {
170 |           success: true,
171 |           output: 'Text typed successfully',
172 |           error: undefined,
173 |           process: { pid: 12345 },
174 |         };
175 |       };
176 | 
177 |       const mockAxeHelpers = createMockAxeHelpers({
178 |         getAxePathReturn: '/usr/local/bin/axe',
179 |         getBundledAxeEnvironmentReturn: {},
180 |       });
181 | 
182 |       await type_textLogic(
183 |         {
184 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
185 |           text: 'Password123!@#',
186 |         },
187 |         trackingExecutor,
188 |         mockAxeHelpers,
189 |       );
190 | 
191 |       expect(capturedCommand).toEqual([
192 |         '/usr/local/bin/axe',
193 |         'type',
194 |         'Password123!@#',
195 |         '--udid',
196 |         '12345678-1234-1234-1234-123456789012',
197 |       ]);
198 |     });
199 | 
200 |     it('should generate correct axe command for long text', async () => {
201 |       let capturedCommand: string[] = [];
202 |       const trackingExecutor = async (command: string[]) => {
203 |         capturedCommand = command;
204 |         return {
205 |           success: true,
206 |           output: 'Text typed successfully',
207 |           error: undefined,
208 |           process: { pid: 12345 },
209 |         };
210 |       };
211 | 
212 |       const mockAxeHelpers = createMockAxeHelpers({
213 |         getAxePathReturn: '/usr/local/bin/axe',
214 |         getBundledAxeEnvironmentReturn: {},
215 |       });
216 | 
217 |       const longText =
218 |         'This is a very long text that needs to be typed into the simulator for testing purposes.';
219 | 
220 |       await type_textLogic(
221 |         {
222 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
223 |           text: longText,
224 |         },
225 |         trackingExecutor,
226 |         mockAxeHelpers,
227 |       );
228 | 
229 |       expect(capturedCommand).toEqual([
230 |         '/usr/local/bin/axe',
231 |         'type',
232 |         longText,
233 |         '--udid',
234 |         '12345678-1234-1234-1234-123456789012',
235 |       ]);
236 |     });
237 | 
238 |     it('should generate correct axe command with bundled axe path', async () => {
239 |       let capturedCommand: string[] = [];
240 |       const trackingExecutor = async (command: string[]) => {
241 |         capturedCommand = command;
242 |         return {
243 |           success: true,
244 |           output: 'Text typed successfully',
245 |           error: undefined,
246 |           process: { pid: 12345 },
247 |         };
248 |       };
249 | 
250 |       const mockAxeHelpers = createMockAxeHelpers({
251 |         getAxePathReturn: '/path/to/bundled/axe',
252 |         getBundledAxeEnvironmentReturn: { AXE_PATH: '/some/path' },
253 |       });
254 | 
255 |       await type_textLogic(
256 |         {
257 |           simulatorUuid: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
258 |           text: 'Test message',
259 |         },
260 |         trackingExecutor,
261 |         mockAxeHelpers,
262 |       );
263 | 
264 |       expect(capturedCommand).toEqual([
265 |         '/path/to/bundled/axe',
266 |         'type',
267 |         'Test message',
268 |         '--udid',
269 |         'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
270 |       ]);
271 |     });
272 |   });
273 | 
274 |   describe('Handler Behavior (Complete Literal Returns)', () => {
275 |     it('should handle axe dependency error', async () => {
276 |       const mockAxeHelpers = createMockAxeHelpers({
277 |         getAxePathReturn: null,
278 |       });
279 | 
280 |       const result = await type_textLogic(
281 |         {
282 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
283 |           text: 'Hello World',
284 |         },
285 |         createNoopExecutor(),
286 |         mockAxeHelpers,
287 |       );
288 | 
289 |       expect(result).toEqual({
290 |         content: [
291 |           {
292 |             type: 'text',
293 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
294 |           },
295 |         ],
296 |         isError: true,
297 |       });
298 |     });
299 | 
300 |     it('should successfully type text', async () => {
301 |       const mockAxeHelpers = createMockAxeHelpers({
302 |         getAxePathReturn: '/usr/local/bin/axe',
303 |         getBundledAxeEnvironmentReturn: {},
304 |       });
305 |       const mockExecutor = createMockExecutor({
306 |         success: true,
307 |         output: 'Text typed successfully',
308 |         error: undefined,
309 |       });
310 | 
311 |       const result = await type_textLogic(
312 |         {
313 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
314 |           text: 'Hello World',
315 |         },
316 |         mockExecutor,
317 |         mockAxeHelpers,
318 |       );
319 | 
320 |       expect(result).toEqual({
321 |         content: [{ type: 'text', text: 'Text typing simulated successfully.' }],
322 |         isError: false,
323 |       });
324 |     });
325 | 
326 |     it('should return success for valid text typing', async () => {
327 |       const mockAxeHelpers = createMockAxeHelpers({
328 |         getAxePathReturn: '/usr/local/bin/axe',
329 |         getBundledAxeEnvironmentReturn: {},
330 |       });
331 | 
332 |       const mockExecutor = createMockExecutor({
333 |         success: true,
334 |         output: 'Text typed successfully',
335 |         error: undefined,
336 |       });
337 | 
338 |       const result = await type_textLogic(
339 |         {
340 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
341 |           text: 'Hello World',
342 |         },
343 |         mockExecutor,
344 |         mockAxeHelpers,
345 |       );
346 | 
347 |       expect(result).toEqual({
348 |         content: [{ type: 'text', text: 'Text typing simulated successfully.' }],
349 |         isError: false,
350 |       });
351 |     });
352 | 
353 |     it('should handle DependencyError when axe binary not found', async () => {
354 |       const mockAxeHelpers = createMockAxeHelpers({
355 |         getAxePathReturn: null,
356 |       });
357 | 
358 |       const result = await type_textLogic(
359 |         {
360 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
361 |           text: 'Hello World',
362 |         },
363 |         createNoopExecutor(),
364 |         mockAxeHelpers,
365 |       );
366 | 
367 |       expect(result).toEqual({
368 |         content: [
369 |           {
370 |             type: 'text',
371 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
372 |           },
373 |         ],
374 |         isError: true,
375 |       });
376 |     });
377 | 
378 |     it('should handle AxeError from command execution', async () => {
379 |       const mockAxeHelpers = createMockAxeHelpers({
380 |         getAxePathReturn: '/usr/local/bin/axe',
381 |         getBundledAxeEnvironmentReturn: {},
382 |       });
383 | 
384 |       const mockExecutor = createMockExecutor({
385 |         success: false,
386 |         output: '',
387 |         error: 'Text field not found',
388 |       });
389 | 
390 |       const result = await type_textLogic(
391 |         {
392 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
393 |           text: 'Hello World',
394 |         },
395 |         mockExecutor,
396 |         mockAxeHelpers,
397 |       );
398 | 
399 |       expect(result).toEqual({
400 |         content: [
401 |           {
402 |             type: 'text',
403 |             text: "Error: Failed to simulate text typing: axe command 'type' failed.\nDetails: Text field not found",
404 |           },
405 |         ],
406 |         isError: true,
407 |       });
408 |     });
409 | 
410 |     it('should handle SystemError from command execution', async () => {
411 |       const mockAxeHelpers = createMockAxeHelpers({
412 |         getAxePathReturn: '/usr/local/bin/axe',
413 |         getBundledAxeEnvironmentReturn: {},
414 |       });
415 | 
416 |       const mockExecutor = createRejectingExecutor(new Error('ENOENT: no such file or directory'));
417 | 
418 |       const result = await type_textLogic(
419 |         {
420 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
421 |           text: 'Hello World',
422 |         },
423 |         mockExecutor,
424 |         mockAxeHelpers,
425 |       );
426 | 
427 |       expect(result).toEqual({
428 |         content: [
429 |           {
430 |             type: 'text',
431 |             text: expect.stringContaining(
432 |               'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory',
433 |             ),
434 |           },
435 |         ],
436 |         isError: true,
437 |       });
438 |     });
439 | 
440 |     it('should handle unexpected Error objects', async () => {
441 |       const mockAxeHelpers = createMockAxeHelpers({
442 |         getAxePathReturn: '/usr/local/bin/axe',
443 |         getBundledAxeEnvironmentReturn: {},
444 |       });
445 | 
446 |       const mockExecutor = createRejectingExecutor(new Error('Unexpected error'));
447 | 
448 |       const result = await type_textLogic(
449 |         {
450 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
451 |           text: 'Hello World',
452 |         },
453 |         mockExecutor,
454 |         mockAxeHelpers,
455 |       );
456 | 
457 |       expect(result).toEqual({
458 |         content: [
459 |           {
460 |             type: 'text',
461 |             text: expect.stringContaining(
462 |               'Error: System error executing axe: Failed to execute axe command: Unexpected error',
463 |             ),
464 |           },
465 |         ],
466 |         isError: true,
467 |       });
468 |     });
469 | 
470 |     it('should handle unexpected string errors', async () => {
471 |       const mockAxeHelpers = createMockAxeHelpers({
472 |         getAxePathReturn: '/usr/local/bin/axe',
473 |         getBundledAxeEnvironmentReturn: {},
474 |       });
475 | 
476 |       const mockExecutor = createRejectingExecutor('String error');
477 | 
478 |       const result = await type_textLogic(
479 |         {
480 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
481 |           text: 'Hello World',
482 |         },
483 |         mockExecutor,
484 |         mockAxeHelpers,
485 |       );
486 | 
487 |       expect(result).toEqual({
488 |         content: [
489 |           {
490 |             type: 'text',
491 |             text: 'Error: System error executing axe: Failed to execute axe command: String error',
492 |           },
493 |         ],
494 |         isError: true,
495 |       });
496 |     });
497 |   });
498 | });
499 | 
```

--------------------------------------------------------------------------------
/docs/TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md:
--------------------------------------------------------------------------------

```markdown
  1 | # TEST_RUNNER_ Environment Variables Implementation Plan
  2 | 
  3 | ## Problem Statement
  4 | 
  5 | **GitHub Issue**: [#101 - Support TEST_RUNNER_ prefixed env vars](https://github.com/cameroncooke/XcodeBuildMCP/issues/101)
  6 | 
  7 | **Core Need**: Enable conditional test behavior by passing TEST_RUNNER_ prefixed environment variables from MCP client configurations to xcodebuild test processes. This addresses the specific use case of disabling `runsForEachTargetApplicationUIConfiguration` for faster development testing.
  8 | 
  9 | ## Background Context
 10 | 
 11 | ### xcodebuild Environment Variable Support
 12 | 
 13 | From the xcodebuild man page:
 14 | ```
 15 | TEST_RUNNER_<VAR>   Set an environment variable whose name is prefixed
 16 |                     with TEST_RUNNER_ to have that variable passed, with
 17 |                     its prefix stripped, to all test runner processes
 18 |                     launched during a test action. For example,
 19 |                     TEST_RUNNER_Foo=Bar xcodebuild test ... sets the
 20 |                     environment variable Foo=Bar in the test runner's
 21 |                     environment.
 22 | ```
 23 | 
 24 | ### User Requirements
 25 | 
 26 | Users want to configure their MCP server with TEST_RUNNER_ prefixed environment variables:
 27 | 
 28 | ```json
 29 | {
 30 |   "mcpServers": {
 31 |     "XcodeBuildMCP": {
 32 |       "type": "stdio",
 33 |       "command": "npx",
 34 |       "args": ["-y", "xcodebuildmcp@latest"],
 35 |       "env": {
 36 |         "TEST_RUNNER_USE_DEV_MODE": "YES"
 37 |       }
 38 |     }
 39 |   }
 40 | }
 41 | ```
 42 | 
 43 | And have tests that can conditionally execute based on these variables:
 44 | 
 45 | ```swift
 46 | func testFoo() throws {
 47 |   let useDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES"
 48 |   guard useDevMode else {
 49 |     XCTFail("Test requires USE_DEV_MODE to be true")
 50 |     return
 51 |   }
 52 |   // Test logic here...
 53 | }
 54 | ```
 55 | 
 56 | ## Current Architecture Analysis
 57 | 
 58 | ### XcodeBuildMCP Execution Flow
 59 | 1. All Xcode commands flow through `executeXcodeBuildCommand()` function
 60 | 2. Generic `CommandExecutor` interface handles all command execution
 61 | 3. Test tools exist for device/simulator/macOS platforms
 62 | 4. Zod schemas provide parameter validation and type safety
 63 | 
 64 | ### Key Files in Current Architecture
 65 | - `src/utils/CommandExecutor.ts` - Command execution interface
 66 | - `src/utils/build-utils.ts` - Contains `executeXcodeBuildCommand`
 67 | - `src/mcp/tools/device/test_device.ts` - Device testing tool
 68 | - `src/mcp/tools/simulator/test_sim.ts` - Simulator testing tool  
 69 | - `src/mcp/tools/macos/test_macos.ts` - macOS testing tool
 70 | - `src/utils/test/index.ts` - Shared test logic for simulator
 71 | 
 72 | ## Solution Analysis
 73 | 
 74 | ### Design Options Considered
 75 | 
 76 | 1. **Automatic Detection** (❌ Rejected)
 77 |    - Scan `process.env` for TEST_RUNNER_ variables and always pass them
 78 |    - **Issue**: Security risk of environment variable leakage
 79 |    - **Issue**: Unpredictable behavior based on server environment
 80 | 
 81 | 2. **Explicit Parameter** (✅ Chosen)
 82 |    - Add `testRunnerEnv` parameter to test tools
 83 |    - Users explicitly specify which variables to pass
 84 |    - **Benefits**: Secure, predictable, well-validated
 85 | 
 86 | 3. **Hybrid Approach** (🤔 Future Enhancement)
 87 |    - Both automatic + explicit with explicit overriding
 88 |    - **Issue**: Adds complexity, deferred for future consideration
 89 | 
 90 | ### Expert Analysis Summary
 91 | 
 92 | **RepoPrompt Analysis**: Comprehensive architectural plan emphasizing security, type safety, and integration with existing patterns.
 93 | 
 94 | **Gemini Analysis**: Confirmed explicit approach as optimal, highlighting:
 95 | - Security benefits of explicit allow-list approach
 96 | - Architectural soundness of extending CommandExecutor
 97 | - Recommendation for automatic prefix handling for better UX
 98 | 
 99 | ## Recommended Solution: Explicit Parameter with Automatic Prefix Handling
100 | 
101 | ### Key Design Decisions
102 | 
103 | 1. **Security-First**: Only explicitly provided variables are passed (no automatic process.env scanning)
104 | 2. **User Experience**: Automatic prefix handling - users provide unprefixed keys
105 | 3. **Architecture**: Extend execution layer generically for future extensibility  
106 | 4. **Validation**: Zod schema enforcement with proper type safety
107 | 
108 | ### User Experience Design
109 | 
110 | **Input** (what users specify):
111 | ```json
112 | {
113 |   "testRunnerEnv": {
114 |     "USE_DEV_MODE": "YES",
115 |     "runsForEachTargetApplicationUIConfiguration": "NO"
116 |   }
117 | }
118 | ```
119 | 
120 | **Output** (what gets passed to xcodebuild):
121 | ```bash
122 | TEST_RUNNER_USE_DEV_MODE=YES \
123 | TEST_RUNNER_runsForEachTargetApplicationUIConfiguration=NO \
124 | xcodebuild test ...
125 | ```
126 | 
127 | ## Implementation Plan
128 | 
129 | ### Phase 0: Test-Driven Development Setup
130 | 
131 | **Objective**: Create reproduction test to validate issue and later prove fix works
132 | 
133 | #### Tasks:
134 | - [ ] Create test in `example_projects/iOS/MCPTest` that checks for environment variable
135 | - [ ] Run current test tools to demonstrate limitation (test should fail)
136 | - [ ] Document baseline behavior
137 | 
138 | **Test Code Example**:
139 | ```swift
140 | func testEnvironmentVariablePassthrough() throws {
141 |   let useDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES"
142 |   guard useDevMode else {
143 |     XCTFail("Test requires USE_DEV_MODE=YES via TEST_RUNNER_USE_DEV_MODE")
144 |     return
145 |   }
146 |   XCTAssertTrue(true, "Environment variable successfully passed through")
147 | }
148 | ```
149 | 
150 | ### Phase 1: Core Infrastructure Updates
151 | 
152 | **Objective**: Extend CommandExecutor and build utilities to support environment variables
153 | 
154 | #### 1.1 Update CommandExecutor Interface
155 | 
156 | **File**: `src/utils/CommandExecutor.ts`
157 | 
158 | **Changes**:
159 | - Add `CommandExecOptions` type for execution options
160 | - Update `CommandExecutor` type signature to accept optional execution options
161 | 
162 | ```typescript
163 | export type CommandExecOptions = {
164 |   cwd?: string;
165 |   env?: Record<string, string | undefined>;
166 | };
167 | 
168 | export type CommandExecutor = (
169 |   args: string[],
170 |   description?: string,
171 |   quiet?: boolean,
172 |   opts?: CommandExecOptions
173 | ) => Promise<CommandResponse>;
174 | ```
175 | 
176 | #### 1.2 Update Execution Facade
177 | 
178 | **File**: `src/utils/execution/index.ts`
179 | 
180 | **Changes**:
181 | - Re-export `CommandExecOptions` type
182 | 
183 | ```typescript
184 | export type { CommandExecutor, CommandResponse, CommandExecOptions } from '../CommandExecutor.js';
185 | ```
186 | 
187 | #### 1.3 Update Default Command Executor
188 | 
189 | **File**: `src/utils/command.ts`
190 | 
191 | **Changes**:
192 | - Modify `getDefaultCommandExecutor` to merge `opts.env` with `process.env` when spawning
193 | 
194 | ```typescript
195 | // In the returned function:
196 | const env = { ...process.env, ...(opts?.env ?? {}) };
197 | // Pass env and opts?.cwd to spawn/exec call
198 | ```
199 | 
200 | #### 1.4 Create Environment Variable Utility
201 | 
202 | **File**: `src/utils/environment.ts`
203 | 
204 | **Changes**:
205 | - Add `normalizeTestRunnerEnv` function
206 | 
207 | ```typescript
208 | export function normalizeTestRunnerEnv(
209 |   userVars?: Record<string, string | undefined>
210 | ): Record<string, string> {
211 |   const result: Record<string, string> = {};
212 |   if (userVars) {
213 |     for (const [key, value] of Object.entries(userVars)) {
214 |       if (value !== undefined) {
215 |         result[`TEST_RUNNER_${key}`] = value;
216 |       }
217 |     }
218 |   }
219 |   return result;
220 | }
221 | ```
222 | 
223 | #### 1.5 Update executeXcodeBuildCommand
224 | 
225 | **File**: `src/utils/build-utils.ts`
226 | 
227 | **Changes**:
228 | - Add optional `execOpts?: CommandExecOptions` parameter (6th parameter)
229 | - Pass execution options through to `CommandExecutor` calls
230 | 
231 | ```typescript
232 | export async function executeXcodeBuildCommand(
233 |   build: { /* existing fields */ },
234 |   runtime: { /* existing fields */ },
235 |   preferXcodebuild = false,
236 |   action: 'build' | 'test' | 'archive' | 'analyze' | string,
237 |   executor: CommandExecutor = getDefaultCommandExecutor(),
238 |   execOpts?: CommandExecOptions, // NEW
239 | ): Promise<ToolResponse>
240 | ```
241 | 
242 | ### Phase 2: Test Tool Integration
243 | 
244 | **Objective**: Add `testRunnerEnv` parameter to all test tools and wire through execution
245 | 
246 | #### 2.1 Update Device Test Tool
247 | 
248 | **File**: `src/mcp/tools/device/test_device.ts`
249 | 
250 | **Changes**:
251 | - Add `testRunnerEnv` to Zod schema with validation
252 | - Import and use `normalizeTestRunnerEnv`
253 | - Pass execution options to `executeXcodeBuildCommand`
254 | 
255 | **Schema Addition**:
256 | ```typescript
257 | testRunnerEnv: z
258 |   .record(z.string(), z.string().optional())
259 |   .optional()
260 |   .describe('Test runner environment variables (TEST_RUNNER_ prefix added automatically)')
261 | ```
262 | 
263 | **Usage**:
264 | ```typescript
265 | const execEnv = normalizeTestRunnerEnv(params.testRunnerEnv);
266 | const testResult = await executeXcodeBuildCommand(
267 |   { /* build params */ },
268 |   { /* runtime params */ },
269 |   params.preferXcodebuild ?? false,
270 |   'test',
271 |   executor,
272 |   { env: execEnv } // NEW
273 | );
274 | ```
275 | 
276 | #### 2.2 Update macOS Test Tool
277 | 
278 | **File**: `src/mcp/tools/macos/test_macos.ts`
279 | 
280 | **Changes**: Same pattern as device test tool
281 | - Schema addition for `testRunnerEnv`
282 | - Import `normalizeTestRunnerEnv` 
283 | - Pass execution options to `executeXcodeBuildCommand`
284 | 
285 | #### 2.3 Update Simulator Test Tool and Logic
286 | 
287 | **File**: `src/mcp/tools/simulator/test_sim.ts`
288 | 
289 | **Changes**:
290 | - Add `testRunnerEnv` to schema
291 | - Pass through to `handleTestLogic`
292 | 
293 | **File**: `src/utils/test/index.ts`
294 | 
295 | **Changes**:
296 | - Update `handleTestLogic` signature to accept `testRunnerEnv?: Record<string, string | undefined>`
297 | - Import and use `normalizeTestRunnerEnv`
298 | - Pass execution options to `executeXcodeBuildCommand`
299 | 
300 | ### Phase 3: Testing and Validation
301 | 
302 | **Objective**: Comprehensive testing coverage for new functionality
303 | 
304 | #### 3.1 Unit Tests
305 | 
306 | **File**: `src/utils/__tests__/environment.test.ts`
307 | 
308 | **Tests**:
309 | - Test `normalizeTestRunnerEnv` with various inputs
310 | - Verify prefix addition
311 | - Verify undefined filtering
312 | - Verify empty input handling
313 | 
314 | #### 3.2 Integration Tests  
315 | 
316 | **Files**: Update existing test files for test tools
317 | 
318 | **Tests**:
319 | - Verify `testRunnerEnv` parameter is properly validated
320 | - Verify environment variables are passed through `CommandExecutor`
321 | - Mock executor to verify correct env object construction
322 | 
323 | #### 3.3 Tool Export Validation
324 | 
325 | **Files**: Test files in each tool directory
326 | 
327 | **Tests**:
328 | - Verify schema exports include new `testRunnerEnv` field
329 | - Verify parameter typing is correct
330 | 
331 | ### Phase 4: End-to-End Validation
332 | 
333 | **Objective**: Prove the fix works with real xcodebuild scenarios
334 | 
335 | #### 4.1 Reproduction Test Validation
336 | 
337 | **Tasks**:
338 | - Run reproduction test from Phase 0 with new `testRunnerEnv` parameter
339 | - Verify test passes (proving env var was successfully passed)
340 | - Document the before/after behavior
341 | 
342 | #### 4.2 Real-World Scenario Testing
343 | 
344 | **Tasks**:
345 | - Test with actual iOS project using `runsForEachTargetApplicationUIConfiguration`
346 | - Verify performance difference when variable is set
347 | - Test with multiple environment variables
348 | - Test edge cases (empty values, special characters)
349 | 
350 | ## Security Considerations
351 | 
352 | ### Security Benefits
353 | - **No Environment Leakage**: Only explicit user-provided variables are passed
354 | - **Command Injection Prevention**: Environment variables passed as separate object, not interpolated into command string
355 | - **Input Validation**: Zod schemas prevent malformed inputs
356 | - **Prefix Enforcement**: Only TEST_RUNNER_ prefixed variables can be set
357 | 
358 | ### Security Best Practices
359 | - Never log environment variable values (keys only for debugging)
360 | - Filter out undefined values to prevent accidental exposure
361 | - Validate all user inputs through Zod schemas
362 | - Document supported TEST_RUNNER_ variables from Apple's documentation
363 | 
364 | ## Architectural Benefits
365 | 
366 | ### Clean Integration
367 | - Extends existing `CommandExecutor` pattern generically
368 | - Maintains backward compatibility (all existing calls remain valid)
369 | - Follows established Zod validation patterns
370 | - Consistent API across all test tools
371 | 
372 | ### Future Extensibility  
373 | - `CommandExecOptions` can support additional execution options (timeout, cwd, etc.)
374 | - Pattern can be extended to other tools that need environment variables
375 | - Generic approach allows for non-TEST_RUNNER_ use cases in the future
376 | 
377 | ## File Modification Summary
378 | 
379 | ### New Files
380 | - `src/utils/__tests__/environment.test.ts` - Unit tests for environment utilities
381 | 
382 | ### Modified Files
383 | - `src/utils/CommandExecutor.ts` - Add execution options types
384 | - `src/utils/execution/index.ts` - Re-export new types  
385 | - `src/utils/command.ts` - Update default executor to handle env
386 | - `src/utils/environment.ts` - Add `normalizeTestRunnerEnv` utility
387 | - `src/utils/build-utils.ts` - Update `executeXcodeBuildCommand` signature
388 | - `src/mcp/tools/device/test_device.ts` - Add schema and integration
389 | - `src/mcp/tools/macos/test_macos.ts` - Add schema and integration
390 | - `src/mcp/tools/simulator/test_sim.ts` - Add schema and pass-through
391 | - `src/utils/test/index.ts` - Update `handleTestLogic` for simulator path
392 | - Test files for each modified tool - Add validation tests
393 | 
394 | ## Success Criteria
395 | 
396 | 1. **Functionality**: Users can pass `testRunnerEnv` parameter to test tools and have variables appear in test runner environment
397 | 2. **Security**: No unintended environment variable leakage from server process
398 | 3. **Usability**: Users specify unprefixed variable names for better UX
399 | 4. **Compatibility**: All existing test tool calls continue to work unchanged
400 | 5. **Validation**: Comprehensive test coverage proves the feature works end-to-end
401 | 
402 | ## Future Enhancements (Out of Scope)
403 | 
404 | 1. **Configuration Profiles**: Allow users to define common TEST_RUNNER_ variable sets in config files
405 | 2. **Variable Discovery**: Help users discover available TEST_RUNNER_ variables
406 | 3. **Build Tool Support**: Extend to build tools if Apple adds similar BUILD_RUNNER_ support
407 | 4. **Performance Monitoring**: Track impact of environment variable passing on build times
408 | 
409 | ## Implementation Timeline
410 | 
411 | - **Phase 0**: 1-2 hours (reproduction test setup)
412 | - **Phase 1**: 4-6 hours (infrastructure changes)
413 | - **Phase 2**: 3-4 hours (tool integration)
414 | - **Phase 3**: 4-5 hours (testing)  
415 | - **Phase 4**: 2-3 hours (validation)
416 | 
417 | **Total Estimated Time**: 14-20 hours
418 | 
419 | ## Conclusion
420 | 
421 | This implementation plan provides a secure, user-friendly, and architecturally sound solution for TEST_RUNNER_ environment variable support. The explicit parameter approach with automatic prefix handling balances security concerns with user experience, while the test-driven development approach ensures we can prove the solution works as intended.
422 | 
423 | The plan leverages XcodeBuildMCP's existing patterns and provides a foundation for future environment variable needs across the tool ecosystem.
```
Page 9/14FirstPrevNextLast