#
tokens: 49893/50000 13/393 files (page 8/12)
lines: off (toggle) GitHub
raw markdown copy
This is page 8 of 12. Use http://codebase.md/cameroncooke/xcodebuildmcp?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
│       ├── README.md
│       ├── release.yml
│       ├── sentry.yml
│       └── stale.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
├── docs
│   ├── CONFIGURATION.md
│   ├── DAP_BACKEND_IMPLEMENTATION_PLAN.md
│   ├── DEBUGGING_ARCHITECTURE.md
│   ├── DEMOS.md
│   ├── dev
│   │   ├── ARCHITECTURE.md
│   │   ├── CODE_QUALITY.md
│   │   ├── CONTRIBUTING.md
│   │   ├── ESLINT_TYPE_SAFETY.md
│   │   ├── MANUAL_TESTING.md
│   │   ├── NODEJS_2025.md
│   │   ├── PLUGIN_DEVELOPMENT.md
│   │   ├── README.md
│   │   ├── RELEASE_PROCESS.md
│   │   ├── RELOADEROO_FOR_XCODEBUILDMCP.md
│   │   ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md
│   │   ├── RELOADEROO.md
│   │   ├── session_management_plan.md
│   │   ├── session-aware-migration-todo.md
│   │   ├── SMITHERY.md
│   │   ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md
│   │   ├── TESTING.md
│   │   └── ZOD_MIGRATION_GUIDE.md
│   ├── DEVICE_CODE_SIGNING.md
│   ├── GETTING_STARTED.md
│   ├── investigations
│   │   ├── issue-154-screenshot-downscaling.md
│   │   ├── issue-163.md
│   │   ├── issue-debugger-attach-stopped.md
│   │   └── issue-describe-ui-empty-after-debugger-resume.md
│   ├── OVERVIEW.md
│   ├── PRIVACY.md
│   ├── README.md
│   ├── SESSION_DEFAULTS.md
│   ├── TOOLS.md
│   └── TROUBLESHOOTING.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
│   │   ├── .gitignore
│   │   ├── 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
│   │   └── MCPTestTests
│   │       └── MCPTestTests.swift
│   └── 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
│   ├── generate-loaders.ts
│   ├── generate-version.ts
│   ├── release.sh
│   ├── tools-cli.ts
│   ├── update-tools-docs.ts
│   └── verify-smithery-bundle.sh
├── server.json
├── smithery.config.js
├── smithery.yaml
├── src
│   ├── core
│   │   ├── __tests__
│   │   │   └── resources.test.ts
│   │   ├── generated-plugins.ts
│   │   ├── generated-resources.ts
│   │   ├── plugin-registry.ts
│   │   ├── plugin-types.ts
│   │   └── resources.ts
│   ├── doctor-cli.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── resources
│   │   │   ├── __tests__
│   │   │   │   ├── devices.test.ts
│   │   │   │   ├── doctor.test.ts
│   │   │   │   ├── session-status.test.ts
│   │   │   │   └── simulators.test.ts
│   │   │   ├── devices.ts
│   │   │   ├── doctor.ts
│   │   │   ├── session-status.ts
│   │   │   └── simulators.ts
│   │   └── tools
│   │       ├── debugging
│   │       │   ├── debug_attach_sim.ts
│   │       │   ├── debug_breakpoint_add.ts
│   │       │   ├── debug_breakpoint_remove.ts
│   │       │   ├── debug_continue.ts
│   │       │   ├── debug_detach.ts
│   │       │   ├── debug_lldb_command.ts
│   │       │   ├── debug_stack.ts
│   │       │   ├── debug_variables.ts
│   │       │   └── index.ts
│   │       ├── 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
│   │       ├── 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
│   │   ├── bootstrap.ts
│   │   └── server.ts
│   ├── smithery.ts
│   ├── test-utils
│   │   └── mock-executors.ts
│   ├── types
│   │   └── common.ts
│   ├── utils
│   │   ├── __tests__
│   │   │   ├── build-utils-suppress-warnings.test.ts
│   │   │   ├── build-utils.test.ts
│   │   │   ├── debugger-simctl.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
│   │   │   └── workflow-selection.test.ts
│   │   ├── axe
│   │   │   └── index.ts
│   │   ├── axe-helpers.ts
│   │   ├── build
│   │   │   └── index.ts
│   │   ├── build-utils.ts
│   │   ├── capabilities.ts
│   │   ├── command.ts
│   │   ├── CommandExecutor.ts
│   │   ├── debugger
│   │   │   ├── __tests__
│   │   │   │   └── debugger-manager-dap.test.ts
│   │   │   ├── backends
│   │   │   │   ├── __tests__
│   │   │   │   │   └── dap-backend.test.ts
│   │   │   │   ├── dap-backend.ts
│   │   │   │   ├── DebuggerBackend.ts
│   │   │   │   └── lldb-cli-backend.ts
│   │   │   ├── dap
│   │   │   │   ├── __tests__
│   │   │   │   │   └── transport-framing.test.ts
│   │   │   │   ├── adapter-discovery.ts
│   │   │   │   ├── transport.ts
│   │   │   │   └── types.ts
│   │   │   ├── debugger-manager.ts
│   │   │   ├── index.ts
│   │   │   ├── simctl.ts
│   │   │   ├── tool-context.ts
│   │   │   ├── types.ts
│   │   │   └── ui-automation-guard.ts
│   │   ├── environment.ts
│   │   ├── errors.ts
│   │   ├── execution
│   │   │   ├── index.ts
│   │   │   └── interactive-process.ts
│   │   ├── FileSystemExecutor.ts
│   │   ├── log_capture.ts
│   │   ├── log-capture
│   │   │   ├── device-log-sessions.ts
│   │   │   └── index.ts
│   │   ├── logger.ts
│   │   ├── logging
│   │   │   └── index.ts
│   │   ├── plugin-registry
│   │   │   └── index.ts
│   │   ├── responses
│   │   │   └── index.ts
│   │   ├── runtime-registry.ts
│   │   ├── schema-helpers.ts
│   │   ├── sentry.ts
│   │   ├── session-status.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
│   │   ├── workflow-selection.ts
│   │   ├── xcode.ts
│   │   ├── xcodemake
│   │   │   └── index.ts
│   │   └── xcodemake.ts
│   └── version.ts
├── tsconfig.json
├── tsconfig.test.json
├── tsconfig.tests.json
├── tsup.config.ts
├── vitest.config.ts
└── XcodeBuildMCP.code-workspace
```

# Files

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

```typescript
/**
 * Utilities Plugin: Scaffold macOS Project
 *
 * Scaffold a new macOS project from templates.
 */

import * as z from 'zod';
import { join, dirname, basename } from 'path';
import { log } from '../../../utils/logging/index.ts';
import { ValidationError } from '../../../utils/responses/index.ts';
import { TemplateManager } from '../../../utils/template/index.ts';
import { ToolResponse } from '../../../types/common.ts';
import {
  CommandExecutor,
  getDefaultCommandExecutor,
  getDefaultFileSystemExecutor,
} from '../../../utils/command.ts';
import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';

// Common base schema for both iOS and macOS
const BaseScaffoldSchema = z.object({
  projectName: z.string().min(1).describe('Name of the new project'),
  outputPath: z.string().describe('Path where the project should be created'),
  bundleIdentifier: z
    .string()
    .optional()
    .describe(
      'Bundle identifier (e.g., com.example.myapp). If not provided, will use com.example.projectname',
    ),
  displayName: z
    .string()
    .optional()
    .describe(
      'App display name (shown on home screen/dock). If not provided, will use projectName',
    ),
  marketingVersion: z
    .string()
    .optional()
    .describe('Marketing version (e.g., 1.0, 2.1.3). If not provided, will use 1.0'),
  currentProjectVersion: z
    .string()
    .optional()
    .describe('Build number (e.g., 1, 42, 100). If not provided, will use 1'),
  customizeNames: z
    .boolean()
    .default(true)
    .describe('Whether to customize project names and identifiers. Default is true.'),
});

// macOS-specific schema
const ScaffoldmacOSProjectSchema = BaseScaffoldSchema.extend({
  deploymentTarget: z
    .string()
    .optional()
    .describe('macOS deployment target (e.g., 15.4, 14.0). If not provided, will use 15.4'),
});

// Use z.infer for type safety
type ScaffoldMacOSProjectParams = z.infer<typeof ScaffoldmacOSProjectSchema>;

/**
 * Update Package.swift file with deployment target
 */
function updatePackageSwiftFile(
  content: string,
  params: ScaffoldMacOSProjectParams & { platform: string },
): string {
  let result = content;

  // Update ALL target name references in Package.swift
  const featureName = `${params.projectName}Feature`;
  const testName = `${params.projectName}FeatureTests`;

  // Replace ALL occurrences of MyProjectFeatureTests first (more specific)
  result = result.replace(/MyProjectFeatureTests/g, testName);
  // Then replace ALL occurrences of MyProjectFeature (less specific, so comes after)
  result = result.replace(/MyProjectFeature/g, featureName);

  // Update deployment targets based on platform
  if (params.platform === 'macOS') {
    if (params.deploymentTarget) {
      // Extract major version (e.g., "14.0" -> "14")
      const majorVersion = params.deploymentTarget.split('.')[0];
      result = result.replace(/\.macOS\(\.v\d+\)/, `.macOS(.v${majorVersion})`);
    }
  }

  return result;
}

/**
 * Update XCConfig file with scaffold parameters
 */
