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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/scripts/tools-cli.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * XcodeBuildMCP Tools CLI
  5 |  *
  6 |  * A unified command-line tool that provides comprehensive information about
  7 |  * XcodeBuildMCP tools and resources. Supports both runtime inspection
  8 |  * (actual server state) and static analysis (source file analysis).
  9 |  *
 10 |  * Usage:
 11 |  *   npm run tools [command] [options]
 12 |  *   npx tsx src/cli/tools-cli.ts [command] [options]
 13 |  *
 14 |  * Commands:
 15 |  *   count, c        Show tool and workflow counts
 16 |  *   list, l         List all tools and resources
 17 |  *   static, s       Show static source file analysis
 18 |  *   help, h         Show this help message
 19 |  *
 20 |  * Options:
 21 |  *   --runtime, -r        Use runtime inspection (respects env config)
 22 |  *   --static, -s         Use static file analysis (development mode)
 23 |  *   --tools, -t          Include tools in output
 24 |  *   --resources          Include resources in output
 25 |  *   --workflows, -w      Include workflow information
 26 |  *   --verbose, -v        Show detailed information
 27 |  *   --json               Output JSON format
 28 |  *   --help              Show help for specific command
 29 |  *
 30 |  * Examples:
 31 |  *   npm run tools                         # Runtime summary with workflows
 32 |  *   npm run tools:count                   # Runtime tool count
 33 |  *   npm run tools:static                  # Static file analysis
 34 |  *   npm run tools:list                    # List runtime tools
 35 |  *   npx tsx src/cli/tools-cli.ts --json   # JSON output
 36 |  */
 37 | 
 38 | import { spawn } from 'child_process';
 39 | import * as path from 'path';
 40 | import { fileURLToPath } from 'url';
 41 | import * as fs from 'fs';
 42 | import { getStaticToolAnalysis, type StaticAnalysisResult } from './analysis/tools-analysis.js';
 43 | 
 44 | // Get project paths
 45 | const __filename = fileURLToPath(import.meta.url);
 46 | const __dirname = path.dirname(__filename);
 47 | 
 48 | // ANSI color codes
 49 | const colors = {
 50 |   reset: '\x1b[0m',
 51 |   bright: '\x1b[1m',
 52 |   red: '\x1b[31m',
 53 |   green: '\x1b[32m',
 54 |   yellow: '\x1b[33m',
 55 |   blue: '\x1b[34m',
 56 |   cyan: '\x1b[36m',
 57 |   magenta: '\x1b[35m',
 58 | } as const;
 59 | 
 60 | // Types
 61 | interface CLIOptions {
 62 |   runtime: boolean;
 63 |   static: boolean;
 64 |   tools: boolean;
 65 |   resources: boolean;
 66 |   workflows: boolean;
 67 |   verbose: boolean;
 68 |   json: boolean;
 69 |   help: boolean;
 70 | }
 71 | 
 72 | interface RuntimeTool {
 73 |   name: string;
 74 |   description: string;
 75 | }
 76 | 
 77 | interface RuntimeResource {
 78 |   uri: string;
 79 |   name: string;
 80 |   description: string;
 81 | }
 82 | 
 83 | interface RuntimeData {
 84 |   tools: RuntimeTool[];
 85 |   resources: RuntimeResource[];
 86 |   toolCount: number;
 87 |   resourceCount: number;
 88 |   dynamicMode: boolean;
 89 |   mode: 'runtime';
 90 | }
 91 | 
 92 | // CLI argument parsing
 93 | const args = process.argv.slice(2);
 94 | 
 95 | // Find the command (first non-flag argument)
 96 | let command = 'count'; // default
 97 | for (const arg of args) {
 98 |   if (!arg.startsWith('-')) {
 99 |     command = arg;
100 |     break;
101 |   }
102 | }
103 | 
104 | const options: CLIOptions = {
105 |   runtime: args.includes('--runtime') || args.includes('-r'),
106 |   static: args.includes('--static') || args.includes('-s'),
107 |   tools: args.includes('--tools') || args.includes('-t'),
108 |   resources: args.includes('--resources'),
109 |   workflows: args.includes('--workflows') || args.includes('-w'),
110 |   verbose: args.includes('--verbose') || args.includes('-v'),
111 |   json: args.includes('--json'),
112 |   help: args.includes('--help') || args.includes('-h'),
113 | };
114 | 
115 | // Set sensible defaults for each command
116 | if (!options.runtime && !options.static) {
117 |   if (command === 'static' || command === 's') {
118 |     options.static = true;
119 |   } else {
120 |     // Default to static analysis for development-friendly usage
121 |     options.static = true;
122 |   }
123 | }
124 | 
125 | // Set sensible content defaults
126 | if (command === 'list' || command === 'l') {
127 |   if (!options.tools && !options.resources && !options.workflows) {
128 |     options.tools = true; // Default to showing tools for list command
129 |   }
130 | } else if (!command || command === 'count' || command === 'c') {
131 |   // For no command or count, show comprehensive summary
132 |   if (!options.tools && !options.resources && !options.workflows) {
133 |     options.workflows = true; // Show workflows by default for summary
134 |   }
135 | }
136 | 
137 | // Help text
138 | const helpText = {
139 |   main: `
140 | ${colors.bright}${colors.blue}XcodeBuildMCP Tools CLI${colors.reset}
141 | 
142 | A unified command-line tool for XcodeBuildMCP tool and resource information.
143 | 
144 | ${colors.bright}COMMANDS:${colors.reset}
145 |   count, c        Show tool and workflow counts
146 |   list, l         List all tools and resources  
147 |   static, s       Show static source file analysis
148 |   help, h         Show this help message
149 | 
150 | ${colors.bright}OPTIONS:${colors.reset}
151 |   --runtime, -r        Use runtime inspection (respects env config)
152 |   --static, -s         Use static file analysis (default, development mode)
153 |   --tools, -t          Include tools in output
154 |   --resources          Include resources in output
155 |   --workflows, -w      Include workflow information
156 |   --verbose, -v        Show detailed information
157 |   --json               Output JSON format
158 | 
159 | ${colors.bright}EXAMPLES:${colors.reset}
160 |   ${colors.cyan}npm run tools${colors.reset}                         # Static summary with workflows (default)
161 |   ${colors.cyan}npm run tools list${colors.reset}                    # List tools
162 |   ${colors.cyan}npm run tools --runtime${colors.reset}               # Runtime analysis (requires build)
163 |   ${colors.cyan}npm run tools static${colors.reset}                  # Static analysis summary
164 |   ${colors.cyan}npm run tools count --json${colors.reset}            # JSON output
165 | 
166 | ${colors.bright}ANALYSIS MODES:${colors.reset}
167 |   ${colors.green}Runtime${colors.reset}  Uses actual server inspection via Reloaderoo
168 |            - Respects XCODEBUILDMCP_DYNAMIC_TOOLS environment variable
169 |            - Shows tools actually enabled at runtime
170 |            - Requires built server (npm run build)
171 |            
172 |   ${colors.yellow}Static${colors.reset}   Scans source files directly using AST parsing
173 |            - Shows all tools in codebase regardless of config
174 |            - Development-time analysis with reliable description extraction
175 |            - No server build required
176 | `,
177 | 
178 |   count: `
179 | ${colors.bright}COUNT COMMAND${colors.reset}
180 | 
181 | Shows tool and workflow counts using runtime or static analysis.
182 | 
183 | ${colors.bright}Usage:${colors.reset} npx tsx scripts/tools-cli.ts count [options]
184 | 
185 | ${colors.bright}Options:${colors.reset}
186 |   --runtime, -r        Count tools from running server
187 |   --static, -s         Count tools from source files
188 |   --workflows, -w      Include workflow directory counts
189 |   --json               Output JSON format
190 | 
191 | ${colors.bright}Examples:${colors.reset}
192 |   ${colors.cyan}npx tsx scripts/tools-cli.ts count${colors.reset}                    # Runtime count
193 |   ${colors.cyan}npx tsx scripts/tools-cli.ts count --static${colors.reset}          # Static count
194 |   ${colors.cyan}npx tsx scripts/tools-cli.ts count --workflows${colors.reset}       # Include workflows
195 | `,
196 | 
197 |   list: `
198 | ${colors.bright}LIST COMMAND${colors.reset}
199 | 
200 | Lists tools and resources with optional details.
201 | 
202 | ${colors.bright}Usage:${colors.reset} npx tsx scripts/tools-cli.ts list [options]
203 | 
204 | ${colors.bright}Options:${colors.reset}
205 |   --runtime, -r        List from running server
206 |   --static, -s         List from source files
207 |   --tools, -t          Show tool names
208 |   --resources          Show resource URIs
209 |   --verbose, -v        Show detailed information
210 |   --json               Output JSON format
211 | 
212 | ${colors.bright}Examples:${colors.reset}
213 |   ${colors.cyan}npx tsx scripts/tools-cli.ts list --tools${colors.reset}            # Runtime tool list
214 |   ${colors.cyan}npx tsx scripts/tools-cli.ts list --resources${colors.reset}        # Runtime resource list
215 |   ${colors.cyan}npx tsx scripts/tools-cli.ts list --static --verbose${colors.reset} # Static detailed list
216 | `,
217 | 
218 |   static: `
219 | ${colors.bright}STATIC COMMAND${colors.reset}
220 | 
221 | Performs detailed static analysis of source files using AST parsing.
222 | 
223 | ${colors.bright}Usage:${colors.reset} npx tsx scripts/tools-cli.ts static [options]
224 | 
225 | ${colors.bright}Options:${colors.reset}
226 |   --tools, -t          Show canonical tool details
227 |   --workflows, -w      Show workflow directory analysis
228 |   --verbose, -v        Show detailed file information
229 |   --json               Output JSON format
230 | 
231 | ${colors.bright}Examples:${colors.reset}
232 |   ${colors.cyan}npx tsx scripts/tools-cli.ts static${colors.reset}                  # Basic static analysis
233 |   ${colors.cyan}npx tsx scripts/tools-cli.ts static --verbose${colors.reset}        # Detailed analysis
234 |   ${colors.cyan}npx tsx scripts/tools-cli.ts static --workflows${colors.reset}      # Include workflow info
235 | `,
236 | };
237 | 
238 | if (options.help) {
239 |   console.log(helpText[command as keyof typeof helpText] || helpText.main);
240 |   process.exit(0);
241 | }
242 | 
243 | if (command === 'help' || command === 'h') {
244 |   const helpCommand = args[1];
245 |   console.log(helpText[helpCommand as keyof typeof helpText] || helpText.main);
246 |   process.exit(0);
247 | }
248 | 
249 | /**
250 |  * Execute reloaderoo command and parse JSON response
251 |  */
252 | async function executeReloaderoo(reloaderooArgs: string[]): Promise<unknown> {
253 |   const buildPath = path.resolve(__dirname, '..', 'build', 'index.js');
254 | 
255 |   if (!fs.existsSync(buildPath)) {
256 |     throw new Error('Build not found. Please run "npm run build" first.');
257 |   }
258 | 
259 |   const tempFile = `/tmp/reloaderoo-output-${Date.now()}.json`;
260 |   const command = `npx -y reloaderoo@latest inspect ${reloaderooArgs.join(' ')} -- node "${buildPath}"`;
261 | 
262 |   return new Promise((resolve, reject) => {
263 |     const child = spawn('bash', ['-c', `${command} > "${tempFile}"`], {
264 |       stdio: 'inherit',
265 |     });
266 | 
267 |     child.on('close', (code) => {
268 |       try {
269 |         if (code !== 0) {
270 |           reject(new Error(`Command failed with code ${code}`));
271 |           return;
272 |         }
273 | 
274 |         const content = fs.readFileSync(tempFile, 'utf8');
275 | 
276 |         // Remove stderr log lines and find JSON
277 |         const lines = content.split('\n');
278 |         const cleanLines: string[] = [];
279 | 
280 |         for (const line of lines) {
281 |           if (
282 |             line.match(/^\[\d{4}-\d{2}-\d{2}T/) ||
283 |             line.includes('[INFO]') ||
284 |             line.includes('[DEBUG]') ||
285 |             line.includes('[ERROR]')
286 |           ) {
287 |             continue;
288 |           }
289 | 
290 |           const trimmed = line.trim();
291 |           if (trimmed) {
292 |             cleanLines.push(line);
293 |           }
294 |         }
295 | 
296 |         // Find JSON start
297 |         let jsonStartIndex = -1;
298 |         for (let i = 0; i < cleanLines.length; i++) {
299 |           if (cleanLines[i].trim().startsWith('{')) {
300 |             jsonStartIndex = i;
301 |             break;
302 |           }
303 |         }
304 | 
305 |         if (jsonStartIndex === -1) {
306 |           reject(
307 |             new Error(`No JSON response found in output.\nOutput: ${content.substring(0, 500)}...`),
308 |           );
309 |           return;
310 |         }
311 | 
312 |         const jsonText = cleanLines.slice(jsonStartIndex).join('\n');
313 |         const response = JSON.parse(jsonText);
314 |         resolve(response);
315 |       } catch (error) {
316 |         reject(new Error(`Failed to parse JSON response: ${(error as Error).message}`));
317 |       } finally {
318 |         try {
319 |           fs.unlinkSync(tempFile);
320 |         } catch {
321 |           // Ignore cleanup errors
322 |         }
323 |       }
324 |     });
325 | 
326 |     child.on('error', (error) => {
327 |       reject(new Error(`Failed to spawn process: ${error.message}`));
328 |     });
329 |   });
330 | }
331 | 
332 | /**
333 |  * Get runtime server information
334 |  */
335 | async function getRuntimeInfo(): Promise<RuntimeData> {
336 |   try {
337 |     const toolsResponse = (await executeReloaderoo(['list-tools'])) as {
338 |       tools?: { name: string; description: string }[];
339 |     };
340 |     const resourcesResponse = (await executeReloaderoo(['list-resources'])) as {
341 |       resources?: { uri: string; name: string; description?: string; title?: string }[];
342 |     };
343 | 
344 |     let tools: RuntimeTool[] = [];
345 |     let toolCount = 0;
346 | 
347 |     if (toolsResponse.tools && Array.isArray(toolsResponse.tools)) {
348 |       toolCount = toolsResponse.tools.length;
349 |       tools = toolsResponse.tools.map((tool) => ({
350 |         name: tool.name,
351 |         description: tool.description,
352 |       }));
353 |     }
354 | 
355 |     let resources: RuntimeResource[] = [];
356 |     let resourceCount = 0;
357 | 
358 |     if (resourcesResponse.resources && Array.isArray(resourcesResponse.resources)) {
359 |       resourceCount = resourcesResponse.resources.length;
360 |       resources = resourcesResponse.resources.map((resource) => ({
361 |         uri: resource.uri,
362 |         name: resource.name,
363 |         description: resource.title ?? resource.description ?? 'No description available',
364 |       }));
365 |     }
366 | 
367 |     return {
368 |       tools,
369 |       resources,
370 |       toolCount,
371 |       resourceCount,
372 |       dynamicMode: process.env.XCODEBUILDMCP_DYNAMIC_TOOLS === 'true',
373 |       mode: 'runtime',
374 |     };
375 |   } catch (error) {
376 |     throw new Error(`Runtime analysis failed: ${(error as Error).message}`);
377 |   }
378 | }
379 | 
380 | /**
381 |  * Display summary information
382 |  */
383 | function displaySummary(
384 |   runtimeData: RuntimeData | null,
385 |   staticData: StaticAnalysisResult | null,
386 | ): void {
387 |   if (options.json) {
388 |     return; // JSON output handled separately
389 |   }
390 | 
391 |   console.log(`${colors.bright}${colors.blue}📊 XcodeBuildMCP Tools Summary${colors.reset}`);
392 |   console.log('═'.repeat(60));
393 | 
394 |   if (runtimeData) {
395 |     console.log(`${colors.green}🚀 Runtime Analysis:${colors.reset}`);
396 |     console.log(`   Mode: ${runtimeData.dynamicMode ? 'Dynamic' : 'Static'}`);
397 |     console.log(`   Tools: ${runtimeData.toolCount}`);
398 |     console.log(`   Resources: ${runtimeData.resourceCount}`);
399 |     console.log(`   Total: ${runtimeData.toolCount + runtimeData.resourceCount}`);
400 | 
401 |     if (runtimeData.dynamicMode) {
402 |       console.log(
403 |         `   ${colors.yellow}ℹ️  Dynamic mode: Only enabled workflow tools shown${colors.reset}`,
404 |       );
405 |     }
406 |     console.log();
407 |   }
408 | 
409 |   if (staticData) {
410 |     console.log(`${colors.cyan}📁 Static Analysis:${colors.reset}`);
411 |     console.log(`   Workflow directories: ${staticData.stats.workflowCount}`);
412 |     console.log(`   Canonical tools: ${staticData.stats.canonicalTools}`);
413 |     console.log(`   Re-export files: ${staticData.stats.reExportTools}`);
414 |     console.log(`   Total tool files: ${staticData.stats.totalTools}`);
415 |     console.log();
416 |   }
417 | }
418 | 
419 | /**
420 |  * Display workflow information
421 |  */
422 | function displayWorkflows(staticData: StaticAnalysisResult | null): void {
423 |   if (!options.workflows || !staticData || options.json) return;
424 | 
425 |   console.log(`${colors.bright}📂 Workflow Directories:${colors.reset}`);
426 |   console.log('─'.repeat(40));
427 | 
428 |   for (const workflow of staticData.workflows) {
429 |     const totalTools = workflow.toolCount;
430 |     console.log(`${colors.green}• ${workflow.displayName}${colors.reset} (${totalTools} tools)`);
431 | 
432 |     if (options.verbose) {
433 |       const canonicalTools = workflow.tools.filter((t) => t.isCanonical).map((t) => t.name);
434 |       const reExportTools = workflow.tools.filter((t) => !t.isCanonical).map((t) => t.name);
435 | 
436 |       if (canonicalTools.length > 0) {
437 |         console.log(`  ${colors.cyan}Canonical:${colors.reset} ${canonicalTools.join(', ')}`);
438 |       }
439 |       if (reExportTools.length > 0) {
440 |         console.log(`  ${colors.yellow}Re-exports:${colors.reset} ${reExportTools.join(', ')}`);
441 |       }
442 |     }
443 |   }
444 |   console.log();
445 | }
446 | 
447 | /**
448 |  * Display tool lists
449 |  */
450 | function displayTools(
451 |   runtimeData: RuntimeData | null,
452 |   staticData: StaticAnalysisResult | null,
453 | ): void {
454 |   if (!options.tools || options.json) return;
455 | 
456 |   if (runtimeData) {
457 |     console.log(`${colors.bright}🛠️  Runtime Tools (${runtimeData.toolCount}):${colors.reset}`);
458 |     console.log('─'.repeat(40));
459 | 
460 |     if (runtimeData.tools.length === 0) {
461 |       console.log('   No tools available');
462 |     } else {
463 |       runtimeData.tools.forEach((tool) => {
464 |         if (options.verbose && tool.description) {
465 |           console.log(
466 |             `   ${colors.green}•${colors.reset} ${colors.bright}${tool.name}${colors.reset}`,
467 |           );
468 |           console.log(`     ${tool.description}`);
469 |         } else {
470 |           console.log(`   ${colors.green}•${colors.reset} ${tool.name}`);
471 |         }
472 |       });
473 |     }
474 |     console.log();
475 |   }
476 | 
477 |   if (staticData && options.static) {
478 |     const canonicalTools = staticData.tools.filter((tool) => tool.isCanonical);
479 |     console.log(`${colors.bright}📁 Static Tools (${canonicalTools.length}):${colors.reset}`);
480 |     console.log('─'.repeat(40));
481 | 
482 |     if (canonicalTools.length === 0) {
483 |       console.log('   No tools found');
484 |     } else {
485 |       canonicalTools
486 |         .sort((a, b) => a.name.localeCompare(b.name))
487 |         .forEach((tool) => {
488 |           if (options.verbose) {
489 |             console.log(
490 |               `   ${colors.green}•${colors.reset} ${colors.bright}${tool.name}${colors.reset} (${tool.workflow})`,
491 |             );
492 |             console.log(`     ${tool.description}`);
493 |             console.log(`     ${colors.cyan}${tool.relativePath}${colors.reset}`);
494 |           } else {
495 |             console.log(`   ${colors.green}•${colors.reset} ${tool.name}`);
496 |           }
497 |         });
498 |     }
499 |     console.log();
500 |   }
501 | }
502 | 
503 | /**
504 |  * Display resource lists
505 |  */
506 | function displayResources(runtimeData: RuntimeData | null): void {
507 |   if (!options.resources || !runtimeData || options.json) return;
508 | 
509 |   console.log(`${colors.bright}📚 Resources (${runtimeData.resourceCount}):${colors.reset}`);
510 |   console.log('─'.repeat(40));
511 | 
512 |   if (runtimeData.resources.length === 0) {
513 |     console.log('   No resources available');
514 |   } else {
515 |     runtimeData.resources.forEach((resource) => {
516 |       if (options.verbose) {
517 |         console.log(
518 |           `   ${colors.magenta}•${colors.reset} ${colors.bright}${resource.uri}${colors.reset}`,
519 |         );
520 |         console.log(`     ${resource.description}`);
521 |       } else {
522 |         console.log(`   ${colors.magenta}•${colors.reset} ${resource.uri}`);
523 |       }
524 |     });
525 |   }
526 |   console.log();
527 | }
528 | 
529 | /**
530 |  * Output JSON format - matches the structure of human-readable output
531 |  */
532 | function outputJSON(
533 |   runtimeData: RuntimeData | null,
534 |   staticData: StaticAnalysisResult | null,
535 | ): void {
536 |   const output: Record<string, unknown> = {};
537 | 
538 |   // Add summary stats (equivalent to the summary table)
539 |   if (runtimeData) {
540 |     output.runtime = {
541 |       toolCount: runtimeData.toolCount,
542 |       resourceCount: runtimeData.resourceCount,
543 |       totalCount: runtimeData.toolCount + runtimeData.resourceCount,
544 |       dynamicMode: runtimeData.dynamicMode,
545 |     };
546 |   }
547 | 
548 |   if (staticData) {
549 |     output.static = {
550 |       workflowCount: staticData.stats.workflowCount,
551 |       canonicalTools: staticData.stats.canonicalTools,
552 |       reExportTools: staticData.stats.reExportTools,
553 |       totalTools: staticData.stats.totalTools,
554 |     };
555 |   }
556 | 
557 |   // Add detailed data only if requested
558 |   if (options.workflows && staticData) {
559 |     output.workflows = staticData.workflows.map((w) => ({
560 |       name: w.displayName,
561 |       toolCount: w.toolCount,
562 |       canonicalCount: w.canonicalCount,
563 |       reExportCount: w.reExportCount,
564 |     }));
565 |   }
566 | 
567 |   if (options.tools) {
568 |     if (runtimeData) {
569 |       output.runtimeTools = runtimeData.tools.map((t) => t.name);
570 |     }
571 |     if (staticData) {
572 |       output.staticTools = staticData.tools
573 |         .filter((t) => t.isCanonical)
574 |         .map((t) => t.name)
575 |         .sort();
576 |     }
577 |   }
578 | 
579 |   if (options.resources && runtimeData) {
580 |     output.resources = runtimeData.resources.map((r) => r.uri);
581 |   }
582 | 
583 |   console.log(JSON.stringify(output, null, 2));
584 | }
585 | 
586 | /**
587 |  * Main execution function
588 |  */
589 | async function main(): Promise<void> {
590 |   try {
591 |     let runtimeData: RuntimeData | null = null;
592 |     let staticData: StaticAnalysisResult | null = null;
593 | 
594 |     // Gather data based on options
595 |     if (options.runtime) {
596 |       if (!options.json) {
597 |         console.log(`${colors.cyan}🔍 Gathering runtime information...${colors.reset}`);
598 |       }
599 |       runtimeData = await getRuntimeInfo();
600 |     }
601 | 
602 |     if (options.static) {
603 |       if (!options.json) {
604 |         console.log(`${colors.cyan}📁 Performing static analysis...${colors.reset}`);
605 |       }
606 |       staticData = await getStaticToolAnalysis();
607 |     }
608 | 
609 |     // For default command or workflows option, always gather static data for workflow info
610 |     if (options.workflows && !staticData) {
611 |       if (!options.json) {
612 |         console.log(`${colors.cyan}📁 Gathering workflow information...${colors.reset}`);
613 |       }
614 |       staticData = await getStaticToolAnalysis();
615 |     }
616 | 
617 |     if (!options.json) {
618 |       console.log(); // Blank line after gathering
619 |     }
620 | 
621 |     // Handle JSON output
622 |     if (options.json) {
623 |       outputJSON(runtimeData, staticData);
624 |       return;
625 |     }
626 | 
627 |     // Display based on command
628 |     switch (command) {
629 |       case 'count':
630 |       case 'c':
631 |         displaySummary(runtimeData, staticData);
632 |         displayWorkflows(staticData);
633 |         break;
634 | 
635 |       case 'list':
636 |       case 'l':
637 |         displaySummary(runtimeData, staticData);
638 |         displayTools(runtimeData, staticData);
639 |         displayResources(runtimeData);
640 |         break;
641 | 
642 |       case 'static':
643 |       case 's':
644 |         if (!staticData) {
645 |           console.log(`${colors.cyan}📁 Performing static analysis...${colors.reset}\n`);
646 |           staticData = await getStaticToolAnalysis();
647 |         }
648 |         displaySummary(null, staticData);
649 |         displayWorkflows(staticData);
650 | 
651 |         if (options.verbose) {
652 |           displayTools(null, staticData);
653 |           const reExportTools = staticData.tools.filter((t) => !t.isCanonical);
654 |           console.log(
655 |             `${colors.bright}🔄 Re-export Files (${reExportTools.length}):${colors.reset}`,
656 |           );
657 |           console.log('─'.repeat(40));
658 |           reExportTools.forEach((file) => {
659 |             console.log(`   ${colors.yellow}•${colors.reset} ${file.name} (${file.workflow})`);
660 |             console.log(`     ${file.relativePath}`);
661 |           });
662 |         }
663 |         break;
664 | 
665 |       default:
666 |         // Default case (no command) - show runtime summary with workflows
667 |         displaySummary(runtimeData, staticData);
668 |         displayWorkflows(staticData);
669 |         break;
670 |     }
671 | 
672 |     if (!options.json) {
673 |       console.log(`${colors.green}✅ Analysis complete!${colors.reset}`);
674 |     }
675 |   } catch (error) {
676 |     if (options.json) {
677 |       console.error(
678 |         JSON.stringify(
679 |           {
680 |             success: false,
681 |             error: (error as Error).message,
682 |             timestamp: new Date().toISOString(),
683 |           },
684 |           null,
685 |           2,
686 |         ),
687 |       );
688 |     } else {
689 |       console.error(`${colors.red}❌ Error: ${(error as Error).message}${colors.reset}`);
690 |     }
691 |     process.exit(1);
692 |   }
693 | }
694 | 
695 | // Run the CLI
696 | main();
697 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for tap plugin
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach } from 'vitest';
  6 | import { z } from 'zod';
  7 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
  8 | 
  9 | import tapPlugin, { AxeHelpers, tapLogic } from '../tap.ts';
 10 | 
 11 | // Helper function to create mock axe helpers
 12 | function createMockAxeHelpers(): AxeHelpers {
 13 |   return {
 14 |     getAxePath: () => '/mocked/axe/path',
 15 |     getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }),
 16 |     createAxeNotAvailableResponse: () => ({
 17 |       content: [
 18 |         {
 19 |           type: 'text',
 20 |           text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
 21 |         },
 22 |       ],
 23 |       isError: true,
 24 |     }),
 25 |   };
 26 | }
 27 | 
 28 | // Helper function to create mock axe helpers with null path (for dependency error tests)
 29 | function createMockAxeHelpersWithNullPath(): AxeHelpers {
 30 |   return {
 31 |     getAxePath: () => null,
 32 |     getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }),
 33 |     createAxeNotAvailableResponse: () => ({
 34 |       content: [
 35 |         {
 36 |           type: 'text',
 37 |           text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
 38 |         },
 39 |       ],
 40 |       isError: true,
 41 |     }),
 42 |   };
 43 | }
 44 | 
 45 | describe('Tap Plugin', () => {
 46 |   describe('Export Field Validation (Literal)', () => {
 47 |     it('should have correct name', () => {
 48 |       expect(tapPlugin.name).toBe('tap');
 49 |     });
 50 | 
 51 |     it('should have correct description', () => {
 52 |       expect(tapPlugin.description).toBe(
 53 |         "Tap at specific coordinates. Use describe_ui to get precise element coordinates (don't guess from screenshots). Supports optional timing delays.",
 54 |       );
 55 |     });
 56 | 
 57 |     it('should have handler function', () => {
 58 |       expect(typeof tapPlugin.handler).toBe('function');
 59 |     });
 60 | 
 61 |     it('should validate schema fields with safeParse', () => {
 62 |       const schema = z.object(tapPlugin.schema);
 63 | 
 64 |       // Valid case
 65 |       expect(
 66 |         schema.safeParse({
 67 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 68 |           x: 100,
 69 |           y: 200,
 70 |         }).success,
 71 |       ).toBe(true);
 72 | 
 73 |       // Invalid simulatorUuid
 74 |       expect(
 75 |         schema.safeParse({
 76 |           simulatorUuid: 'invalid-uuid',
 77 |           x: 100,
 78 |           y: 200,
 79 |         }).success,
 80 |       ).toBe(false);
 81 | 
 82 |       // Invalid x coordinate - non-integer
 83 |       expect(
 84 |         schema.safeParse({
 85 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 86 |           x: 3.14,
 87 |           y: 200,
 88 |         }).success,
 89 |       ).toBe(false);
 90 | 
 91 |       // Invalid y coordinate - non-integer
 92 |       expect(
 93 |         schema.safeParse({
 94 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 95 |           x: 100,
 96 |           y: 3.14,
 97 |         }).success,
 98 |       ).toBe(false);
 99 | 
100 |       // Invalid preDelay - negative
101 |       expect(
102 |         schema.safeParse({
103 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
104 |           x: 100,
105 |           y: 200,
106 |           preDelay: -1,
107 |         }).success,
108 |       ).toBe(false);
109 | 
110 |       // Invalid postDelay - negative
111 |       expect(
112 |         schema.safeParse({
113 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
114 |           x: 100,
115 |           y: 200,
116 |           postDelay: -1,
117 |         }).success,
118 |       ).toBe(false);
119 | 
120 |       // Valid with optional delays
121 |       expect(
122 |         schema.safeParse({
123 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
124 |           x: 100,
125 |           y: 200,
126 |           preDelay: 0.5,
127 |           postDelay: 1.0,
128 |         }).success,
129 |       ).toBe(true);
130 | 
131 |       // Missing required fields
132 |       expect(schema.safeParse({}).success).toBe(false);
133 |     });
134 |   });
135 | 
136 |   describe('Command Generation', () => {
137 |     let callHistory: Array<{
138 |       command: string[];
139 |       logPrefix?: string;
140 |       useShell?: boolean;
141 |       env?: Record<string, string>;
142 |     }>;
143 | 
144 |     beforeEach(() => {
145 |       callHistory = [];
146 |     });
147 | 
148 |     it('should generate correct axe command with minimal parameters', async () => {
149 |       const mockExecutor = createMockExecutor({
150 |         success: true,
151 |         output: 'Tap completed',
152 |       });
153 | 
154 |       const wrappedExecutor = async (
155 |         command: string[],
156 |         logPrefix?: string,
157 |         useShell?: boolean,
158 |         env?: Record<string, string>,
159 |       ) => {
160 |         callHistory.push({ command, logPrefix, useShell, env });
161 |         return mockExecutor(command, logPrefix, useShell, env);
162 |       };
163 | 
164 |       const mockAxeHelpers = createMockAxeHelpers();
165 | 
166 |       await tapLogic(
167 |         {
168 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
169 |           x: 100,
170 |           y: 200,
171 |         },
172 |         wrappedExecutor,
173 |         mockAxeHelpers,
174 |       );
175 | 
176 |       expect(callHistory).toHaveLength(1);
177 |       expect(callHistory[0]).toEqual({
178 |         command: [
179 |           '/mocked/axe/path',
180 |           'tap',
181 |           '-x',
182 |           '100',
183 |           '-y',
184 |           '200',
185 |           '--udid',
186 |           '12345678-1234-1234-1234-123456789012',
187 |         ],
188 |         logPrefix: '[AXe]: tap',
189 |         useShell: false,
190 |         env: { SOME_ENV: 'value' },
191 |       });
192 |     });
193 | 
194 |     it('should generate correct axe command with pre-delay', async () => {
195 |       const mockExecutor = createMockExecutor({
196 |         success: true,
197 |         output: 'Tap completed',
198 |       });
199 | 
200 |       const wrappedExecutor = async (
201 |         command: string[],
202 |         logPrefix?: string,
203 |         useShell?: boolean,
204 |         env?: Record<string, string>,
205 |       ) => {
206 |         callHistory.push({ command, logPrefix, useShell, env });
207 |         return mockExecutor(command, logPrefix, useShell, env);
208 |       };
209 | 
210 |       const mockAxeHelpers = createMockAxeHelpers();
211 | 
212 |       await tapLogic(
213 |         {
214 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
215 |           x: 150,
216 |           y: 300,
217 |           preDelay: 0.5,
218 |         },
219 |         wrappedExecutor,
220 |         mockAxeHelpers,
221 |       );
222 | 
223 |       expect(callHistory).toHaveLength(1);
224 |       expect(callHistory[0]).toEqual({
225 |         command: [
226 |           '/mocked/axe/path',
227 |           'tap',
228 |           '-x',
229 |           '150',
230 |           '-y',
231 |           '300',
232 |           '--pre-delay',
233 |           '0.5',
234 |           '--udid',
235 |           '12345678-1234-1234-1234-123456789012',
236 |         ],
237 |         logPrefix: '[AXe]: tap',
238 |         useShell: false,
239 |         env: { SOME_ENV: 'value' },
240 |       });
241 |     });
242 | 
243 |     it('should generate correct axe command with post-delay', async () => {
244 |       const mockExecutor = createMockExecutor({
245 |         success: true,
246 |         output: 'Tap completed',
247 |       });
248 | 
249 |       const wrappedExecutor = async (
250 |         command: string[],
251 |         logPrefix?: string,
252 |         useShell?: boolean,
253 |         env?: Record<string, string>,
254 |       ) => {
255 |         callHistory.push({ command, logPrefix, useShell, env });
256 |         return mockExecutor(command, logPrefix, useShell, env);
257 |       };
258 | 
259 |       const mockAxeHelpers = createMockAxeHelpers();
260 | 
261 |       await tapLogic(
262 |         {
263 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
264 |           x: 250,
265 |           y: 400,
266 |           postDelay: 1.0,
267 |         },
268 |         wrappedExecutor,
269 |         mockAxeHelpers,
270 |       );
271 | 
272 |       expect(callHistory).toHaveLength(1);
273 |       expect(callHistory[0]).toEqual({
274 |         command: [
275 |           '/mocked/axe/path',
276 |           'tap',
277 |           '-x',
278 |           '250',
279 |           '-y',
280 |           '400',
281 |           '--post-delay',
282 |           '1',
283 |           '--udid',
284 |           '12345678-1234-1234-1234-123456789012',
285 |         ],
286 |         logPrefix: '[AXe]: tap',
287 |         useShell: false,
288 |         env: { SOME_ENV: 'value' },
289 |       });
290 |     });
291 | 
292 |     it('should generate correct axe command with both delays', async () => {
293 |       const mockExecutor = createMockExecutor({
294 |         success: true,
295 |         output: 'Tap completed',
296 |       });
297 | 
298 |       const wrappedExecutor = async (
299 |         command: string[],
300 |         logPrefix?: string,
301 |         useShell?: boolean,
302 |         env?: Record<string, string>,
303 |       ) => {
304 |         callHistory.push({ command, logPrefix, useShell, env });
305 |         return mockExecutor(command, logPrefix, useShell, env);
306 |       };
307 | 
308 |       const mockAxeHelpers = createMockAxeHelpers();
309 | 
310 |       await tapLogic(
311 |         {
312 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
313 |           x: 350,
314 |           y: 500,
315 |           preDelay: 0.3,
316 |           postDelay: 0.7,
317 |         },
318 |         wrappedExecutor,
319 |         mockAxeHelpers,
320 |       );
321 | 
322 |       expect(callHistory).toHaveLength(1);
323 |       expect(callHistory[0]).toEqual({
324 |         command: [
325 |           '/mocked/axe/path',
326 |           'tap',
327 |           '-x',
328 |           '350',
329 |           '-y',
330 |           '500',
331 |           '--pre-delay',
332 |           '0.3',
333 |           '--post-delay',
334 |           '0.7',
335 |           '--udid',
336 |           '12345678-1234-1234-1234-123456789012',
337 |         ],
338 |         logPrefix: '[AXe]: tap',
339 |         useShell: false,
340 |         env: { SOME_ENV: 'value' },
341 |       });
342 |     });
343 |   });
344 | 
345 |   describe('Success Response Processing', () => {
346 |     it('should return successful response for basic tap', async () => {
347 |       const mockExecutor = createMockExecutor({
348 |         success: true,
349 |         output: 'Tap completed',
350 |       });
351 | 
352 |       const mockAxeHelpers = createMockAxeHelpers();
353 | 
354 |       const result = await tapLogic(
355 |         {
356 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
357 |           x: 100,
358 |           y: 200,
359 |         },
360 |         mockExecutor,
361 |         mockAxeHelpers,
362 |       );
363 | 
364 |       expect(result).toEqual({
365 |         content: [
366 |           {
367 |             type: 'text',
368 |             text: 'Tap at (100, 200) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
369 |           },
370 |         ],
371 |         isError: false,
372 |       });
373 |     });
374 | 
375 |     it('should return successful response with coordinate warning when describe_ui not called', async () => {
376 |       const mockExecutor = createMockExecutor({
377 |         success: true,
378 |         output: 'Tap completed',
379 |       });
380 | 
381 |       const mockAxeHelpers = createMockAxeHelpers();
382 | 
383 |       const result = await tapLogic(
384 |         {
385 |           simulatorUuid: '87654321-4321-4321-4321-210987654321',
386 |           x: 150,
387 |           y: 300,
388 |         },
389 |         mockExecutor,
390 |         mockAxeHelpers,
391 |       );
392 | 
393 |       expect(result).toEqual({
394 |         content: [
395 |           {
396 |             type: 'text',
397 |             text: 'Tap at (150, 300) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
398 |           },
399 |         ],
400 |         isError: false,
401 |       });
402 |     });
403 | 
404 |     it('should return successful response with delays', async () => {
405 |       const mockExecutor = createMockExecutor({
406 |         success: true,
407 |         output: 'Tap completed',
408 |       });
409 | 
410 |       const mockAxeHelpers = createMockAxeHelpers();
411 | 
412 |       const result = await tapLogic(
413 |         {
414 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
415 |           x: 250,
416 |           y: 400,
417 |           preDelay: 0.5,
418 |           postDelay: 1.0,
419 |         },
420 |         mockExecutor,
421 |         mockAxeHelpers,
422 |       );
423 | 
424 |       expect(result).toEqual({
425 |         content: [
426 |           {
427 |             type: 'text',
428 |             text: 'Tap at (250, 400) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
429 |           },
430 |         ],
431 |         isError: false,
432 |       });
433 |     });
434 | 
435 |     it('should return successful response with integer coordinates', async () => {
436 |       const mockExecutor = createMockExecutor({
437 |         success: true,
438 |         output: 'Tap completed',
439 |       });
440 | 
441 |       const mockAxeHelpers = createMockAxeHelpers();
442 | 
443 |       const result = await tapLogic(
444 |         {
445 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
446 |           x: 0,
447 |           y: 0,
448 |         },
449 |         mockExecutor,
450 |         mockAxeHelpers,
451 |       );
452 | 
453 |       expect(result).toEqual({
454 |         content: [
455 |           {
456 |             type: 'text',
457 |             text: 'Tap at (0, 0) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
458 |           },
459 |         ],
460 |         isError: false,
461 |       });
462 |     });
463 | 
464 |     it('should return successful response with large coordinates', async () => {
465 |       const mockExecutor = createMockExecutor({
466 |         success: true,
467 |         output: 'Tap completed',
468 |       });
469 | 
470 |       const mockAxeHelpers = createMockAxeHelpers();
471 | 
472 |       const result = await tapLogic(
473 |         {
474 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
475 |           x: 1920,
476 |           y: 1080,
477 |         },
478 |         mockExecutor,
479 |         mockAxeHelpers,
480 |       );
481 | 
482 |       expect(result).toEqual({
483 |         content: [
484 |           {
485 |             type: 'text',
486 |             text: 'Tap at (1920, 1080) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
487 |           },
488 |         ],
489 |         isError: false,
490 |       });
491 |     });
492 |   });
493 | 
494 |   describe('Plugin Handler Validation', () => {
495 |     it('should return Zod validation error for missing simulatorUuid', async () => {
496 |       const result = await tapPlugin.handler({
497 |         x: 100,
498 |         y: 200,
499 |       });
500 | 
501 |       expect(result).toEqual({
502 |         content: [
503 |           {
504 |             type: 'text',
505 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Required',
506 |           },
507 |         ],
508 |         isError: true,
509 |       });
510 |     });
511 | 
512 |     it('should return Zod validation error for missing x coordinate', async () => {
513 |       const result = await tapPlugin.handler({
514 |         simulatorUuid: '12345678-1234-1234-1234-123456789012',
515 |         y: 200,
516 |       });
517 | 
518 |       expect(result).toEqual({
519 |         content: [
520 |           {
521 |             type: 'text',
522 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nx: Required',
523 |           },
524 |         ],
525 |         isError: true,
526 |       });
527 |     });
528 | 
529 |     it('should return Zod validation error for missing y coordinate', async () => {
530 |       const result = await tapPlugin.handler({
531 |         simulatorUuid: '12345678-1234-1234-1234-123456789012',
532 |         x: 100,
533 |       });
534 | 
535 |       expect(result).toEqual({
536 |         content: [
537 |           {
538 |             type: 'text',
539 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\ny: Required',
540 |           },
541 |         ],
542 |         isError: true,
543 |       });
544 |     });
545 | 
546 |     it('should return Zod validation error for invalid UUID format', async () => {
547 |       const result = await tapPlugin.handler({
548 |         simulatorUuid: 'invalid-uuid',
549 |         x: 100,
550 |         y: 200,
551 |       });
552 | 
553 |       expect(result).toEqual({
554 |         content: [
555 |           {
556 |             type: 'text',
557 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Invalid Simulator UUID format',
558 |           },
559 |         ],
560 |         isError: true,
561 |       });
562 |     });
563 | 
564 |     it('should return Zod validation error for non-integer x coordinate', async () => {
565 |       const result = await tapPlugin.handler({
566 |         simulatorUuid: '12345678-1234-1234-1234-123456789012',
567 |         x: 3.14,
568 |         y: 200,
569 |       });
570 | 
571 |       expect(result).toEqual({
572 |         content: [
573 |           {
574 |             type: 'text',
575 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nx: X coordinate must be an integer',
576 |           },
577 |         ],
578 |         isError: true,
579 |       });
580 |     });
581 | 
582 |     it('should return Zod validation error for non-integer y coordinate', async () => {
583 |       const result = await tapPlugin.handler({
584 |         simulatorUuid: '12345678-1234-1234-1234-123456789012',
585 |         x: 100,
586 |         y: 3.14,
587 |       });
588 | 
589 |       expect(result).toEqual({
590 |         content: [
591 |           {
592 |             type: 'text',
593 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\ny: Y coordinate must be an integer',
594 |           },
595 |         ],
596 |         isError: true,
597 |       });
598 |     });
599 | 
600 |     it('should return Zod validation error for negative preDelay', async () => {
601 |       const result = await tapPlugin.handler({
602 |         simulatorUuid: '12345678-1234-1234-1234-123456789012',
603 |         x: 100,
604 |         y: 200,
605 |         preDelay: -1,
606 |       });
607 | 
608 |       expect(result).toEqual({
609 |         content: [
610 |           {
611 |             type: 'text',
612 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\npreDelay: Pre-delay must be non-negative',
613 |           },
614 |         ],
615 |         isError: true,
616 |       });
617 |     });
618 | 
619 |     it('should return Zod validation error for negative postDelay', async () => {
620 |       const result = await tapPlugin.handler({
621 |         simulatorUuid: '12345678-1234-1234-1234-123456789012',
622 |         x: 100,
623 |         y: 200,
624 |         postDelay: -1,
625 |       });
626 | 
627 |       expect(result).toEqual({
628 |         content: [
629 |           {
630 |             type: 'text',
631 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\npostDelay: Post-delay must be non-negative',
632 |           },
633 |         ],
634 |         isError: true,
635 |       });
636 |     });
637 |   });
638 | 
639 |   describe('Handler Behavior (Complete Literal Returns)', () => {
640 |     it('should return DependencyError when axe binary is not found', async () => {
641 |       const mockExecutor = createMockExecutor({
642 |         success: true,
643 |         output: 'Tap completed',
644 |         error: undefined,
645 |       });
646 | 
647 |       const mockAxeHelpers = createMockAxeHelpersWithNullPath();
648 | 
649 |       const result = await tapLogic(
650 |         {
651 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
652 |           x: 100,
653 |           y: 200,
654 |           preDelay: 0.5,
655 |           postDelay: 1.0,
656 |         },
657 |         mockExecutor,
658 |         mockAxeHelpers,
659 |       );
660 | 
661 |       expect(result).toEqual({
662 |         content: [
663 |           {
664 |             type: 'text',
665 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
666 |           },
667 |         ],
668 |         isError: true,
669 |       });
670 |     });
671 | 
672 |     it('should handle DependencyError when axe binary not found (second test)', async () => {
673 |       const mockExecutor = createMockExecutor({
674 |         success: false,
675 |         output: '',
676 |         error: 'Coordinates out of bounds',
677 |       });
678 | 
679 |       const mockAxeHelpers = createMockAxeHelpersWithNullPath();
680 | 
681 |       const result = await tapLogic(
682 |         {
683 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
684 |           x: 100,
685 |           y: 200,
686 |         },
687 |         mockExecutor,
688 |         mockAxeHelpers,
689 |       );
690 | 
691 |       expect(result).toEqual({
692 |         content: [
693 |           {
694 |             type: 'text',
695 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
696 |           },
697 |         ],
698 |         isError: true,
699 |       });
700 |     });
701 | 
702 |     it('should handle DependencyError when axe binary not found (third test)', async () => {
703 |       const mockExecutor = createMockExecutor({
704 |         success: false,
705 |         output: '',
706 |         error: 'System error occurred',
707 |       });
708 | 
709 |       const mockAxeHelpers = createMockAxeHelpersWithNullPath();
710 | 
711 |       const result = await tapLogic(
712 |         {
713 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
714 |           x: 100,
715 |           y: 200,
716 |         },
717 |         mockExecutor,
718 |         mockAxeHelpers,
719 |       );
720 | 
721 |       expect(result).toEqual({
722 |         content: [
723 |           {
724 |             type: 'text',
725 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
726 |           },
727 |         ],
728 |         isError: true,
729 |       });
730 |     });
731 | 
732 |     it('should handle DependencyError when axe binary not found (fourth test)', async () => {
733 |       const mockExecutor = async () => {
734 |         throw new Error('ENOENT: no such file or directory');
735 |       };
736 | 
737 |       const mockAxeHelpers = createMockAxeHelpersWithNullPath();
738 | 
739 |       const result = await tapLogic(
740 |         {
741 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
742 |           x: 100,
743 |           y: 200,
744 |         },
745 |         mockExecutor,
746 |         mockAxeHelpers,
747 |       );
748 | 
749 |       expect(result).toEqual({
750 |         content: [
751 |           {
752 |             type: 'text',
753 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
754 |           },
755 |         ],
756 |         isError: true,
757 |       });
758 |     });
759 | 
760 |     it('should handle DependencyError when axe binary not found (fifth test)', async () => {
761 |       const mockExecutor = async () => {
762 |         throw new Error('Unexpected error');
763 |       };
764 | 
765 |       const mockAxeHelpers = createMockAxeHelpersWithNullPath();
766 | 
767 |       const result = await tapLogic(
768 |         {
769 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
770 |           x: 100,
771 |           y: 200,
772 |         },
773 |         mockExecutor,
774 |         mockAxeHelpers,
775 |       );
776 | 
777 |       expect(result).toEqual({
778 |         content: [
779 |           {
780 |             type: 'text',
781 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
782 |           },
783 |         ],
784 |         isError: true,
785 |       });
786 |     });
787 | 
788 |     it('should handle DependencyError when axe binary not found (sixth test)', async () => {
789 |       const mockExecutor = async () => {
790 |         throw 'String error';
791 |       };
792 | 
793 |       const mockAxeHelpers = createMockAxeHelpersWithNullPath();
794 | 
795 |       const result = await tapLogic(
796 |         {
797 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
798 |           x: 100,
799 |           y: 200,
800 |         },
801 |         mockExecutor,
802 |         mockAxeHelpers,
803 |       );
804 | 
805 |       expect(result).toEqual({
806 |         content: [
807 |           {
808 |             type: 'text',
809 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
810 |           },
811 |         ],
812 |         isError: true,
813 |       });
814 |     });
815 |   });
816 | });
817 | 
```

--------------------------------------------------------------------------------
/docs/ARCHITECTURE.md:
--------------------------------------------------------------------------------

```markdown
  1 | # XcodeBuildMCP Architecture
  2 | 
  3 | ## Table of Contents
  4 | 
  5 | 1. [Overview](#overview)
  6 | 2. [Core Architecture](#core-architecture)
  7 | 3. [Design Principles](#design-principles)
  8 | 4. [Component Details](#component-details)
  9 | 5. [Registration System](#registration-system)
 10 | 6. [Tool Naming Conventions & Glossary](#tool-naming-conventions--glossary)
 11 | 7. [Testing Architecture](#testing-architecture)
 12 | 8. [Build and Deployment](#build-and-deployment)
 13 | 9. [Extension Guidelines](#extension-guidelines)
 14 | 10. [Performance Considerations](#performance-considerations)
 15 | 11. [Security Considerations](#security-considerations)
 16 | 
 17 | ## Overview
 18 | 
 19 | XcodeBuildMCP is a Model Context Protocol (MCP) server that exposes Xcode operations as tools for AI assistants. The architecture emphasizes modularity, type safety, and selective enablement to support diverse development workflows.
 20 | 
 21 | ### High-Level Objectives
 22 | 
 23 | - Expose Xcode-related tools (build, test, deploy, UI automation, etc.) through MCP
 24 | - Run as a long-lived stdio-based server for LLM agents, CLIs, or editors
 25 | - Enable fine-grained, opt-in activation of individual tools or tool groups
 26 | - Support incremental builds via experimental xcodemake with xcodebuild fallback
 27 | 
 28 | ## Core Architecture
 29 | 
 30 | ### Runtime Flow
 31 | 
 32 | 1. **Initialization**
 33 |    - The `xcodebuildmcp` executable, as defined in `package.json`, points to the compiled `build/index.js` which executes the main logic from `src/index.ts`.
 34 |    - Sentry initialized for error tracking (optional)
 35 |    - Version information loaded from `package.json`
 36 | 
 37 | 2. **Server Creation**
 38 |    - MCP server created with stdio transport
 39 |    - Plugin discovery system initialized
 40 | 
 41 | 3. **Plugin Discovery (Build-Time)**
 42 |    - A build-time script (`build-plugins/plugin-discovery.ts`) scans the `src/mcp/tools/` and `src/mcp/resources/` directories
 43 |    - It generates `src/core/generated-plugins.ts` and `src/core/generated-resources.ts` with dynamic import maps
 44 |    - This approach improves startup performance by avoiding synchronous file system scans and enables code-splitting
 45 |    - Tool code is only loaded when needed, reducing initial memory footprint
 46 | 
 47 | 4. **Plugin & Resource Loading (Runtime)**
 48 |    - At runtime, `loadPlugins()` and `loadResources()` use the generated loaders from the previous step
 49 |    - In **Static Mode**, all workflow loaders are executed at startup to register all tools
 50 |    - In **Dynamic Mode**, only the `discover_tools` tool is registered initially
 51 |    - The `enableWorkflows` function in `src/core/dynamic-tools.ts` uses generated loaders to dynamically import and register selected workflow tools on demand
 52 | 
 53 | 5. **Tool Registration**
 54 |    - Discovered tools automatically registered with server using pre-generated maps
 55 |    - No manual registration or configuration required
 56 |    - Environment variables control dynamic tool discovery behavior
 57 | 
 58 | 5. **Request Handling**
 59 |    - MCP client calls tool → server routes to tool handler
 60 |    - Zod validates parameters before execution
 61 |    - Tool handler uses shared utilities (build, simctl, etc.)
 62 |    - Returns standardized `ToolResponse`
 63 | 
 64 | 6. **Response Streaming**
 65 |    - Server streams response back to client
 66 |    - Consistent error handling with `isError` flag
 67 | 
 68 | ## Design Principles
 69 | 
 70 | ### 1. **Plugin Autonomy**
 71 | Tools are self-contained units that export a standardized interface. They don't know about the server implementation, ensuring loose coupling and high testability.
 72 | 
 73 | ### 2. **Pure Functions vs Stateful Components**
 74 | - Most utilities are stateless pure functions
 75 | - Stateful components (e.g., process tracking) isolated in specific tool modules
 76 | - Clear separation between computation and side effects
 77 | 
 78 | ### 3. **Single Source of Truth**
 79 | - Version from `package.json` drives all version references
 80 | - Tool directory structure is authoritative tool source
 81 | - Environment variables provide consistent configuration interface
 82 | 
 83 | ### 4. **Feature Isolation**
 84 | - Experimental features behind environment flags
 85 | - Optional dependencies (Sentry, xcodemake) gracefully degrade
 86 | - Tool directory structure enables workflow-specific organization
 87 | 
 88 | ### 5. **Type Safety Throughout**
 89 | - TypeScript strict mode enabled
 90 | - Zod schemas for runtime validation
 91 | - Generic type constraints ensure compile-time safety
 92 | 
 93 | ## Module Organization and Import Strategy
 94 | 
 95 | ### Focused Facades Pattern
 96 | 
 97 | XcodeBuildMCP has migrated from a traditional "barrel file" export pattern (`src/utils/index.ts`) to a more structured **focused facades** pattern. Each distinct area of functionality within `src/utils` is exposed through its own `index.ts` file in a dedicated subdirectory.
 98 | 
 99 | **Example Structure:**
100 | 
101 | ```
102 | src/utils/
103 | ├── execution/
104 | │   └── index.ts  # Facade for CommandExecutor, FileSystemExecutor
105 | ├── logging/
106 | │   └── index.ts  # Facade for the logger
107 | ├── responses/
108 | │   └── index.ts  # Facade for error types and response creators
109 | ├── validation/
110 | │   └── index.ts  # Facade for validation utilities
111 | ├── axe/
112 | │   └── index.ts  # Facade for axe UI automation helpers
113 | ├── plugin-registry/
114 | │   └── index.ts  # Facade for plugin system utilities
115 | ├── xcodemake/
116 | │   └── index.ts  # Facade for xcodemake utilities
117 | ├── template/
118 | │   └── index.ts  # Facade for template management utilities
119 | ├── version/
120 | │   └── index.ts  # Facade for version information
121 | ├── test/
122 | │   └── index.ts  # Facade for test utilities
123 | ├── log-capture/
124 | │   └── index.ts  # Facade for log capture utilities
125 | └── index.ts      # Deprecated barrel file (legacy/external use only)
126 | ```
127 | 
128 | This approach offers several architectural benefits:
129 | 
130 | - **Clear Dependencies**: It makes the dependency graph explicit. Importing from `utils/execution` clearly indicates a dependency on command execution logic
131 | - **Reduced Coupling**: Modules only import the functionality they need, reducing coupling between unrelated utility components
132 | - **Prevention of Circular Dependencies**: It's much harder to create circular dependencies, which were a risk with the large barrel file
133 | - **Improved Tree-Shaking**: Bundlers can more effectively eliminate unused code
134 | - **Performance**: Eliminates loading of unused modules, reducing startup time and memory usage
135 | 
136 | ### ESLint Enforcement
137 | 
138 | To maintain this architecture, an ESLint rule in `eslint.config.js` explicitly forbids importing from the deprecated barrel file within the `src/` directory.
139 | 
140 | **ESLint Rule Snippet** (`eslint.config.js`):
141 | 
142 | ```javascript
143 | 'no-restricted-imports': ['error', {
144 |   patterns: [{
145 |     group: ['**/utils/index.js', '../utils/index.js', '../../utils/index.js', '../../../utils/index.js'],
146 |     message: 'Barrel imports from utils/index.js are prohibited. Use focused facade imports instead (e.g., utils/logging/index.js, utils/execution/index.js).'
147 |   }]
148 | }],
149 | ```
150 | 
151 | This rule prevents regression to the previous barrel import pattern and ensures all new code follows the focused facade architecture.
152 | 
153 | ## Component Details
154 | 
155 | ### Entry Points
156 | 
157 | #### `src/index.ts`
158 | Main server entry point responsible for:
159 | - Sentry initialization (if enabled)
160 | - xcodemake availability check
161 | - Server creation and startup
162 | - Process lifecycle management (SIGTERM, SIGINT)
163 | - Error handling and logging
164 | 
165 | #### `src/doctor-cli.ts`
166 | Standalone doctor tool for:
167 | - Environment validation
168 | - Dependency checking
169 | - Configuration verification
170 | - Troubleshooting assistance
171 | 
172 | ### Server Layer
173 | 
174 | #### `src/server/server.ts`
175 | MCP server wrapper providing:
176 | - Server instance creation
177 | - stdio transport configuration
178 | - Request/response handling
179 | - Error boundary implementation
180 | 
181 | ### Tool Discovery System
182 | 
183 | #### `src/core/plugin-registry.ts`
184 | Runtime plugin loading system that leverages build-time generated code:
185 | - Uses `WORKFLOW_LOADERS` and `WORKFLOW_METADATA` maps from the generated `src/core/generated-plugins.ts` file
186 | - `loadWorkflowGroups()` iterates through the loaders, dynamically importing each workflow module using `await loader()`
187 | - Validates that each imported module contains the required `workflow` metadata export
188 | - Aggregates all tools from the loaded workflows into a single map
189 | - This system eliminates runtime file system scanning, providing significant startup performance boost
190 | 
191 | #### `src/core/plugin-types.ts`
192 | Plugin type definitions:
193 | - `PluginMeta` interface for plugin structure
194 | - `WorkflowMeta` interface for workflow metadata
195 | - `WorkflowGroup` interface for directory organization
196 | 
197 | ### Tool Implementation
198 | 
199 | Each tool is implemented in TypeScript and follows a standardized pattern that separates the core business logic from the MCP handler boilerplate. This is achieved using the `createTypedTool` factory, which provides compile-time and runtime type safety.
200 | 
201 | **Standard Tool Pattern** (`src/mcp/tools/some-workflow/some_tool.ts`):
202 | 
203 | ```typescript
204 | import { z } from 'zod';
205 | import { createTypedTool } from '../../../utils/typed-tool-factory.js';
206 | import type { CommandExecutor } from '../../../utils/execution/index.js';
207 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
208 | import { log } from '../../../utils/logging/index.js';
209 | import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.js';
210 | 
211 | // 1. Define the Zod schema for parameters
212 | const someToolSchema = z.object({
213 |   requiredParam: z.string().describe('Description for AI'),
214 |   optionalParam: z.boolean().optional().describe('Optional parameter'),
215 | });
216 | 
217 | // 2. Infer the parameter type from the schema
218 | type SomeToolParams = z.infer<typeof someToolSchema>;
219 | 
220 | // 3. Implement the core logic in a separate, testable function
221 | // This function receives strongly-typed parameters and an injected executor.
222 | export async function someToolLogic(
223 |   params: SomeToolParams,
224 |   executor: CommandExecutor,
225 | ): Promise<ToolResponse> {
226 |   log('info', `Executing some_tool with param: ${params.requiredParam}`);
227 |   
228 |   try {
229 |     const result = await executor(['some', 'command'], 'Some Tool Operation');
230 |     
231 |     if (!result.success) {
232 |       return createErrorResponse('Operation failed', result.error);
233 |     }
234 |     
235 |     return createTextResponse(`✅ Success: ${result.output}`);
236 |   } catch (error) {
237 |     const errorMessage = error instanceof Error ? error.message : String(error);
238 |     return createErrorResponse('Tool execution failed', errorMessage);
239 |   }
240 | }
241 | 
242 | // 4. Export the tool definition for auto-discovery
243 | export default {
244 |   name: 'some_tool',
245 |   description: 'Tool description for AI agents. Example: some_tool({ requiredParam: "value" })',
246 |   schema: someToolSchema.shape, // Expose shape for MCP SDK
247 |   
248 |   // 5. Create the handler using the type-safe factory
249 |   handler: createTypedTool(
250 |     someToolSchema,
251 |     someToolLogic,
252 |     getDefaultCommandExecutor,
253 |   ),
254 | };
255 | ```
256 | 
257 | This pattern ensures that:
258 | - The `someToolLogic` function is highly testable via dependency injection
259 | - Zod handles all runtime parameter validation automatically
260 | - The handler is type-safe, preventing unsafe access to parameters
261 | - Import paths use focused facades for clear dependency management
262 | ```
263 | 
264 | ### MCP Resources System
265 | 
266 | XcodeBuildMCP provides dual interfaces: traditional MCP tools and efficient MCP resources for supported clients. Resources are located in `src/mcp/resources/` and are automatically discovered **at build time**. The build process generates `src/core/generated-resources.ts`, which contains dynamic loaders for each resource, improving startup performance. For more details on creating resources, see the [Plugin Development Guide](docs/PLUGIN_DEVELOPMENT.md).
267 | 
268 | #### Resource Architecture
269 | 
270 | ```
271 | src/mcp/resources/
272 | ├── simulators.ts           # Simulator data resource
273 | └── __tests__/              # Resource-specific tests
274 | ```
275 | 
276 | #### Client Capability Detection
277 | 
278 | The system automatically detects client MCP capabilities:
279 | 
280 | ```typescript
281 | // src/core/resources.ts
282 | export function supportsResources(server?: unknown): boolean {
283 |   // Detects client capabilities via getClientCapabilities()
284 |   // Conservative fallback: assumes resource support
285 | }
286 | ```
287 | 
288 | #### Resource Implementation Pattern
289 | 
290 | Resources can reuse existing tool logic for consistency:
291 | 
292 | ```typescript
293 | // src/mcp/resources/some_resource.ts
294 | import { log } from '../../utils/logging/index.js';
295 | import { getDefaultCommandExecutor, CommandExecutor } from '../../utils/execution/index.js';
296 | import { getSomeResourceLogic } from '../tools/some-workflow/get_some_resource.js';
297 | 
298 | // Testable resource logic separated from MCP handler
299 | export async function someResourceResourceLogic(
300 |   executor: CommandExecutor = getDefaultCommandExecutor(),
301 | ): Promise<{ contents: Array<{ text: string }> }> {
302 |   try {
303 |     log('info', 'Processing some resource request');
304 | 
305 |     const result = await getSomeResourceLogic({}, executor);
306 | 
307 |     if (result.isError) {
308 |       const errorText = result.content[0]?.text;
309 |       throw new Error(
310 |         typeof errorText === 'string' ? errorText : 'Failed to retrieve some resource data',
311 |       );
312 |     }
313 | 
314 |     return {
315 |       contents: [
316 |         {
317 |           text:
318 |             typeof result.content[0]?.text === 'string'
319 |               ? result.content[0].text
320 |               : 'No data for that resource is available',
321 |         },
322 |       ],
323 |     };
324 |   } catch (error) {
325 |     const errorMessage = error instanceof Error ? error.message : String(error);
326 |     log('error', `Error in some_resource resource handler: ${errorMessage}`);
327 | 
328 |     return {
329 |       contents: [
330 |         {
331 |           text: `Error retrieving resource data: ${errorMessage}`,
332 |         },
333 |       ],
334 |     };
335 |   }
336 | }
337 | 
338 | export default {
339 |   uri: 'xcodebuildmcp://some_resource',
340 |   name: 'some_resource',
341 |   description: 'Returns some resource information',
342 |   mimeType: 'text/plain',
343 |   async handler(_uri: URL): Promise<{ contents: Array<{ text: string }> }> {
344 |     return someResourceResourceLogic();
345 |   },
346 | };
347 | ```
348 | 
349 | ## Registration System
350 | 
351 | XcodeBuildMCP supports two primary operating modes for tool registration, controlled by the `XCODEBUILDMCP_DYNAMIC_TOOLS` environment variable.
352 | 
353 | ### Static Mode (Default)
354 | 
355 | - **Environment**: `XCODEBUILDMCP_DYNAMIC_TOOLS` is `false` or not set.
356 | - **Behavior**: All available tools are loaded and registered with the MCP server at startup.
357 | - **Use Case**: This mode is ideal for environments where the full suite of tools is desired immediately, providing a comprehensive and predictable toolset for the AI assistant.
358 | 
359 | ### Dynamic Mode (AI-Powered Workflow Selection)
360 | 
361 | - **Environment**: `XCODEBUILDMCP_DYNAMIC_TOOLS=true`
362 | - **Behavior**: At startup, only the `discover_tools` tool is registered. This tool is designed to analyze a natural language task description from the user.
363 | - **Workflow**:
364 |     1. The client sends a task description (e.g., "I want to build and test my iOS app") to the `discover_tools` tool.
365 |     2. The tool uses the client's LLM via an MCP sampling request to determine the most relevant workflow group (e.g., `simulator-workspace`).
366 |     3. The server then dynamically loads and registers all tools from the selected workflow group.
367 |     4. The client is notified of the newly available tools.
368 | - **Use Case**: This mode is beneficial for conserving the LLM's context window by only loading a relevant subset of tools, leading to more focused and efficient interactions.
369 | 
370 | ## Tool Naming Conventions & Glossary
371 | 
372 | Tools follow a consistent naming pattern to ensure predictability and clarity. Understanding this convention is crucial for both using and developing tools.
373 | 
374 | ### Naming Pattern
375 | 
376 | The standard naming convention for tools is:
377 | 
378 | `{action}_{target}_{specifier}_{projectType}`
379 | 
380 | - **action**: The primary verb describing the tool's function (e.g., `build`, `test`, `get`, `list`).
381 | - **target**: The main subject of the action (e.g., `sim` for simulator, `dev` for device, `mac` for macOS).
382 | - **specifier**: A variant that specifies *how* the target is identified (e.g., `id` for UUID, `name` for by-name).
383 | - **projectType**: The type of Xcode project the tool operates on (e.g., `ws` for workspace, `proj` for project).
384 | 
385 | Not all parts are required for every tool. For example, `swift_package_build` has an action and a target, but no specifier or project type.
386 | 
387 | ### Examples
388 | 
389 | - `build_sim_id_ws`: **Build** for a **simulator** identified by its **ID (UUID)** from a **workspace**.
390 | - `test_dev_proj`: **Test** on a **device** from a **project**.
391 | - `get_mac_app_path_ws`: **Get** the app path for a **macOS** application from a **workspace**.
392 | - `list_sims`: **List** all **simulators**.
393 | 
394 | ### Glossary
395 | 
396 | | Term/Abbreviation | Meaning | Description |
397 | |---|---|---|
398 | | `ws` | Workspace | Refers to an `.xcworkspace` file. Used for projects with multiple `.xcodeproj` files or dependencies managed by CocoaPods or SPM. |
399 | | `proj` | Project | Refers to an `.xcodeproj` file. Used for single-project setups. |
400 | | `sim` | Simulator | Refers to the iOS, watchOS, tvOS, or visionOS simulator. |
401 | | `dev` | Device | Refers to a physical Apple device (iPhone, iPad, etc.). |
402 | | `mac` | macOS | Refers to a native macOS application target. |
403 | | `id` | Identifier | Refers to the unique identifier (UUID/UDID) of a simulator or device. |
404 | | `name` | Name | Refers to the human-readable name of a simulator (e.g., "iPhone 15 Pro"). |
405 | | `cap` | Capture | Used in logging tools, e.g., `start_sim_log_cap`. |
406 | 
407 | ## Testing Architecture
408 | 
409 | ### Framework and Configuration
410 | 
411 | - **Test Runner**: Vitest 3.x
412 | - **Environment**: Node.js
413 | - **Configuration**: `vitest.config.ts`
414 | - **Test Pattern**: `*.test.ts` files alongside implementation
415 | 
416 | ### Testing Principles
417 | 
418 | XcodeBuildMCP uses a strict **Dependency Injection (DI)** pattern for testing, which completely bans the use of traditional mocking libraries like Vitest's `vi.mock` or `vi.fn`. This ensures that tests are robust, maintainable, and verify the actual integration between components.
419 | 
420 | For detailed guidelines, see the [Testing Guide](docs/TESTING.md).
421 | 
422 | ### Test Structure Example
423 | 
424 | Tests inject mock "executors" for external interactions like command-line execution or file system access. This allows for deterministic testing of tool logic without mocking the implementation itself. The project provides helper functions like `createMockExecutor` and `createMockFileSystemExecutor` in `src/test-utils/mock-executors.ts` to facilitate this pattern.
425 | 
426 | ```typescript
427 | import { describe, it, expect } from 'vitest';
428 | import { someToolLogic } from '../tool-file.js'; // Import the logic function
429 | import { createMockExecutor } from '../../../test-utils/mock-executors.js';
430 | 
431 | describe('Tool Name', () => {
432 |   it('should execute successfully', async () => {
433 |     // 1. Create a mock executor to simulate command-line results
434 |     const mockExecutor = createMockExecutor({
435 |       success: true,
436 |       output: 'Command output'
437 |     });
438 | 
439 |     // 2. Call the tool's logic function, injecting the mock executor
440 |     const result = await someToolLogic({ requiredParam: 'value' }, mockExecutor);
441 |     
442 |     // 3. Assert the final result
443 |     expect(result).toEqual({
444 |       content: [{ type: 'text', text: 'Expected output' }],
445 |       isError: false
446 |     });
447 |   });
448 | });
449 | ```
450 | 
451 | ## Build and Deployment
452 | 
453 | ### Build Process
454 | 
455 | 1. **Version Generation**
456 |    ```bash
457 |    npm run build
458 |    ```
459 |    - Reads version from `package.json`
460 |    - Generates `src/version.ts`
461 | 
462 | 2. **Plugin & Resource Loader Generation**
463 |    - The `build-plugins/plugin-discovery.ts` script is executed
464 |    - It scans `src/mcp/tools/` and `src/mcp/resources/` to find all workflows and resources
465 |    - It generates `src/core/generated-plugins.ts` and `src/core/generated-resources.ts` with dynamic import maps
466 |    - This eliminates runtime file system scanning and enables code-splitting
467 | 
468 | 3. **TypeScript Compilation**
469 |    - `tsup` compiles the TypeScript source, including the newly generated files, into JavaScript
470 |    - Compiles TypeScript with tsup
471 | 
472 | 4. **Build Configuration** (`tsup.config.ts`)
473 |    - Entry points: `index.ts`, `doctor-cli.ts`
474 |    - Output format: ESM
475 |    - Target: Node 18+
476 |    - Source maps enabled
477 | 
478 | 5. **Distribution Structure**
479 |    ```
480 |    build/
481 |    ├── index.js          # Main server executable
482 |    ├── doctor-cli.js # Doctor tool
483 |    └── *.js.map         # Source maps
484 |    ```
485 | 
486 | ### npm Package
487 | 
488 | - **Name**: `xcodebuildmcp`
489 | - **Executables**:
490 |   - `xcodebuildmcp` → Main server
491 |   - `xcodebuildmcp-doctor` → Doctor tool
492 | - **Dependencies**: Minimal runtime dependencies
493 | - **Platform**: macOS only (due to Xcode requirement)
494 | 
495 | ### Bundled Resources
496 | 
497 | ```
498 | bundled/
499 | ├── axe              # UI automation binary
500 | └── Frameworks/      # Facebook device frameworks
501 |     ├── FBControlCore.framework
502 |     ├── FBDeviceControl.framework
503 |     └── FBSimulatorControl.framework
504 | ```
505 | 
506 | ## Extension Guidelines
507 | 
508 | This project is designed to be extensible. For comprehensive instructions on creating new tools, workflow groups, and resources, please refer to the dedicated [**Plugin Development Guide**](docs/PLUGIN_DEVELOPMENT.md).
509 | 
510 | The guide covers:
511 | - The auto-discovery system architecture.
512 | - The dependency injection pattern required for all new tools.
513 | - How to organize tools into workflow groups.
514 | - Testing guidelines and patterns.
515 | 
516 | ## Performance Considerations
517 | 
518 | ### Startup Performance
519 | 
520 | - **Build-Time Plugin Discovery**: The server avoids expensive and slow file system scans at startup by using pre-generated loader maps. This is the single most significant performance optimization
521 | - **Code-Splitting**: In Dynamic Mode, tool code is only loaded into memory when its workflow is enabled, reducing the initial memory footprint and parse time
522 | - **Focused Facades**: Using targeted imports instead of a large barrel file improves module resolution speed for the Node.js runtime
523 | - **Lazy Loading**: Tools only initialized when registered
524 | - **Selective Registration**: Fewer tools = faster startup
525 | - **Minimal Dependencies**: Fast module resolution
526 | 
527 | ### Runtime Performance
528 | 
529 | - **Stateless Operations**: Most tools complete quickly
530 | - **Process Management**: Long-running processes tracked separately
531 | - **Incremental Builds**: xcodemake provides significant speedup
532 | - **Parallel Execution**: Tools can run concurrently
533 | 
534 | ### Memory Management
535 | 
536 | - **Process Cleanup**: Proper process termination handling
537 | - **Log Rotation**: Captured logs have size limits
538 | - **Resource Disposal**: Explicit cleanup in lifecycle hooks
539 | 
540 | ### Optimization Strategies
541 | 
542 | 1. **Use Tool Groups**: Enable only needed workflows
543 | 2. **Enable Incremental Builds**: Set `INCREMENTAL_BUILDS_ENABLED=true`
544 | 3. **Limit Log Capture**: Use structured logging when possible
545 | 
546 | ## Security Considerations
547 | 
548 | ### Input Validation
549 | 
550 | - All tool inputs validated with Zod schemas
551 | - Command injection prevented via proper escaping
552 | - Path traversal protection in file operations
553 | 
554 | ### Process Isolation
555 | 
556 | - Tools run with user permissions
557 | - No privilege escalation
558 | - Sandboxed execution environment
559 | 
560 | ### Error Handling
561 | 
562 | - Sensitive information scrubbed from errors
563 | - Stack traces limited to application code
564 | - Sentry integration respects privacy settings
565 | 
```

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

```typescript
  1 | /**
  2 |  * Vitest test for scaffold_ios_project plugin
  3 |  *
  4 |  * Tests the plugin structure and iOS scaffold tool functionality
  5 |  * including parameter validation, file operations, template processing, and response formatting.
  6 |  *
  7 |  * Plugin location: plugins/utilities/scaffold_ios_project.js
  8 |  */
  9 | 
 10 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
 11 | import { z } from 'zod';
 12 | import scaffoldIosProject, { scaffold_ios_projectLogic } from '../scaffold_ios_project.ts';
 13 | import {
 14 |   createMockExecutor,
 15 |   createMockFileSystemExecutor,
 16 | } from '../../../../test-utils/mock-executors.ts';
 17 | 
 18 | describe('scaffold_ios_project plugin', () => {
 19 |   let mockCommandExecutor: any;
 20 |   let mockFileSystemExecutor: any;
 21 |   let originalEnv: string | undefined;
 22 | 
 23 |   beforeEach(() => {
 24 |     // Create mock executor using approved utility
 25 |     mockCommandExecutor = createMockExecutor({
 26 |       success: true,
 27 |       output: 'Command executed successfully',
 28 |     });
 29 | 
 30 |     mockFileSystemExecutor = createMockFileSystemExecutor({
 31 |       existsSync: (path) => {
 32 |         // Mock template directories exist but project files don't
 33 |         return (
 34 |           path.includes('xcodebuild-mcp-template') ||
 35 |           path.includes('XcodeBuildMCP-iOS-Template') ||
 36 |           path.includes('/template') ||
 37 |           path.endsWith('template') ||
 38 |           path.includes('extracted') ||
 39 |           path.includes('/mock/template/path')
 40 |         );
 41 |       },
 42 |       readFile: async () => 'template content with MyProject placeholder',
 43 |       readdir: async () => [
 44 |         { name: 'Package.swift', isDirectory: () => false, isFile: () => true } as any,
 45 |         { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true } as any,
 46 |       ],
 47 |       mkdir: async () => {},
 48 |       rm: async () => {},
 49 |       cp: async () => {},
 50 |       writeFile: async () => {},
 51 |       stat: async () => ({ isDirectory: () => true }),
 52 |     });
 53 | 
 54 |     // Store original environment for cleanup
 55 |     originalEnv = process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;
 56 |     // Set local template path to avoid download and chdir issues
 57 |     process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
 58 |   });
 59 | 
 60 |   afterEach(() => {
 61 |     // Restore original environment
 62 |     if (originalEnv !== undefined) {
 63 |       process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = originalEnv;
 64 |     } else {
 65 |       delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;
 66 |     }
 67 |   });
 68 | 
 69 |   describe('Export Field Validation (Literal)', () => {
 70 |     it('should have correct name field', () => {
 71 |       expect(scaffoldIosProject.name).toBe('scaffold_ios_project');
 72 |     });
 73 | 
 74 |     it('should have correct description field', () => {
 75 |       expect(scaffoldIosProject.description).toBe(
 76 |         'Scaffold a new iOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper iOS configuration.',
 77 |       );
 78 |     });
 79 | 
 80 |     it('should have handler as function', () => {
 81 |       expect(typeof scaffoldIosProject.handler).toBe('function');
 82 |     });
 83 | 
 84 |     it('should have valid schema with required fields', () => {
 85 |       const schema = z.object(scaffoldIosProject.schema);
 86 | 
 87 |       // Test valid input
 88 |       expect(
 89 |         schema.safeParse({
 90 |           projectName: 'MyTestApp',
 91 |           outputPath: '/path/to/output',
 92 |           bundleIdentifier: 'com.test.myapp',
 93 |           displayName: 'My Test App',
 94 |           marketingVersion: '1.0',
 95 |           currentProjectVersion: '1',
 96 |           customizeNames: true,
 97 |           deploymentTarget: '18.4',
 98 |           targetedDeviceFamily: ['iphone', 'ipad'],
 99 |           supportedOrientations: ['portrait', 'landscape-left'],
100 |           supportedOrientationsIpad: ['portrait', 'landscape-left', 'landscape-right'],
101 |         }).success,
102 |       ).toBe(true);
103 | 
104 |       // Test minimal valid input
105 |       expect(
106 |         schema.safeParse({
107 |           projectName: 'MyTestApp',
108 |           outputPath: '/path/to/output',
109 |         }).success,
110 |       ).toBe(true);
111 | 
112 |       // Test invalid input - missing projectName
113 |       expect(
114 |         schema.safeParse({
115 |           outputPath: '/path/to/output',
116 |         }).success,
117 |       ).toBe(false);
118 | 
119 |       // Test invalid input - missing outputPath
120 |       expect(
121 |         schema.safeParse({
122 |           projectName: 'MyTestApp',
123 |         }).success,
124 |       ).toBe(false);
125 | 
126 |       // Test invalid input - wrong type for customizeNames
127 |       expect(
128 |         schema.safeParse({
129 |           projectName: 'MyTestApp',
130 |           outputPath: '/path/to/output',
131 |           customizeNames: 'true',
132 |         }).success,
133 |       ).toBe(false);
134 | 
135 |       // Test invalid input - wrong enum value for targetedDeviceFamily
136 |       expect(
137 |         schema.safeParse({
138 |           projectName: 'MyTestApp',
139 |           outputPath: '/path/to/output',
140 |           targetedDeviceFamily: ['invalid-device'],
141 |         }).success,
142 |       ).toBe(false);
143 | 
144 |       // Test invalid input - wrong enum value for supportedOrientations
145 |       expect(
146 |         schema.safeParse({
147 |           projectName: 'MyTestApp',
148 |           outputPath: '/path/to/output',
149 |           supportedOrientations: ['invalid-orientation'],
150 |         }).success,
151 |       ).toBe(false);
152 |     });
153 |   });
154 | 
155 |   describe('Command Generation Tests', () => {
156 |     it('should generate correct curl command for iOS template download', async () => {
157 |       // Temporarily disable local template to force download
158 |       delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;
159 | 
160 |       // Track commands executed
161 |       let capturedCommands: string[][] = [];
162 |       const trackingCommandExecutor = createMockExecutor({
163 |         success: true,
164 |         output: 'Command executed successfully',
165 |       });
166 |       // Wrap to capture commands
167 |       const capturingExecutor = async (command: string[], ...args: any[]) => {
168 |         capturedCommands.push(command);
169 |         return trackingCommandExecutor(command, ...args);
170 |       };
171 | 
172 |       await scaffold_ios_projectLogic(
173 |         {
174 |           projectName: 'TestIOSApp',
175 |           outputPath: '/tmp/test-projects',
176 |         },
177 |         capturingExecutor,
178 |         mockFileSystemExecutor,
179 |       );
180 | 
181 |       // Verify curl command was executed
182 |       const curlCommand = capturedCommands.find((cmd) => cmd.includes('curl'));
183 |       expect(curlCommand).toBeDefined();
184 |       expect(curlCommand).toEqual([
185 |         'curl',
186 |         '-L',
187 |         '-f',
188 |         '-o',
189 |         expect.stringMatching(/template\.zip$/),
190 |         expect.stringMatching(
191 |           /https:\/\/github\.com\/cameroncooke\/XcodeBuildMCP-iOS-Template\/releases\/download\/v\d+\.\d+\.\d+\/XcodeBuildMCP-iOS-Template-\d+\.\d+\.\d+\.zip/,
192 |         ),
193 |       ]);
194 | 
195 |       // Restore environment variable
196 |       process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
197 |     });
198 | 
199 |     it.skip('should generate correct unzip command for iOS template extraction', async () => {
200 |       // Temporarily disable local template to force download
201 |       delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;
202 | 
203 |       // Create a mock that returns false for local template paths to force download
204 |       const downloadMockFileSystemExecutor = createMockFileSystemExecutor({
205 |         existsSync: (path) => {
206 |           // Only return true for extracted template directories, false for local template paths
207 |           return (
208 |             path.includes('xcodebuild-mcp-template') ||
209 |             path.includes('XcodeBuildMCP-iOS-Template') ||
210 |             path.includes('extracted')
211 |           );
212 |         },
213 |         readFile: async () => 'template content with MyProject placeholder',
214 |         readdir: async () => [
215 |           { name: 'Package.swift', isDirectory: () => false, isFile: () => true } as any,
216 |           { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true } as any,
217 |         ],
218 |         mkdir: async () => {},
219 |         rm: async () => {},
220 |         cp: async () => {},
221 |         writeFile: async () => {},
222 |         stat: async () => ({ isDirectory: () => true }),
223 |       });
224 | 
225 |       // Track commands executed
226 |       let capturedCommands: string[][] = [];
227 |       const trackingCommandExecutor = createMockExecutor({
228 |         success: true,
229 |         output: 'Command executed successfully',
230 |       });
231 |       // Wrap to capture commands
232 |       const capturingExecutor = async (command: string[], ...args: any[]) => {
233 |         capturedCommands.push(command);
234 |         return trackingCommandExecutor(command, ...args);
235 |       };
236 | 
237 |       await scaffold_ios_projectLogic(
238 |         {
239 |           projectName: 'TestIOSApp',
240 |           outputPath: '/tmp/test-projects',
241 |         },
242 |         capturingExecutor,
243 |         downloadMockFileSystemExecutor,
244 |       );
245 | 
246 |       // Verify unzip command was executed
247 |       const unzipCommand = capturedCommands.find((cmd) => cmd.includes('unzip'));
248 |       expect(unzipCommand).toBeDefined();
249 |       expect(unzipCommand).toEqual(['unzip', '-q', expect.stringMatching(/template\.zip$/)]);
250 | 
251 |       // Restore environment variable
252 |       process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
253 |     });
254 | 
255 |     it('should generate correct commands when using custom template version', async () => {
256 |       // Temporarily disable local template to force download
257 |       delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;
258 | 
259 |       // Set custom template version
260 |       const originalVersion = process.env.XCODEBUILD_MCP_IOS_TEMPLATE_VERSION;
261 |       process.env.XCODEBUILD_MCP_IOS_TEMPLATE_VERSION = 'v2.0.0';
262 | 
263 |       // Track commands executed
264 |       let capturedCommands: string[][] = [];
265 |       const trackingCommandExecutor = createMockExecutor({
266 |         success: true,
267 |         output: 'Command executed successfully',
268 |       });
269 |       // Wrap to capture commands
270 |       const capturingExecutor = async (command: string[], ...args: any[]) => {
271 |         capturedCommands.push(command);
272 |         return trackingCommandExecutor(command, ...args);
273 |       };
274 | 
275 |       await scaffold_ios_projectLogic(
276 |         {
277 |           projectName: 'TestIOSApp',
278 |           outputPath: '/tmp/test-projects',
279 |         },
280 |         capturingExecutor,
281 |         mockFileSystemExecutor,
282 |       );
283 | 
284 |       // Verify curl command uses custom version
285 |       const curlCommand = capturedCommands.find((cmd) => cmd.includes('curl'));
286 |       expect(curlCommand).toBeDefined();
287 |       expect(curlCommand).toEqual([
288 |         'curl',
289 |         '-L',
290 |         '-f',
291 |         '-o',
292 |         expect.stringMatching(/template\.zip$/),
293 |         'https://github.com/cameroncooke/XcodeBuildMCP-iOS-Template/releases/download/v2.0.0/XcodeBuildMCP-iOS-Template-2.0.0.zip',
294 |       ]);
295 | 
296 |       // Restore original version
297 |       if (originalVersion) {
298 |         process.env.XCODEBUILD_MCP_IOS_TEMPLATE_VERSION = originalVersion;
299 |       } else {
300 |         delete process.env.XCODEBUILD_MCP_IOS_TEMPLATE_VERSION;
301 |       }
302 | 
303 |       // Restore environment variable
304 |       process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
305 |     });
306 | 
307 |     it.skip('should generate correct commands with no command executor passed', async () => {
308 |       // Temporarily disable local template to force download
309 |       delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;
310 | 
311 |       // Create a mock that returns false for local template paths to force download
312 |       const downloadMockFileSystemExecutor = createMockFileSystemExecutor({
313 |         existsSync: (path) => {
314 |           // Only return true for extracted template directories, false for local template paths
315 |           return (
316 |             path.includes('xcodebuild-mcp-template') ||
317 |             path.includes('XcodeBuildMCP-iOS-Template') ||
318 |             path.includes('extracted')
319 |           );
320 |         },
321 |         readFile: async () => 'template content with MyProject placeholder',
322 |         readdir: async () => [
323 |           { name: 'Package.swift', isDirectory: () => false, isFile: () => true } as any,
324 |           { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true } as any,
325 |         ],
326 |         mkdir: async () => {},
327 |         rm: async () => {},
328 |         cp: async () => {},
329 |         writeFile: async () => {},
330 |         stat: async () => ({ isDirectory: () => true }),
331 |       });
332 | 
333 |       // Track commands executed - using default executor path
334 |       let capturedCommands: string[][] = [];
335 |       const trackingCommandExecutor = createMockExecutor({
336 |         success: true,
337 |         output: 'Command executed successfully',
338 |       });
339 |       // Wrap to capture commands
340 |       const capturingExecutor = async (command: string[], ...args: any[]) => {
341 |         capturedCommands.push(command);
342 |         return trackingCommandExecutor(command, ...args);
343 |       };
344 | 
345 |       await scaffold_ios_projectLogic(
346 |         {
347 |           projectName: 'TestIOSApp',
348 |           outputPath: '/tmp/test-projects',
349 |         },
350 |         capturingExecutor,
351 |         downloadMockFileSystemExecutor,
352 |       );
353 | 
354 |       // Verify both curl and unzip commands were executed in sequence
355 |       expect(capturedCommands.length).toBeGreaterThanOrEqual(2);
356 | 
357 |       const curlCommand = capturedCommands.find((cmd) => cmd.includes('curl'));
358 |       const unzipCommand = capturedCommands.find((cmd) => cmd.includes('unzip'));
359 | 
360 |       expect(curlCommand).toBeDefined();
361 |       expect(unzipCommand).toBeDefined();
362 |       expect(curlCommand[0]).toBe('curl');
363 |       expect(unzipCommand[0]).toBe('unzip');
364 | 
365 |       // Restore environment variable
366 |       process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
367 |     });
368 |   });
369 | 
370 |   describe('Handler Behavior (Complete Literal Returns)', () => {
371 |     it('should return success response for valid scaffold iOS project request', async () => {
372 |       const result = await scaffold_ios_projectLogic(
373 |         {
374 |           projectName: 'TestIOSApp',
375 |           outputPath: '/tmp/test-projects',
376 |           bundleIdentifier: 'com.test.iosapp',
377 |         },
378 |         mockCommandExecutor,
379 |         mockFileSystemExecutor,
380 |       );
381 | 
382 |       expect(result).toEqual({
383 |         content: [
384 |           {
385 |             type: 'text',
386 |             text: JSON.stringify(
387 |               {
388 |                 success: true,
389 |                 projectPath: '/tmp/test-projects',
390 |                 platform: 'iOS',
391 |                 message: 'Successfully scaffolded iOS project "TestIOSApp" in /tmp/test-projects',
392 |                 nextSteps: [
393 |                   'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
394 |                   'Build for simulator: build_sim({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject", simulatorName: "iPhone 16" })',
395 |                   'Build and run on simulator: build_run_sim({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject", simulatorName: "iPhone 16" })',
396 |                 ],
397 |               },
398 |               null,
399 |               2,
400 |             ),
401 |           },
402 |         ],
403 |       });
404 |     });
405 | 
406 |     it('should return success response with all optional parameters', async () => {
407 |       const result = await scaffold_ios_projectLogic(
408 |         {
409 |           projectName: 'TestIOSApp',
410 |           outputPath: '/tmp/test-projects',
411 |           bundleIdentifier: 'com.test.iosapp',
412 |           displayName: 'Test iOS App',
413 |           marketingVersion: '2.0',
414 |           currentProjectVersion: '5',
415 |           customizeNames: true,
416 |           deploymentTarget: '17.0',
417 |           targetedDeviceFamily: ['iphone'],
418 |           supportedOrientations: ['portrait'],
419 |           supportedOrientationsIpad: ['portrait', 'landscape-left'],
420 |         },
421 |         mockCommandExecutor,
422 |         mockFileSystemExecutor,
423 |       );
424 | 
425 |       expect(result).toEqual({
426 |         content: [
427 |           {
428 |             type: 'text',
429 |             text: JSON.stringify(
430 |               {
431 |                 success: true,
432 |                 projectPath: '/tmp/test-projects',
433 |                 platform: 'iOS',
434 |                 message: 'Successfully scaffolded iOS project "TestIOSApp" in /tmp/test-projects',
435 |                 nextSteps: [
436 |                   'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
437 |                   'Build for simulator: build_sim({ workspacePath: "/tmp/test-projects/TestIOSApp.xcworkspace", scheme: "TestIOSApp", simulatorName: "iPhone 16" })',
438 |                   'Build and run on simulator: build_run_sim({ workspacePath: "/tmp/test-projects/TestIOSApp.xcworkspace", scheme: "TestIOSApp", simulatorName: "iPhone 16" })',
439 |                 ],
440 |               },
441 |               null,
442 |               2,
443 |             ),
444 |           },
445 |         ],
446 |       });
447 |     });
448 | 
449 |     it('should return success response with customizeNames false', async () => {
450 |       const result = await scaffold_ios_projectLogic(
451 |         {
452 |           projectName: 'TestIOSApp',
453 |           outputPath: '/tmp/test-projects',
454 |           customizeNames: false,
455 |         },
456 |         mockCommandExecutor,
457 |         mockFileSystemExecutor,
458 |       );
459 | 
460 |       expect(result).toEqual({
461 |         content: [
462 |           {
463 |             type: 'text',
464 |             text: JSON.stringify(
465 |               {
466 |                 success: true,
467 |                 projectPath: '/tmp/test-projects',
468 |                 platform: 'iOS',
469 |                 message: 'Successfully scaffolded iOS project "TestIOSApp" in /tmp/test-projects',
470 |                 nextSteps: [
471 |                   'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
472 |                   'Build for simulator: build_sim({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject", simulatorName: "iPhone 16" })',
473 |                   'Build and run on simulator: build_run_sim({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject", simulatorName: "iPhone 16" })',
474 |                 ],
475 |               },
476 |               null,
477 |               2,
478 |             ),
479 |           },
480 |         ],
481 |       });
482 |     });
483 | 
484 |     it('should return error response for invalid project name', async () => {
485 |       const result = await scaffold_ios_projectLogic(
486 |         {
487 |           projectName: '123InvalidName',
488 |           outputPath: '/tmp/test-projects',
489 |         },
490 |         mockCommandExecutor,
491 |         mockFileSystemExecutor,
492 |       );
493 | 
494 |       expect(result).toEqual({
495 |         content: [
496 |           {
497 |             type: 'text',
498 |             text: JSON.stringify(
499 |               {
500 |                 success: false,
501 |                 error:
502 |                   'Project name must start with a letter and contain only letters, numbers, and underscores',
503 |               },
504 |               null,
505 |               2,
506 |             ),
507 |           },
508 |         ],
509 |         isError: true,
510 |       });
511 |     });
512 | 
513 |     it('should return error response for existing project files', async () => {
514 |       // Update mock to return true for existing files
515 |       mockFileSystemExecutor = createMockFileSystemExecutor({
516 |         existsSync: () => true,
517 |         readFile: async () => 'template content with MyProject placeholder',
518 |         readdir: async () => [
519 |           { name: 'Package.swift', isDirectory: () => false, isFile: () => true } as any,
520 |           { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true } as any,
521 |         ],
522 |       });
523 | 
524 |       const result = await scaffold_ios_projectLogic(
525 |         {
526 |           projectName: 'TestIOSApp',
527 |           outputPath: '/tmp/test-projects',
528 |         },
529 |         mockCommandExecutor,
530 |         mockFileSystemExecutor,
531 |       );
532 | 
533 |       expect(result).toEqual({
534 |         content: [
535 |           {
536 |             type: 'text',
537 |             text: JSON.stringify(
538 |               {
539 |                 success: false,
540 |                 error: 'Xcode project files already exist in /tmp/test-projects',
541 |               },
542 |               null,
543 |               2,
544 |             ),
545 |           },
546 |         ],
547 |         isError: true,
548 |       });
549 |     });
550 | 
551 |     it('should return error response for template download failure', async () => {
552 |       // Temporarily disable local template to force download
553 |       delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;
554 | 
555 |       // Mock command executor to fail for curl commands
556 |       const failingMockCommandExecutor = createMockExecutor({
557 |         success: false,
558 |         output: '',
559 |         error: 'Template download failed',
560 |       });
561 | 
562 |       const result = await scaffold_ios_projectLogic(
563 |         {
564 |           projectName: 'TestIOSApp',
565 |           outputPath: '/tmp/test-projects',
566 |         },
567 |         failingMockCommandExecutor,
568 |         mockFileSystemExecutor,
569 |       );
570 | 
571 |       expect(result).toEqual({
572 |         content: [
573 |           {
574 |             type: 'text',
575 |             text: JSON.stringify(
576 |               {
577 |                 success: false,
578 |                 error:
579 |                   'Failed to get template for iOS: Failed to download template: Template download failed',
580 |               },
581 |               null,
582 |               2,
583 |             ),
584 |           },
585 |         ],
586 |         isError: true,
587 |       });
588 | 
589 |       // Restore environment variable
590 |       process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
591 |     });
592 | 
593 |     it.skip('should return error response for template extraction failure', async () => {
594 |       // Temporarily disable local template to force download
595 |       delete process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH;
596 | 
597 |       // Create a mock that returns false for local template paths to force download
598 |       const downloadMockFileSystemExecutor = createMockFileSystemExecutor({
599 |         existsSync: (path) => {
600 |           // Only return true for extracted template directories, false for local template paths
601 |           return (
602 |             path.includes('xcodebuild-mcp-template') ||
603 |             path.includes('XcodeBuildMCP-iOS-Template') ||
604 |             path.includes('extracted')
605 |           );
606 |         },
607 |         readFile: async () => 'template content with MyProject placeholder',
608 |         readdir: async () => [
609 |           { name: 'Package.swift', isDirectory: () => false, isFile: () => true } as any,
610 |           { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true } as any,
611 |         ],
612 |         mkdir: async () => {},
613 |         rm: async () => {},
614 |         cp: async () => {},
615 |         writeFile: async () => {},
616 |         stat: async () => ({ isDirectory: () => true }),
617 |       });
618 | 
619 |       // Mock command executor to fail for unzip commands
620 |       const failingMockCommandExecutor = createMockExecutor({
621 |         success: false,
622 |         output: '',
623 |         error: 'Extraction failed',
624 |       });
625 | 
626 |       const result = await scaffold_ios_projectLogic(
627 |         {
628 |           projectName: 'TestIOSApp',
629 |           outputPath: '/tmp/test-projects',
630 |         },
631 |         failingMockCommandExecutor,
632 |         downloadMockFileSystemExecutor,
633 |       );
634 | 
635 |       expect(result).toEqual({
636 |         content: [
637 |           {
638 |             type: 'text',
639 |             text: JSON.stringify(
640 |               {
641 |                 success: false,
642 |                 error:
643 |                   'Failed to get template for iOS: Failed to extract template: Extraction failed',
644 |               },
645 |               null,
646 |               2,
647 |             ),
648 |           },
649 |         ],
650 |         isError: true,
651 |       });
652 | 
653 |       // Restore environment variable
654 |       process.env.XCODEBUILDMCP_IOS_TEMPLATE_PATH = '/mock/template/path';
655 |     });
656 |   });
657 | });
658 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/discovery/__tests__/discover_tools.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for discover_tools plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  8 | import { z } from 'zod';
  9 | import discoverTools, { discover_toolsLogic } from '../discover_tools.ts';
 10 | 
 11 | // Mock dependencies interface for dependency injection
 12 | interface MockDependencies {
 13 |   getAvailableWorkflows?: () => string[];
 14 |   generateWorkflowDescriptions?: () => string;
 15 |   enableWorkflows?: (server: any, workflows: string[], additive?: boolean) => Promise<void>;
 16 | }
 17 | 
 18 | // Track function calls manually for verification
 19 | interface CallTracker {
 20 |   getAvailableWorkflowsCalls: Array<any[]>;
 21 |   generateWorkflowDescriptionsCalls: Array<any[]>;
 22 |   enableWorkflowsCalls: Array<any[]>;
 23 | }
 24 | 
 25 | function createMockDependencies(
 26 |   config: {
 27 |     availableWorkflows?: string[];
 28 |     workflowDescriptions?: string;
 29 |     enableWorkflowsError?: Error;
 30 |     getAvailableWorkflowsError?: Error;
 31 |   },
 32 |   callTracker: CallTracker,
 33 | ): MockDependencies {
 34 |   const workflowNames = config.availableWorkflows ?? ['simulator-workspace'];
 35 |   const descriptions =
 36 |     config.workflowDescriptions ??
 37 |     `Available workflows:
 38 | 1. simulator-workspace: iOS Simulator Workspace - iOS development for workspaces`;
 39 | 
 40 |   return {
 41 |     getAvailableWorkflows: config.getAvailableWorkflowsError
 42 |       ? () => {
 43 |           callTracker.getAvailableWorkflowsCalls.push([]);
 44 |           throw config.getAvailableWorkflowsError;
 45 |         }
 46 |       : () => {
 47 |           callTracker.getAvailableWorkflowsCalls.push([]);
 48 |           return workflowNames;
 49 |         },
 50 |     generateWorkflowDescriptions: () => {
 51 |       callTracker.generateWorkflowDescriptionsCalls.push([]);
 52 |       return descriptions;
 53 |     },
 54 |     enableWorkflows: config.enableWorkflowsError
 55 |       ? async (server: any, workflows: string[], additive?: boolean) => {
 56 |           callTracker.enableWorkflowsCalls.push([server, workflows, additive]);
 57 |           throw config.enableWorkflowsError;
 58 |         }
 59 |       : async (server: any, workflows: string[], additive?: boolean) => {
 60 |           callTracker.enableWorkflowsCalls.push([server, workflows, additive]);
 61 |           return undefined;
 62 |         },
 63 |   };
 64 | }
 65 | 
 66 | describe('discover_tools', () => {
 67 |   let mockServer: Record<string, unknown>;
 68 |   let originalGlobalThis: Record<string, unknown>;
 69 |   let callTracker: CallTracker;
 70 |   let requestCalls: Array<any[]>;
 71 |   let sendToolListChangedCalls: Array<any[]>;
 72 | 
 73 |   beforeEach(() => {
 74 |     // Save original globalThis
 75 |     originalGlobalThis = globalThis.mcpServer;
 76 |     // Initialize call trackers
 77 |     callTracker = {
 78 |       getAvailableWorkflowsCalls: [],
 79 |       generateWorkflowDescriptionsCalls: [],
 80 |       enableWorkflowsCalls: [],
 81 |     };
 82 |     requestCalls = [];
 83 |     sendToolListChangedCalls = [];
 84 |     // Create mock server
 85 |     mockServer = {
 86 |       server: {
 87 |         _clientCapabilities: { sampling: true },
 88 |         getClientCapabilities: () => ({ sampling: true }),
 89 |         createMessage: async (...args: any[]) => {
 90 |           requestCalls.push(args);
 91 |           throw new Error('Mock createMessage not configured');
 92 |         },
 93 |         request: async (...args: any[]) => {
 94 |           requestCalls.push(args);
 95 |           throw new Error('Mock request not configured');
 96 |         },
 97 |       },
 98 |       sendToolListChanged: (...args: any[]) => {
 99 |         sendToolListChangedCalls.push(args);
100 |       },
101 |     };
102 |     // Set up global server
103 |     (globalThis as any).mcpServer = mockServer;
104 |     // Reset all mocks
105 |   });
106 | 
107 |   afterEach(() => {
108 |     // Restore original globalThis
109 |     globalThis.mcpServer = originalGlobalThis;
110 |   });
111 | 
112 |   describe('Export Field Validation (Literal)', () => {
113 |     it('should have correct name', () => {
114 |       expect(discoverTools.name).toBe('discover_tools');
115 |     });
116 | 
117 |     it('should have correct description', () => {
118 |       expect(discoverTools.description).toBe(
119 |         'Analyzes a natural language task description and enables the most relevant development workflow. Prioritizes project/workspace workflows (simulator/device/macOS) and also supports task-based workflows (simulator-management, logging) and Swift packages.',
120 |       );
121 |     });
122 | 
123 |     it('should have handler function', () => {
124 |       expect(typeof discover_toolsLogic).toBe('function');
125 |     });
126 | 
127 |     it('should have correct schema with task_description string field', () => {
128 |       const schema = z.object(discoverTools.schema);
129 | 
130 |       // Valid inputs
131 |       expect(schema.safeParse({ task_description: 'Build my iOS app' }).success).toBe(true);
132 |       expect(schema.safeParse({ task_description: 'Test my React Native app' }).success).toBe(true);
133 | 
134 |       // Invalid inputs
135 |       expect(schema.safeParse({ task_description: 123 }).success).toBe(false);
136 |       expect(schema.safeParse({ task_description: null }).success).toBe(false);
137 |       expect(schema.safeParse({ task_description: undefined }).success).toBe(false);
138 |       expect(schema.safeParse({}).success).toBe(false);
139 |     });
140 |   });
141 | 
142 |   describe('Capability Detection', () => {
143 |     it('should return error when client lacks sampling capability', async () => {
144 |       // Mock server without sampling capability
145 |       mockServer.server._clientCapabilities = {};
146 |       (mockServer.server as any).getClientCapabilities = () => ({});
147 | 
148 |       const result = await discover_toolsLogic({ task_description: 'Build my app' }, undefined);
149 | 
150 |       expect(result).toEqual({
151 |         content: [
152 |           {
153 |             type: 'text',
154 |             text: 'Your client does not support the sampling feature required for dynamic tool discovery. Please use XCODEBUILDMCP_DYNAMIC_TOOLS=false to use the standard tool set.',
155 |           },
156 |         ],
157 |         isError: true,
158 |       });
159 |     });
160 | 
161 |     it('should proceed when client has sampling capability', async () => {
162 |       const mockDeps = createMockDependencies(
163 |         {
164 |           availableWorkflows: ['simulator-workspace'],
165 |           workflowDescriptions: `Available workflows:
166 | 1. simulator-workspace: iOS Simulator Workspace - iOS development for workspaces`,
167 |         },
168 |         callTracker,
169 |       );
170 | 
171 |       // Configure mock request to return successful response
172 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
173 |         requestCalls.push(args);
174 |         return {
175 |           content: [{ type: 'text', text: '["simulator-workspace"]' }],
176 |         };
177 |       };
178 | 
179 |       const result = await discover_toolsLogic(
180 |         { task_description: 'Build my iOS app' },
181 |         undefined,
182 |         mockDeps,
183 |       );
184 | 
185 |       expect(result.isError).toBeFalsy();
186 |       expect(callTracker.getAvailableWorkflowsCalls).toHaveLength(1);
187 |     });
188 |   });
189 | 
190 |   describe('Workflow Loading', () => {
191 |     it('should load workflow groups and build descriptions', async () => {
192 |       const mockDeps = createMockDependencies(
193 |         {
194 |           availableWorkflows: ['simulator-workspace', 'macos-project'],
195 |           workflowDescriptions: `Available workflows:
196 | 1. simulator-workspace: iOS Simulator Workspace - Complete iOS development workflow for .xcworkspace files targeting simulators
197 | 2. macos-project: macOS Project - Complete macOS development workflow for .xcodeproj files`,
198 |         },
199 |         callTracker,
200 |       );
201 | 
202 |       // Configure mock request to capture calls and return response
203 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
204 |         requestCalls.push(args);
205 |         return {
206 |           content: [{ type: 'text', text: '["simulator-workspace"]' }],
207 |         };
208 |       };
209 | 
210 |       await discover_toolsLogic({ task_description: 'Build my iOS app' }, undefined, mockDeps);
211 | 
212 |       // Verify workflow groups were loaded
213 |       expect(callTracker.getAvailableWorkflowsCalls).toHaveLength(1);
214 | 
215 |       // Verify LLM prompt includes workflow descriptions
216 |       expect(requestCalls).toHaveLength(1);
217 |       const requestCall = requestCalls[0];
218 |       const prompt = requestCall[0].messages[0].content.text;
219 | 
220 |       expect(prompt).toContain('simulator-workspace');
221 |       expect(prompt).toContain(
222 |         'Complete iOS development workflow for .xcworkspace files targeting simulators',
223 |       );
224 |       expect(prompt).toContain('macos-project');
225 |       expect(prompt).toContain('Complete macOS development workflow for .xcodeproj files');
226 |     });
227 |   });
228 | 
229 |   describe('LLM Interaction', () => {
230 |     let mockDeps: MockDependencies;
231 |     let localCallTracker: CallTracker;
232 | 
233 |     beforeEach(() => {
234 |       // Reset local call tracker for this describe block
235 |       localCallTracker = {
236 |         getAvailableWorkflowsCalls: [],
237 |         generateWorkflowDescriptionsCalls: [],
238 |         enableWorkflowsCalls: [],
239 |       };
240 |       mockDeps = createMockDependencies(
241 |         {
242 |           availableWorkflows: ['simulator-workspace'],
243 |           workflowDescriptions: `Available workflows:
244 | 1. simulator-workspace: iOS Simulator Workspace - iOS development for workspaces`,
245 |         },
246 |         localCallTracker,
247 |       );
248 |     });
249 | 
250 |     it('should send correct sampling request to LLM', async () => {
251 |       // Reset request calls for this test
252 |       requestCalls.length = 0;
253 | 
254 |       // Configure mock request to capture calls and return response
255 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
256 |         requestCalls.push(args);
257 |         return {
258 |           content: [{ type: 'text', text: '["simulator-workspace"]' }],
259 |         };
260 |       };
261 | 
262 |       await discover_toolsLogic(
263 |         { task_description: 'Build my iOS app and test it' },
264 |         undefined,
265 |         mockDeps,
266 |       );
267 | 
268 |       expect(requestCalls).toHaveLength(1);
269 |       const requestCall = requestCalls[0];
270 |       expect(requestCall[0]).toEqual({
271 |         messages: [
272 |           {
273 |             role: 'user',
274 |             content: {
275 |               type: 'text',
276 |               text: expect.stringContaining('Build my iOS app and test it'),
277 |             },
278 |           },
279 |         ],
280 |         maxTokens: 200,
281 |       });
282 |       // Note: Schema parameter was removed in TypeScript fix - request method now only accepts one parameter
283 |     });
284 | 
285 |     it('should handle array content format in LLM response', async () => {
286 |       // Reset call trackers for this test
287 |       localCallTracker.enableWorkflowsCalls.length = 0;
288 | 
289 |       // Configure mock request to return response
290 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
291 |         return {
292 |           content: [{ type: 'text', text: '["simulator-workspace"]' }],
293 |         };
294 |       };
295 | 
296 |       const result = await discover_toolsLogic(
297 |         { task_description: 'Build my app' },
298 |         undefined,
299 |         mockDeps,
300 |       );
301 | 
302 |       expect(result.isError).toBeFalsy();
303 |       expect(localCallTracker.enableWorkflowsCalls).toHaveLength(1);
304 |       expect(localCallTracker.enableWorkflowsCalls[0]).toEqual([
305 |         mockServer,
306 |         ['simulator-workspace'],
307 |         false,
308 |       ]);
309 |     });
310 | 
311 |     it('should handle single object content format in LLM response', async () => {
312 |       // Reset call trackers for this test
313 |       localCallTracker.enableWorkflowsCalls.length = 0;
314 | 
315 |       // Configure mock request to return response with single object format
316 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
317 |         return {
318 |           content: { type: 'text', text: '["simulator-workspace"]' },
319 |         };
320 |       };
321 | 
322 |       const result = await discover_toolsLogic(
323 |         { task_description: 'Build my app' },
324 |         undefined,
325 |         mockDeps,
326 |       );
327 | 
328 |       expect(result.isError).toBeFalsy();
329 |       expect(localCallTracker.enableWorkflowsCalls).toHaveLength(1);
330 |       expect(localCallTracker.enableWorkflowsCalls[0]).toEqual([
331 |         mockServer,
332 |         ['simulator-workspace'],
333 |         false,
334 |       ]);
335 |     });
336 | 
337 |     it('should filter out invalid workflow names from LLM response', async () => {
338 |       // Reset call trackers for this test
339 |       localCallTracker.enableWorkflowsCalls.length = 0;
340 | 
341 |       // Configure mock request to return response with invalid workflows
342 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
343 |         return {
344 |           content: [
345 |             {
346 |               type: 'text',
347 |               text: '["simulator-workspace", "invalid-workflow", "another-invalid"]',
348 |             },
349 |           ],
350 |         };
351 |       };
352 | 
353 |       const result = await discover_toolsLogic(
354 |         { task_description: 'Build my app' },
355 |         undefined,
356 |         mockDeps,
357 |       );
358 | 
359 |       expect(result.isError).toBeFalsy();
360 |       expect(localCallTracker.enableWorkflowsCalls).toHaveLength(1);
361 |       expect(localCallTracker.enableWorkflowsCalls[0]).toEqual([
362 |         mockServer,
363 |         ['simulator-workspace'], // Only valid workflow should remain
364 |         false,
365 |       ]);
366 |     });
367 | 
368 |     it('should handle malformed JSON in LLM response', async () => {
369 |       // Configure mock request to return malformed JSON
370 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
371 |         return {
372 |           content: [{ type: 'text', text: 'This is not JSON at all!' }],
373 |         };
374 |       };
375 | 
376 |       const result = await discover_toolsLogic(
377 |         { task_description: 'Build my app' },
378 |         undefined,
379 |         mockDeps,
380 |       );
381 | 
382 |       expect(result).toEqual({
383 |         content: [
384 |           {
385 |             type: 'text',
386 |             text: 'I was unable to determine the right tools for your task. The AI model returned: "This is not JSON at all!". Could you please rephrase your request or try a more specific description?',
387 |           },
388 |         ],
389 |         isError: true,
390 |       });
391 |     });
392 | 
393 |     it('should handle non-array JSON in LLM response', async () => {
394 |       // Configure mock request to return non-array JSON
395 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
396 |         return {
397 |           content: [{ type: 'text', text: '{"workflow": "simulator-workspace"}' }],
398 |         };
399 |       };
400 | 
401 |       const result = await discover_toolsLogic(
402 |         { task_description: 'Build my app' },
403 |         undefined,
404 |         mockDeps,
405 |       );
406 | 
407 |       expect(result).toEqual({
408 |         content: [
409 |           {
410 |             type: 'text',
411 |             text: 'I was unable to determine the right tools for your task. The AI model returned: "{"workflow": "simulator-workspace"}". Could you please rephrase your request or try a more specific description?',
412 |           },
413 |         ],
414 |         isError: true,
415 |       });
416 |     });
417 | 
418 |     it('should handle empty workflow selection', async () => {
419 |       // Reset call trackers for this test
420 |       localCallTracker.enableWorkflowsCalls.length = 0;
421 | 
422 |       // Configure mock request to return empty array
423 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
424 |         return {
425 |           content: [{ type: 'text', text: '[]' }],
426 |         };
427 |       };
428 | 
429 |       const result = await discover_toolsLogic(
430 |         { task_description: 'Just saying hello' },
431 |         undefined,
432 |         mockDeps,
433 |       );
434 | 
435 |       expect(result).toEqual({
436 |         content: [
437 |           {
438 |             type: 'text',
439 |             text: "No specific Xcode tools seem necessary for that task. Could you provide more details about what you'd like to accomplish with Xcode?",
440 |           },
441 |         ],
442 |         isError: false,
443 |       });
444 |       expect(localCallTracker.enableWorkflowsCalls).toHaveLength(0);
445 |     });
446 |   });
447 | 
448 |   describe('Workflow Enabling', () => {
449 |     let mockDeps: MockDependencies;
450 |     let workflowCallTracker: CallTracker;
451 | 
452 |     beforeEach(() => {
453 |       // Reset call tracker for this describe block
454 |       workflowCallTracker = {
455 |         getAvailableWorkflowsCalls: [],
456 |         generateWorkflowDescriptionsCalls: [],
457 |         enableWorkflowsCalls: [],
458 |       };
459 |       mockDeps = createMockDependencies(
460 |         {
461 |           availableWorkflows: ['simulator-workspace'],
462 |           workflowDescriptions: `Available workflows:
463 | 1. simulator-workspace: iOS Simulator Workspace - iOS development for workspaces`,
464 |         },
465 |         workflowCallTracker,
466 |       );
467 |     });
468 | 
469 |     it('should enable selected workflows and return success message', async () => {
470 |       // Configure mock request to return successful response
471 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
472 |         return {
473 |           content: [{ type: 'text', text: '["simulator-workspace"]' }],
474 |         };
475 |       };
476 | 
477 |       const result = await discover_toolsLogic(
478 |         { task_description: 'Build my iOS app' },
479 |         undefined,
480 |         mockDeps,
481 |       );
482 | 
483 |       expect(workflowCallTracker.enableWorkflowsCalls).toHaveLength(1);
484 |       expect(workflowCallTracker.enableWorkflowsCalls[0]).toEqual([
485 |         mockServer,
486 |         ['simulator-workspace'],
487 |         false,
488 |       ]);
489 | 
490 |       expect(result).toEqual({
491 |         content: [
492 |           {
493 |             type: 'text',
494 |             text: '✅ Enabled XcodeBuildMCP tools for: simulator-workspace.\n\nReplaced previous tools with simulator-workspace workflow tools.\n\nUse XcodeBuildMCP tools for all Apple platform development tasks from now on. Call tools/list to see all available tools for your workflow.',
495 |           },
496 |         ],
497 |         isError: false,
498 |       });
499 |     });
500 | 
501 |     it('should handle workflow enabling errors gracefully', async () => {
502 |       const errorCallTracker: CallTracker = {
503 |         getAvailableWorkflowsCalls: [],
504 |         generateWorkflowDescriptionsCalls: [],
505 |         enableWorkflowsCalls: [],
506 |       };
507 | 
508 |       const mockDepsWithError = createMockDependencies(
509 |         {
510 |           availableWorkflows: ['simulator-workspace'],
511 |           enableWorkflowsError: new Error('Failed to enable workflows'),
512 |         },
513 |         errorCallTracker,
514 |       );
515 | 
516 |       // Configure mock request to return successful response
517 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
518 |         return {
519 |           content: [{ type: 'text', text: '["simulator-workspace"]' }],
520 |         };
521 |       };
522 | 
523 |       const result = await discover_toolsLogic(
524 |         { task_description: 'Build my app' },
525 |         undefined,
526 |         mockDepsWithError,
527 |       );
528 | 
529 |       expect(result).toEqual({
530 |         content: [
531 |           {
532 |             type: 'text',
533 |             text: 'An error occurred while discovering tools: Failed to enable workflows',
534 |           },
535 |         ],
536 |         isError: true,
537 |       });
538 |     });
539 |   });
540 | 
541 |   describe('Error Handling', () => {
542 |     it('should handle missing server instance', async () => {
543 |       (globalThis as any).mcpServer = undefined;
544 | 
545 |       const result = await discover_toolsLogic({ task_description: 'Build my app' }, undefined);
546 | 
547 |       expect(result).toEqual({
548 |         content: [
549 |           {
550 |             type: 'text',
551 |             text: 'An error occurred while discovering tools: Server instance not available',
552 |           },
553 |         ],
554 |         isError: true,
555 |       });
556 |     });
557 | 
558 |     it('should handle workflow loading errors', async () => {
559 |       const errorCallTracker: CallTracker = {
560 |         getAvailableWorkflowsCalls: [],
561 |         generateWorkflowDescriptionsCalls: [],
562 |         enableWorkflowsCalls: [],
563 |       };
564 | 
565 |       const mockDepsWithError = createMockDependencies(
566 |         {
567 |           getAvailableWorkflowsError: new Error('Failed to load workflows'),
568 |         },
569 |         errorCallTracker,
570 |       );
571 | 
572 |       const result = await discover_toolsLogic(
573 |         { task_description: 'Build my app' },
574 |         undefined,
575 |         mockDepsWithError,
576 |       );
577 | 
578 |       expect(result).toEqual({
579 |         content: [
580 |           {
581 |             type: 'text',
582 |             text: 'An error occurred while discovering tools: Failed to load workflows',
583 |           },
584 |         ],
585 |         isError: true,
586 |       });
587 |     });
588 | 
589 |     it('should handle LLM request errors', async () => {
590 |       const errorCallTracker: CallTracker = {
591 |         getAvailableWorkflowsCalls: [],
592 |         generateWorkflowDescriptionsCalls: [],
593 |         enableWorkflowsCalls: [],
594 |       };
595 | 
596 |       const mockDeps = createMockDependencies({ availableWorkflows: [] }, errorCallTracker);
597 | 
598 |       // Configure mock request to throw error
599 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
600 |         throw new Error('LLM request failed');
601 |       };
602 | 
603 |       const result = await discover_toolsLogic(
604 |         { task_description: 'Build my app' },
605 |         undefined,
606 |         mockDeps,
607 |       );
608 | 
609 |       expect(result).toEqual({
610 |         content: [
611 |           {
612 |             type: 'text',
613 |             text: 'An error occurred while discovering tools: LLM request failed',
614 |           },
615 |         ],
616 |         isError: true,
617 |       });
618 |     });
619 |   });
620 | 
621 |   describe('Prompt Generation', () => {
622 |     it('should include task description in LLM prompt', async () => {
623 |       const promptCallTracker: CallTracker = {
624 |         getAvailableWorkflowsCalls: [],
625 |         generateWorkflowDescriptionsCalls: [],
626 |         enableWorkflowsCalls: [],
627 |       };
628 | 
629 |       const mockDeps = createMockDependencies(
630 |         {
631 |           availableWorkflows: ['simulator-workspace'],
632 |           workflowDescriptions: `Available workflows:
633 | 1. simulator-workspace: iOS Simulator Workspace - iOS development for workspaces`,
634 |         },
635 |         promptCallTracker,
636 |       );
637 | 
638 |       // Reset request calls for this test
639 |       requestCalls.length = 0;
640 | 
641 |       // Configure mock request to capture calls and return response
642 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
643 |         requestCalls.push(args);
644 |         return {
645 |           content: [{ type: 'text', text: '["simulator-workspace"]' }],
646 |         };
647 |       };
648 | 
649 |       const taskDescription =
650 |         'I need to build my React Native iOS app for the simulator and run tests';
651 | 
652 |       await discover_toolsLogic({ task_description: taskDescription }, undefined, mockDeps);
653 | 
654 |       expect(requestCalls).toHaveLength(1);
655 |       const requestCall = requestCalls[0];
656 |       const prompt = requestCall[0].messages[0].content.text;
657 | 
658 |       expect(prompt).toContain(taskDescription);
659 |       expect(prompt).toContain('Select EXACTLY ONE workflow');
660 |       expect(prompt).toContain('Primary (project/workspace-based) workflows:');
661 |       expect(prompt).toContain('Secondary (task-based, no project/workspace needed):');
662 |       expect(prompt).toContain('All available workflows:');
663 |     });
664 | 
665 |     it('should provide clear selection guidelines in prompt', async () => {
666 |       const promptCallTracker: CallTracker = {
667 |         getAvailableWorkflowsCalls: [],
668 |         generateWorkflowDescriptionsCalls: [],
669 |         enableWorkflowsCalls: [],
670 |       };
671 | 
672 |       const mockDeps = createMockDependencies(
673 |         {
674 |           availableWorkflows: [],
675 |           workflowDescriptions: `Available workflows:`,
676 |         },
677 |         promptCallTracker,
678 |       );
679 | 
680 |       // Reset request calls for this test
681 |       requestCalls.length = 0;
682 | 
683 |       // Configure mock request to capture calls and return response
684 |       (mockServer.server as any).createMessage = async (...args: any[]) => {
685 |         requestCalls.push(args);
686 |         return {
687 |           content: [{ type: 'text', text: '[]' }],
688 |         };
689 |       };
690 | 
691 |       await discover_toolsLogic({ task_description: 'Build my app' }, undefined, mockDeps);
692 | 
693 |       expect(requestCalls).toHaveLength(1);
694 |       const requestCall = requestCalls[0];
695 |       const prompt = requestCall[0].messages[0].content.text;
696 | 
697 |       expect(prompt).toContain('Select EXACTLY ONE workflow');
698 |       expect(prompt).toContain('.xcworkspace');
699 |       expect(prompt).toContain('.xcodeproj');
700 |       expect(prompt).toContain('simulator-management');
701 |       expect(prompt).toContain('macOS');
702 |       expect(prompt).toContain('Respond with ONLY a JSON array');
703 |     });
704 |   });
705 | });
706 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for touch tool plugin
  3 |  * Following CLAUDE.md testing standards with dependency injection
  4 |  */
  5 | 
  6 | import { describe, it, expect, beforeEach } from 'vitest';
  7 | import { z } from 'zod';
  8 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
  9 | import touchPlugin, { touchLogic } from '../touch.ts';
 10 | 
 11 | describe('Touch Plugin', () => {
 12 |   describe('Export Field Validation (Literal)', () => {
 13 |     it('should have correct name', () => {
 14 |       expect(touchPlugin.name).toBe('touch');
 15 |     });
 16 | 
 17 |     it('should have correct description', () => {
 18 |       expect(touchPlugin.description).toBe(
 19 |         "Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots).",
 20 |       );
 21 |     });
 22 | 
 23 |     it('should have handler function', () => {
 24 |       expect(typeof touchPlugin.handler).toBe('function');
 25 |     });
 26 | 
 27 |     it('should validate schema fields with safeParse', () => {
 28 |       const schema = z.object(touchPlugin.schema);
 29 | 
 30 |       // Valid case with down
 31 |       expect(
 32 |         schema.safeParse({
 33 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 34 |           x: 100,
 35 |           y: 200,
 36 |           down: true,
 37 |         }).success,
 38 |       ).toBe(true);
 39 | 
 40 |       // Valid case with up
 41 |       expect(
 42 |         schema.safeParse({
 43 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 44 |           x: 100,
 45 |           y: 200,
 46 |           up: true,
 47 |         }).success,
 48 |       ).toBe(true);
 49 | 
 50 |       // Invalid simulatorUuid
 51 |       expect(
 52 |         schema.safeParse({
 53 |           simulatorUuid: 'invalid-uuid',
 54 |           x: 100,
 55 |           y: 200,
 56 |           down: true,
 57 |         }).success,
 58 |       ).toBe(false);
 59 | 
 60 |       // Invalid x (not integer)
 61 |       expect(
 62 |         schema.safeParse({
 63 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 64 |           x: 100.5,
 65 |           y: 200,
 66 |           down: true,
 67 |         }).success,
 68 |       ).toBe(false);
 69 | 
 70 |       // Invalid y (not integer)
 71 |       expect(
 72 |         schema.safeParse({
 73 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 74 |           x: 100,
 75 |           y: 200.5,
 76 |           down: true,
 77 |         }).success,
 78 |       ).toBe(false);
 79 | 
 80 |       // Valid with delay
 81 |       expect(
 82 |         schema.safeParse({
 83 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 84 |           x: 100,
 85 |           y: 200,
 86 |           down: true,
 87 |           delay: 1.5,
 88 |         }).success,
 89 |       ).toBe(true);
 90 | 
 91 |       // Invalid delay (negative)
 92 |       expect(
 93 |         schema.safeParse({
 94 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 95 |           x: 100,
 96 |           y: 200,
 97 |           down: true,
 98 |           delay: -1,
 99 |         }).success,
100 |       ).toBe(false);
101 |     });
102 |   });
103 | 
104 |   describe('Command Generation', () => {
105 |     it('should generate correct axe command for touch down', async () => {
106 |       let capturedCommand: string[] = [];
107 |       const trackingExecutor = async (command: string[]) => {
108 |         capturedCommand = command;
109 |         return {
110 |           success: true,
111 |           output: 'touch completed',
112 |           error: undefined,
113 |           process: { pid: 12345 },
114 |         };
115 |       };
116 | 
117 |       const mockAxeHelpers = {
118 |         getAxePath: () => '/usr/local/bin/axe',
119 |         getBundledAxeEnvironment: () => ({}),
120 |         createAxeNotAvailableResponse: () => ({
121 |           content: [
122 |             {
123 |               type: 'text',
124 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
125 |             },
126 |           ],
127 |           isError: true,
128 |         }),
129 |       };
130 | 
131 |       await touchLogic(
132 |         {
133 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
134 |           x: 100,
135 |           y: 200,
136 |           down: true,
137 |         },
138 |         trackingExecutor,
139 |         mockAxeHelpers,
140 |       );
141 | 
142 |       expect(capturedCommand).toEqual([
143 |         '/usr/local/bin/axe',
144 |         'touch',
145 |         '-x',
146 |         '100',
147 |         '-y',
148 |         '200',
149 |         '--down',
150 |         '--udid',
151 |         '12345678-1234-1234-1234-123456789012',
152 |       ]);
153 |     });
154 | 
155 |     it('should generate correct axe command for touch up', async () => {
156 |       let capturedCommand: string[] = [];
157 |       const trackingExecutor = async (command: string[]) => {
158 |         capturedCommand = command;
159 |         return {
160 |           success: true,
161 |           output: 'touch completed',
162 |           error: undefined,
163 |           process: { pid: 12345 },
164 |         };
165 |       };
166 | 
167 |       const mockAxeHelpers = {
168 |         getAxePath: () => '/usr/local/bin/axe',
169 |         getBundledAxeEnvironment: () => ({}),
170 |         createAxeNotAvailableResponse: () => ({
171 |           content: [
172 |             {
173 |               type: 'text',
174 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
175 |             },
176 |           ],
177 |           isError: true,
178 |         }),
179 |       };
180 | 
181 |       await touchLogic(
182 |         {
183 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
184 |           x: 150,
185 |           y: 250,
186 |           up: true,
187 |         },
188 |         trackingExecutor,
189 |         mockAxeHelpers,
190 |       );
191 | 
192 |       expect(capturedCommand).toEqual([
193 |         '/usr/local/bin/axe',
194 |         'touch',
195 |         '-x',
196 |         '150',
197 |         '-y',
198 |         '250',
199 |         '--up',
200 |         '--udid',
201 |         '12345678-1234-1234-1234-123456789012',
202 |       ]);
203 |     });
204 | 
205 |     it('should generate correct axe command for touch down+up', async () => {
206 |       let capturedCommand: string[] = [];
207 |       const trackingExecutor = async (command: string[]) => {
208 |         capturedCommand = command;
209 |         return {
210 |           success: true,
211 |           output: 'touch completed',
212 |           error: undefined,
213 |           process: { pid: 12345 },
214 |         };
215 |       };
216 | 
217 |       const mockAxeHelpers = {
218 |         getAxePath: () => '/usr/local/bin/axe',
219 |         getBundledAxeEnvironment: () => ({}),
220 |         createAxeNotAvailableResponse: () => ({
221 |           content: [
222 |             {
223 |               type: 'text',
224 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
225 |             },
226 |           ],
227 |           isError: true,
228 |         }),
229 |       };
230 | 
231 |       await touchLogic(
232 |         {
233 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
234 |           x: 300,
235 |           y: 400,
236 |           down: true,
237 |           up: true,
238 |         },
239 |         trackingExecutor,
240 |         mockAxeHelpers,
241 |       );
242 | 
243 |       expect(capturedCommand).toEqual([
244 |         '/usr/local/bin/axe',
245 |         'touch',
246 |         '-x',
247 |         '300',
248 |         '-y',
249 |         '400',
250 |         '--down',
251 |         '--up',
252 |         '--udid',
253 |         '12345678-1234-1234-1234-123456789012',
254 |       ]);
255 |     });
256 | 
257 |     it('should generate correct axe command for touch with delay', async () => {
258 |       let capturedCommand: string[] = [];
259 |       const trackingExecutor = async (command: string[]) => {
260 |         capturedCommand = command;
261 |         return {
262 |           success: true,
263 |           output: 'touch completed',
264 |           error: undefined,
265 |           process: { pid: 12345 },
266 |         };
267 |       };
268 | 
269 |       const mockAxeHelpers = {
270 |         getAxePath: () => '/usr/local/bin/axe',
271 |         getBundledAxeEnvironment: () => ({}),
272 |         createAxeNotAvailableResponse: () => ({
273 |           content: [
274 |             {
275 |               type: 'text',
276 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
277 |             },
278 |           ],
279 |           isError: true,
280 |         }),
281 |       };
282 | 
283 |       await touchLogic(
284 |         {
285 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
286 |           x: 50,
287 |           y: 75,
288 |           down: true,
289 |           up: true,
290 |           delay: 1.5,
291 |         },
292 |         trackingExecutor,
293 |         mockAxeHelpers,
294 |       );
295 | 
296 |       expect(capturedCommand).toEqual([
297 |         '/usr/local/bin/axe',
298 |         'touch',
299 |         '-x',
300 |         '50',
301 |         '-y',
302 |         '75',
303 |         '--down',
304 |         '--up',
305 |         '--delay',
306 |         '1.5',
307 |         '--udid',
308 |         '12345678-1234-1234-1234-123456789012',
309 |       ]);
310 |     });
311 | 
312 |     it('should generate correct axe command with bundled axe path', async () => {
313 |       let capturedCommand: string[] = [];
314 |       const trackingExecutor = async (command: string[]) => {
315 |         capturedCommand = command;
316 |         return {
317 |           success: true,
318 |           output: 'touch completed',
319 |           error: undefined,
320 |           process: { pid: 12345 },
321 |         };
322 |       };
323 | 
324 |       const mockAxeHelpers = {
325 |         getAxePath: () => '/path/to/bundled/axe',
326 |         getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }),
327 |       };
328 | 
329 |       await touchLogic(
330 |         {
331 |           simulatorUuid: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
332 |           x: 0,
333 |           y: 0,
334 |           up: true,
335 |           delay: 0.5,
336 |         },
337 |         trackingExecutor,
338 |         mockAxeHelpers,
339 |       );
340 | 
341 |       expect(capturedCommand).toEqual([
342 |         '/path/to/bundled/axe',
343 |         'touch',
344 |         '-x',
345 |         '0',
346 |         '-y',
347 |         '0',
348 |         '--up',
349 |         '--delay',
350 |         '0.5',
351 |         '--udid',
352 |         'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
353 |       ]);
354 |     });
355 |   });
356 | 
357 |   describe('Handler Behavior (Complete Literal Returns)', () => {
358 |     it('should handle axe dependency error', async () => {
359 |       const mockExecutor = createMockExecutor({ success: true });
360 |       const mockAxeHelpers = {
361 |         getAxePath: () => null,
362 |         getBundledAxeEnvironment: () => ({}),
363 |         createAxeNotAvailableResponse: () => ({
364 |           content: [
365 |             {
366 |               type: 'text',
367 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
368 |             },
369 |           ],
370 |           isError: true,
371 |         }),
372 |       };
373 | 
374 |       const result = await touchLogic(
375 |         {
376 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
377 |           x: 100,
378 |           y: 200,
379 |           down: true,
380 |         },
381 |         mockExecutor,
382 |         mockAxeHelpers,
383 |       );
384 | 
385 |       expect(result).toEqual({
386 |         content: [
387 |           {
388 |             type: 'text',
389 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
390 |           },
391 |         ],
392 |         isError: true,
393 |       });
394 |     });
395 | 
396 |     it('should successfully perform touch down', async () => {
397 |       const mockExecutor = createMockExecutor({ success: true, output: 'Touch down completed' });
398 |       const mockAxeHelpers = {
399 |         getAxePath: () => '/usr/local/bin/axe',
400 |         getBundledAxeEnvironment: () => ({}),
401 |         createAxeNotAvailableResponse: () => ({
402 |           content: [
403 |             {
404 |               type: 'text',
405 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
406 |             },
407 |           ],
408 |           isError: true,
409 |         }),
410 |       };
411 | 
412 |       const result = await touchLogic(
413 |         {
414 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
415 |           x: 100,
416 |           y: 200,
417 |           down: true,
418 |         },
419 |         mockExecutor,
420 |         mockAxeHelpers,
421 |       );
422 | 
423 |       expect(result).toEqual({
424 |         content: [
425 |           {
426 |             type: 'text',
427 |             text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
428 |           },
429 |         ],
430 |         isError: false,
431 |       });
432 |     });
433 | 
434 |     it('should successfully perform touch up', async () => {
435 |       const mockExecutor = createMockExecutor({ success: true, output: 'Touch up completed' });
436 |       const mockAxeHelpers = {
437 |         getAxePath: () => '/usr/local/bin/axe',
438 |         getBundledAxeEnvironment: () => ({}),
439 |         createAxeNotAvailableResponse: () => ({
440 |           content: [
441 |             {
442 |               type: 'text',
443 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
444 |             },
445 |           ],
446 |           isError: true,
447 |         }),
448 |       };
449 | 
450 |       const result = await touchLogic(
451 |         {
452 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
453 |           x: 100,
454 |           y: 200,
455 |           up: true,
456 |         },
457 |         mockExecutor,
458 |         mockAxeHelpers,
459 |       );
460 | 
461 |       expect(result).toEqual({
462 |         content: [
463 |           {
464 |             type: 'text',
465 |             text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
466 |           },
467 |         ],
468 |         isError: false,
469 |       });
470 |     });
471 | 
472 |     it('should return error when neither down nor up is specified', async () => {
473 |       const mockExecutor = createMockExecutor({ success: true });
474 | 
475 |       const result = await touchLogic(
476 |         {
477 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
478 |           x: 100,
479 |           y: 200,
480 |         },
481 |         mockExecutor,
482 |       );
483 | 
484 |       expect(result).toEqual({
485 |         content: [{ type: 'text', text: 'Error: At least one of "down" or "up" must be true' }],
486 |         isError: true,
487 |       });
488 |     });
489 | 
490 |     it('should return success for touch down event', async () => {
491 |       const mockExecutor = createMockExecutor({
492 |         success: true,
493 |         output: 'touch completed',
494 |         error: undefined,
495 |       });
496 | 
497 |       const mockAxeHelpers = {
498 |         getAxePath: () => '/usr/local/bin/axe',
499 |         getBundledAxeEnvironment: () => ({}),
500 |         createAxeNotAvailableResponse: () => ({
501 |           content: [
502 |             {
503 |               type: 'text',
504 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
505 |             },
506 |           ],
507 |           isError: true,
508 |         }),
509 |       };
510 | 
511 |       const result = await touchLogic(
512 |         {
513 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
514 |           x: 100,
515 |           y: 200,
516 |           down: true,
517 |         },
518 |         mockExecutor,
519 |         mockAxeHelpers,
520 |       );
521 | 
522 |       expect(result).toEqual({
523 |         content: [
524 |           {
525 |             type: 'text',
526 |             text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
527 |           },
528 |         ],
529 |         isError: false,
530 |       });
531 |     });
532 | 
533 |     it('should return success for touch up event', async () => {
534 |       const mockExecutor = createMockExecutor({
535 |         success: true,
536 |         output: 'touch completed',
537 |         error: undefined,
538 |       });
539 | 
540 |       const mockAxeHelpers = {
541 |         getAxePath: () => '/usr/local/bin/axe',
542 |         getBundledAxeEnvironment: () => ({}),
543 |         createAxeNotAvailableResponse: () => ({
544 |           content: [
545 |             {
546 |               type: 'text',
547 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
548 |             },
549 |           ],
550 |           isError: true,
551 |         }),
552 |       };
553 | 
554 |       const result = await touchLogic(
555 |         {
556 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
557 |           x: 100,
558 |           y: 200,
559 |           up: true,
560 |         },
561 |         mockExecutor,
562 |         mockAxeHelpers,
563 |       );
564 | 
565 |       expect(result).toEqual({
566 |         content: [
567 |           {
568 |             type: 'text',
569 |             text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
570 |           },
571 |         ],
572 |         isError: false,
573 |       });
574 |     });
575 | 
576 |     it('should return success for touch down+up event', async () => {
577 |       const mockExecutor = createMockExecutor({
578 |         success: true,
579 |         output: 'touch completed',
580 |         error: undefined,
581 |       });
582 | 
583 |       const mockAxeHelpers = {
584 |         getAxePath: () => '/usr/local/bin/axe',
585 |         getBundledAxeEnvironment: () => ({}),
586 |         createAxeNotAvailableResponse: () => ({
587 |           content: [
588 |             {
589 |               type: 'text',
590 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
591 |             },
592 |           ],
593 |           isError: true,
594 |         }),
595 |       };
596 | 
597 |       const result = await touchLogic(
598 |         {
599 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
600 |           x: 100,
601 |           y: 200,
602 |           down: true,
603 |           up: true,
604 |         },
605 |         mockExecutor,
606 |         mockAxeHelpers,
607 |       );
608 | 
609 |       expect(result).toEqual({
610 |         content: [
611 |           {
612 |             type: 'text',
613 |             text: 'Touch event (touch down+up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
614 |           },
615 |         ],
616 |         isError: false,
617 |       });
618 |     });
619 | 
620 |     it('should handle DependencyError when axe is not available', async () => {
621 |       const mockExecutor = createMockExecutor({ success: true });
622 | 
623 |       const mockAxeHelpers = {
624 |         getAxePath: () => null,
625 |         getBundledAxeEnvironment: () => ({}),
626 |         createAxeNotAvailableResponse: () => ({
627 |           content: [
628 |             {
629 |               type: 'text',
630 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
631 |             },
632 |           ],
633 |           isError: true,
634 |         }),
635 |       };
636 | 
637 |       const result = await touchLogic(
638 |         {
639 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
640 |           x: 100,
641 |           y: 200,
642 |           down: true,
643 |         },
644 |         mockExecutor,
645 |         mockAxeHelpers,
646 |       );
647 | 
648 |       expect(result).toEqual({
649 |         content: [
650 |           {
651 |             type: 'text',
652 |             text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
653 |           },
654 |         ],
655 |         isError: true,
656 |       });
657 |     });
658 | 
659 |     it('should handle AxeError from failed command execution', async () => {
660 |       const mockExecutor = createMockExecutor({
661 |         success: false,
662 |         output: '',
663 |         error: 'axe command failed',
664 |       });
665 | 
666 |       const mockAxeHelpers = {
667 |         getAxePath: () => '/usr/local/bin/axe',
668 |         getBundledAxeEnvironment: () => ({}),
669 |         createAxeNotAvailableResponse: () => ({
670 |           content: [
671 |             {
672 |               type: 'text',
673 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
674 |             },
675 |           ],
676 |           isError: true,
677 |         }),
678 |       };
679 | 
680 |       const result = await touchLogic(
681 |         {
682 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
683 |           x: 100,
684 |           y: 200,
685 |           down: true,
686 |         },
687 |         mockExecutor,
688 |         mockAxeHelpers,
689 |       );
690 | 
691 |       expect(result).toEqual({
692 |         content: [
693 |           {
694 |             type: 'text',
695 |             text: "Error: Failed to execute touch event: axe command 'touch' failed.\nDetails: axe command failed",
696 |           },
697 |         ],
698 |         isError: true,
699 |       });
700 |     });
701 | 
702 |     it('should handle SystemError from command execution', async () => {
703 |       const mockExecutor = async () => {
704 |         throw new Error('System error occurred');
705 |       };
706 | 
707 |       const mockAxeHelpers = {
708 |         getAxePath: () => '/usr/local/bin/axe',
709 |         getBundledAxeEnvironment: () => ({}),
710 |         createAxeNotAvailableResponse: () => ({
711 |           content: [
712 |             {
713 |               type: 'text',
714 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
715 |             },
716 |           ],
717 |           isError: true,
718 |         }),
719 |       };
720 | 
721 |       const result = await touchLogic(
722 |         {
723 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
724 |           x: 100,
725 |           y: 200,
726 |           down: true,
727 |         },
728 |         mockExecutor,
729 |         mockAxeHelpers,
730 |       );
731 | 
732 |       expect(result).toMatchObject({
733 |         content: [
734 |           {
735 |             type: 'text',
736 |             text: expect.stringContaining(
737 |               'Error: System error executing axe: Failed to execute axe command: System error occurred',
738 |             ),
739 |           },
740 |         ],
741 |         isError: true,
742 |       });
743 |     });
744 | 
745 |     it('should handle unexpected Error objects', async () => {
746 |       const mockExecutor = async () => {
747 |         throw new Error('Unexpected error');
748 |       };
749 | 
750 |       const mockAxeHelpers = {
751 |         getAxePath: () => '/usr/local/bin/axe',
752 |         getBundledAxeEnvironment: () => ({}),
753 |         createAxeNotAvailableResponse: () => ({
754 |           content: [
755 |             {
756 |               type: 'text',
757 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
758 |             },
759 |           ],
760 |           isError: true,
761 |         }),
762 |       };
763 | 
764 |       const result = await touchLogic(
765 |         {
766 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
767 |           x: 100,
768 |           y: 200,
769 |           down: true,
770 |         },
771 |         mockExecutor,
772 |         mockAxeHelpers,
773 |       );
774 | 
775 |       expect(result).toMatchObject({
776 |         content: [
777 |           {
778 |             type: 'text',
779 |             text: expect.stringContaining(
780 |               'Error: System error executing axe: Failed to execute axe command: Unexpected error',
781 |             ),
782 |           },
783 |         ],
784 |         isError: true,
785 |       });
786 |     });
787 | 
788 |     it('should handle unexpected string errors', async () => {
789 |       const mockExecutor = async () => {
790 |         throw 'String error';
791 |       };
792 | 
793 |       const mockAxeHelpers = {
794 |         getAxePath: () => '/usr/local/bin/axe',
795 |         getBundledAxeEnvironment: () => ({}),
796 |         createAxeNotAvailableResponse: () => ({
797 |           content: [
798 |             {
799 |               type: 'text',
800 |               text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
801 |             },
802 |           ],
803 |           isError: true,
804 |         }),
805 |       };
806 | 
807 |       const result = await touchLogic(
808 |         {
809 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
810 |           x: 100,
811 |           y: 200,
812 |           down: true,
813 |         },
814 |         mockExecutor,
815 |         mockAxeHelpers,
816 |       );
817 | 
818 |       expect(result).toEqual({
819 |         content: [
820 |           {
821 |             type: 'text',
822 |             text: 'Error: System error executing axe: Failed to execute axe command: String error',
823 |           },
824 |         ],
825 |         isError: true,
826 |       });
827 |     });
828 |   });
829 | });
830 | 
```
Page 13/14FirstPrevNextLast