function updateXCConfigFile(
  content: string,
  params: ScaffoldMacOSProjectParams & { platform: string },
): string {
  let result = content;

  // Update project identity settings
  result = result.replace(/PRODUCT_NAME = .+/g, `PRODUCT_NAME = ${params.projectName}`);
  result = result.replace(
    /PRODUCT_DISPLAY_NAME = .+/g,
    `PRODUCT_DISPLAY_NAME = ${params.displayName ?? params.projectName}`,
  );
  result = result.replace(
    /PRODUCT_BUNDLE_IDENTIFIER = .+/g,
    `PRODUCT_BUNDLE_IDENTIFIER = ${params.bundleIdentifier ?? `com.example.${params.projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`}`,
  );
  result = result.replace(
    /MARKETING_VERSION = .+/g,
    `MARKETING_VERSION = ${params.marketingVersion ?? '1.0'}`,
  );
  result = result.replace(
    /CURRENT_PROJECT_VERSION = .+/g,
    `CURRENT_PROJECT_VERSION = ${params.currentProjectVersion ?? '1'}`,
  );

  // Platform-specific updates
  if (params.platform === 'macOS') {
    // macOS deployment target
    if (params.deploymentTarget) {
      result = result.replace(
        /MACOSX_DEPLOYMENT_TARGET = .+/g,
        `MACOSX_DEPLOYMENT_TARGET = ${params.deploymentTarget}`,
      );
    }

    // Update entitlements path for macOS
    result = result.replace(
      /CODE_SIGN_ENTITLEMENTS = .+/g,
      `CODE_SIGN_ENTITLEMENTS = Config/${params.projectName}.entitlements`,
    );
  }

  // Update test bundle identifier and target name
  result = result.replace(/TEST_TARGET_NAME = .+/g, `TEST_TARGET_NAME = ${params.projectName}`);

  // Update comments that reference MyProject in entitlements paths
  result = result.replace(
    /Config\/MyProject\.entitlements/g,
    `Config/${params.projectName}.entitlements`,
  );

  return result;
}

/**
 * Replace placeholders in a string (for non-XCConfig files)
 */
function replacePlaceholders(
  content: string,
  projectName: string,
  bundleIdentifier: string,
): string {
  let result = content;

  // Replace project name
  result = result.replace(/MyProject/g, projectName);

  // Replace bundle identifier - check for both patterns used in templates
  if (bundleIdentifier) {
    result = result.replace(/com\.example\.MyProject/g, bundleIdentifier);
    result = result.replace(/com\.mycompany\.MyProject/g, bundleIdentifier);
  }

  return result;
}

/**
 * Process a single file, replacing placeholders if it's a text file
 */
async function processFile(
  sourcePath: string,
  destPath: string,
  params: ScaffoldMacOSProjectParams & { platform: string },
  fileSystemExecutor: FileSystemExecutor,
): Promise<void> {
  // Determine the destination file path
  let finalDestPath = destPath;
  if (params.customizeNames) {
    // Replace MyProject in file/directory names
    const fileName = basename(destPath);
    const dirName = dirname(destPath);
    const newFileName = fileName.replace(/MyProject/g, params.projectName);
    finalDestPath = join(dirName, newFileName);
  }

  // Text file extensions that should be processed
  const textExtensions = [
    '.swift',
    '.h',
    '.m',
    '.mm',
    '.cpp',
    '.c',
    '.pbxproj',
    '.plist',
    '.xcscheme',
    '.xctestplan',
    '.xcworkspacedata',
    '.xcconfig',
    '.json',
    '.xml',
    '.entitlements',
    '.storyboard',
    '.xib',
    '.md',
  ];

  const ext = sourcePath.toLowerCase();
  const isTextFile = textExtensions.some((textExt) => ext.endsWith(textExt));
  const isXCConfig = sourcePath.endsWith('.xcconfig');
  const isPackageSwift = sourcePath.endsWith('Package.swift');

  if (isTextFile && params.customizeNames) {
    // Read the file content
    const content = await fileSystemExecutor.readFile(sourcePath, 'utf-8');

    let processedContent;

    if (isXCConfig) {
      // Use special XCConfig processing
      processedContent = updateXCConfigFile(content, params);
    } else if (isPackageSwift) {
      // Use special Package.swift processing
      processedContent = updatePackageSwiftFile(content, params);
    } else {
      // Use standard placeholder replacement
      const bundleIdentifier =
        params.bundleIdentifier ??
        `com.example.${params.projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
      processedContent = replacePlaceholders(content, params.projectName, bundleIdentifier);
    }

    await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
    await fileSystemExecutor.writeFile(finalDestPath, processedContent, 'utf-8');
  } else {
    // Copy binary files as-is
    await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
    await fileSystemExecutor.cp(sourcePath, finalDestPath, { recursive: true });
  }
}

/**
 * Recursively process a directory
 */
async function processDirectory(
  sourceDir: string,
  destDir: string,
  params: ScaffoldMacOSProjectParams & { platform: string },
  fileSystemExecutor: FileSystemExecutor,
): Promise<void> {
  const entries = await fileSystemExecutor.readdir(sourceDir, { withFileTypes: true });

  for (const entry of entries) {
    const dirent = entry as { isDirectory(): boolean; isFile(): boolean; name: string };
    const sourcePath = join(sourceDir, dirent.name);
    let destName = dirent.name;

    if (params.customizeNames) {
      // Replace MyProject in directory names
      destName = destName.replace(/MyProject/g, params.projectName);
    }

    const destPath = join(destDir, destName);

    if (dirent.isDirectory()) {
      // Skip certain directories
      if (dirent.name === '.git' || dirent.name === 'xcuserdata') {
        continue;
      }
      await fileSystemExecutor.mkdir(destPath, { recursive: true });
      await processDirectory(sourcePath, destPath, params, fileSystemExecutor);
    } else if (dirent.isFile()) {
      // Skip certain files
      if (dirent.name === '.DS_Store' || dirent.name.endsWith('.xcuserstate')) {
        continue;
      }
      await processFile(sourcePath, destPath, params, fileSystemExecutor);
    }
  }
}

/**
 * Scaffold a new iOS or macOS project
 */
async function scaffoldProject(
  params: ScaffoldMacOSProjectParams & { platform: string },
  commandExecutor: CommandExecutor,
  fileSystemExecutor: FileSystemExecutor,
): Promise<string> {
  const projectName = params.projectName;
  const outputPath = params.outputPath;
  const platform = params.platform;
  const customizeNames = params.customizeNames ?? true;

  log('info', `Scaffolding project: ${projectName} (${platform}) at ${outputPath}`);

  // Validate project name
  if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(projectName)) {
    throw new ValidationError(
      'Project name must start with a letter and contain only letters, numbers, and underscores',
    );
  }

  // Get template path from TemplateManager
  let templatePath;
  try {
    templatePath = await TemplateManager.getTemplatePath(
      platform as 'macOS' | 'iOS',
      commandExecutor,
      fileSystemExecutor,
    );
  } catch (error) {
    throw new ValidationError(
      `Failed to get template for ${platform}: ${error instanceof Error ? error.message : String(error)}`,
    );
  }

  // Use outputPath directly as the destination
  const projectPath = outputPath;

  // Check if the output directory already has Xcode project files
  const xcworkspaceExists = fileSystemExecutor.existsSync(
    join(projectPath, `${customizeNames ? projectName : 'MyProject'}.xcworkspace`),
  );
  const xcodeprojExists = fileSystemExecutor.existsSync(
    join(projectPath, `${customizeNames ? projectName : 'MyProject'}.xcodeproj`),
  );

  if (xcworkspaceExists || xcodeprojExists) {
    throw new ValidationError(`Xcode project files already exist in ${projectPath}`);
  }

  try {
    // Process the template directly into the output path
    await processDirectory(templatePath, projectPath, params, fileSystemExecutor);

    return projectPath;
  } finally {
    // Clean up downloaded template if needed
    await TemplateManager.cleanup(templatePath, fileSystemExecutor);
  }
}

/**
 * Business logic for scaffolding macOS projects
 * Extracted for testability and Separation of Concerns
 */
export async function scaffold_macos_projectLogic(
  params: ScaffoldMacOSProjectParams,
  commandExecutor: CommandExecutor,
  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<ToolResponse> {
  try {
    const projectParams = { ...params, platform: 'macOS' as const };
    const projectPath = await scaffoldProject(projectParams, commandExecutor, fileSystemExecutor);

    const response = {
      success: true,
      projectPath,
      platform: 'macOS',
      message: `Successfully scaffolded macOS project "${params.projectName}" in ${projectPath}`,
      nextSteps: [
        `Important: Before working on the project make sure to read the README.md file in the workspace root directory.`,
        `Build for macOS: build_macos({ workspacePath: "${projectPath}/${params.customizeNames ? params.projectName : 'MyProject'}.xcworkspace", scheme: "${params.customizeNames ? params.projectName : 'MyProject'}" })`,
        `Build & Run on macOS: build_run_macos({ workspacePath: "${projectPath}/${params.customizeNames ? params.projectName : 'MyProject'}.xcworkspace", scheme: "${params.customizeNames ? params.projectName : 'MyProject'}" })`,
      ],
    };

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(response, null, 2),
        },
      ],
    };
  } catch (error) {
    log(
      'error',
      `Failed to scaffold macOS project: ${error instanceof Error ? error.message : String(error)}`,
    );

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(
            {
              success: false,
              error: error instanceof Error ? error.message : 'Unknown error occurred',
            },
            null,
            2,
          ),
        },
      ],
      isError: true,
    };
  }
}

export default {
  name: 'scaffold_macos_project',
  description:
    'Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration.',
  schema: ScaffoldmacOSProjectSchema.shape,
  annotations: {
    title: 'Scaffold macOS Project',
    destructiveHint: true,
  },
  async handler(args: Record<string, unknown>): Promise<ToolResponse> {
    // Validate the arguments against the schema before processing
    const validatedArgs = ScaffoldmacOSProjectSchema.parse(args);
    return scaffold_macos_projectLogic(
      validatedArgs,
      getDefaultCommandExecutor(),
      getDefaultFileSystemExecutor(),
    );
  },
};

```

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

```typescript
/**
 * Build Utilities - Higher-level abstractions for Xcode build operations
 *
 * This utility module provides specialized functions for build-related operations
 * across different platforms (macOS, iOS, watchOS, etc.). It serves as a higher-level
 * abstraction layer on top of the core Xcode utilities.
 *
 * Responsibilities:
 * - Providing a unified interface (executeXcodeBuild) for all build operations
 * - Handling build-specific parameter formatting and validation
 * - Standardizing response formatting for build results
 * - Managing build-specific error handling and reporting
 * - Supporting various build actions (build, clean, showBuildSettings, etc.)
 * - Supporting xcodemake as an alternative build strategy for faster incremental builds
 *
 * This file depends on the lower-level utilities in xcode.ts for command execution
 * while adding build-specific behavior, formatting, and error handling.
 */

import { log } from './logger.ts';
import { XcodePlatform, constructDestinationString } from './xcode.ts';
import { CommandExecutor, CommandExecOptions } from './command.ts';
import { ToolResponse, SharedBuildParams, PlatformBuildOptions } from '../types/common.ts';
import { createTextResponse, consolidateContentForClaudeCode } from './validation.ts';
import {
  isXcodemakeEnabled,
  isXcodemakeAvailable,
  executeXcodemakeCommand,
  executeMakeCommand,
  doesMakefileExist,
  doesMakeLogFileExist,
} from './xcodemake.ts';
import { sessionStore } from './session-store.ts';
import path from 'path';

/**
 * Common function to execute an Xcode build command across platforms
 * @param params Common build parameters
 * @param platformOptions Platform-specific options
 * @param preferXcodebuild Whether to prefer xcodebuild over xcodemake, useful for if xcodemake is failing
 * @param buildAction The xcodebuild action to perform (e.g., 'build', 'clean', 'test')
 * @param executor Optional command executor for dependency injection (used for testing)
 * @returns Promise resolving to tool response
 */
export async function executeXcodeBuildCommand(
  params: SharedBuildParams,
  platformOptions: PlatformBuildOptions,
  preferXcodebuild: boolean = false,
  buildAction: string = 'build',
  executor: CommandExecutor,
  execOpts?: CommandExecOptions,
): Promise<ToolResponse> {
  // Collect warnings, errors, and stderr messages from the build output
  const buildMessages: { type: 'text'; text: string }[] = [];
  function grepWarningsAndErrors(text: string): { type: 'warning' | 'error'; content: string }[] {
    return text
      .split('\n')
      .map((content) => {
        if (/warning:/i.test(content)) return { type: 'warning', content };
        if (/error:/i.test(content)) return { type: 'error', content };
        return null;
      })
      .filter(Boolean) as { type: 'warning' | 'error'; content: string }[];
  }

  log('info', `Starting ${platformOptions.logPrefix} ${buildAction} for scheme ${params.scheme}`);

  // Check if xcodemake is enabled and available
  const isXcodemakeEnabledFlag = isXcodemakeEnabled();
  let xcodemakeAvailableFlag = false;

  if (isXcodemakeEnabledFlag && buildAction === 'build') {
    xcodemakeAvailableFlag = await isXcodemakeAvailable();

    if (xcodemakeAvailableFlag && preferXcodebuild) {
      log(
        'info',
        'xcodemake is enabled but preferXcodebuild is set to true. Falling back to xcodebuild.',
      );
      buildMessages.push({
        type: 'text',
        text: '⚠️ incremental build support is enabled but preferXcodebuild is set to true. Falling back to xcodebuild.',
      });
    } else if (!xcodemakeAvailableFlag) {
      buildMessages.push({
        type: 'text',
        text: '⚠️ xcodemake is enabled but not available. Falling back to xcodebuild.',
      });
      log('info', 'xcodemake is enabled but not available. Falling back to xcodebuild.');
    } else {
      log('info', 'xcodemake is enabled and available, using it for incremental builds.');
      buildMessages.push({
        type: 'text',
        text: 'ℹ️ xcodemake is enabled and available, using it for incremental builds.',
      });
    }
  }

  try {
    const command = ['xcodebuild'];

    let projectDir = '';
    if (params.workspacePath) {
      projectDir = path.dirname(params.workspacePath);
      command.push('-workspace', params.workspacePath);
    } else if (params.projectPath) {
      projectDir = path.dirname(params.projectPath);
      command.push('-project', params.projectPath);
    }

    command.push('-scheme', params.scheme);
    command.push('-configuration', params.configuration);
    command.push('-skipMacroValidation');

    // Construct destination string based on platform
    let destinationString: string;
    const isSimulatorPlatform = [
      XcodePlatform.iOSSimulator,
      XcodePlatform.watchOSSimulator,
      XcodePlatform.tvOSSimulator,
      XcodePlatform.visionOSSimulator,
    ].includes(platformOptions.platform);

    if (isSimulatorPlatform) {
      if (platformOptions.simulatorId) {
        destinationString = constructDestinationString(
          platformOptions.platform,
          undefined,
          platformOptions.simulatorId,
        );
      } else if (platformOptions.simulatorName) {
        destinationString = constructDestinationString(
          platformOptions.platform,
          platformOptions.simulatorName,
          undefined,
          platformOptions.useLatestOS,
        );
      } else {
        return createTextResponse(
          `For ${platformOptions.platform} platform, either simulatorId or simulatorName must be provided`,
          true,
        );
      }
    } else if (platformOptions.platform === XcodePlatform.macOS) {
      destinationString = constructDestinationString(
        platformOptions.platform,
        undefined,
        undefined,
        false,
        platformOptions.arch,
      );
    } else if (platformOptions.platform === XcodePlatform.iOS) {
      if (platformOptions.deviceId) {
        destinationString = `platform=iOS,id=${platformOptions.deviceId}`;
      } else {
        destinationString = 'generic/platform=iOS';
      }
    } else if (platformOptions.platform === XcodePlatform.watchOS) {
      if (platformOptions.deviceId) {
        destinationString = `platform=watchOS,id=${platformOptions.deviceId}`;
      } else {
        destinationString = 'generic/platform=watchOS';
      }
    } else if (platformOptions.platform === XcodePlatform.tvOS) {
      if (platformOptions.deviceId) {
        destinationString = `platform=tvOS,id=${platformOptions.deviceId}`;
      } else {
        destinationString = 'generic/platform=tvOS';
      }
    } else if (platformOptions.platform === XcodePlatform.visionOS) {
      if (platformOptions.deviceId) {
        destinationString = `platform=visionOS,id=${platformOptions.deviceId}`;
      } else {
        destinationString = 'generic/platform=visionOS';
      }
    } else {
      return createTextResponse(`Unsupported platform: ${platformOptions.platform}`, true);
    }

    command.push('-destination', destinationString);

    if (params.derivedDataPath) {
      command.push('-derivedDataPath', params.derivedDataPath);
    }

    if (params.extraArgs && params.extraArgs.length > 0) {
      command.push(...params.extraArgs);
    }

    command.push(buildAction);

    // Execute the command using xcodemake or xcodebuild
    let result;
    if (
      isXcodemakeEnabledFlag &&
      xcodemakeAvailableFlag &&
      buildAction === 'build' &&
      !preferXcodebuild
    ) {
      // Check if Makefile already exists
      const makefileExists = doesMakefileExist(projectDir);
      log('debug', 'Makefile exists: ' + makefileExists);

      // Check if Makefile log already exists
      const makeLogFileExists = doesMakeLogFileExist(projectDir, command);
      log('debug', 'Makefile log exists: ' + makeLogFileExists);

      if (makefileExists && makeLogFileExists) {
        // Use make for incremental builds
        buildMessages.push({
          type: 'text',
          text: 'ℹ️ Using make for incremental build',
        });
        result = await executeMakeCommand(projectDir, platformOptions.logPrefix);
      } else {
        // Generate Makefile using xcodemake
        buildMessages.push({
          type: 'text',
          text: 'ℹ️ Generating Makefile with xcodemake (first build may take longer)',
        });
        // Remove 'xcodebuild' from the command array before passing to executeXcodemakeCommand
        result = await executeXcodemakeCommand(
          projectDir,
          command.slice(1),
          platformOptions.logPrefix,
        );
      }
    } else {
      // Use standard xcodebuild
      // Pass projectDir as cwd to ensure CocoaPods relative paths resolve correctly
      result = await executor(command, platformOptions.logPrefix, true, {
        ...execOpts,
        cwd: projectDir,
      });
    }

    // Grep warnings and errors from stdout (build output)
    const warningOrErrorLines = grepWarningsAndErrors(result.output);
    const suppressWarnings = sessionStore.get('suppressWarnings');
    warningOrErrorLines.forEach(({ type, content }) => {
      if (type === 'warning' && suppressWarnings) {
        return;
      }
      buildMessages.push({
        type: 'text',
        text: type === 'warning' ? `⚠️ Warning: ${content}` : `❌ Error: ${content}`,
      });
    });

    // Include all stderr lines as errors
    if (result.error) {
      result.error.split('\n').forEach((content) => {
        if (content.trim()) {
          buildMessages.push({ type: 'text', text: `❌ [stderr] ${content}` });
        }
      });
    }

    if (!result.success) {
      const isMcpError = result.exitCode === 64;

      log(
        isMcpError ? 'error' : 'warning',
        `${platformOptions.logPrefix} ${buildAction} failed: ${result.error}`,
        { sentry: isMcpError },
      );
      const errorResponse = createTextResponse(
        `❌ ${platformOptions.logPrefix} ${buildAction} failed for scheme ${params.scheme}.`,
        true,
      );

      if (buildMessages.length > 0 && errorResponse.content) {
        errorResponse.content.unshift(...buildMessages);
      }

      // If using xcodemake and build failed but no compiling errors, suggest using xcodebuild
      if (
        warningOrErrorLines.length == 0 &&
        isXcodemakeEnabledFlag &&
        xcodemakeAvailableFlag &&
        buildAction === 'build' &&
        !preferXcodebuild
      ) {
        errorResponse.content.push({
          type: 'text',
          text: `💡 Incremental build using xcodemake failed, suggest using preferXcodebuild option to try build again using slower xcodebuild command.`,
        });
      }

      return consolidateContentForClaudeCode(errorResponse);
    }

    log('info', `✅ ${platformOptions.logPrefix} ${buildAction} succeeded.`);

    // Create additional info based on platform and action
    let additionalInfo = '';

    // Add xcodemake info if relevant
    if (
      isXcodemakeEnabledFlag &&
      xcodemakeAvailableFlag &&
      buildAction === 'build' &&
      !preferXcodebuild
    ) {
      additionalInfo += `xcodemake: Using faster incremental builds with xcodemake. 
Future builds will use the generated Makefile for improved performance.

`;
    }

    // Only show next steps for 'build' action
    if (buildAction === 'build') {
      if (platformOptions.platform === XcodePlatform.macOS) {
        additionalInfo = `Next Steps:
1. Get app path: get_mac_app_path({ scheme: '${params.scheme}' })
2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })`;
      } else if (platformOptions.platform === XcodePlatform.iOS) {
        additionalInfo = `Next Steps:
1. Get app path: get_device_app_path({ scheme: '${params.scheme}' })
2. Get bundle ID: get_app_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
3. Launch: launch_app_device({ bundleId: 'BUNDLE_ID_FROM_STEP_2' })`;
      } else if (isSimulatorPlatform) {
        const simIdParam = platformOptions.simulatorId ? 'simulatorId' : 'simulatorName';
        const simIdValue = platformOptions.simulatorId ?? platformOptions.simulatorName;

        additionalInfo = `Next Steps:
1. Get app path: get_sim_app_path({ ${simIdParam}: '${simIdValue}', scheme: '${params.scheme}', platform: 'iOS Simulator' })
2. Get bundle ID: get_app_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
3. Launch: launch_app_sim({ ${simIdParam}: '${simIdValue}', bundleId: 'BUNDLE_ID_FROM_STEP_2' })
   Or with logs: launch_app_logs_sim({ ${simIdParam}: '${simIdValue}', bundleId: 'BUNDLE_ID_FROM_STEP_2' })`;
      }
    }

    const successResponse: ToolResponse = {
      content: [
        ...buildMessages,
        {
          type: 'text',
          text: `✅ ${platformOptions.logPrefix} ${buildAction} succeeded for scheme ${params.scheme}.`,
        },
      ],
    };

    // Only add additional info if we have any
    if (additionalInfo) {
      successResponse.content.push({
        type: 'text',
        text: additionalInfo,
      });
    }

    return consolidateContentForClaudeCode(successResponse);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);

    const isSpawnError =
      error instanceof Error &&
      'code' in error &&
      ['ENOENT', 'EACCES', 'EPERM'].includes((error as NodeJS.ErrnoException).code ?? '');

    log('error', `Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`, {
      sentry: !isSpawnError,
    });

    return consolidateContentForClaudeCode(
      createTextResponse(
        `Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`,
        true,
      ),
    );
  }
}

```

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

```markdown
# TEST_RUNNER_ Environment Variables Implementation Plan

## Problem Statement

**GitHub Issue**: [#101 - Support TEST_RUNNER_ prefixed env vars](https://github.com/cameroncooke/XcodeBuildMCP/issues/101)

**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.

## Background Context

### xcodebuild Environment Variable Support

From the xcodebuild man page:
```
TEST_RUNNER_<VAR>   Set an environment variable whose name is prefixed
                    with TEST_RUNNER_ to have that variable passed, with
                    its prefix stripped, to all test runner processes
                    launched during a test action. For example,
                    TEST_RUNNER_Foo=Bar xcodebuild test ... sets the
                    environment variable Foo=Bar in the test runner's
                    environment.
```

### User Requirements

Users want to configure their MCP server with TEST_RUNNER_ prefixed environment variables:

```json
{
  "mcpServers": {
    "XcodeBuildMCP": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "xcodebuildmcp@latest"],
      "env": {
        "TEST_RUNNER_USE_DEV_MODE": "YES"
      }
    }
  }
}
```

And have tests that can conditionally execute based on these variables:

```swift
func testFoo() throws {
  let useDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES"
  guard useDevMode else {
    XCTFail("Test requires USE_DEV_MODE to be true")
    return
  }
  // Test logic here...
}
```

## Current Architecture Analysis

### XcodeBuildMCP Execution Flow
1. All Xcode commands flow through `executeXcodeBuildCommand()` function
2. Generic `CommandExecutor` interface handles all command execution
3. Test tools exist for device/simulator/macOS platforms
4. Zod schemas provide parameter validation and type safety

### Key Files in Current Architecture
- `src/utils/CommandExecutor.ts` - Command execution interface
- `src/utils/build-utils.ts` - Contains `executeXcodeBuildCommand`
- `src/mcp/tools/device/test_device.ts` - Device testing tool
- `src/mcp/tools/simulator/test_sim.ts` - Simulator testing tool  
- `src/mcp/tools/macos/test_macos.ts` - macOS testing tool
- `src/utils/test/index.ts` - Shared test logic for simulator

## Solution Analysis

### Design Options Considered

1. **Automatic Detection** (❌ Rejected)
   - Scan `process.env` for TEST_RUNNER_ variables and always pass them
   - **Issue**: Security risk of environment variable leakage
   - **Issue**: Unpredictable behavior based on server environment

2. **Explicit Parameter** (✅ Chosen)
   - Add `testRunnerEnv` parameter to test tools
   - Users explicitly specify which variables to pass
   - **Benefits**: Secure, predictable, well-validated

3. **Hybrid Approach** (🤔 Future Enhancement)
   - Both automatic + explicit with explicit overriding
   - **Issue**: Adds complexity, deferred for future consideration

### Expert Analysis Summary

**RepoPrompt Analysis**: Comprehensive architectural plan emphasizing security, type safety, and integration with existing patterns.

**Gemini Analysis**: Confirmed explicit approach as optimal, highlighting:
- Security benefits of explicit allow-list approach
- Architectural soundness of extending CommandExecutor
- Recommendation for automatic prefix handling for better UX

## Recommended Solution: Explicit Parameter with Automatic Prefix Handling

### Key Design Decisions

1. **Security-First**: Only explicitly provided variables are passed (no automatic process.env scanning)
2. **User Experience**: Automatic prefix handling - users provide unprefixed keys
3. **Architecture**: Extend execution layer generically for future extensibility  
4. **Validation**: Zod schema enforcement with proper type safety

### User Experience Design

**Input** (what users specify):
```json
{
  "testRunnerEnv": {
    "USE_DEV_MODE": "YES",
    "runsForEachTargetApplicationUIConfiguration": "NO"
  }
}
```

**Output** (what gets passed to xcodebuild):
```bash
TEST_RUNNER_USE_DEV_MODE=YES \
TEST_RUNNER_runsForEachTargetApplicationUIConfiguration=NO \
xcodebuild test ...
```

## Implementation Plan

### Phase 0: Test-Driven Development Setup

**Objective**: Create reproduction test to validate issue and later prove fix works

#### Tasks:
- [ ] Create test in `example_projects/iOS/MCPTest` that checks for environment variable
- [ ] Run current test tools to demonstrate limitation (test should fail)
- [ ] Document baseline behavior

**Test Code Example**:
```swift
func testEnvironmentVariablePassthrough() throws {
  let useDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES"
  guard useDevMode else {
    XCTFail("Test requires USE_DEV_MODE=YES via TEST_RUNNER_USE_DEV_MODE")
    return
  }
  XCTAssertTrue(true, "Environment variable successfully passed through")
}
```

### Phase 1: Core Infrastructure Updates

**Objective**: Extend CommandExecutor and build utilities to support environment variables

#### 1.1 Update CommandExecutor Interface

**File**: `src/utils/CommandExecutor.ts`

**Changes**:
- Add `CommandExecOptions` type for execution options
- Update `CommandExecutor` type signature to accept optional execution options

```typescript
export type CommandExecOptions = {
  cwd?: string;
  env?: Record<string, string | undefined>;
};

export type CommandExecutor = (
  args: string[],
  description?: string,
  quiet?: boolean,
  opts?: CommandExecOptions
) => Promise<CommandResponse>;
```

#### 1.2 Update Execution Facade

**File**: `src/utils/execution/index.ts`

**Changes**:
- Re-export `CommandExecOptions` type

```typescript
export type { CommandExecutor, CommandResponse, CommandExecOptions } from '../CommandExecutor.js';
```

#### 1.3 Update Default Command Executor

**File**: `src/utils/command.ts`

**Changes**:
- Modify `getDefaultCommandExecutor` to merge `opts.env` with `process.env` when spawning

```typescript
// In the returned function:
const env = { ...process.env, ...(opts?.env ?? {}) };
// Pass env and opts?.cwd to spawn/exec call
```

#### 1.4 Create Environment Variable Utility

**File**: `src/utils/environment.ts`

**Changes**:
- Add `normalizeTestRunnerEnv` function

```typescript
export function normalizeTestRunnerEnv(
  userVars?: Record<string, string | undefined>
): Record<string, string> {
  const result: Record<string, string> = {};
  if (userVars) {
    for (const [key, value] of Object.entries(userVars)) {
      if (value !== undefined) {
        result[`TEST_RUNNER_${key}`] = value;
      }
    }
  }
  return result;
}
```

#### 1.5 Update executeXcodeBuildCommand

**File**: `src/utils/build-utils.ts`

**Changes**:
- Add optional `execOpts?: CommandExecOptions` parameter (6th parameter)
- Pass execution options through to `CommandExecutor` calls

```typescript
export async function executeXcodeBuildCommand(
  build: { /* existing fields */ },
  runtime: { /* existing fields */ },
  preferXcodebuild = false,
  action: 'build' | 'test' | 'archive' | 'analyze' | string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  execOpts?: CommandExecOptions, // NEW
): Promise<ToolResponse>
```

### Phase 2: Test Tool Integration

**Objective**: Add `testRunnerEnv` parameter to all test tools and wire through execution

#### 2.1 Update Device Test Tool

**File**: `src/mcp/tools/device/test_device.ts`

**Changes**:
- Add `testRunnerEnv` to Zod schema with validation
- Import and use `normalizeTestRunnerEnv`
- Pass execution options to `executeXcodeBuildCommand`

**Schema Addition**:
```typescript
testRunnerEnv: z
  .record(z.string(), z.string().optional())
  .optional()
  .describe('Test runner environment variables (TEST_RUNNER_ prefix added automatically)')
```

**Usage**:
```typescript
const execEnv = normalizeTestRunnerEnv(params.testRunnerEnv);
const testResult = await executeXcodeBuildCommand(
  { /* build params */ },
  { /* runtime params */ },
  params.preferXcodebuild ?? false,
  'test',
  executor,
  { env: execEnv } // NEW
);
```

#### 2.2 Update macOS Test Tool

**File**: `src/mcp/tools/macos/test_macos.ts`

**Changes**: Same pattern as device test tool
- Schema addition for `testRunnerEnv`
- Import `normalizeTestRunnerEnv` 
- Pass execution options to `executeXcodeBuildCommand`

#### 2.3 Update Simulator Test Tool and Logic

**File**: `src/mcp/tools/simulator/test_sim.ts`

**Changes**:
- Add `testRunnerEnv` to schema
- Pass through to `handleTestLogic`

**File**: `src/utils/test/index.ts`

**Changes**:
- Update `handleTestLogic` signature to accept `testRunnerEnv?: Record<string, string | undefined>`
- Import and use `normalizeTestRunnerEnv`
- Pass execution options to `executeXcodeBuildCommand`

### Phase 3: Testing and Validation

**Objective**: Comprehensive testing coverage for new functionality

#### 3.1 Unit Tests

**File**: `src/utils/__tests__/environment.test.ts`

**Tests**:
- Test `normalizeTestRunnerEnv` with various inputs
- Verify prefix addition
- Verify undefined filtering
- Verify empty input handling

#### 3.2 Integration Tests  

**Files**: Update existing test files for test tools

**Tests**:
- Verify `testRunnerEnv` parameter is properly validated
- Verify environment variables are passed through `CommandExecutor`
- Mock executor to verify correct env object construction

#### 3.3 Tool Export Validation

**Files**: Test files in each tool directory

**Tests**:
- Verify schema exports include new `testRunnerEnv` field
- Verify parameter typing is correct

### Phase 4: End-to-End Validation

**Objective**: Prove the fix works with real xcodebuild scenarios

#### 4.1 Reproduction Test Validation

**Tasks**:
- Run reproduction test from Phase 0 with new `testRunnerEnv` parameter
- Verify test passes (proving env var was successfully passed)
- Document the before/after behavior

#### 4.2 Real-World Scenario Testing

**Tasks**:
- Test with actual iOS project using `runsForEachTargetApplicationUIConfiguration`
- Verify performance difference when variable is set
- Test with multiple environment variables
- Test edge cases (empty values, special characters)

## Security Considerations

### Security Benefits
- **No Environment Leakage**: Only explicit user-provided variables are passed
- **Command Injection Prevention**: Environment variables passed as separate object, not interpolated into command string
- **Input Validation**: Zod schemas prevent malformed inputs
- **Prefix Enforcement**: Only TEST_RUNNER_ prefixed variables can be set

### Security Best Practices
- Never log environment variable values (keys only for debugging)
- Filter out undefined values to prevent accidental exposure
- Validate all user inputs through Zod schemas
- Document supported TEST_RUNNER_ variables from Apple's documentation

## Architectural Benefits

### Clean Integration
- Extends existing `CommandExecutor` pattern generically
- Maintains backward compatibility (all existing calls remain valid)
- Follows established Zod validation patterns
- Consistent API across all test tools

### Future Extensibility  
- `CommandExecOptions` can support additional execution options (timeout, cwd, etc.)
- Pattern can be extended to other tools that need environment variables
- Generic approach allows for non-TEST_RUNNER_ use cases in the future

## File Modification Summary

### New Files
- `src/utils/__tests__/environment.test.ts` - Unit tests for environment utilities

### Modified Files
- `src/utils/CommandExecutor.ts` - Add execution options types
- `src/utils/execution/index.ts` - Re-export new types  
- `src/utils/command.ts` - Update default executor to handle env
- `src/utils/environment.ts` - Add `normalizeTestRunnerEnv` utility
- `src/utils/build-utils.ts` - Update `executeXcodeBuildCommand` signature
- `src/mcp/tools/device/test_device.ts` - Add schema and integration
- `src/mcp/tools/macos/test_macos.ts` - Add schema and integration
- `src/mcp/tools/simulator/test_sim.ts` - Add schema and pass-through
- `src/utils/test/index.ts` - Update `handleTestLogic` for simulator path
- Test files for each modified tool - Add validation tests

## Success Criteria

1. **Functionality**: Users can pass `testRunnerEnv` parameter to test tools and have variables appear in test runner environment
2. **Security**: No unintended environment variable leakage from server process
3. **Usability**: Users specify unprefixed variable names for better UX
4. **Compatibility**: All existing test tool calls continue to work unchanged
5. **Validation**: Comprehensive test coverage proves the feature works end-to-end

## Future Enhancements (Out of Scope)

1. **Configuration Profiles**: Allow users to define common TEST_RUNNER_ variable sets in config files
2. **Variable Discovery**: Help users discover available TEST_RUNNER_ variables
3. **Build Tool Support**: Extend to build tools if Apple adds similar BUILD_RUNNER_ support
4. **Performance Monitoring**: Track impact of environment variable passing on build times

## Implementation Timeline

- **Phase 0**: 1-2 hours (reproduction test setup)
- **Phase 1**: 4-6 hours (infrastructure changes)
- **Phase 2**: 3-4 hours (tool integration)
- **Phase 3**: 4-5 hours (testing)  
- **Phase 4**: 2-3 hours (validation)

**Total Estimated Time**: 14-20 hours

## Conclusion

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.

The plan leverages XcodeBuildMCP's existing patterns and provides a foundation for future environment variable needs across the tool ecosystem.
```

--------------------------------------------------------------------------------
/docs/dev/RELOADEROO.md:
--------------------------------------------------------------------------------

```markdown
# Reloaderoo Integration Guide

This guide explains how to use Reloaderoo v1.1.2+ for testing and developing XcodeBuildMCP with both CLI inspection tools and transparent proxy capabilities.

## Overview

**Reloaderoo** is a dual-mode MCP development tool that operates as both a CLI inspection tool and a transparent proxy server for the Model Context Protocol (MCP). It provides two distinct operational modes for different development workflows.

## Installation

Reloaderoo is available via npm and can be used with npx for universal compatibility.

```bash
# Use npx to run reloaderoo (works on any system)
npx reloaderoo@latest --help

# Or install globally if preferred
npm install -g reloaderoo
reloaderoo --help
```

## Two Operational Modes

### 🔍 **CLI Mode** (Inspection & Testing)

Direct command-line access to MCP servers without client setup - perfect for testing and debugging:

**Key Benefits:**
- ✅ **One-shot commands** - Test tools, list resources, get server info
- ✅ **No MCP client required** - Perfect for testing and debugging
- ✅ **Raw JSON output** - Ideal for scripts and automation  
- ✅ **8 inspection commands** - Complete MCP protocol coverage
- ✅ **AI agent friendly** - Designed for terminal-based AI development workflows

**Basic Commands:**

```bash
# List all available tools
npx reloaderoo@latest inspect list-tools -- node build/index.js

# Call any tool with parameters  
npx reloaderoo@latest inspect call-tool <tool_name> --params '<json>' -- node build/index.js

# Get server information
npx reloaderoo@latest inspect server-info -- node build/index.js

# List available resources
npx reloaderoo@latest inspect list-resources -- node build/index.js

# Read a specific resource
npx reloaderoo@latest inspect read-resource "<uri>" -- node build/index.js

# List available prompts
npx reloaderoo@latest inspect list-prompts -- node build/index.js

# Get a specific prompt
npx reloaderoo@latest inspect get-prompt <name> --args '<json>' -- node build/index.js

# Check server connectivity
npx reloaderoo@latest inspect ping -- node build/index.js
```

**Example Tool Calls:**

```bash
# List connected devices
npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js

# Get doctor information
npx reloaderoo@latest inspect call-tool doctor --params '{}' -- node build/index.js

# List iOS simulators
npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/index.js

# Read devices resource
npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -- node build/index.js
```

### 🔄 **Proxy Mode** (Hot-Reload Development)

Transparent MCP proxy server that enables seamless hot-reloading during development:

**Key Benefits:**
- ✅ **Hot-reload MCP servers** without disconnecting your AI client
- ✅ **Session persistence** - Keep your development context intact
- ✅ **Automatic `restart_server` tool** - AI agents can restart servers on demand
- ✅ **Transparent forwarding** - Full MCP protocol passthrough
- ✅ **Process management** - Spawns, monitors, and restarts your server process

**Usage:**

```bash
# Start proxy mode (your AI client connects to this)
npx reloaderoo@latest proxy -- node build/index.js

# With debug logging
npx reloaderoo@latest proxy --log-level debug -- node build/index.js

# Then in your AI session, request:
# "Please restart the MCP server to load my latest changes"
```

The AI agent will automatically call the `restart_server` tool, preserving your session while reloading code changes.

## MCP Inspection Server Mode

Start CLI mode as a persistent MCP server for interactive debugging through MCP clients:

```bash
# Start reloaderoo in CLI mode as an MCP server
npx reloaderoo@latest inspect mcp -- node build/index.js
```

This runs CLI mode as a persistent MCP server, exposing 8 debug tools through the MCP protocol:
- `list_tools` - List all server tools
- `call_tool` - Call any server tool
- `list_resources` - List all server resources  
- `read_resource` - Read any server resource
- `list_prompts` - List all server prompts
- `get_prompt` - Get any server prompt
- `get_server_info` - Get comprehensive server information
- `ping` - Test server connectivity

## Claude Code Compatibility

When running under Claude Code, XcodeBuildMCP automatically detects the environment and consolidates multiple content blocks into single responses with `---` separators.

**Automatic Detection Methods:**
1. **Environment Variables**: `CLAUDECODE=1` or `CLAUDE_CODE_ENTRYPOINT=cli`
2. **Parent Process Analysis**: Checks if parent process contains 'claude'
3. **Graceful Fallback**: Falls back to environment variables if process detection fails

**No Configuration Required**: The consolidation happens automatically when Claude Code is detected.

## Command Reference

### Command Structure

```bash
npx reloaderoo@latest [options] [command]

Two modes, one tool:
• Proxy MCP server that adds support for hot-reloading MCP servers.
• CLI tool for inspecting MCP servers.

Global Options:
  -V, --version    Output the version number
  -h, --help       Display help for command

Commands:
  proxy [options]  🔄 Run as MCP proxy server (default behavior)
  inspect          🔍 Inspect and debug MCP servers
  info [options]   📊 Display version and configuration information
  help [command]   ❓ Display help for command
```

### 🔄 **Proxy Mode Commands**

```bash
npx reloaderoo@latest proxy [options] -- <child-command> [child-args...]

Options:
  -w, --working-dir <directory>    Working directory for the child process
  -l, --log-level <level>          Log level (debug, info, notice, warning, error, critical)
  -f, --log-file <path>            Custom log file path (logs to stderr by default)
  -t, --restart-timeout <ms>       Timeout for restart operations (default: 30000ms)
  -m, --max-restarts <number>      Maximum restart attempts (0-10, default: 3)
  -d, --restart-delay <ms>         Delay between restart attempts (default: 1000ms)
  -q, --quiet                      Suppress non-essential output
  --no-auto-restart                Disable automatic restart on crashes
  --debug                          Enable debug mode with verbose logging
  --dry-run                        Validate configuration without starting proxy

Examples:
  npx reloaderoo proxy -- node build/index.js
  npx reloaderoo -- node build/index.js                    # Same as above (proxy is default)
  npx reloaderoo proxy --log-level debug -- node build/index.js
```

### 🔍 **CLI Mode Commands**

```bash
npx reloaderoo@latest inspect [subcommand] [options] -- <child-command> [child-args...]

Subcommands:
  server-info [options]            Get server information and capabilities
  list-tools [options]             List all available tools
  call-tool [options] <name>       Call a specific tool
  list-resources [options]         List all available resources
  read-resource [options] <uri>    Read a specific resource
  list-prompts [options]           List all available prompts
  get-prompt [options] <name>      Get a specific prompt
  ping [options]                   Check server connectivity

Examples:
  npx reloaderoo@latest inspect list-tools -- node build/index.js
  npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
  npx reloaderoo@latest inspect server-info -- node build/index.js
```

### **Info Command**

```bash
npx reloaderoo@latest info [options]

Options:
  -v, --verbose                    Show detailed information
  -h, --help                       Display help for command
  
Examples:
  npx reloaderoo@latest info              # Show basic system information
  npx reloaderoo@latest info --verbose    # Show detailed system information
```

### Response Format

All CLI commands return structured JSON:

```json
{
  "success": true,
  "data": {
    // Command-specific response data
  },
  "metadata": {
    "command": "call-tool:list_devices",
    "timestamp": "2025-07-25T08:32:47.042Z",
    "duration": 1782
  }
}
```

### Error Handling

When commands fail, you'll receive:

```json
{
  "success": false,
  "error": {
    "message": "Error description",
    "code": "ERROR_CODE"
  },
  "metadata": {
    "command": "failed-command",
    "timestamp": "2025-07-25T08:32:47.042Z",
    "duration": 100
  }
}
```

## Development Workflow

### 🔍 **CLI Mode Workflow** (Testing & Debugging)

Perfect for testing individual tools or debugging server issues without MCP client setup:

```bash
# 1. Build XcodeBuildMCP
npm run build

# 2. Test your server quickly
npx reloaderoo@latest inspect list-tools -- node build/index.js

# 3. Call specific tools to verify behavior
npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js

# 4. Check server health and resources
npx reloaderoo@latest inspect ping -- node build/index.js
npx reloaderoo@latest inspect list-resources -- node build/index.js
```

### 🔄 **Proxy Mode Workflow** (Hot-Reload Development)

For full development sessions with AI clients that need persistent connections:

#### 1. **Start Development Session**
Configure your AI client to connect to reloaderoo proxy instead of your server directly:
```bash
npx reloaderoo@latest proxy -- node build/index.js
# or with debug logging:
npx reloaderoo@latest proxy --log-level debug -- node build/index.js
```

#### 2. **Develop Your MCP Server**
Work on your XcodeBuildMCP code as usual - make changes, add tools, modify functionality.

#### 3. **Test Changes Instantly**
```bash
# Rebuild your changes
npm run build

# Then ask your AI agent to restart the server:
# "Please restart the MCP server to load my latest changes"
```

The agent will call the `restart_server` tool automatically. Your new capabilities are immediately available!

#### 4. **Continue Development**
Your AI session continues with the updated server capabilities. No connection loss, no context reset.

### 🛠️ **MCP Inspection Server** (Interactive CLI Debugging)

For interactive debugging through MCP clients:

```bash
# Start reloaderoo CLI mode as an MCP server
npx reloaderoo@latest inspect mcp -- node build/index.js

# Then connect with an MCP client to access debug tools
# Available tools: list_tools, call_tool, list_resources, etc.
```

## Troubleshooting

### 🔄 **Proxy Mode Issues**

**Server won't start in proxy mode:**
```bash
# Check if XcodeBuildMCP runs independently first
node build/index.js

# Then try with reloaderoo proxy to validate configuration
npx reloaderoo@latest proxy -- node build/index.js
```

**Connection problems with MCP clients:**
```bash
# Enable debug logging to see what's happening
npx reloaderoo@latest proxy --log-level debug -- node build/index.js

# Check system info and configuration
npx reloaderoo@latest info --verbose
```

**Restart failures in proxy mode:**
```bash
# Increase restart timeout
npx reloaderoo@latest proxy --restart-timeout 60000 -- node build/index.js

# Check restart limits  
npx reloaderoo@latest proxy --max-restarts 5 -- node build/index.js
```

### 🔍 **CLI Mode Issues**

**CLI commands failing:**
```bash
# Test basic connectivity first
npx reloaderoo@latest inspect ping -- node build/index.js

# Enable debug logging for CLI commands (via proxy debug mode)
npx reloaderoo@latest proxy --log-level debug -- node build/index.js
```

**JSON parsing errors:**
```bash
# Check server information for troubleshooting
npx reloaderoo@latest inspect server-info -- node build/index.js

# Ensure your server outputs valid JSON
node build/index.js | head -10
```

### **General Issues**

**Command not found:**
```bash
# Ensure npx can find reloaderoo
npx reloaderoo@latest --help

# If that fails, try installing globally
npm install -g reloaderoo
```

**Parameter validation:**
```bash
# Ensure JSON parameters are properly quoted
npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
```

### **General Debug Mode**

```bash
# Get detailed information about what's happening
npx reloaderoo@latest proxy --debug -- node build/index.js  # For proxy mode
npx reloaderoo@latest proxy --log-level debug -- node build/index.js  # For detailed proxy logging

# View system information
npx reloaderoo@latest info --verbose
```

### Debug Tips

1. **Always build first**: Run `npm run build` before testing
2. **Check tool names**: Use `inspect list-tools` to see exact tool names
3. **Validate JSON**: Ensure parameters are valid JSON strings
4. **Enable debug logging**: Use `--log-level debug` or `--debug` for verbose output
5. **Test connectivity**: Use `inspect ping` to verify server communication

## Advanced Usage

### Environment Variables

Configure reloaderoo behavior via environment variables:

```bash
# Logging Configuration
export MCPDEV_PROXY_LOG_LEVEL=debug           # Log level (debug, info, notice, warning, error, critical)
export MCPDEV_PROXY_LOG_FILE=/path/to/log     # Custom log file path (default: stderr)
export MCPDEV_PROXY_DEBUG_MODE=true           # Enable debug mode (true/false)

# Process Management
export MCPDEV_PROXY_RESTART_LIMIT=5           # Maximum restart attempts (0-10, default: 3)
export MCPDEV_PROXY_AUTO_RESTART=true         # Enable/disable auto-restart (true/false)
export MCPDEV_PROXY_TIMEOUT=30000             # Operation timeout in milliseconds
export MCPDEV_PROXY_RESTART_DELAY=1000        # Delay between restart attempts in milliseconds
export MCPDEV_PROXY_CWD=/path/to/directory     # Default working directory
```

### Custom Working Directory

```bash
npx reloaderoo@latest proxy --working-dir /custom/path -- node build/index.js
npx reloaderoo@latest inspect list-tools --working-dir /custom/path -- node build/index.js
```

### Timeout Configuration

```bash
npx reloaderoo@latest proxy --restart-timeout 60000 -- node build/index.js
```

## Integration with XcodeBuildMCP

Reloaderoo is specifically configured to work with XcodeBuildMCP's:

- **84+ Tools**: All workflow groups accessible via CLI
- **4 Resources**: Direct access to devices, simulators, environment, swift-packages
- **Claude Code Detection**: Automatic consolidation of multiple content blocks
- **Hot-Reload Support**: Seamless development workflow with `restart_server`

For more information about XcodeBuildMCP's architecture and capabilities, see:
- [Architecture Guide](ARCHITECTURE.md)
- [Plugin Development Guide](PLUGIN_DEVELOPMENT.md)
- [Testing Guide](TESTING.md)

```

--------------------------------------------------------------------------------
/scripts/analysis/tools-analysis.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

/**
 * XcodeBuildMCP Tools Analysis
 *
 * Core TypeScript module for analyzing XcodeBuildMCP tools using AST parsing.
 * Provides reliable extraction of tool information without fallback strategies.
 */

import {
  createSourceFile,
  forEachChild,
  isExportAssignment,
  isIdentifier,
  isNoSubstitutionTemplateLiteral,
  isObjectLiteralExpression,
  isPropertyAssignment,
  isStringLiteral,
  isTemplateExpression,
  isVariableDeclaration,
  isVariableStatement,
  type Node,
  type ObjectLiteralExpression,
  ScriptTarget,
  type SourceFile,
  SyntaxKind,
} from 'typescript';
import * as fs from 'fs';
import * as path from 'path';
import { glob } from 'glob';
import { fileURLToPath } from 'url';

// Get project root
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..', '..');
const toolsDir = path.join(projectRoot, 'src', 'mcp', 'tools');

export interface ToolInfo {
  name: string;
  workflow: string;
  path: string;
  relativePath: string;
  description: string;
  isCanonical: boolean;
}

export interface WorkflowInfo {
  name: string;
  displayName: string;
  description: string;
  tools: ToolInfo[];
  toolCount: number;
  canonicalCount: number;
  reExportCount: number;
}

export interface AnalysisStats {
  totalTools: number;
  canonicalTools: number;
  reExportTools: number;
  workflowCount: number;
}

export interface StaticAnalysisResult {
  workflows: WorkflowInfo[];
  tools: ToolInfo[];
  stats: AnalysisStats;
}

/**
 * Extract the description from a tool's default export using TypeScript AST
 */
function extractToolDescription(sourceFile: SourceFile): string {
  let description: string | null = null;

  function visit(node: Node): void {
    let objectExpression: ObjectLiteralExpression | null = null;

    // Look for export default { ... } - the standard TypeScript pattern
    // isExportEquals is undefined for `export default` and true for `export = `
    if (isExportAssignment(node) && !node.isExportEquals) {
      if (isObjectLiteralExpression(node.expression)) {
        objectExpression = node.expression;
      }
    }

    if (objectExpression) {
      // Found export default { ... }, now look for description property
      for (const property of objectExpression.properties) {
        if (
          isPropertyAssignment(property) &&
          isIdentifier(property.name) &&
          property.name.text === 'description'
        ) {
          // Extract the description value
          if (isStringLiteral(property.initializer)) {
            // This is the most common case - simple string literal
            description = property.initializer.text;
          } else if (
            isTemplateExpression(property.initializer) ||
            isNoSubstitutionTemplateLiteral(property.initializer)
          ) {
            // Handle template literals - get the raw text and clean it
            description = property.initializer.getFullText(sourceFile).trim();
            // Remove surrounding backticks
            if (description.startsWith('`') && description.endsWith('`')) {
              description = description.slice(1, -1);
            }
          } else {
            // Handle any other expression (multiline strings, computed values)
            const fullText = property.initializer.getFullText(sourceFile).trim();
            // This covers cases where the description spans multiple lines
            // Remove surrounding quotes and normalize whitespace
            let cleaned = fullText;
            if (
              (cleaned.startsWith('"') && cleaned.endsWith('"')) ||
              (cleaned.startsWith("'") && cleaned.endsWith("'"))
            ) {
              cleaned = cleaned.slice(1, -1);
            }
            // Collapse multiple whitespaces and newlines into single spaces
            description = cleaned.replace(/\s+/g, ' ').trim();
          }
          return; // Found description, stop looking
        }
      }
    }

    forEachChild(node, visit);
  }

  visit(sourceFile);

  if (description === null) {
    throw new Error('Could not extract description from tool export default object');
  }

  return description;
}

/**
 * Check if a file is a re-export by examining its content
 */
function isReExportFile(filePath: string): boolean {
  const content = fs.readFileSync(filePath, 'utf-8');

  // Remove comments and empty lines, then check for re-export pattern
  // First remove multi-line comments
  const contentWithoutBlockComments = content.replace(/\/\*[\s\S]*?\*\//g, '');

  const cleanedLines = contentWithoutBlockComments
    .split('\n')
    .map((line) => {
      // Remove inline comments but preserve the code before them
      const codeBeforeComment = line.split('//')[0].trim();
      return codeBeforeComment;
    })
    .filter((line) => line.length > 0);

  // Should have exactly one line: export { default } from '...';
  if (cleanedLines.length !== 1) {
    return false;
  }

  const exportLine = cleanedLines[0];
  return /^export\s*{\s*default\s*}\s*from\s*['"][^'"]+['"];?\s*$/.test(exportLine);
}

/**
 * Get workflow metadata from index.ts file if it exists
 */
async function getWorkflowMetadata(
  workflowDir: string,
): Promise<{ displayName: string; description: string } | null> {
  const indexPath = path.join(toolsDir, workflowDir, 'index.ts');

  if (!fs.existsSync(indexPath)) {
    return null;
  }

  try {
    const content = fs.readFileSync(indexPath, 'utf-8');
    const sourceFile = createSourceFile(indexPath, content, ScriptTarget.Latest, true);

    const workflowExport: { name?: string; description?: string } = {};

    function visit(node: Node): void {
      // Look for: export const workflow = { ... }
      if (
        isVariableStatement(node) &&
        node.modifiers?.some((mod) => mod.kind === SyntaxKind.ExportKeyword)
      ) {
        for (const declaration of node.declarationList.declarations) {
          if (
            isVariableDeclaration(declaration) &&
            isIdentifier(declaration.name) &&
            declaration.name.text === 'workflow' &&
            declaration.initializer &&
            isObjectLiteralExpression(declaration.initializer)
          ) {
            // Extract name and description properties
            for (const property of declaration.initializer.properties) {
              if (isPropertyAssignment(property) && isIdentifier(property.name)) {
                const propertyName = property.name.text;

                if (propertyName === 'name' && isStringLiteral(property.initializer)) {
                  workflowExport.name = property.initializer.text;
                } else if (
                  propertyName === 'description' &&
                  isStringLiteral(property.initializer)
                ) {
                  workflowExport.description = property.initializer.text;
                }
              }
            }
          }
        }
      }

      forEachChild(node, visit);
    }

    visit(sourceFile);

    if (workflowExport.name && workflowExport.description) {
      return {
        displayName: workflowExport.name,
        description: workflowExport.description,
      };
    }
  } catch (error) {
    console.error(`Warning: Could not parse workflow metadata from ${indexPath}: ${error}`);
  }

  return null;
}

/**
 * Get a human-readable workflow name from directory name
 */
function getWorkflowDisplayName(workflowDir: string): string {
  const displayNames: Record<string, string> = {
    device: 'iOS Device Development',
    doctor: 'System Doctor',
    logging: 'Logging & Monitoring',
    macos: 'macOS Development',
    'project-discovery': 'Project Discovery',
    'project-scaffolding': 'Project Scaffolding',
    simulator: 'iOS Simulator Development',
    'simulator-management': 'Simulator Management',
    'swift-package': 'Swift Package Manager',
    'ui-testing': 'UI Testing & Automation',
    utilities: 'Utilities',
  };

  return displayNames[workflowDir] || workflowDir;
}

/**
 * Get workflow description
 */
function getWorkflowDescription(workflowDir: string): string {
  const descriptions: Record<string, string> = {
    device: 'Physical device development, testing, and deployment',
    doctor: 'System health checks and environment validation',
    logging: 'Log capture and monitoring across platforms',
    macos: 'Native macOS application development and testing',
    'project-discovery': 'Project analysis and information gathering',
    'project-scaffolding': 'Create new projects from templates',
    simulator: 'Simulator-based development, testing, and deployment',
    'simulator-management': 'Simulator environment and configuration management',
    'swift-package': 'Swift Package development and testing',
    'ui-testing': 'Automated UI interaction and testing',
    utilities: 'General utility operations',
  };

  return descriptions[workflowDir] || `${workflowDir} related tools`;
}

/**
 * Perform static analysis of all tools in the project
 */
export async function getStaticToolAnalysis(): Promise<StaticAnalysisResult> {
  // Find all workflow directories
  const workflowDirs = fs
    .readdirSync(toolsDir, { withFileTypes: true })
    .filter((dirent) => dirent.isDirectory())
    .map((dirent) => dirent.name)
    .sort();

  // Find all tool files
  const files = await glob('**/*.ts', {
    cwd: toolsDir,
    ignore: [
      '**/__tests__/**',
      '**/index.ts',
      '**/*.test.ts',
      '**/lib/**',
      '**/*-processes.ts', // Process management utilities
      '**/*.deps.ts', // Dependency files
      '**/*-utils.ts', // Utility files
      '**/*-common.ts', // Common/shared code
      '**/*-types.ts', // Type definition files
    ],
    absolute: true,
  });

  const allTools: ToolInfo[] = [];
  const workflowMap = new Map<string, ToolInfo[]>();

  let canonicalCount = 0;
  let reExportCount = 0;

  // Initialize workflow map
  for (const workflowDir of workflowDirs) {
    workflowMap.set(workflowDir, []);
  }

  // Process each tool file
  for (const filePath of files) {
    const toolName = path.basename(filePath, '.ts');
    const workflowDir = path.basename(path.dirname(filePath));
    const relativePath = path.relative(projectRoot, filePath);

    const isReExport = isReExportFile(filePath);

    let description = '';

    if (!isReExport) {
      // Extract description from canonical tool using AST
      try {
        const content = fs.readFileSync(filePath, 'utf-8');
        const sourceFile = createSourceFile(filePath, content, ScriptTarget.Latest, true);

        description = extractToolDescription(sourceFile);
        canonicalCount++;
      } catch (error) {
        throw new Error(`Failed to extract description from ${relativePath}: ${error}`);
      }
    } else {
      description = '(Re-exported from shared workflow)';
      reExportCount++;
    }

    const toolInfo: ToolInfo = {
      name: toolName,
      workflow: workflowDir,
      path: filePath,
      relativePath,
      description,
      isCanonical: !isReExport,
    };

    allTools.push(toolInfo);

    const workflowTools = workflowMap.get(workflowDir);
    if (workflowTools) {
      workflowTools.push(toolInfo);
    }
  }

  // Build workflow information
  const workflows: WorkflowInfo[] = [];

  for (const workflowDir of workflowDirs) {
    const workflowTools = workflowMap.get(workflowDir) ?? [];
    const canonicalTools = workflowTools.filter((t) => t.isCanonical);
    const reExportTools = workflowTools.filter((t) => !t.isCanonical);

    // Try to get metadata from index.ts, fall back to hardcoded names/descriptions
    const metadata = await getWorkflowMetadata(workflowDir);

    const workflowInfo: WorkflowInfo = {
      name: workflowDir,
      displayName: metadata?.displayName ?? getWorkflowDisplayName(workflowDir),
      description: metadata?.description ?? getWorkflowDescription(workflowDir),
      tools: workflowTools.sort((a, b) => a.name.localeCompare(b.name)),
      toolCount: workflowTools.length,
      canonicalCount: canonicalTools.length,
      reExportCount: reExportTools.length,
    };

    workflows.push(workflowInfo);
  }

  const stats: AnalysisStats = {
    totalTools: allTools.length,
    canonicalTools: canonicalCount,
    reExportTools: reExportCount,
    workflowCount: workflows.length,
  };

  return {
    workflows: workflows.sort((a, b) => a.displayName.localeCompare(b.displayName)),
    tools: allTools.sort((a, b) => a.name.localeCompare(b.name)),
    stats,
  };
}

/**
 * Get only canonical tools (excluding re-exports) for documentation generation
 */
export async function getCanonicalTools(): Promise<ToolInfo[]> {
  const analysis = await getStaticToolAnalysis();
  return analysis.tools.filter((tool) => tool.isCanonical);
}

/**
 * Get tools grouped by workflow for documentation generation
 */
export async function getToolsByWorkflow(): Promise<Map<string, ToolInfo[]>> {
  const analysis = await getStaticToolAnalysis();
  const workflowMap = new Map<string, ToolInfo[]>();

  for (const workflow of analysis.workflows) {
    // Only include canonical tools for documentation
    const canonicalTools = workflow.tools.filter((tool) => tool.isCanonical);
    if (canonicalTools.length > 0) {
      workflowMap.set(workflow.name, canonicalTools);
    }
  }

  return workflowMap;
}

// CLI support - if run directly, perform analysis and output results
if (import.meta.url === `file://${process.argv[1]}`) {
  async function main(): Promise<void> {
    try {
      console.log('🔍 Performing static analysis...');
      const analysis = await getStaticToolAnalysis();

      console.log('\n📊 Analysis Results:');
      console.log(`   Workflows: ${analysis.stats.workflowCount}`);
      console.log(`   Total tools: ${analysis.stats.totalTools}`);
      console.log(`   Canonical tools: ${analysis.stats.canonicalTools}`);
      console.log(`   Re-export tools: ${analysis.stats.reExportTools}`);

      if (process.argv.includes('--json')) {
        console.log('\n' + JSON.stringify(analysis, null, 2));
      } else {
        console.log('\n📂 Workflows:');
        for (const workflow of analysis.workflows) {
          console.log(
            `   • ${workflow.displayName} (${workflow.canonicalCount} canonical, ${workflow.reExportCount} re-exports)`,
          );
        }
      }
    } catch (error) {
      console.error('❌ Analysis failed:', error);
      process.exit(1);
    }
  }

  main();
}

```

--------------------------------------------------------------------------------
/example_projects/iOS_Calculator/CalculatorAppPackage/Tests/CalculatorAppFeatureTests/CalculatorServiceTests.swift:
--------------------------------------------------------------------------------

```swift
import Testing
import Foundation
@testable import CalculatorAppFeature

// MARK: - Calculator Basic Tests
@Suite("Calculator Basic Functionality")
struct CalculatorBasicTests {
    
    @Test("Calculator initializes with correct default values")
    func testInitialState() {
        let calculator = CalculatorService()
        #expect(calculator.display == "0")
        #expect(calculator.currentValue == 0)
        #expect(calculator.previousValue == 0)
        #expect(calculator.currentOperation == nil)
        #expect(calculator.willResetDisplay == false)
    }
    
    @Test("Clear function resets calculator to initial state")
    func testClear() {
        let calculator = CalculatorService()
        calculator.inputNumber("5")
        calculator.setOperation(.add)
        calculator.inputNumber("3")
        
        calculator.clear()
        
        #expect(calculator.display == "0")
        #expect(calculator.currentValue == 0)
        #expect(calculator.previousValue == 0)
    }
    
    @Test("This test should fail to verify error reporting")
    func testIntentionalFailure() {
        let calculator = CalculatorService()
        // This test is designed to fail to test error reporting
        #expect(calculator.display == "999", "This should fail - display should be 0, not 999")
        #expect(calculator.currentOperation == nil)
        #expect(calculator.willResetDisplay == false)
    }
}

// MARK: - Number Input Tests
@Suite("Number Input")
struct NumberInputTests {
    
    @Test("Adding single digit numbers")
    func testSingleDigitInput() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("5")
        #expect(calculator.display == "5")
        #expect(calculator.currentValue == 5)
    }
    
    @Test("Adding multiple digit numbers")
    func testMultipleDigitInput() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("1")
        calculator.inputNumber("2")
        calculator.inputNumber("3")
        
        #expect(calculator.display == "123")
        #expect(calculator.currentValue == 123)
    }
    
    @Test("Adding decimal numbers")
    func testDecimalInput() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("1")
        calculator.inputDecimal()
        calculator.inputNumber("5")
        
        #expect(calculator.display == "1.5")
        #expect(calculator.currentValue == 1.5)
    }
    
    @Test("Multiple decimal points should be ignored")
    func testMultipleDecimalPoints() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("1")
        calculator.inputDecimal()
        calculator.inputNumber("5")
        calculator.inputDecimal() // This should be ignored
        calculator.inputNumber("2")
        
        #expect(calculator.display == "1.52")
        #expect(calculator.currentValue == 1.52)
    }
    
    @Test("Decimal point at start creates 0.")
    func testDecimalAtStart() {
        let calculator = CalculatorService()
        
        calculator.inputDecimal()
        calculator.inputNumber("5")
        
        #expect(calculator.display == "0.5")
        #expect(calculator.currentValue == 0.5)
    }
}

// MARK: - Operation Tests
@Suite("Mathematical Operations")
struct OperationTests {
    
    @Test("Addition operation", arguments: [
        (5.0, 3.0, 8.0),
        (10.0, -2.0, 8.0),
        (0.0, 5.0, 5.0),
        (-3.0, -7.0, -10.0)
    ])
    func testAddition(a: Double, b: Double, expected: Double) {
        let result = CalculatorService.Operation.add.calculate(a, b)
        #expect(result == expected)
    }
    
    @Test("Subtraction operation", arguments: [
        (10.0, 3.0, 7.0),
        (5.0, 8.0, -3.0),
        (0.0, 5.0, -5.0),
        (-3.0, -7.0, 4.0)
    ])
    func testSubtraction(a: Double, b: Double, expected: Double) {
        let result = CalculatorService.Operation.subtract.calculate(a, b)
        #expect(result == expected)
    }
    
    @Test("Multiplication operation", arguments: [
        (5.0, 3.0, 15.0),
        (4.0, -2.0, -8.0),
        (0.0, 5.0, 0.0),
        (-3.0, -7.0, 21.0)
    ])
    func testMultiplication(a: Double, b: Double, expected: Double) {
        let result = CalculatorService.Operation.multiply.calculate(a, b)
        #expect(result == expected)
    }
    
    @Test("Division operation", arguments: [
        (10.0, 2.0, 5.0),
        (15.0, 3.0, 5.0),
        (-8.0, 2.0, -4.0),
        (7.0, 2.0, 3.5)
    ])
    func testDivision(a: Double, b: Double, expected: Double) {
        let result = CalculatorService.Operation.divide.calculate(a, b)
        #expect(result == expected)
    }
    
    @Test("Division by zero returns zero")
    func testDivisionByZero() {
        let result = CalculatorService.Operation.divide.calculate(10.0, 0.0)
        #expect(result == 0.0)
    }
}

// MARK: - Calculator Integration Tests
@Suite("Calculator Integration Tests")
struct CalculatorIntegrationTests {
    
    @Test("Simple addition calculation")
    func testSimpleAddition() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("5")
        calculator.setOperation(.add)
        calculator.inputNumber("3")
        calculator.calculate()
        
        #expect(calculator.display == "8")
        #expect(calculator.currentValue == 8)
    }
    
    @Test("Chain calculations")
    func testChainCalculations() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("5")
        calculator.setOperation(.add)
        calculator.inputNumber("3")
        calculator.setOperation(.multiply) // Should calculate 5+3=8 first
        calculator.inputNumber("2")
        calculator.calculate()
        
        #expect(calculator.currentValue == 16) // (5+3) * 2 = 16
    }
    
    @Test("Complex calculation sequence")
    func testComplexCalculation() {
        let calculator = CalculatorService()
        
        // Calculate: 10 + 5 * 2 - 3
        calculator.inputNumber("1")
        calculator.inputNumber("0")
        calculator.setOperation(.add)
        calculator.inputNumber("5")
        calculator.setOperation(.multiply)
        calculator.inputNumber("2")
        calculator.setOperation(.subtract)
        calculator.inputNumber("3")
        calculator.calculate()
        
        #expect(calculator.currentValue == 27) // ((10+5)*2)-3 = 27
    }

    @Test("Repetitive equals press repeats last operation")
    func testRepetitiveEquals() {
        let calculator = CalculatorService()

        calculator.inputNumber("5")
        calculator.setOperation(.add)
        calculator.inputNumber("3")
        calculator.calculate() // 5 + 3 = 8

        #expect(calculator.currentValue == 8)

        calculator.calculate() // Should be 8 + 3 = 11
        #expect(calculator.currentValue == 11)

        calculator.calculate() // Should be 11 + 3 = 14
        #expect(calculator.currentValue == 14)
    }

    @Test("Expression display updates correctly")
    func testExpressionDisplay() {
        let calculator = CalculatorService()

        calculator.inputNumber("1")
        calculator.inputNumber("2")
        #expect(calculator.expressionDisplay == "")

        calculator.setOperation(.add)
        #expect(calculator.expressionDisplay == "12 +")

        calculator.inputNumber("3")
        #expect(calculator.expressionDisplay == "12 +") 

        calculator.calculate()
        #expect(calculator.expressionDisplay == "12 + 3 =")
    }
}

// MARK: - Special Functions Tests
@Suite("Special Functions")
struct SpecialFunctionsTests {
    
    @Test("Toggle sign on positive number")
    func testToggleSignPositive() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("5")
        calculator.toggleSign()
        
        #expect(calculator.display == "-5")
        #expect(calculator.currentValue == -5)
    }
    
    @Test("Toggle sign on negative number")
    func testToggleSignNegative() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("5")
        calculator.toggleSign()
        calculator.toggleSign()
        
        #expect(calculator.display == "5")
        #expect(calculator.currentValue == 5)
    }
    
    @Test("Toggle sign on zero has no effect")
    func testToggleSignZero() {
        let calculator = CalculatorService()
        
        calculator.toggleSign()
        
        #expect(calculator.display == "0")
        #expect(calculator.currentValue == 0)
    }
    
    @Test("Percentage calculation", arguments: [
        ("100", 1.0),
        ("50", 0.5),
        ("25", 0.25),
        ("200", 2.0)
    ])
    func testPercentage(input: String, expected: Double) {
        let calculator = CalculatorService()
        
        calculator.inputNumber(input)
        calculator.percentage()
        
        #expect(calculator.currentValue == expected)
    }
}

// MARK: - Input Handler Tests
@Suite("Input Handler Integration")
struct InputHandlerTests {
    
    @Test("Number input through handler")
    func testNumberInputThroughHandler() {
        let calculator = CalculatorService()
        let handler = CalculatorInputHandler(service: calculator)
        
        handler.handleInput("1")
        handler.handleInput("2")
        handler.handleInput("3")
        
        #expect(calculator.display == "123")
    }
    
    @Test("Operation input through handler")
    func testOperationInputThroughHandler() {
        let calculator = CalculatorService()
        let handler = CalculatorInputHandler(service: calculator)
        
        handler.handleInput("5")
        handler.handleInput("+")
        handler.handleInput("3")
        handler.handleInput("=")
        
        #expect(calculator.currentValue == 8)
    }
    
    @Test("Clear input through handler")
    func testClearInputThroughHandler() {
        let calculator = CalculatorService()
        let handler = CalculatorInputHandler(service: calculator)
        
        handler.handleInput("5")
        handler.handleInput("+")
        handler.handleInput("3")
        handler.handleInput("C")
        
        #expect(calculator.display == "0")
        #expect(calculator.currentValue == 0)
    }
    
    @Test("Decimal input through handler")
    func testDecimalInputThroughHandler() {
        let calculator = CalculatorService()
        let handler = CalculatorInputHandler(service: calculator)
        
        handler.handleInput("1")
        handler.handleInput(".")
        handler.handleInput("5")
        
        #expect(calculator.display == "1.5")
    }
}

// MARK: - Edge Cases Tests
@Suite("Edge Cases")
struct EdgeCaseTests {
    
    @Test("Calculate without setting operation")
    func testCalculateWithoutOperation() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("5")
        calculator.calculate()
        
        #expect(calculator.currentValue == 5) // Should remain unchanged
    }
    
    @Test("Setting operation without previous number")
    func testOperationWithoutPreviousNumber() {
        let calculator = CalculatorService()
        
        calculator.setOperation(.add)
        calculator.inputNumber("5")
        calculator.calculate()
        
        #expect(calculator.currentValue == 5) // 0 + 5 = 5
    }
    
    @Test("Multiple equals presses")
    func testMultipleEquals() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("5")
        calculator.setOperation(.add)
        calculator.inputNumber("3")
        calculator.calculate()
        
        let firstResult = calculator.currentValue
        calculator.calculate() // Second equals press
        
        #expect(firstResult == 8)
        #expect(calculator.currentValue == 11) // Should repeat last operation: 8 + 3 = 11
    }
}

// MARK: - Error Handling Tests
@Suite("Error Handling")
struct ErrorHandlingTests {
    
    @Test("Calculator handles invalid input gracefully")
    func testInvalidInputHandling() {
        let calculator = CalculatorService()
        let handler = CalculatorInputHandler(service: calculator)
        
        // Test pressing operation without any number
        handler.handleInput("+")
        handler.handleInput("5")
        handler.handleInput("=")
        
        #expect(calculator.currentValue == 5) // Should be 0 + 5 = 5
    }
    
    @Test("Calculator state after multiple clears")
    func testMultipleClearOperations() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("123")
        calculator.setOperation(.add)
        calculator.inputNumber("456")
        
        // Multiple clear operations
        calculator.clear()
        calculator.clear()
        calculator.clear()
        
        #expect(calculator.display == "0")
        #expect(calculator.currentValue == 0)
        #expect(calculator.currentOperation == nil)
    }
    
    @Test("Large number error handling")
    func testLargeNumberError() {
        let calculator = CalculatorService()
        calculator.inputNumber("1000000000000") // 1e12
        calculator.setOperation(.multiply)
        calculator.inputNumber("2")
        calculator.calculate()

        #expect(calculator.hasError == true)
        #expect(calculator.display == "Error")
        #expect(calculator.expressionDisplay == "Number too large")
    }
}

// MARK: - Decimal Edge Cases
@Suite("Decimal Edge Cases")
struct DecimalEdgeCaseTests {
    
    @Test("Very small decimal numbers")
    func testVerySmallDecimals() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("0")
        calculator.inputDecimal()
        calculator.inputNumber("0")
        calculator.inputNumber("0")
        calculator.inputNumber("1")
        
        #expect(calculator.display == "0.001")
        #expect(calculator.currentValue == 0.001)
    }
    
    @Test("Decimal operations precision")
    func testDecimalPrecision() {
        let calculator = CalculatorService()
        
        calculator.inputNumber("0")
        calculator.inputDecimal()
        calculator.inputNumber("1")
        calculator.setOperation(.add)
        calculator.inputNumber("0")
        calculator.inputDecimal()
        calculator.inputNumber("2")
        calculator.calculate()
        
        // 0.1 + 0.2 should equal 0.3 (within floating point precision)
        #expect(abs(calculator.currentValue - 0.3) < 0.0001)
    }
}

```

--------------------------------------------------------------------------------
/docs/dev/RELOADEROO_FOR_XCODEBUILDMCP.md:
--------------------------------------------------------------------------------

```markdown
# Reloaderoo Usage Guide for XcodeBuildMCP

This guide explains how to use Reloaderoo for interacting with XcodeBuildMCP as a CLI to save context window space.

You can use this guide to prompt your agent, but providing the entire document will give you no actual benefits. You will end up using more context than just using MCP server directly. So it's recommended that you curate this document by removing the example commands that you don't need and just keeping the ones that are right for your project. You'll then want to keep this file within your project workspace and then include it in the context window when you need to interact your agent to use XcodeBuildMCP tools.

> [!IMPORTANT]
> Please remove this introduction before you prompt your agent with this file or any derrived version of it.

## Installation

Reloaderoo is available via npm and can be used with npx for universal compatibility.

```bash
# Use npx to run reloaderoo
npx reloaderoo@latest --help
```

**Example Tool Calls:**

### iOS Device Development

- **`build_device`**: Builds an app for a physical device.
  ```bash
  npx reloaderoo@latest inspect call-tool build_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
  ```
- **`get_device_app_path`**: Gets the `.app` bundle path for a device build.
  ```bash
  npx reloaderoo@latest inspect call-tool get_device_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
  ```
- **`install_app_device`**: Installs an app on a physical device.
  ```bash
  npx reloaderoo@latest inspect call-tool install_app_device --params '{"deviceId": "DEVICE_UDID", "appPath": "/path/to/MyApp.app"}' -- node build/index.js
  ```
- **`launch_app_device`**: Launches an app on a physical device.
  ```bash
  npx reloaderoo@latest inspect call-tool launch_app_device --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -- node build/index.js
  ```
- **`list_devices`**: Lists connected physical devices.
  ```bash
  npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
  ```
- **`stop_app_device`**: Stops an app on a physical device.
  ```bash
  npx reloaderoo@latest inspect call-tool stop_app_device --params '{"deviceId": "DEVICE_UDID", "processId": 12345}' -- node build/index.js
  ```
- **`test_device`**: Runs tests on a physical device.
  ```bash
  npx reloaderoo@latest inspect call-tool test_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "deviceId": "DEVICE_UDID"}' -- node build/index.js
  ```

### iOS Simulator Development

- **`boot_sim`**: Boots a simulator.
  ```bash
  npx reloaderoo@latest inspect call-tool boot_sim --params '{"simulatorId": "SIMULATOR_UUID"}' -- node build/index.js
  ```
- **`build_run_sim`**: Builds and runs an app on a simulator.
  ```bash
  npx reloaderoo@latest inspect call-tool build_run_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js
  ```
- **`build_sim`**: Builds an app for a simulator.
  ```bash
  npx reloaderoo@latest inspect call-tool build_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js
  ```
- **`get_sim_app_path`**: Gets the `.app` bundle path for a simulator build.
  ```bash
  npx reloaderoo@latest inspect call-tool get_sim_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "platform": "iOS Simulator", "simulatorName": "iPhone 16"}' -- node build/index.js
  ```
- **`install_app_sim`**: Installs an app on a simulator.
  ```bash
  npx reloaderoo@latest inspect call-tool install_app_sim --params '{"simulatorId": "SIMULATOR_UUID", "appPath": "/path/to/MyApp.app"}' -- node build/index.js
  ```
- **`launch_app_logs_sim`**: Launches an app on a simulator with log capture.
  ```bash
  npx reloaderoo@latest inspect call-tool launch_app_logs_sim --params '{"simulatorId": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -- node build/index.js
  ```
- **`launch_app_sim`**: Launches an app on a simulator.
  ```bash
  npx reloaderoo@latest inspect call-tool launch_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -- node build/index.js
  ```
- **`list_sims`**: Lists available simulators.
  ```bash
  npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/index.js
  ```
- **`open_sim`**: Opens the Simulator application.
  ```bash
  npx reloaderoo@latest inspect call-tool open_sim --params '{}' -- node build/index.js
  ```
- **`stop_app_sim`**: Stops an app on a simulator.
  ```bash
  npx reloaderoo@latest inspect call-tool stop_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -- node build/index.js
  ```
- **`test_sim`**: Runs tests on a simulator.
  ```bash
  npx reloaderoo@latest inspect call-tool test_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js
  ```

### Log Capture & Management

- **`start_device_log_cap`**: Starts log capture for a physical device.
  ```bash
  npx reloaderoo@latest inspect call-tool start_device_log_cap --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -- node build/index.js
  ```
- **`start_sim_log_cap`**: Starts log capture for a simulator.
  ```bash
  npx reloaderoo@latest inspect call-tool start_sim_log_cap --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -- node build/index.js
  ```
- **`stop_device_log_cap`**: Stops log capture for a physical device.
  ```bash
  npx reloaderoo@latest inspect call-tool stop_device_log_cap --params '{"logSessionId": "SESSION_ID"}' -- node build/index.js
  ```
- **`stop_sim_log_cap`**: Stops log capture for a simulator.
  ```bash
  npx reloaderoo@latest inspect call-tool stop_sim_log_cap --params '{"logSessionId": "SESSION_ID"}' -- node build/index.js
  ```

### macOS Development

- **`build_macos`**: Builds a macOS app.
  ```bash
  npx reloaderoo@latest inspect call-tool build_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
  ```
- **`build_run_macos`**: Builds and runs a macOS app.
  ```bash
  npx reloaderoo@latest inspect call-tool build_run_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
  ```
- **`get_mac_app_path`**: Gets the `.app` bundle path for a macOS build.
  ```bash
  npx reloaderoo@latest inspect call-tool get_mac_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
  ```
- **`launch_mac_app`**: Launches a macOS app.
  ```bash
  npx reloaderoo@latest inspect call-tool launch_mac_app --params '{"appPath": "/Applications/Calculator.app"}' -- node build/index.js
  ```
- **`stop_mac_app`**: Stops a macOS app.
  ```bash
  npx reloaderoo@latest inspect call-tool stop_mac_app --params '{"appName": "Calculator"}' -- node build/index.js
  ```
- **`test_macos`**: Runs tests for a macOS project.
  ```bash
  npx reloaderoo@latest inspect call-tool test_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
  ```

### Project Discovery

- **`discover_projs`**: Discovers Xcode projects and workspaces.
  ```bash
  npx reloaderoo@latest inspect call-tool discover_projs --params '{"workspaceRoot": "/path/to/workspace"}' -- node build/index.js
  ```
- **`get_app_bundle_id`**: Gets an app's bundle identifier.
  ```bash
  npx reloaderoo@latest inspect call-tool get_app_bundle_id --params '{"appPath": "/path/to/MyApp.app"}' -- node build/index.js
  ```
- **`get_mac_bundle_id`**: Gets a macOS app's bundle identifier.
  ```bash
  npx reloaderoo@latest inspect call-tool get_mac_bundle_id --params '{"appPath": "/Applications/Calculator.app"}' -- node build/index.js
  ```
- **`list_schemes`**: Lists schemes in a project or workspace.
  ```bash
  npx reloaderoo@latest inspect call-tool list_schemes --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -- node build/index.js
  ```
- **`show_build_settings`**: Shows build settings for a scheme.
  ```bash
  npx reloaderoo@latest inspect call-tool show_build_settings --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
  ```

### Project Scaffolding

- **`scaffold_ios_project`**: Scaffolds a new iOS project.
  ```bash
  npx reloaderoo@latest inspect call-tool scaffold_ios_project --params '{"projectName": "MyNewApp", "outputPath": "/path/to/projects"}' -- node build/index.js
  ```
- **`scaffold_macos_project`**: Scaffolds a new macOS project.
  ```bash
  npx reloaderoo@latest inspect call-tool scaffold_macos_project --params '{"projectName": "MyNewMacApp", "outputPath": "/path/to/projects"}' -- node build/index.js
  ```

### Project Utilities

- **`clean`**: Cleans build artifacts.
  ```bash
  # For a project
  npx reloaderoo@latest inspect call-tool clean --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -- node build/index.js
  # For a workspace
  npx reloaderoo@latest inspect call-tool clean --params '{"workspacePath": "/path/to/MyWorkspace.xcworkspace", "scheme": "MyScheme"}' -- node build/index.js
  ```

### Simulator Management

- **`reset_sim_location`**: Resets a simulator's location.
  ```bash
  npx reloaderoo@latest inspect call-tool reset_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js
  ```
- **`set_sim_appearance`**: Sets a simulator's appearance (dark/light mode).
  ```bash
  npx reloaderoo@latest inspect call-tool set_sim_appearance --params '{"simulatorUuid": "SIMULATOR_UUID", "mode": "dark"}' -- node build/index.js
  ```
- **`set_sim_location`**: Sets a simulator's GPS location.
  ```bash
  npx reloaderoo@latest inspect call-tool set_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID", "latitude": 37.7749, "longitude": -122.4194}' -- node build/index.js
  ```
- **`sim_statusbar`**: Overrides a simulator's status bar.
  ```bash
  npx reloaderoo@latest inspect call-tool sim_statusbar --params '{"simulatorUuid": "SIMULATOR_UUID", "dataNetwork": "wifi"}' -- node build/index.js
  ```

### Swift Package Manager

- **`swift_package_build`**: Builds a Swift package.
  ```bash
  npx reloaderoo@latest inspect call-tool swift_package_build --params '{"packagePath": "/path/to/package"}' -- node build/index.js
  ```
- **`swift_package_clean`**: Cleans a Swift package.
  ```bash
  npx reloaderoo@latest inspect call-tool swift_package_clean --params '{"packagePath": "/path/to/package"}' -- node build/index.js
  ```
- **`swift_package_list`**: Lists running Swift package processes.
  ```bash
  npx reloaderoo@latest inspect call-tool swift_package_list --params '{}' -- node build/index.js
  ```
- **`swift_package_run`**: Runs a Swift package executable.
  ```bash
  npx reloaderoo@latest inspect call-tool swift_package_run --params '{"packagePath": "/path/to/package"}' -- node build/index.js
  ```
- **`swift_package_stop`**: Stops a running Swift package process.
  ```bash
  npx reloaderoo@latest inspect call-tool swift_package_stop --params '{"pid": 12345}' -- node build/index.js
  ```
- **`swift_package_test`**: Tests a Swift package.
  ```bash
  npx reloaderoo@latest inspect call-tool swift_package_test --params '{"packagePath": "/path/to/package"}' -- node build/index.js
  ```

### System Doctor

- **`doctor`**: Runs system diagnostics.
  ```bash
  npx reloaderoo@latest inspect call-tool doctor --params '{}' -- node build/index.js
  ```

### UI Testing & Automation

- **`button`**: Simulates a hardware button press.
  ```bash
  npx reloaderoo@latest inspect call-tool button --params '{"simulatorUuid": "SIMULATOR_UUID", "buttonType": "home"}' -- node build/index.js
  ```
- **`describe_ui`**: Gets the UI hierarchy of the current screen.
  ```bash
  npx reloaderoo@latest inspect call-tool describe_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js
  ```
- **`gesture`**: Performs a pre-defined gesture.
  ```bash
  npx reloaderoo@latest inspect call-tool gesture --params '{"simulatorUuid": "SIMULATOR_UUID", "preset": "scroll-up"}' -- node build/index.js
  ```
- **`key_press`**: Simulates a key press.
  ```bash
  npx reloaderoo@latest inspect call-tool key_press --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCode": 40}' -- node build/index.js
  ```
- **`key_sequence`**: Simulates a sequence of key presses.
  ```bash
  npx reloaderoo@latest inspect call-tool key_sequence --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCodes": [40, 42, 44]}' -- node build/index.js
  ```
- **`long_press`**: Performs a long press at coordinates.
  ```bash
  npx reloaderoo@latest inspect call-tool long_press --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "duration": 1500}' -- node build/index.js
  ```
- **`screenshot`**: Takes a screenshot.
  ```bash
  npx reloaderoo@latest inspect call-tool screenshot --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js
  ```
- **`swipe`**: Performs a swipe gesture.
  ```bash
  npx reloaderoo@latest inspect call-tool swipe --params '{"simulatorUuid": "SIMULATOR_UUID", "x1": 100, "y1": 200, "x2": 100, "y2": 400}' -- node build/index.js
  ```
- **`tap`**: Performs a tap at coordinates.
  ```bash
  npx reloaderoo@latest inspect call-tool tap --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200}' -- node build/index.js
  ```
- **`touch`**: Simulates a touch down or up event.
  ```bash
  npx reloaderoo@latest inspect call-tool touch --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "down": true}' -- node build/index.js
  ```
- **`type_text`**: Types text into the focused element.
  ```bash
  npx reloaderoo@latest inspect call-tool type_text --params '{"simulatorUuid": "SIMULATOR_UUID", "text": "Hello, World!"}' -- node build/index.js
  ```

### Resources

- **Read devices resource**:
  ```bash
  npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -- node build/index.js
  ```
- **Read simulators resource**:
  ```bash
  npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -- node build/index.js
  ```
- **Read doctor resource**:
  ```bash
  npx reloaderoo@latest inspect read-resource "xcodebuildmcp://doctor" -- node build/index.js
  ```

```

--------------------------------------------------------------------------------
/src/mcp/tools/macos/__tests__/build_macos.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for build_macos plugin (unified)
 * Following CLAUDE.md testing standards with literal validation
 * Using pure dependency injection for deterministic testing
 * NO VITEST MOCKING ALLOWED - Only createMockExecutor and createMockFileSystemExecutor
 */

import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
import buildMacOS, { buildMacOSLogic } from '../build_macos.ts';

describe('build_macos plugin', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(buildMacOS.name).toBe('build_macos');
    });

    it('should have correct description', () => {
      expect(buildMacOS.description).toBe('Builds a macOS app.');
    });

    it('should have handler function', () => {
      expect(typeof buildMacOS.handler).toBe('function');
    });

    it('should validate schema correctly', () => {
      const schema = z.object(buildMacOS.schema);

      expect(schema.safeParse({}).success).toBe(true);
      expect(
        schema.safeParse({
          derivedDataPath: '/path/to/derived-data',
          extraArgs: ['--arg1', '--arg2'],
          preferXcodebuild: true,
        }).success,
      ).toBe(true);

      expect(schema.safeParse({ derivedDataPath: 42 }).success).toBe(false);
      expect(schema.safeParse({ extraArgs: ['--ok', 1] }).success).toBe(false);
      expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);

      const schemaKeys = Object.keys(buildMacOS.schema).sort();
      expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort());
    });
  });

  describe('Handler Requirements', () => {
    it('should require scheme when no defaults provided', async () => {
      const result = await buildMacOS.handler({});

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('scheme is required');
      expect(result.content[0].text).toContain('session-set-defaults');
    });

    it('should require project or workspace once scheme default exists', async () => {
      sessionStore.setDefaults({ scheme: 'MyScheme' });

      const result = await buildMacOS.handler({});

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Provide a project or workspace');
    });

    it('should reject when both projectPath and workspacePath provided explicitly', async () => {
      sessionStore.setDefaults({ scheme: 'MyScheme' });

      const result = await buildMacOS.handler({
        projectPath: '/path/to/project.xcodeproj',
        workspacePath: '/path/to/workspace.xcworkspace',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
      expect(result.content[0].text).toContain('projectPath');
      expect(result.content[0].text).toContain('workspacePath');
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should return exact successful build response', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'BUILD SUCCEEDED',
      });

      const result = await buildMacOSLogic(
        {
          projectPath: '/path/to/MyProject.xcodeproj',
          scheme: 'MyScheme',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ macOS Build build succeeded for scheme MyScheme.',
          },
          {
            type: 'text',
            text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyScheme' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })",
          },
        ],
      });
    });

    it('should return exact build failure response', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'error: Compilation error in main.swift',
      });

      const result = await buildMacOSLogic(
        {
          projectPath: '/path/to/MyProject.xcodeproj',
          scheme: 'MyScheme',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '❌ [stderr] error: Compilation error in main.swift',
          },
          {
            type: 'text',
            text: '❌ macOS Build build failed for scheme MyScheme.',
          },
        ],
        isError: true,
      });
    });

    it('should return exact successful build response with optional parameters', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'BUILD SUCCEEDED',
      });

      const result = await buildMacOSLogic(
        {
          projectPath: '/path/to/MyProject.xcodeproj',
          scheme: 'MyScheme',
          configuration: 'Release',
          arch: 'arm64',
          derivedDataPath: '/path/to/derived-data',
          extraArgs: ['--verbose'],
          preferXcodebuild: true,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ macOS Build build succeeded for scheme MyScheme.',
          },
          {
            type: 'text',
            text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyScheme' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })",
          },
        ],
      });
    });

    it('should return exact exception handling response', async () => {
      // Create executor that throws error during command execution
      // This will be caught by executeXcodeBuildCommand's try-catch block
      const mockExecutor = async () => {
        throw new Error('Network error');
      };

      const result = await buildMacOSLogic(
        {
          projectPath: '/path/to/MyProject.xcodeproj',
          scheme: 'MyScheme',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error during macOS Build build: Network error',
          },
        ],
        isError: true,
      });
    });

    it('should return exact spawn error handling response', async () => {
      // Create executor that throws spawn error during command execution
      // This will be caught by executeXcodeBuildCommand's try-catch block
      const mockExecutor = async () => {
        throw new Error('Spawn error');
      };

      const result = await buildMacOSLogic(
        {
          projectPath: '/path/to/MyProject.xcodeproj',
          scheme: 'MyScheme',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error during macOS Build build: Spawn error',
          },
        ],
        isError: true,
      });
    });
  });

  describe('Command Generation', () => {
    it('should generate correct xcodebuild command with minimal parameters', async () => {
      let capturedCommand: string[] = [];
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      // Override the executor to capture the command
      const spyExecutor = async (command: string[]) => {
        capturedCommand = command;
        return mockExecutor(command);
      };

      const result = await buildMacOSLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
        },
        spyExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcodebuild',
        '-project',
        '/path/to/project.xcodeproj',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=macOS',
        'build',
      ]);
    });

    it('should generate correct xcodebuild command with all parameters', async () => {
      let capturedCommand: string[] = [];
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      // Override the executor to capture the command
      const spyExecutor = async (command: string[]) => {
        capturedCommand = command;
        return mockExecutor(command);
      };

      const result = await buildMacOSLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          configuration: 'Release',
          arch: 'x86_64',
          derivedDataPath: '/custom/derived',
          extraArgs: ['--verbose'],
          preferXcodebuild: true,
        },
        spyExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcodebuild',
        '-project',
        '/path/to/project.xcodeproj',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Release',
        '-skipMacroValidation',
        '-destination',
        'platform=macOS,arch=x86_64',
        '-derivedDataPath',
        '/custom/derived',
        '--verbose',
        'build',
      ]);
    });

    it('should generate correct xcodebuild command with only derivedDataPath', async () => {
      let capturedCommand: string[] = [];
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      // Override the executor to capture the command
      const spyExecutor = async (command: string[]) => {
        capturedCommand = command;
        return mockExecutor(command);
      };

      const result = await buildMacOSLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          derivedDataPath: '/custom/derived/data',
        },
        spyExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcodebuild',
        '-project',
        '/path/to/project.xcodeproj',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=macOS',
        '-derivedDataPath',
        '/custom/derived/data',
        'build',
      ]);
    });

    it('should generate correct xcodebuild command with arm64 architecture only', async () => {
      let capturedCommand: string[] = [];
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      // Override the executor to capture the command
      const spyExecutor = async (command: string[]) => {
        capturedCommand = command;
        return mockExecutor(command);
      };

      const result = await buildMacOSLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          arch: 'arm64',
        },
        spyExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcodebuild',
        '-project',
        '/path/to/project.xcodeproj',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=macOS,arch=arm64',
        'build',
      ]);
    });

    it('should handle paths with spaces in command generation', async () => {
      let capturedCommand: string[] = [];
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      // Override the executor to capture the command
      const spyExecutor = async (command: string[]) => {
        capturedCommand = command;
        return mockExecutor(command);
      };

      const result = await buildMacOSLogic(
        {
          projectPath: '/Users/dev/My Project/MyProject.xcodeproj',
          scheme: 'MyScheme',
        },
        spyExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcodebuild',
        '-project',
        '/Users/dev/My Project/MyProject.xcodeproj',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=macOS',
        'build',
      ]);
    });

    it('should generate correct xcodebuild workspace command with minimal parameters', async () => {
      let capturedCommand: string[] = [];
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      // Override the executor to capture the command
      const spyExecutor = async (command: string[]) => {
        capturedCommand = command;
        return mockExecutor(command);
      };

      const result = await buildMacOSLogic(
        {
          workspacePath: '/path/to/workspace.xcworkspace',
          scheme: 'MyScheme',
        },
        spyExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/workspace.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=macOS',
        'build',
      ]);
    });
  });

  describe('XOR Validation', () => {
    it('should error when neither projectPath nor workspacePath provided', async () => {
      const result = await buildMacOS.handler({ scheme: 'MyScheme' });
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Provide a project or workspace');
    });

    it('should error when both projectPath and workspacePath provided', async () => {
      const result = await buildMacOS.handler({
        projectPath: '/path/to/project.xcodeproj',
        workspacePath: '/path/to/workspace.xcworkspace',
        scheme: 'MyScheme',
      });
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
    });

    it('should succeed with valid projectPath', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'BUILD SUCCEEDED',
      });

      const result = await buildMacOSLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
        },
        mockExecutor,
      );

      expect(result.isError).toBeUndefined();
    });

    it('should succeed with valid workspacePath', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'BUILD SUCCEEDED',
      });

      const result = await buildMacOSLogic(
        {
          workspacePath: '/path/to/workspace.xcworkspace',
          scheme: 'MyScheme',
        },
        mockExecutor,
      );

      expect(result.isError).toBeUndefined();
    });
  });
});

```

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

```typescript
/**
 * Test for scaffold_macos_project plugin - Dependency Injection Architecture
 *
 * Tests the plugin structure and exported components for scaffold_macos_project tool.
 * Uses pure dependency injection with createMockFileSystemExecutor.
 * NO VITEST MOCKING ALLOWED - Only createMockExecutor/createMockFileSystemExecutor
 *
 * Plugin location: plugins/utilities/scaffold_macos_project.js
 */

import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import {
  createMockFileSystemExecutor,
  createNoopExecutor,
  createMockExecutor,
  createMockCommandResponse,
} from '../../../../test-utils/mock-executors.ts';
import plugin, { scaffold_macos_projectLogic } from '../scaffold_macos_project.ts';
import { TemplateManager } from '../../../../utils/template/index.ts';

// ONLY ALLOWED MOCKING: createMockFileSystemExecutor

describe('scaffold_macos_project plugin', () => {
  let mockFileSystemExecutor: ReturnType<typeof createMockFileSystemExecutor>;
  let templateManagerStub: {
    getTemplatePath: (
      platform: string,
      commandExecutor?: unknown,
      fileSystemExecutor?: unknown,
    ) => Promise<string>;
    cleanup: (path: string) => Promise<void>;
    setError: (error: Error | string | null) => void;
    getCalls: () => string;
    resetCalls: () => void;
  };

  beforeEach(async () => {
    // Create template manager stub using pure JavaScript approach
    let templateManagerCall = '';
    let templateManagerError: Error | string | null = null;

    templateManagerStub = {
      getTemplatePath: async (
        platform: string,
        commandExecutor?: unknown,
        fileSystemExecutor?: unknown,
      ) => {
        templateManagerCall = `getTemplatePath(${platform})`;
        if (templateManagerError) {
          throw templateManagerError;
        }
        return '/tmp/test-templates/macos';
      },
      cleanup: async (path: string) => {
        templateManagerCall += `,cleanup(${path})`;
        return undefined;
      },
      // Test helpers
      setError: (error: Error | string | null) => {
        templateManagerError = error;
      },
      getCalls: () => templateManagerCall,
      resetCalls: () => {
        templateManagerCall = '';
      },
    };

    // Create fresh mock file system executor for each test
    mockFileSystemExecutor = createMockFileSystemExecutor({
      existsSync: () => false,
      mkdir: async () => {},
      cp: async () => {},
      readFile: async () => 'template content with MyProject placeholder',
      writeFile: async () => {},
      readdir: async () => [
        { name: 'Package.swift', isDirectory: () => false, isFile: () => true },
        { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true },
      ],
    });

    // Replace the real TemplateManager with our stub for most tests
    (TemplateManager as any).getTemplatePath = templateManagerStub.getTemplatePath;
    (TemplateManager as any).cleanup = templateManagerStub.cleanup;
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name field', () => {
      expect(plugin.name).toBe('scaffold_macos_project');
    });

    it('should have correct description field', () => {
      expect(plugin.description).toBe(
        'Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration.',
      );
    });

    it('should have handler as function', () => {
      expect(typeof plugin.handler).toBe('function');
    });

    it('should have valid schema with required fields', () => {
      // Test the schema object exists
      expect(plugin.schema).toBeDefined();
      expect(plugin.schema.projectName).toBeDefined();
      expect(plugin.schema.outputPath).toBeDefined();
      expect(plugin.schema.bundleIdentifier).toBeDefined();
      expect(plugin.schema.customizeNames).toBeDefined();
      expect(plugin.schema.deploymentTarget).toBeDefined();
    });
  });

  describe('Command Generation', () => {
    it('should generate correct curl command for macOS template download', async () => {
      // This test validates that the curl command would be generated correctly
      // by verifying the URL construction logic
      const expectedUrl =
        'https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/';

      // The curl command should be structured correctly for macOS template
      expect(expectedUrl).toContain('XcodeBuildMCP-macOS-Template');
      expect(expectedUrl).toContain('releases/download');

      // The template zip file should follow the expected pattern
      const expectedFilename = 'template.zip';
      expect(expectedFilename).toMatch(/template\.zip$/);

      // The curl command flags should be correct
      const expectedCurlFlags = ['-L', '-f', '-o'];
      expect(expectedCurlFlags).toContain('-L'); // Follow redirects
      expect(expectedCurlFlags).toContain('-f'); // Fail on HTTP errors
      expect(expectedCurlFlags).toContain('-o'); // Output to file
    });

    it('should generate correct unzip command for template extraction', async () => {
      // This test validates that the unzip command would be generated correctly
      // by verifying the command structure
      const expectedUnzipCommand = ['unzip', '-q', 'template.zip'];

      // The unzip command should use the quiet flag
      expect(expectedUnzipCommand).toContain('-q');

      // The unzip command should target the template zip file
      expect(expectedUnzipCommand).toContain('template.zip');

      // The unzip command should be structured correctly
      expect(expectedUnzipCommand[0]).toBe('unzip');
      expect(expectedUnzipCommand[1]).toBe('-q');
      expect(expectedUnzipCommand[2]).toMatch(/template\.zip$/);
    });

    it('should generate correct commands for template with version', async () => {
      // This test validates that the curl command would be generated correctly with version
      const testVersion = 'v1.0.0';
      const expectedUrlWithVersion = `https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/${testVersion}/`;

      // The URL should contain the specific version
      expect(expectedUrlWithVersion).toContain(testVersion);
      expect(expectedUrlWithVersion).toContain('XcodeBuildMCP-macOS-Template');
      expect(expectedUrlWithVersion).toContain('releases/download');

      // The version should be in the correct format
      expect(testVersion).toMatch(/^v\d+\.\d+\.\d+$/);

      // The full URL should be correctly constructed
      expect(expectedUrlWithVersion).toBe(
        `https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/${testVersion}/`,
      );
    });

    it('should not generate commands when using local template path', async () => {
      let capturedCommands: string[][] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommands.push(command);
        return createMockCommandResponse({
          success: true,
          output: 'Command successful',
        });
      };

      // Store original environment variable
      const originalEnv = process.env.XCODEBUILDMCP_MACOS_TEMPLATE_PATH;

      // Mock local template path exists
      mockFileSystemExecutor.existsSync = (path: string) => {
        return path === '/local/template/path' || path === '/local/template/path/template';
      };

      // Set environment variable for local template path
      process.env.XCODEBUILDMCP_MACOS_TEMPLATE_PATH = '/local/template/path';

      // Restore original TemplateManager for command generation tests
      const { TemplateManager: OriginalTemplateManager } = await import(
        '../../../../utils/template/index.ts'
      );
      (TemplateManager as any).getTemplatePath = OriginalTemplateManager.getTemplatePath;
      (TemplateManager as any).cleanup = OriginalTemplateManager.cleanup;

      await scaffold_macos_projectLogic(
        {
          projectName: 'TestMacApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        trackingExecutor,
        mockFileSystemExecutor,
      );

      // Should not generate any curl or unzip commands when using local template
      expect(capturedCommands).not.toContainEqual(
        expect.arrayContaining(['curl', expect.anything(), expect.anything()]),
      );
      expect(capturedCommands).not.toContainEqual(
        expect.arrayContaining(['unzip', expect.anything(), expect.anything()]),
      );

      // Clean up environment variable
      process.env.XCODEBUILDMCP_MACOS_TEMPLATE_PATH = originalEnv;

      // Restore stub after test
      (TemplateManager as any).getTemplatePath = templateManagerStub.getTemplatePath;
      (TemplateManager as any).cleanup = templateManagerStub.cleanup;
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should return success response for valid scaffold macOS project request', async () => {
      const result = await scaffold_macos_projectLogic(
        {
          projectName: 'TestMacApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
          bundleIdentifier: 'com.test.macapp',
        },
        createNoopExecutor(),
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: true,
                projectPath: '/tmp/test-projects',
                platform: 'macOS',
                message: 'Successfully scaffolded macOS project "TestMacApp" in /tmp/test-projects',
                nextSteps: [
                  'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
                  'Build for macOS: build_macos({ workspacePath: "/tmp/test-projects/TestMacApp.xcworkspace", scheme: "TestMacApp" })',
                  'Build & Run on macOS: build_run_macos({ workspacePath: "/tmp/test-projects/TestMacApp.xcworkspace", scheme: "TestMacApp" })',
                ],
              },
              null,
              2,
            ),
          },
        ],
      });

      // Verify template manager calls using manual tracking
      expect(templateManagerStub.getCalls()).toBe(
        'getTemplatePath(macOS),cleanup(/tmp/test-templates/macos)',
      );
    });

    it('should return success response with customizeNames false', async () => {
      const result = await scaffold_macos_projectLogic(
        {
          projectName: 'TestMacApp',
          outputPath: '/tmp/test-projects',
          customizeNames: false,
        },
        createNoopExecutor(),
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: true,
                projectPath: '/tmp/test-projects',
                platform: 'macOS',
                message: 'Successfully scaffolded macOS project "TestMacApp" in /tmp/test-projects',
                nextSteps: [
                  'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
                  'Build for macOS: build_macos({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject" })',
                  'Build & Run on macOS: build_run_macos({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject" })',
                ],
              },
              null,
              2,
            ),
          },
        ],
      });
    });

    it('should return error response for invalid project name', async () => {
      const result = await scaffold_macos_projectLogic(
        {
          projectName: '123InvalidName',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        createNoopExecutor(),
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: false,
                error:
                  'Project name must start with a letter and contain only letters, numbers, and underscores',
              },
              null,
              2,
            ),
          },
        ],
        isError: true,
      });
    });

    it('should return error response for existing project files', async () => {
      // Override existsSync to return true for workspace file
      mockFileSystemExecutor.existsSync = () => true;

      const result = await scaffold_macos_projectLogic(
        {
          projectName: 'TestMacApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        createNoopExecutor(),
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: false,
                error: 'Xcode project files already exist in /tmp/test-projects',
              },
              null,
              2,
            ),
          },
        ],
        isError: true,
      });
    });

    it('should return error response for template manager failure', async () => {
      templateManagerStub.setError(new Error('Template not found'));

      const result = await scaffold_macos_projectLogic(
        {
          projectName: 'TestMacApp',
          customizeNames: true,
          outputPath: '/tmp/test-projects',
        },
        createNoopExecutor(),
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(
              {
                success: false,
                error: 'Failed to get template for macOS: Template not found',
              },
              null,
              2,
            ),
          },
        ],
        isError: true,
      });
    });
  });

  describe('File System Operations', () => {
    it('should create directories and process files correctly', async () => {
      await scaffold_macos_projectLogic(
        {
          projectName: 'TestApp',
          customizeNames: true,
          outputPath: '/tmp/test',
        },
        createNoopExecutor(),
        mockFileSystemExecutor,
      );

      // Verify template manager calls using manual tracking
      expect(templateManagerStub.getCalls()).toBe(
        'getTemplatePath(macOS),cleanup(/tmp/test-templates/macos)',
      );

      // File system operations are called by the mock implementation
      // but we can't verify them without vitest mocking patterns
      // This test validates the integration works correctly
    });
  });
});

```

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

```typescript
/**
 * Tests for long_press tool plugin
 */

import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import { createMockExecutor, mockProcess } from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
import longPressPlugin, { long_pressLogic } from '../long_press.ts';

describe('Long Press Plugin', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(longPressPlugin.name).toBe('long_press');
    });

    it('should have correct description', () => {
      expect(longPressPlugin.description).toBe(
        "Long press at specific coordinates for given duration (ms). Use describe_ui for precise coordinates (don't guess from screenshots).",
      );
    });

    it('should have handler function', () => {
      expect(typeof longPressPlugin.handler).toBe('function');
    });

    it('should validate schema fields with safeParse', () => {
      const schema = z.object(longPressPlugin.schema);

      expect(
        schema.safeParse({
          x: 100,
          y: 200,
          duration: 1500,
        }).success,
      ).toBe(true);

      expect(
        schema.safeParse({
          x: 100.5,
          y: 200,
          duration: 1500,
        }).success,
      ).toBe(false);

      expect(
        schema.safeParse({
          x: 100,
          y: 200.5,
          duration: 1500,
        }).success,
      ).toBe(false);

      expect(
        schema.safeParse({
          x: 100,
          y: 200,
          duration: 0,
        }).success,
      ).toBe(false);

      expect(
        schema.safeParse({
          x: 100,
          y: 200,
          duration: -100,
        }).success,
      ).toBe(false);

      const withSimId = schema.safeParse({
        simulatorId: '12345678-1234-4234-8234-123456789012',
        x: 100,
        y: 200,
        duration: 1500,
      });
      expect(withSimId.success).toBe(true);
      expect('simulatorId' in (withSimId.data as Record<string, unknown>)).toBe(false);
    });
  });

  describe('Handler Requirements', () => {
    it('should require simulatorId session default', async () => {
      const result = await longPressPlugin.handler({ x: 100, y: 200, duration: 1500 });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Missing required session defaults');
      expect(message).toContain('simulatorId is required');
      expect(message).toContain('session-set-defaults');
    });

    it('should surface validation errors once simulator default exists', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await longPressPlugin.handler({ x: 100, y: 200, duration: 0 });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('duration: Duration of the long press in milliseconds');
    });
  });

  describe('Command Generation', () => {
    it('should generate correct axe command for basic long press', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'long press completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'Mock axe not available' }],
          isError: true,
        }),
      };

      await long_pressLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          duration: 1500,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'touch',
        '-x',
        '100',
        '-y',
        '200',
        '--down',
        '--up',
        '--delay',
        '1.5',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for long press with different coordinates', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'long press completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'Mock axe not available' }],
          isError: true,
        }),
      };

      await long_pressLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 50,
          y: 75,
          duration: 2000,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'touch',
        '-x',
        '50',
        '-y',
        '75',
        '--down',
        '--up',
        '--delay',
        '2',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for short duration long press', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'long press completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'Mock axe not available' }],
          isError: true,
        }),
      };

      await long_pressLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 300,
          y: 400,
          duration: 500,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'touch',
        '-x',
        '300',
        '-y',
        '400',
        '--down',
        '--up',
        '--delay',
        '0.5',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command with bundled axe path', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'long press completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/path/to/bundled/axe',
        getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'Mock axe not available' }],
          isError: true,
        }),
      };

      await long_pressLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 150,
          y: 250,
          duration: 3000,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/path/to/bundled/axe',
        'touch',
        '-x',
        '150',
        '-y',
        '250',
        '--down',
        '--up',
        '--delay',
        '3',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should return success for valid long press execution', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'long press completed',
        error: '',
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'Mock axe not available' }],
          isError: true,
        }),
      };

      const result = await long_pressLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          duration: 1500,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: 'Long press at (100, 200) for 1500ms simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
          },
        ],
        isError: false,
      });
    });

    it('should handle DependencyError when axe is not available', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: '',
        error: undefined,
        process: mockProcess,
      });

      const mockAxeHelpers = {
        getAxePath: () => null, // Mock axe not found
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await long_pressLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          duration: 1500,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should handle AxeError from failed command execution', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'axe command failed',
        process: mockProcess,
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'Mock axe not available' }],
          isError: true,
        }),
      };

      const result = await long_pressLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          duration: 1500,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: "Error: Failed to simulate long press at (100, 200): axe command 'touch' failed.\nDetails: axe command failed",
          },
        ],
        isError: true,
      });
    });

    it('should handle SystemError from command execution', async () => {
      const mockExecutor = () => {
        throw new Error('ENOENT: no such file or directory');
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'Mock axe not available' }],
          isError: true,
        }),
      };

      const result = await long_pressLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          duration: 1500,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: expect.stringContaining(
              'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory',
            ),
          },
        ],
        isError: true,
      });
    });

    it('should handle unexpected Error objects', async () => {
      const mockExecutor = () => {
        throw new Error('Unexpected error');
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'Mock axe not available' }],
          isError: true,
        }),
      };

      const result = await long_pressLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          duration: 1500,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: expect.stringContaining(
              'Error: System error executing axe: Failed to execute axe command: Unexpected error',
            ),
          },
        ],
        isError: true,
      });
    });

    it('should handle unexpected string errors', async () => {
      const mockExecutor = () => {
        throw 'String error';
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'Mock axe not available' }],
          isError: true,
        }),
      };

      const result = await long_pressLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          x: 100,
          y: 200,
          duration: 1500,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: 'Error: System error executing axe: Failed to execute axe command: String error',
          },
        ],
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/test-utils/mock-executors.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Mock Executors for Testing - Dependency Injection Architecture
 *
 * This module provides mock implementations of CommandExecutor and FileSystemExecutor
 * for testing purposes. These mocks are completely isolated from production dependencies
 * to avoid import chains that could trigger native module loading issues in test environments.
 *
 * IMPORTANT: These are EXACT copies of the mock functions originally in utils/command.js
 * to ensure zero behavioral changes during the file reorganization.
 *
 * Responsibilities:
 * - Providing mock command execution for tests
 * - Providing mock file system operations for tests
 * - Maintaining exact behavior compatibility with original implementations
 * - Avoiding any dependencies on production logging or instrumentation
 */

import { ChildProcess } from 'child_process';
import type { WriteStream } from 'fs';
import { EventEmitter } from 'node:events';
import { PassThrough } from 'node:stream';
import { CommandExecutor, type CommandResponse } from '../utils/CommandExecutor.ts';
import { FileSystemExecutor } from '../utils/FileSystemExecutor.ts';
import type { InteractiveProcess, InteractiveSpawner } from '../utils/execution/index.ts';

export type { CommandExecutor, FileSystemExecutor };

export const mockProcess = { pid: 12345 } as unknown as ChildProcess;

export function createMockCommandResponse(
  overrides: Partial<CommandResponse> = {},
): CommandResponse {
  return {
    success: overrides.success ?? true,
    output: overrides.output ?? '',
    error: overrides.error,
    process: overrides.process ?? mockProcess,
    exitCode: overrides.exitCode ?? (overrides.success === false ? 1 : 0),
  };
}

/**
 * Create a mock executor for testing
 * @param result Mock command result or error to throw
 * @returns Mock executor function
 */
export function createMockExecutor(
  result:
    | {
        success?: boolean;
        output?: string;
        error?: string;
        process?: unknown;
        exitCode?: number;
        shouldThrow?: Error;
        onExecute?: (
          command: string[],
          logPrefix?: string,
          useShell?: boolean,
          opts?: { env?: Record<string, string>; cwd?: string },
          detached?: boolean,
        ) => void;
      }
    | Error
    | string,
): CommandExecutor {
  // If result is Error or string, return executor that rejects
  if (result instanceof Error || typeof result === 'string') {
    return async () => {
      throw result;
    };
  }

  // If shouldThrow is specified, return executor that rejects with that error
  if (result.shouldThrow) {
    return async () => {
      throw result.shouldThrow;
    };
  }

  const mockProcess = {
    pid: 12345,
    stdout: null,
    stderr: null,
    stdin: null,
    stdio: [null, null, null],
    killed: false,
    connected: false,
    exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
    signalCode: null,
    spawnargs: [],
    spawnfile: 'sh',
  } as unknown as ChildProcess;

  return async (command, logPrefix, useShell, opts, detached) => {
    // Call onExecute callback if provided
    if (result.onExecute) {
      result.onExecute(command, logPrefix, useShell, opts, detached);
    }

    return {
      success: result.success ?? true,
      output: result.output ?? '',
      error: result.error,
      process: (result.process ?? mockProcess) as ChildProcess,
      exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
    };
  };
}

/**
 * Create a no-op executor that throws an error if called
 * Use this for tests where an executor is required but should never be called
 * @returns CommandExecutor that throws on invocation
 */
export function createNoopExecutor(): CommandExecutor {
  return async (command) => {
    throw new Error(
      `🚨 NOOP EXECUTOR CALLED! 🚨\n` +
        `Command: ${command.join(' ')}\n` +
        `This executor should never be called in this test context.\n` +
        `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
        `Either fix the test to avoid this code path, or use createMockExecutor() instead.`,
    );
  };
}

/**
 * Create a command-matching mock executor for testing multi-command scenarios
 * Perfect for tools that execute multiple commands (like screenshot: simctl + sips)
 *
 * @param commandMap - Map of command patterns to their mock responses
 * @returns CommandExecutor that matches commands and returns appropriate responses
 *
 * @example
 * ```typescript
 * const mockExecutor = createCommandMatchingMockExecutor({
 *   'xcrun simctl': { output: 'Screenshot saved' },
 *   'sips': { output: 'Image optimized' }
 * });
 * ```
 */
export function createCommandMatchingMockExecutor(
  commandMap: Record<
    string,
    {
      success?: boolean;
      output?: string;
      error?: string;
      process?: unknown;
      exitCode?: number;
    }
  >,
): CommandExecutor {
  return async (command) => {
    const commandStr = command.join(' ');

    // Find matching command pattern
    const matchedKey = Object.keys(commandMap).find((key) => commandStr.includes(key));

    if (!matchedKey) {
      throw new Error(
        `🚨 UNEXPECTED COMMAND! 🚨\n` +
          `Command: ${commandStr}\n` +
          `Expected one of: ${Object.keys(commandMap).join(', ')}\n` +
          `Available patterns: ${JSON.stringify(Object.keys(commandMap), null, 2)}`,
      );
    }

    const result = commandMap[matchedKey];

    const mockProcess = {
      pid: 12345,
      stdout: null,
      stderr: null,
      stdin: null,
      stdio: [null, null, null],
      killed: false,
      connected: false,
      exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
      signalCode: null,
      spawnargs: [],
      spawnfile: 'sh',
    } as unknown as ChildProcess;

    return {
      success: result.success ?? true, // Success by default (as discussed)
      output: result.output ?? '',
      error: result.error,
      process: (result.process ?? mockProcess) as ChildProcess,
      exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
    };
  };
}

export type MockInteractiveSession = {
  stdout: PassThrough;
  stderr: PassThrough;
  stdin: PassThrough;
  emitExit: (code?: number | null, signal?: NodeJS.Signals | null) => void;
  emitError: (error: Error) => void;
};

export type MockInteractiveSpawnerScript = {
  onSpawn?: (session: MockInteractiveSession) => void;
  onWrite?: (data: string, session: MockInteractiveSession) => void;
  onKill?: (signal: NodeJS.Signals | undefined, session: MockInteractiveSession) => void;
  onDispose?: (session: MockInteractiveSession) => void;
};

export function createMockInteractiveSpawner(
  script: MockInteractiveSpawnerScript = {},
): InteractiveSpawner {
  return (): InteractiveProcess => {
    const stdout = new PassThrough();
    const stderr = new PassThrough();
    const stdin = new PassThrough();
    const emitter = new EventEmitter();
    const mockProcess = emitter as unknown as ChildProcess;
    const mutableProcess = mockProcess as unknown as {
      stdout: PassThrough | null;
      stderr: PassThrough | null;
      stdin: PassThrough | null;
      killed: boolean;
      exitCode: number | null;
      signalCode: NodeJS.Signals | null;
      spawnargs: string[];
      spawnfile: string;
      pid: number;
    };

    mutableProcess.stdout = stdout;
    mutableProcess.stderr = stderr;
    mutableProcess.stdin = stdin;
    mutableProcess.killed = false;
    mutableProcess.exitCode = null;
    mutableProcess.signalCode = null;
    mutableProcess.spawnargs = [];
    mutableProcess.spawnfile = 'mock';
    mutableProcess.pid = 12345;
    mockProcess.kill = ((signal?: NodeJS.Signals): boolean => {
      mutableProcess.killed = true;
      emitter.emit('exit', 0, signal ?? null);
      return true;
    }) as ChildProcess['kill'];

    const session: MockInteractiveSession = {
      stdout,
      stderr,
      stdin,
      emitExit: (code = 0, signal = null) => {
        emitter.emit('exit', code, signal);
      },
      emitError: (error) => {
        emitter.emit('error', error);
      },
    };

    script.onSpawn?.(session);

    let disposed = false;

    return {
      process: mockProcess,
      write(data: string): void {
        if (disposed) {
          throw new Error('Mock interactive process disposed');
        }
        script.onWrite?.(data, session);
      },
      kill(signal?: NodeJS.Signals): void {
        if (disposed) return;
        mutableProcess.killed = true;
        script.onKill?.(signal, session);
        emitter.emit('exit', 0, signal ?? null);
      },
      dispose(): void {
        if (disposed) return;
        disposed = true;
        script.onDispose?.(session);
        stdout.end();
        stderr.end();
        stdin.end();
        emitter.removeAllListeners();
      },
    };
  };
}

/**
 * Create a mock file system executor for testing
 */
export function createMockFileSystemExecutor(
  overrides?: Partial<FileSystemExecutor>,
): FileSystemExecutor {
  const mockWriteStream = ((): WriteStream => {
    const emitter = new EventEmitter();
    const stream = Object.assign(emitter, {
      destroyed: false,
      write: () => true,
      end: () => {
        stream.destroyed = true;
        emitter.emit('close');
      },
    }) as unknown as WriteStream;
    return stream;
  })();

  return {
    mkdir: async (): Promise<void> => {},
    readFile: async (): Promise<string> => 'mock file content',
    writeFile: async (): Promise<void> => {},
    createWriteStream: () => mockWriteStream,
    cp: async (): Promise<void> => {},
    readdir: async (): Promise<unknown[]> => [],
    rm: async (): Promise<void> => {},
    existsSync: (): boolean => false,
    stat: async (): Promise<{ isDirectory(): boolean; mtimeMs: number }> => ({
      isDirectory: (): boolean => true,
      mtimeMs: Date.now(),
    }),
    mkdtemp: async (): Promise<string> => '/tmp/mock-temp-123456',
    tmpdir: (): string => '/tmp',
    ...overrides,
  };
}

/**
 * Create a no-op file system executor that throws an error if called
 * Use this for tests where an executor is required but should never be called
 * @returns CommandExecutor that throws on invocation
 */
export function createNoopFileSystemExecutor(): FileSystemExecutor {
  return {
    mkdir: async (): Promise<void> => {
      throw new Error(
        `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
          `This executor should never be called in this test context.\n` +
          `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
          `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
      );
    },
    readFile: async (): Promise<string> => {
      throw new Error(
        `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
          `This executor should never be called in this test context.\n` +
          `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
          `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
      );
    },
    writeFile: async (): Promise<void> => {
      throw new Error(
        `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
          `This executor should never be called in this test context.\n` +
          `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
          `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
      );
    },
    createWriteStream: (): WriteStream => {
      throw new Error(
        `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
          `This executor should never be called in this test context.\n` +
          `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
          `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
      );
    },
    cp: async (): Promise<void> => {
      throw new Error(
        `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
          `This executor should never be called in this test context.\n` +
          `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
          `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
      );
    },
    readdir: async (): Promise<unknown[]> => {
      throw new Error(
        `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
          `This executor should never be called in this test context.\n` +
          `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
          `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
      );
    },
    rm: async (): Promise<void> => {
      throw new Error(
        `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
          `This executor should never be called in this test context.\n` +
          `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
          `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
      );
    },
    existsSync: (): boolean => {
      throw new Error(
        `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
          `This executor should never be called in this test context.\n` +
          `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
          `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
      );
    },
    stat: async (): Promise<{ isDirectory(): boolean; mtimeMs: number }> => {
      throw new Error(
        `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
          `This executor should never be called in this test context.\n` +
          `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
          `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
      );
    },
    mkdtemp: async (): Promise<string> => {
      throw new Error(
        `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
          `This executor should never be called in this test context.\n` +
          `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
          `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
      );
    },
    tmpdir: (): string => '/tmp',
  };
}

/**
 * Create a mock environment detector for testing
 * @param options Mock options for environment detection
 * @returns Mock environment detector
 */
export function createMockEnvironmentDetector(
  options: {
    isRunningUnderClaudeCode?: boolean;
  } = {},
): import('../utils/environment.js').EnvironmentDetector {
  return {
    isRunningUnderClaudeCode: () => options.isRunningUnderClaudeCode ?? false,
  };
}

```

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

```typescript
/**
 * Tests for type_text plugin
 */

import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import {
  createMockExecutor,
  createMockFileSystemExecutor,
  createNoopExecutor,
  mockProcess,
} from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
import typeTextPlugin, { type_textLogic } from '../type_text.ts';

// Mock axe helpers for dependency injection
function createMockAxeHelpers(
  overrides: {
    getAxePathReturn?: string | null;
    getBundledAxeEnvironmentReturn?: Record<string, string>;
  } = {},
) {
  return {
    getAxePath: () =>
      overrides.getAxePathReturn !== undefined ? overrides.getAxePathReturn : '/usr/local/bin/axe',
    getBundledAxeEnvironment: () => overrides.getBundledAxeEnvironmentReturn ?? {},
    createAxeNotAvailableResponse: () => ({
      content: [
        {
          type: 'text',
          text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
        },
      ],
      isError: true,
    }),
  };
}

// Mock executor that tracks rejections for testing
function createRejectingExecutor(error: any) {
  return async () => {
    throw error;
  };
}

describe('Type Text Plugin', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(typeTextPlugin.name).toBe('type_text');
    });

    it('should have correct description', () => {
      expect(typeTextPlugin.description).toBe(
        'Type text (supports US keyboard characters). Use describe_ui to find text field, tap to focus, then type.',
      );
    });

    it('should have handler function', () => {
      expect(typeof typeTextPlugin.handler).toBe('function');
    });

    it('should validate schema fields with safeParse', () => {
      const schema = z.object(typeTextPlugin.schema);

      expect(
        schema.safeParse({
          text: 'Hello World',
        }).success,
      ).toBe(true);

      expect(
        schema.safeParse({
          text: '',
        }).success,
      ).toBe(false);

      expect(
        schema.safeParse({
          text: 123,
        }).success,
      ).toBe(false);

      expect(schema.safeParse({}).success).toBe(false);

      const withSimId = schema.safeParse({
        simulatorId: '12345678-1234-4234-8234-123456789012',
        text: 'Hello World',
      });
      expect(withSimId.success).toBe(true);
      expect('simulatorId' in (withSimId.data as Record<string, unknown>)).toBe(false);
    });
  });

  describe('Handler Requirements', () => {
    it('should require simulatorId session default', async () => {
      const result = await typeTextPlugin.handler({ text: 'Hello' });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Missing required session defaults');
      expect(message).toContain('simulatorId is required');
      expect(message).toContain('session-set-defaults');
    });

    it('should surface validation errors when defaults exist', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await typeTextPlugin.handler({});

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('text: Invalid input: expected string, received undefined');
    });
  });

  describe('Command Generation', () => {
    it('should generate correct axe command for basic text typing', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'Text typed successfully',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/usr/local/bin/axe',
        getBundledAxeEnvironmentReturn: {},
      });

      await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: 'Hello World',
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'type',
        'Hello World',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for text with special characters', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'Text typed successfully',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/usr/local/bin/axe',
        getBundledAxeEnvironmentReturn: {},
      });

      await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: '[email protected]',
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'type',
        '[email protected]',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for text with numbers and symbols', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'Text typed successfully',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/usr/local/bin/axe',
        getBundledAxeEnvironmentReturn: {},
      });

      await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: 'Password123!@#',
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'type',
        'Password123!@#',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for long text', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'Text typed successfully',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/usr/local/bin/axe',
        getBundledAxeEnvironmentReturn: {},
      });

      const longText =
        'This is a very long text that needs to be typed into the simulator for testing purposes.';

      await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: longText,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'type',
        longText,
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command with bundled axe path', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'Text typed successfully',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/path/to/bundled/axe',
        getBundledAxeEnvironmentReturn: { AXE_PATH: '/some/path' },
      });

      await type_textLogic(
        {
          simulatorId: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
          text: 'Test message',
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/path/to/bundled/axe',
        'type',
        'Test message',
        '--udid',
        'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
      ]);
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should handle axe dependency error', async () => {
      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: null,
      });

      const result = await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: 'Hello World',
        },
        createNoopExecutor(),
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should successfully type text', async () => {
      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/usr/local/bin/axe',
        getBundledAxeEnvironmentReturn: {},
      });
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Text typed successfully',
        error: undefined,
      });

      const result = await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: 'Hello World',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Text typing simulated successfully.' }],
        isError: false,
      });
    });

    it('should return success for valid text typing', async () => {
      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/usr/local/bin/axe',
        getBundledAxeEnvironmentReturn: {},
      });

      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Text typed successfully',
        error: undefined,
      });

      const result = await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: 'Hello World',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Text typing simulated successfully.' }],
        isError: false,
      });
    });

    it('should handle DependencyError when axe binary not found', async () => {
      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: null,
      });

      const result = await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: 'Hello World',
        },
        createNoopExecutor(),
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should handle AxeError from command execution', async () => {
      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/usr/local/bin/axe',
        getBundledAxeEnvironmentReturn: {},
      });

      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Text field not found',
      });

      const result = await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: 'Hello World',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: "Error: Failed to simulate text typing: axe command 'type' failed.\nDetails: Text field not found",
          },
        ],
        isError: true,
      });
    });

    it('should handle SystemError from command execution', async () => {
      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/usr/local/bin/axe',
        getBundledAxeEnvironmentReturn: {},
      });

      const mockExecutor = createRejectingExecutor(new Error('ENOENT: no such file or directory'));

      const result = await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: 'Hello World',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: expect.stringContaining(
              'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory',
            ),
          },
        ],
        isError: true,
      });
    });

    it('should handle unexpected Error objects', async () => {
      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/usr/local/bin/axe',
        getBundledAxeEnvironmentReturn: {},
      });

      const mockExecutor = createRejectingExecutor(new Error('Unexpected error'));

      const result = await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: 'Hello World',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: expect.stringContaining(
              'Error: System error executing axe: Failed to execute axe command: Unexpected error',
            ),
          },
        ],
        isError: true,
      });
    });

    it('should handle unexpected string errors', async () => {
      const mockAxeHelpers = createMockAxeHelpers({
        getAxePathReturn: '/usr/local/bin/axe',
        getBundledAxeEnvironmentReturn: {},
      });

      const mockExecutor = createRejectingExecutor('String error');

      const result = await type_textLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          text: 'Hello World',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: System error executing axe: Failed to execute axe command: String error',
          },
        ],
        isError: true,
      });
    });
  });
});

```

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

```typescript
/**
 * Tests for button tool plugin
 */

import { describe, it, expect } from 'vitest';
import * as z from 'zod';
import {
  createMockExecutor,
  createNoopExecutor,
  createMockCommandResponse,
} from '../../../../test-utils/mock-executors.ts';
import buttonPlugin, { buttonLogic } from '../button.ts';
import type { CommandExecutor } from '../../../../utils/execution/index.ts';

describe('Button Plugin', () => {
  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(buttonPlugin.name).toBe('button');
    });

    it('should have correct description', () => {
      expect(buttonPlugin.description).toBe(
        'Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri',
      );
    });

    it('should have handler function', () => {
      expect(typeof buttonPlugin.handler).toBe('function');
    });

    it('should expose public schema without simulatorId field', () => {
      const schema = z.object(buttonPlugin.schema);

      expect(schema.safeParse({ buttonType: 'home' }).success).toBe(true);
      expect(schema.safeParse({ buttonType: 'home', duration: 2.5 }).success).toBe(true);
      expect(schema.safeParse({ buttonType: 'invalid-button' }).success).toBe(false);
      expect(schema.safeParse({ buttonType: 'home', duration: -1 }).success).toBe(false);

      const withSimId = schema.safeParse({
        simulatorId: '12345678-1234-4234-8234-123456789012',
        buttonType: 'home',
      });
      expect(withSimId.success).toBe(true);
      expect('simulatorId' in (withSimId.data as any)).toBe(false);

      expect(schema.safeParse({}).success).toBe(false);
    });
  });

  describe('Command Generation', () => {
    it('should generate correct axe command for basic button press', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor: CommandExecutor = async (command) => {
        capturedCommand = command;
        return createMockCommandResponse({
          success: true,
          output: 'button press completed',
          error: undefined,
        });
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'axe not available' }],
          isError: true,
        }),
      };

      await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'home',
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'button',
        'home',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for button press with duration', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor: CommandExecutor = async (command) => {
        capturedCommand = command;
        return createMockCommandResponse({
          success: true,
          output: 'button press completed',
          error: undefined,
        });
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'axe not available' }],
          isError: true,
        }),
      };

      await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'side-button',
          duration: 2.5,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'button',
        'side-button',
        '--duration',
        '2.5',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for different button types', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor: CommandExecutor = async (command) => {
        capturedCommand = command;
        return createMockCommandResponse({
          success: true,
          output: 'button press completed',
          error: undefined,
        });
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'axe not available' }],
          isError: true,
        }),
      };

      await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'apple-pay',
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'button',
        'apple-pay',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command with bundled axe path', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor: CommandExecutor = async (command) => {
        capturedCommand = command;
        return createMockCommandResponse({
          success: true,
          output: 'button press completed',
          error: undefined,
        });
      };

      const mockAxeHelpers = {
        getAxePath: () => '/path/to/bundled/axe',
        getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'axe not available' }],
          isError: true,
        }),
      };

      await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'siri',
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/path/to/bundled/axe',
        'button',
        'siri',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should surface session default requirement when simulatorId is missing', async () => {
      const result = await buttonPlugin.handler({ buttonType: 'home' });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Missing required session defaults');
      expect(result.content[0].text).toContain('simulatorId is required');
    });

    it('should return error for missing buttonType', async () => {
      const result = await buttonPlugin.handler({
        simulatorId: '12345678-1234-4234-8234-123456789012',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain(
        'buttonType: Invalid option: expected one of "apple-pay"|"home"|"lock"|"side-button"|"siri"',
      );
    });

    it('should return error for invalid simulatorId format', async () => {
      const result = await buttonPlugin.handler({
        simulatorId: 'invalid-uuid-format',
        buttonType: 'home',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('Invalid Simulator UUID format');
    });

    it('should return error for invalid buttonType', async () => {
      const result = await buttonPlugin.handler({
        simulatorId: '12345678-1234-4234-8234-123456789012',
        buttonType: 'invalid-button',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
    });

    it('should return error for negative duration', async () => {
      const result = await buttonPlugin.handler({
        simulatorId: '12345678-1234-4234-8234-123456789012',
        buttonType: 'home',
        duration: -1,
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('Duration must be non-negative');
    });

    it('should return success for valid button press', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'button press completed',
        error: undefined,
        process: { pid: 12345 },
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'axe not available' }],
          isError: true,
        }),
      };

      const result = await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'home',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [{ type: 'text' as const, text: "Hardware button 'home' pressed successfully." }],
        isError: false,
      });
    });

    it('should return success for button press with duration', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'button press completed',
        error: undefined,
        process: { pid: 12345 },
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'axe not available' }],
          isError: true,
        }),
      };

      const result = await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'side-button',
          duration: 2.5,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          { type: 'text' as const, text: "Hardware button 'side-button' pressed successfully." },
        ],
        isError: false,
      });
    });

    it('should handle DependencyError when axe is not available', async () => {
      const mockAxeHelpers = {
        getAxePath: () => null,
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'home',
        },
        createNoopExecutor(),
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should handle AxeError from failed command execution', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'axe command failed',
        process: { pid: 12345 },
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'axe not available' }],
          isError: true,
        }),
      };

      const result = await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'home',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: "Error: Failed to press button 'home': axe command 'button' failed.\nDetails: axe command failed",
          },
        ],
        isError: true,
      });
    });

    it('should handle SystemError from command execution', async () => {
      const mockExecutor = async () => {
        throw new Error('ENOENT: no such file or directory');
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'axe not available' }],
          isError: true,
        }),
      };

      const result = await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'home',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result.content[0].text).toMatch(
        /^Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory/,
      );
      expect(result.isError).toBe(true);
    });

    it('should handle unexpected Error objects', async () => {
      const mockExecutor = async () => {
        throw new Error('Unexpected error');
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'axe not available' }],
          isError: true,
        }),
      };

      const result = await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'home',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result.content[0].text).toMatch(
        /^Error: System error executing axe: Failed to execute axe command: Unexpected error/,
      );
      expect(result.isError).toBe(true);
    });

    it('should handle unexpected string errors', async () => {
      const mockExecutor = async () => {
        throw 'String error';
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [{ type: 'text' as const, text: 'axe not available' }],
          isError: true,
        }),
      };

      const result = await buttonLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          buttonType: 'home',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: 'Error: System error executing axe: Failed to execute axe command: String error',
          },
        ],
        isError: true,
      });
    });
  });
});

```
Page 8/12FirstPrevNextLast