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

# Directory Structure

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

# Files

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

```typescript
  1 | /**
  2 |  * Tests for swipe tool plugin
  3 |  */
  4 | 
  5 | import { describe, it, expect } from 'vitest';
  6 | import { z } from 'zod';
  7 | import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts';
  8 | import { SystemError, DependencyError } from '../../../../utils/responses/index.ts';
  9 | 
 10 | // Import the plugin module to test
 11 | import swipePlugin, { AxeHelpers, swipeLogic, SwipeParams } from '../swipe.ts';
 12 | 
 13 | // Helper function to create mock axe helpers
 14 | function createMockAxeHelpers(): AxeHelpers {
 15 |   return {
 16 |     getAxePath: () => '/mocked/axe/path',
 17 |     getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }),
 18 |     createAxeNotAvailableResponse: () => ({
 19 |       content: [
 20 |         {
 21 |           type: 'text',
 22 |           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.',
 23 |         },
 24 |       ],
 25 |       isError: true,
 26 |     }),
 27 |   };
 28 | }
 29 | 
 30 | // Helper function to create mock axe helpers with null path (for dependency error tests)
 31 | function createMockAxeHelpersWithNullPath(): AxeHelpers {
 32 |   return {
 33 |     getAxePath: () => null,
 34 |     getBundledAxeEnvironment: () => ({ SOME_ENV: 'value' }),
 35 |     createAxeNotAvailableResponse: () => ({
 36 |       content: [
 37 |         {
 38 |           type: 'text',
 39 |           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.',
 40 |         },
 41 |       ],
 42 |       isError: true,
 43 |     }),
 44 |   };
 45 | }
 46 | 
 47 | describe('Swipe Plugin', () => {
 48 |   describe('Export Field Validation (Literal)', () => {
 49 |     it('should have correct name', () => {
 50 |       expect(swipePlugin.name).toBe('swipe');
 51 |     });
 52 | 
 53 |     it('should have correct description', () => {
 54 |       expect(swipePlugin.description).toBe(
 55 |         "Swipe from one point to another. Use describe_ui for precise coordinates (don't guess from screenshots). Supports configurable timing.",
 56 |       );
 57 |     });
 58 | 
 59 |     it('should have handler function', () => {
 60 |       expect(typeof swipePlugin.handler).toBe('function');
 61 |     });
 62 | 
 63 |     it('should validate schema fields with safeParse', () => {
 64 |       const schema = z.object(swipePlugin.schema);
 65 | 
 66 |       // Valid case
 67 |       expect(
 68 |         schema.safeParse({
 69 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 70 |           x1: 100,
 71 |           y1: 200,
 72 |           x2: 300,
 73 |           y2: 400,
 74 |         }).success,
 75 |       ).toBe(true);
 76 | 
 77 |       // Invalid simulatorUuid
 78 |       expect(
 79 |         schema.safeParse({
 80 |           simulatorUuid: 'invalid-uuid',
 81 |           x1: 100,
 82 |           y1: 200,
 83 |           x2: 300,
 84 |           y2: 400,
 85 |         }).success,
 86 |       ).toBe(false);
 87 | 
 88 |       // Invalid x1 (not integer)
 89 |       expect(
 90 |         schema.safeParse({
 91 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 92 |           x1: 100.5,
 93 |           y1: 200,
 94 |           x2: 300,
 95 |           y2: 400,
 96 |         }).success,
 97 |       ).toBe(false);
 98 | 
 99 |       // Valid with optional parameters
100 |       expect(
101 |         schema.safeParse({
102 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
103 |           x1: 100,
104 |           y1: 200,
105 |           x2: 300,
106 |           y2: 400,
107 |           duration: 1.5,
108 |           delta: 10,
109 |           preDelay: 0.5,
110 |           postDelay: 0.2,
111 |         }).success,
112 |       ).toBe(true);
113 | 
114 |       // Invalid duration (negative)
115 |       expect(
116 |         schema.safeParse({
117 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
118 |           x1: 100,
119 |           y1: 200,
120 |           x2: 300,
121 |           y2: 400,
122 |           duration: -1,
123 |         }).success,
124 |       ).toBe(false);
125 |     });
126 |   });
127 | 
128 |   describe('Command Generation', () => {
129 |     it('should generate correct axe command for basic swipe', async () => {
130 |       let capturedCommand: string[] = [];
131 |       const trackingExecutor = async (command: string[]) => {
132 |         capturedCommand = command;
133 |         return {
134 |           success: true,
135 |           output: 'swipe completed',
136 |           error: undefined,
137 |           process: { pid: 12345 },
138 |         };
139 |       };
140 | 
141 |       const mockAxeHelpers = createMockAxeHelpers();
142 | 
143 |       await swipeLogic(
144 |         {
145 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
146 |           x1: 100,
147 |           y1: 200,
148 |           x2: 300,
149 |           y2: 400,
150 |         },
151 |         trackingExecutor,
152 |         mockAxeHelpers,
153 |       );
154 | 
155 |       expect(capturedCommand).toEqual([
156 |         '/mocked/axe/path',
157 |         'swipe',
158 |         '--start-x',
159 |         '100',
160 |         '--start-y',
161 |         '200',
162 |         '--end-x',
163 |         '300',
164 |         '--end-y',
165 |         '400',
166 |         '--udid',
167 |         '12345678-1234-1234-1234-123456789012',
168 |       ]);
169 |     });
170 | 
171 |     it('should generate correct axe command for swipe with duration', async () => {
172 |       let capturedCommand: string[] = [];
173 |       const trackingExecutor = async (command: string[]) => {
174 |         capturedCommand = command;
175 |         return {
176 |           success: true,
177 |           output: 'swipe completed',
178 |           error: undefined,
179 |           process: { pid: 12345 },
180 |         };
181 |       };
182 | 
183 |       const mockAxeHelpers = createMockAxeHelpers();
184 | 
185 |       await swipeLogic(
186 |         {
187 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
188 |           x1: 50,
189 |           y1: 75,
190 |           x2: 250,
191 |           y2: 350,
192 |           duration: 1.5,
193 |         },
194 |         trackingExecutor,
195 |         mockAxeHelpers,
196 |       );
197 | 
198 |       expect(capturedCommand).toEqual([
199 |         '/mocked/axe/path',
200 |         'swipe',
201 |         '--start-x',
202 |         '50',
203 |         '--start-y',
204 |         '75',
205 |         '--end-x',
206 |         '250',
207 |         '--end-y',
208 |         '350',
209 |         '--duration',
210 |         '1.5',
211 |         '--udid',
212 |         '12345678-1234-1234-1234-123456789012',
213 |       ]);
214 |     });
215 | 
216 |     it('should generate correct axe command for swipe with all optional parameters', async () => {
217 |       let capturedCommand: string[] = [];
218 |       const trackingExecutor = async (command: string[]) => {
219 |         capturedCommand = command;
220 |         return {
221 |           success: true,
222 |           output: 'swipe completed',
223 |           error: undefined,
224 |           process: { pid: 12345 },
225 |         };
226 |       };
227 | 
228 |       const mockAxeHelpers = createMockAxeHelpers();
229 | 
230 |       await swipeLogic(
231 |         {
232 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
233 |           x1: 0,
234 |           y1: 0,
235 |           x2: 500,
236 |           y2: 800,
237 |           duration: 2.0,
238 |           delta: 10,
239 |           preDelay: 0.5,
240 |           postDelay: 0.3,
241 |         },
242 |         trackingExecutor,
243 |         mockAxeHelpers,
244 |       );
245 | 
246 |       expect(capturedCommand).toEqual([
247 |         '/mocked/axe/path',
248 |         'swipe',
249 |         '--start-x',
250 |         '0',
251 |         '--start-y',
252 |         '0',
253 |         '--end-x',
254 |         '500',
255 |         '--end-y',
256 |         '800',
257 |         '--duration',
258 |         '2',
259 |         '--delta',
260 |         '10',
261 |         '--pre-delay',
262 |         '0.5',
263 |         '--post-delay',
264 |         '0.3',
265 |         '--udid',
266 |         '12345678-1234-1234-1234-123456789012',
267 |       ]);
268 |     });
269 | 
270 |     it('should generate correct axe command with bundled axe path', async () => {
271 |       let capturedCommand: string[] = [];
272 |       const trackingExecutor = async (command: string[]) => {
273 |         capturedCommand = command;
274 |         return {
275 |           success: true,
276 |           output: 'swipe completed',
277 |           error: undefined,
278 |           process: { pid: 12345 },
279 |         };
280 |       };
281 | 
282 |       const mockAxeHelpers = {
283 |         getAxePath: () => '/path/to/bundled/axe',
284 |         getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }),
285 |         createAxeNotAvailableResponse: () => ({
286 |           content: [{ type: 'text', text: 'AXe tools not available' }],
287 |           isError: true,
288 |         }),
289 |       };
290 | 
291 |       await swipeLogic(
292 |         {
293 |           simulatorUuid: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
294 |           x1: 150,
295 |           y1: 250,
296 |           x2: 400,
297 |           y2: 600,
298 |           delta: 5,
299 |         },
300 |         trackingExecutor,
301 |         mockAxeHelpers,
302 |       );
303 | 
304 |       expect(capturedCommand).toEqual([
305 |         '/path/to/bundled/axe',
306 |         'swipe',
307 |         '--start-x',
308 |         '150',
309 |         '--start-y',
310 |         '250',
311 |         '--end-x',
312 |         '400',
313 |         '--end-y',
314 |         '600',
315 |         '--delta',
316 |         '5',
317 |         '--udid',
318 |         'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
319 |       ]);
320 |     });
321 |   });
322 | 
323 |   describe('Handler Behavior (Complete Literal Returns)', () => {
324 |     it('should return error for missing simulatorUuid via handler', async () => {
325 |       const result = await swipePlugin.handler({ x1: 100, y1: 200, x2: 300, y2: 400 });
326 | 
327 |       expect(result.isError).toBe(true);
328 |       expect(result.content[0].type).toBe('text');
329 |       expect(result.content[0].text).toContain('Parameter validation failed');
330 |       expect(result.content[0].text).toContain('simulatorUuid');
331 |     });
332 | 
333 |     it('should return error for missing x1 via handler', async () => {
334 |       const result = await swipePlugin.handler({
335 |         simulatorUuid: '12345678-1234-1234-1234-123456789012',
336 |         y1: 200,
337 |         x2: 300,
338 |         y2: 400,
339 |       });
340 | 
341 |       expect(result.isError).toBe(true);
342 |       expect(result.content[0].type).toBe('text');
343 |       expect(result.content[0].text).toContain('Parameter validation failed');
344 |       expect(result.content[0].text).toContain('x1');
345 |     });
346 | 
347 |     it('should return success for valid swipe execution', async () => {
348 |       const mockExecutor = createMockExecutor({
349 |         success: true,
350 |         output: 'swipe completed',
351 |         error: '',
352 |       });
353 | 
354 |       const mockAxeHelpers = createMockAxeHelpers();
355 | 
356 |       const result = await swipeLogic(
357 |         {
358 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
359 |           x1: 100,
360 |           y1: 200,
361 |           x2: 300,
362 |           y2: 400,
363 |         },
364 |         mockExecutor,
365 |         mockAxeHelpers,
366 |       );
367 | 
368 |       expect(result).toEqual({
369 |         content: [
370 |           {
371 |             type: 'text',
372 |             text: 'Swipe from (100, 200) to (300, 400) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
373 |           },
374 |         ],
375 |         isError: false,
376 |       });
377 |     });
378 | 
379 |     it('should return success for swipe with duration', async () => {
380 |       const mockExecutor = createMockExecutor({
381 |         success: true,
382 |         output: 'swipe completed',
383 |         error: '',
384 |       });
385 | 
386 |       const mockAxeHelpers = createMockAxeHelpers();
387 | 
388 |       const result = await swipeLogic(
389 |         {
390 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
391 |           x1: 100,
392 |           y1: 200,
393 |           x2: 300,
394 |           y2: 400,
395 |           duration: 1.5,
396 |         },
397 |         mockExecutor,
398 |         mockAxeHelpers,
399 |       );
400 | 
401 |       expect(result).toEqual({
402 |         content: [
403 |           {
404 |             type: 'text',
405 |             text: 'Swipe from (100, 200) to (300, 400) duration=1.5s simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.',
406 |           },
407 |         ],
408 |         isError: false,
409 |       });
410 |     });
411 | 
412 |     it('should handle DependencyError when axe is not available', async () => {
413 |       const mockExecutor = createMockExecutor({
414 |         success: true,
415 |         output: 'swipe completed',
416 |         error: '',
417 |       });
418 | 
419 |       const mockAxeHelpers = createMockAxeHelpersWithNullPath();
420 | 
421 |       const result = await swipeLogic(
422 |         {
423 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
424 |           x1: 100,
425 |           y1: 200,
426 |           x2: 300,
427 |           y2: 400,
428 |         },
429 |         mockExecutor,
430 |         mockAxeHelpers,
431 |       );
432 | 
433 |       expect(result).toEqual({
434 |         content: [
435 |           {
436 |             type: 'text',
437 |             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.',
438 |           },
439 |         ],
440 |         isError: true,
441 |       });
442 |     });
443 | 
444 |     it('should handle AxeError from failed command execution', async () => {
445 |       const mockExecutor = createMockExecutor({
446 |         success: false,
447 |         output: '',
448 |         error: 'axe command failed',
449 |       });
450 | 
451 |       const mockAxeHelpers = createMockAxeHelpers();
452 | 
453 |       const result = await swipeLogic(
454 |         {
455 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
456 |           x1: 100,
457 |           y1: 200,
458 |           x2: 300,
459 |           y2: 400,
460 |         },
461 |         mockExecutor,
462 |         mockAxeHelpers,
463 |       );
464 | 
465 |       expect(result).toEqual({
466 |         content: [
467 |           {
468 |             type: 'text',
469 |             text: "Error: Failed to simulate swipe: axe command 'swipe' failed.\nDetails: axe command failed",
470 |           },
471 |         ],
472 |         isError: true,
473 |       });
474 |     });
475 | 
476 |     it('should handle SystemError from command execution', async () => {
477 |       // Override the executor to throw SystemError for this test
478 |       const systemErrorExecutor = async () => {
479 |         throw new SystemError('System error occurred');
480 |       };
481 | 
482 |       const mockAxeHelpers = createMockAxeHelpers();
483 | 
484 |       const result = await swipeLogic(
485 |         {
486 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
487 |           x1: 100,
488 |           y1: 200,
489 |           x2: 300,
490 |           y2: 400,
491 |         },
492 |         systemErrorExecutor,
493 |         mockAxeHelpers,
494 |       );
495 | 
496 |       expect(result.isError).toBe(true);
497 |       expect(result.content[0].type).toBe('text');
498 |       expect(result.content[0].text).toContain(
499 |         'Error: System error executing axe: Failed to execute axe command: System error occurred',
500 |       );
501 |       expect(result.content[0].text).toContain('Details: SystemError: System error occurred');
502 |     });
503 | 
504 |     it('should handle unexpected Error objects', async () => {
505 |       // Override the executor to throw an unexpected Error for this test
506 |       const unexpectedErrorExecutor = async () => {
507 |         throw new Error('Unexpected error');
508 |       };
509 | 
510 |       const mockAxeHelpers = createMockAxeHelpers();
511 | 
512 |       const result = await swipeLogic(
513 |         {
514 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
515 |           x1: 100,
516 |           y1: 200,
517 |           x2: 300,
518 |           y2: 400,
519 |         },
520 |         unexpectedErrorExecutor,
521 |         mockAxeHelpers,
522 |       );
523 | 
524 |       expect(result.isError).toBe(true);
525 |       expect(result.content[0].type).toBe('text');
526 |       expect(result.content[0].text).toContain(
527 |         'Error: System error executing axe: Failed to execute axe command: Unexpected error',
528 |       );
529 |       expect(result.content[0].text).toContain('Details: Error: Unexpected error');
530 |     });
531 | 
532 |     it('should handle unexpected string errors', async () => {
533 |       // Override the executor to throw a string error for this test
534 |       const stringErrorExecutor = async () => {
535 |         throw 'String error';
536 |       };
537 | 
538 |       const mockAxeHelpers = createMockAxeHelpers();
539 | 
540 |       const result = await swipeLogic(
541 |         {
542 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
543 |           x1: 100,
544 |           y1: 200,
545 |           x2: 300,
546 |           y2: 400,
547 |         },
548 |         stringErrorExecutor,
549 |         mockAxeHelpers,
550 |       );
551 | 
552 |       expect(result).toEqual({
553 |         content: [
554 |           {
555 |             type: 'text',
556 |             text: 'Error: System error executing axe: Failed to execute axe command: String error',
557 |           },
558 |         ],
559 |         isError: true,
560 |       });
561 |     });
562 |   });
563 | });
564 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for button tool plugin
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach } from 'vitest';
  6 | import { z } from 'zod';
  7 | import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts';
  8 | import buttonPlugin, { buttonLogic } from '../button.ts';
  9 | 
 10 | describe('Button Plugin', () => {
 11 |   describe('Export Field Validation (Literal)', () => {
 12 |     it('should have correct name', () => {
 13 |       expect(buttonPlugin.name).toBe('button');
 14 |     });
 15 | 
 16 |     it('should have correct description', () => {
 17 |       expect(buttonPlugin.description).toBe(
 18 |         'Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri',
 19 |       );
 20 |     });
 21 | 
 22 |     it('should have handler function', () => {
 23 |       expect(typeof buttonPlugin.handler).toBe('function');
 24 |     });
 25 | 
 26 |     it('should validate schema fields with safeParse', () => {
 27 |       const schema = z.object(buttonPlugin.schema);
 28 | 
 29 |       // Valid case
 30 |       expect(
 31 |         schema.safeParse({
 32 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 33 |           buttonType: 'home',
 34 |         }).success,
 35 |       ).toBe(true);
 36 | 
 37 |       // Invalid simulatorUuid
 38 |       expect(
 39 |         schema.safeParse({
 40 |           simulatorUuid: 'invalid-uuid',
 41 |           buttonType: 'home',
 42 |         }).success,
 43 |       ).toBe(false);
 44 | 
 45 |       // Invalid buttonType
 46 |       expect(
 47 |         schema.safeParse({
 48 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 49 |           buttonType: 'invalid-button',
 50 |         }).success,
 51 |       ).toBe(false);
 52 | 
 53 |       // Valid with duration
 54 |       expect(
 55 |         schema.safeParse({
 56 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 57 |           buttonType: 'home',
 58 |           duration: 2.5,
 59 |         }).success,
 60 |       ).toBe(true);
 61 | 
 62 |       // Invalid duration (negative)
 63 |       expect(
 64 |         schema.safeParse({
 65 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 66 |           buttonType: 'home',
 67 |           duration: -1,
 68 |         }).success,
 69 |       ).toBe(false);
 70 | 
 71 |       // Test all valid button types
 72 |       const validButtons = ['apple-pay', 'home', 'lock', 'side-button', 'siri'];
 73 |       validButtons.forEach((buttonType) => {
 74 |         expect(
 75 |           schema.safeParse({
 76 |             simulatorUuid: '12345678-1234-1234-1234-123456789012',
 77 |             buttonType,
 78 |           }).success,
 79 |         ).toBe(true);
 80 |       });
 81 |     });
 82 |   });
 83 | 
 84 |   describe('Command Generation', () => {
 85 |     it('should generate correct axe command for basic button press', async () => {
 86 |       let capturedCommand: string[] = [];
 87 |       const trackingExecutor = async (command: string[]) => {
 88 |         capturedCommand = command;
 89 |         return {
 90 |           success: true,
 91 |           output: 'button press completed',
 92 |           error: undefined,
 93 |           process: { pid: 12345 },
 94 |         };
 95 |       };
 96 | 
 97 |       const mockAxeHelpers = {
 98 |         getAxePath: () => '/usr/local/bin/axe',
 99 |         getBundledAxeEnvironment: () => ({}),
100 |         createAxeNotAvailableResponse: () => ({
101 |           content: [{ type: 'text', text: 'axe not available' }],
102 |           isError: true,
103 |         }),
104 |       };
105 | 
106 |       await buttonLogic(
107 |         {
108 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
109 |           buttonType: 'home',
110 |         },
111 |         trackingExecutor,
112 |         mockAxeHelpers,
113 |       );
114 | 
115 |       expect(capturedCommand).toEqual([
116 |         '/usr/local/bin/axe',
117 |         'button',
118 |         'home',
119 |         '--udid',
120 |         '12345678-1234-1234-1234-123456789012',
121 |       ]);
122 |     });
123 | 
124 |     it('should generate correct axe command for button press with duration', async () => {
125 |       let capturedCommand: string[] = [];
126 |       const trackingExecutor = async (command: string[]) => {
127 |         capturedCommand = command;
128 |         return {
129 |           success: true,
130 |           output: 'button press completed',
131 |           error: undefined,
132 |           process: { pid: 12345 },
133 |         };
134 |       };
135 | 
136 |       const mockAxeHelpers = {
137 |         getAxePath: () => '/usr/local/bin/axe',
138 |         getBundledAxeEnvironment: () => ({}),
139 |         createAxeNotAvailableResponse: () => ({
140 |           content: [{ type: 'text', text: 'axe not available' }],
141 |           isError: true,
142 |         }),
143 |       };
144 | 
145 |       await buttonLogic(
146 |         {
147 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
148 |           buttonType: 'side-button',
149 |           duration: 2.5,
150 |         },
151 |         trackingExecutor,
152 |         mockAxeHelpers,
153 |       );
154 | 
155 |       expect(capturedCommand).toEqual([
156 |         '/usr/local/bin/axe',
157 |         'button',
158 |         'side-button',
159 |         '--duration',
160 |         '2.5',
161 |         '--udid',
162 |         '12345678-1234-1234-1234-123456789012',
163 |       ]);
164 |     });
165 | 
166 |     it('should generate correct axe command for different button types', async () => {
167 |       let capturedCommand: string[] = [];
168 |       const trackingExecutor = async (command: string[]) => {
169 |         capturedCommand = command;
170 |         return {
171 |           success: true,
172 |           output: 'button press completed',
173 |           error: undefined,
174 |           process: { pid: 12345 },
175 |         };
176 |       };
177 | 
178 |       const mockAxeHelpers = {
179 |         getAxePath: () => '/usr/local/bin/axe',
180 |         getBundledAxeEnvironment: () => ({}),
181 |         createAxeNotAvailableResponse: () => ({
182 |           content: [{ type: 'text', text: 'axe not available' }],
183 |           isError: true,
184 |         }),
185 |       };
186 | 
187 |       await buttonLogic(
188 |         {
189 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
190 |           buttonType: 'apple-pay',
191 |         },
192 |         trackingExecutor,
193 |         mockAxeHelpers,
194 |       );
195 | 
196 |       expect(capturedCommand).toEqual([
197 |         '/usr/local/bin/axe',
198 |         'button',
199 |         'apple-pay',
200 |         '--udid',
201 |         '12345678-1234-1234-1234-123456789012',
202 |       ]);
203 |     });
204 | 
205 |     it('should generate correct axe command with bundled axe path', async () => {
206 |       let capturedCommand: string[] = [];
207 |       const trackingExecutor = async (command: string[]) => {
208 |         capturedCommand = command;
209 |         return {
210 |           success: true,
211 |           output: 'button press completed',
212 |           error: undefined,
213 |           process: { pid: 12345 },
214 |         };
215 |       };
216 | 
217 |       const mockAxeHelpers = {
218 |         getAxePath: () => '/path/to/bundled/axe',
219 |         getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }),
220 |       };
221 | 
222 |       await buttonLogic(
223 |         {
224 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
225 |           buttonType: 'siri',
226 |         },
227 |         trackingExecutor,
228 |         mockAxeHelpers,
229 |       );
230 | 
231 |       expect(capturedCommand).toEqual([
232 |         '/path/to/bundled/axe',
233 |         'button',
234 |         'siri',
235 |         '--udid',
236 |         '12345678-1234-1234-1234-123456789012',
237 |       ]);
238 |     });
239 |   });
240 | 
241 |   describe('Handler Behavior (Complete Literal Returns)', () => {
242 |     it('should return error for missing simulatorUuid', async () => {
243 |       const result = await buttonPlugin.handler({ buttonType: 'home' });
244 | 
245 |       expect(result.isError).toBe(true);
246 |       expect(result.content[0].text).toContain('Parameter validation failed');
247 |       expect(result.content[0].text).toContain('simulatorUuid: Required');
248 |     });
249 | 
250 |     it('should return error for missing buttonType', async () => {
251 |       const result = await buttonPlugin.handler({
252 |         simulatorUuid: '12345678-1234-1234-1234-123456789012',
253 |       });
254 | 
255 |       expect(result.isError).toBe(true);
256 |       expect(result.content[0].text).toContain('Parameter validation failed');
257 |       expect(result.content[0].text).toContain('buttonType: Required');
258 |     });
259 | 
260 |     it('should return error for invalid simulatorUuid format', async () => {
261 |       const result = await buttonPlugin.handler({
262 |         simulatorUuid: 'invalid-uuid-format',
263 |         buttonType: 'home',
264 |       });
265 | 
266 |       expect(result.isError).toBe(true);
267 |       expect(result.content[0].text).toContain('Parameter validation failed');
268 |       expect(result.content[0].text).toContain('Invalid Simulator UUID format');
269 |     });
270 | 
271 |     it('should return error for invalid buttonType', async () => {
272 |       const result = await buttonPlugin.handler({
273 |         simulatorUuid: '12345678-1234-1234-1234-123456789012',
274 |         buttonType: 'invalid-button',
275 |       });
276 | 
277 |       expect(result.isError).toBe(true);
278 |       expect(result.content[0].text).toContain('Parameter validation failed');
279 |     });
280 | 
281 |     it('should return error for negative duration', async () => {
282 |       const result = await buttonPlugin.handler({
283 |         simulatorUuid: '12345678-1234-1234-1234-123456789012',
284 |         buttonType: 'home',
285 |         duration: -1,
286 |       });
287 | 
288 |       expect(result.isError).toBe(true);
289 |       expect(result.content[0].text).toContain('Parameter validation failed');
290 |       expect(result.content[0].text).toContain('Duration must be non-negative');
291 |     });
292 | 
293 |     it('should return success for valid button press', async () => {
294 |       const mockExecutor = createMockExecutor({
295 |         success: true,
296 |         output: 'button press completed',
297 |         error: undefined,
298 |         process: { pid: 12345 },
299 |       });
300 | 
301 |       const mockAxeHelpers = {
302 |         getAxePath: () => '/usr/local/bin/axe',
303 |         getBundledAxeEnvironment: () => ({}),
304 |         createAxeNotAvailableResponse: () => ({
305 |           content: [{ type: 'text', text: 'axe not available' }],
306 |           isError: true,
307 |         }),
308 |       };
309 | 
310 |       const result = await buttonLogic(
311 |         {
312 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
313 |           buttonType: 'home',
314 |         },
315 |         mockExecutor,
316 |         mockAxeHelpers,
317 |       );
318 | 
319 |       expect(result).toEqual({
320 |         content: [{ type: 'text', text: "Hardware button 'home' pressed successfully." }],
321 |         isError: false,
322 |       });
323 |     });
324 | 
325 |     it('should return success for button press with duration', async () => {
326 |       const mockExecutor = createMockExecutor({
327 |         success: true,
328 |         output: 'button press completed',
329 |         error: undefined,
330 |         process: { pid: 12345 },
331 |       });
332 | 
333 |       const mockAxeHelpers = {
334 |         getAxePath: () => '/usr/local/bin/axe',
335 |         getBundledAxeEnvironment: () => ({}),
336 |         createAxeNotAvailableResponse: () => ({
337 |           content: [{ type: 'text', text: 'axe not available' }],
338 |           isError: true,
339 |         }),
340 |       };
341 | 
342 |       const result = await buttonLogic(
343 |         {
344 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
345 |           buttonType: 'side-button',
346 |           duration: 2.5,
347 |         },
348 |         mockExecutor,
349 |         mockAxeHelpers,
350 |       );
351 | 
352 |       expect(result).toEqual({
353 |         content: [{ type: 'text', text: "Hardware button 'side-button' pressed successfully." }],
354 |         isError: false,
355 |       });
356 |     });
357 | 
358 |     it('should handle DependencyError when axe is not available', async () => {
359 |       const mockAxeHelpers = {
360 |         getAxePath: () => null,
361 |         getBundledAxeEnvironment: () => ({}),
362 |         createAxeNotAvailableResponse: () => ({
363 |           content: [
364 |             {
365 |               type: 'text',
366 |               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.',
367 |             },
368 |           ],
369 |           isError: true,
370 |         }),
371 |       };
372 | 
373 |       const result = await buttonLogic(
374 |         {
375 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
376 |           buttonType: 'home',
377 |         },
378 |         createNoopExecutor(),
379 |         mockAxeHelpers,
380 |       );
381 | 
382 |       expect(result).toEqual({
383 |         content: [
384 |           {
385 |             type: 'text',
386 |             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.',
387 |           },
388 |         ],
389 |         isError: true,
390 |       });
391 |     });
392 | 
393 |     it('should handle AxeError from failed command execution', async () => {
394 |       const mockExecutor = createMockExecutor({
395 |         success: false,
396 |         output: '',
397 |         error: 'axe command failed',
398 |         process: { pid: 12345 },
399 |       });
400 | 
401 |       const mockAxeHelpers = {
402 |         getAxePath: () => '/usr/local/bin/axe',
403 |         getBundledAxeEnvironment: () => ({}),
404 |         createAxeNotAvailableResponse: () => ({
405 |           content: [{ type: 'text', text: 'axe not available' }],
406 |           isError: true,
407 |         }),
408 |       };
409 | 
410 |       const result = await buttonLogic(
411 |         {
412 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
413 |           buttonType: 'home',
414 |         },
415 |         mockExecutor,
416 |         mockAxeHelpers,
417 |       );
418 | 
419 |       expect(result).toEqual({
420 |         content: [
421 |           {
422 |             type: 'text',
423 |             text: "Error: Failed to press button 'home': axe command 'button' failed.\nDetails: axe command failed",
424 |           },
425 |         ],
426 |         isError: true,
427 |       });
428 |     });
429 | 
430 |     it('should handle SystemError from command execution', async () => {
431 |       const mockExecutor = async () => {
432 |         throw new Error('ENOENT: no such file or directory');
433 |       };
434 | 
435 |       const mockAxeHelpers = {
436 |         getAxePath: () => '/usr/local/bin/axe',
437 |         getBundledAxeEnvironment: () => ({}),
438 |         createAxeNotAvailableResponse: () => ({
439 |           content: [{ type: 'text', text: 'axe not available' }],
440 |           isError: true,
441 |         }),
442 |       };
443 | 
444 |       const result = await buttonLogic(
445 |         {
446 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
447 |           buttonType: 'home',
448 |         },
449 |         mockExecutor,
450 |         mockAxeHelpers,
451 |       );
452 | 
453 |       expect(result.content[0].text).toMatch(
454 |         /^Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory/,
455 |       );
456 |       expect(result.isError).toBe(true);
457 |     });
458 | 
459 |     it('should handle unexpected Error objects', async () => {
460 |       const mockExecutor = async () => {
461 |         throw new Error('Unexpected error');
462 |       };
463 | 
464 |       const mockAxeHelpers = {
465 |         getAxePath: () => '/usr/local/bin/axe',
466 |         getBundledAxeEnvironment: () => ({}),
467 |         createAxeNotAvailableResponse: () => ({
468 |           content: [{ type: 'text', text: 'axe not available' }],
469 |           isError: true,
470 |         }),
471 |       };
472 | 
473 |       const result = await buttonLogic(
474 |         {
475 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
476 |           buttonType: 'home',
477 |         },
478 |         mockExecutor,
479 |         mockAxeHelpers,
480 |       );
481 | 
482 |       expect(result.content[0].text).toMatch(
483 |         /^Error: System error executing axe: Failed to execute axe command: Unexpected error/,
484 |       );
485 |       expect(result.isError).toBe(true);
486 |     });
487 | 
488 |     it('should handle unexpected string errors', async () => {
489 |       const mockExecutor = async () => {
490 |         throw 'String error';
491 |       };
492 | 
493 |       const mockAxeHelpers = {
494 |         getAxePath: () => '/usr/local/bin/axe',
495 |         getBundledAxeEnvironment: () => ({}),
496 |         createAxeNotAvailableResponse: () => ({
497 |           content: [{ type: 'text', text: 'axe not available' }],
498 |           isError: true,
499 |         }),
500 |       };
501 | 
502 |       const result = await buttonLogic(
503 |         {
504 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
505 |           buttonType: 'home',
506 |         },
507 |         mockExecutor,
508 |         mockAxeHelpers,
509 |       );
510 | 
511 |       expect(result).toEqual({
512 |         content: [
513 |           {
514 |             type: 'text',
515 |             text: 'Error: System error executing axe: Failed to execute axe command: String error',
516 |           },
517 |         ],
518 |         isError: true,
519 |       });
520 |     });
521 |   });
522 | });
523 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for key_press tool plugin
  3 |  */
  4 | 
  5 | import { describe, it, expect } from 'vitest';
  6 | import { z } from 'zod';
  7 | import {
  8 |   createMockExecutor,
  9 |   createMockFileSystemExecutor,
 10 |   createNoopExecutor,
 11 | } from '../../../../test-utils/mock-executors.ts';
 12 | import keyPressPlugin, { key_pressLogic } from '../key_press.ts';
 13 | 
 14 | describe('Key Press Plugin', () => {
 15 |   describe('Export Field Validation (Literal)', () => {
 16 |     it('should have correct name', () => {
 17 |       expect(keyPressPlugin.name).toBe('key_press');
 18 |     });
 19 | 
 20 |     it('should have correct description', () => {
 21 |       expect(keyPressPlugin.description).toBe(
 22 |         'Press a single key by keycode on the simulator. Common keycodes: 40=Return, 42=Backspace, 43=Tab, 44=Space, 58-67=F1-F10.',
 23 |       );
 24 |     });
 25 | 
 26 |     it('should have handler function', () => {
 27 |       expect(typeof keyPressPlugin.handler).toBe('function');
 28 |     });
 29 | 
 30 |     it('should validate schema fields with safeParse', () => {
 31 |       const schema = z.object(keyPressPlugin.schema);
 32 | 
 33 |       // Valid case
 34 |       expect(
 35 |         schema.safeParse({
 36 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 37 |           keyCode: 40,
 38 |         }).success,
 39 |       ).toBe(true);
 40 | 
 41 |       // Invalid simulatorUuid
 42 |       expect(
 43 |         schema.safeParse({
 44 |           simulatorUuid: 'invalid-uuid',
 45 |           keyCode: 40,
 46 |         }).success,
 47 |       ).toBe(false);
 48 | 
 49 |       // Invalid keyCode (string)
 50 |       expect(
 51 |         schema.safeParse({
 52 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 53 |           keyCode: 'invalid',
 54 |         }).success,
 55 |       ).toBe(false);
 56 | 
 57 |       // Invalid keyCode (below range)
 58 |       expect(
 59 |         schema.safeParse({
 60 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 61 |           keyCode: -1,
 62 |         }).success,
 63 |       ).toBe(false);
 64 | 
 65 |       // Invalid keyCode (above range)
 66 |       expect(
 67 |         schema.safeParse({
 68 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 69 |           keyCode: 256,
 70 |         }).success,
 71 |       ).toBe(false);
 72 | 
 73 |       // Valid with duration
 74 |       expect(
 75 |         schema.safeParse({
 76 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 77 |           keyCode: 40,
 78 |           duration: 1.5,
 79 |         }).success,
 80 |       ).toBe(true);
 81 | 
 82 |       // Invalid duration (negative)
 83 |       expect(
 84 |         schema.safeParse({
 85 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
 86 |           keyCode: 40,
 87 |           duration: -1,
 88 |         }).success,
 89 |       ).toBe(false);
 90 |     });
 91 |   });
 92 | 
 93 |   describe('Command Generation', () => {
 94 |     it('should generate correct axe command for basic key press', async () => {
 95 |       let capturedCommand: string[] = [];
 96 |       const trackingExecutor = async (command: string[]) => {
 97 |         capturedCommand = command;
 98 |         return {
 99 |           success: true,
100 |           output: 'key press completed',
101 |           error: undefined,
102 |           process: { pid: 12345 },
103 |         };
104 |       };
105 | 
106 |       const mockAxeHelpers = {
107 |         getAxePath: () => '/usr/local/bin/axe',
108 |         getBundledAxeEnvironment: () => ({}),
109 |         createAxeNotAvailableResponse: () => ({
110 |           content: [
111 |             {
112 |               type: 'text',
113 |               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.',
114 |             },
115 |           ],
116 |           isError: true,
117 |         }),
118 |       };
119 | 
120 |       await key_pressLogic(
121 |         {
122 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
123 |           keyCode: 40,
124 |         },
125 |         trackingExecutor,
126 |         mockAxeHelpers,
127 |       );
128 | 
129 |       expect(capturedCommand).toEqual([
130 |         '/usr/local/bin/axe',
131 |         'key',
132 |         '40',
133 |         '--udid',
134 |         '12345678-1234-1234-1234-123456789012',
135 |       ]);
136 |     });
137 | 
138 |     it('should generate correct axe command for key press with duration', async () => {
139 |       let capturedCommand: string[] = [];
140 |       const trackingExecutor = async (command: string[]) => {
141 |         capturedCommand = command;
142 |         return {
143 |           success: true,
144 |           output: 'key press completed',
145 |           error: undefined,
146 |           process: { pid: 12345 },
147 |         };
148 |       };
149 | 
150 |       const mockAxeHelpers = {
151 |         getAxePath: () => '/usr/local/bin/axe',
152 |         getBundledAxeEnvironment: () => ({}),
153 |         createAxeNotAvailableResponse: () => ({
154 |           content: [
155 |             {
156 |               type: 'text',
157 |               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.',
158 |             },
159 |           ],
160 |           isError: true,
161 |         }),
162 |       };
163 | 
164 |       await key_pressLogic(
165 |         {
166 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
167 |           keyCode: 42,
168 |           duration: 1.5,
169 |         },
170 |         trackingExecutor,
171 |         mockAxeHelpers,
172 |       );
173 | 
174 |       expect(capturedCommand).toEqual([
175 |         '/usr/local/bin/axe',
176 |         'key',
177 |         '42',
178 |         '--duration',
179 |         '1.5',
180 |         '--udid',
181 |         '12345678-1234-1234-1234-123456789012',
182 |       ]);
183 |     });
184 | 
185 |     it('should generate correct axe command for different key codes', async () => {
186 |       let capturedCommand: string[] = [];
187 |       const trackingExecutor = async (command: string[]) => {
188 |         capturedCommand = command;
189 |         return {
190 |           success: true,
191 |           output: 'key press completed',
192 |           error: undefined,
193 |           process: { pid: 12345 },
194 |         };
195 |       };
196 | 
197 |       const mockAxeHelpers = {
198 |         getAxePath: () => '/usr/local/bin/axe',
199 |         getBundledAxeEnvironment: () => ({}),
200 |         createAxeNotAvailableResponse: () => ({
201 |           content: [
202 |             {
203 |               type: 'text',
204 |               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.',
205 |             },
206 |           ],
207 |           isError: true,
208 |         }),
209 |       };
210 | 
211 |       await key_pressLogic(
212 |         {
213 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
214 |           keyCode: 255,
215 |         },
216 |         trackingExecutor,
217 |         mockAxeHelpers,
218 |       );
219 | 
220 |       expect(capturedCommand).toEqual([
221 |         '/usr/local/bin/axe',
222 |         'key',
223 |         '255',
224 |         '--udid',
225 |         '12345678-1234-1234-1234-123456789012',
226 |       ]);
227 |     });
228 | 
229 |     it('should generate correct axe command with bundled axe path', async () => {
230 |       let capturedCommand: string[] = [];
231 |       const trackingExecutor = async (command: string[]) => {
232 |         capturedCommand = command;
233 |         return {
234 |           success: true,
235 |           output: 'key press completed',
236 |           error: undefined,
237 |           process: { pid: 12345 },
238 |         };
239 |       };
240 | 
241 |       const mockAxeHelpers = {
242 |         getAxePath: () => '/path/to/bundled/axe',
243 |         getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }),
244 |         createAxeNotAvailableResponse: () => ({
245 |           content: [
246 |             {
247 |               type: 'text',
248 |               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.',
249 |             },
250 |           ],
251 |           isError: true,
252 |         }),
253 |       };
254 | 
255 |       await key_pressLogic(
256 |         {
257 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
258 |           keyCode: 44,
259 |         },
260 |         trackingExecutor,
261 |         mockAxeHelpers,
262 |       );
263 | 
264 |       expect(capturedCommand).toEqual([
265 |         '/path/to/bundled/axe',
266 |         'key',
267 |         '44',
268 |         '--udid',
269 |         '12345678-1234-1234-1234-123456789012',
270 |       ]);
271 |     });
272 |   });
273 | 
274 |   describe('Handler Behavior (Complete Literal Returns)', () => {
275 |     // Note: Parameter validation is now handled by Zod schema validation in createTypedTool wrapper.
276 |     // The key_pressLogic function expects valid parameters and focuses on business logic testing.
277 | 
278 |     it('should return success for valid key press execution', async () => {
279 |       const mockExecutor = createMockExecutor({
280 |         success: true,
281 |         output: 'key press completed',
282 |         error: '',
283 |       });
284 | 
285 |       const mockAxeHelpers = {
286 |         getAxePath: () => '/usr/local/bin/axe',
287 |         getBundledAxeEnvironment: () => ({}),
288 |         createAxeNotAvailableResponse: () => ({
289 |           content: [
290 |             {
291 |               type: 'text',
292 |               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.',
293 |             },
294 |           ],
295 |           isError: true,
296 |         }),
297 |       };
298 | 
299 |       const result = await key_pressLogic(
300 |         {
301 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
302 |           keyCode: 40,
303 |         },
304 |         mockExecutor,
305 |         mockAxeHelpers,
306 |       );
307 | 
308 |       expect(result).toEqual({
309 |         content: [{ type: 'text', text: 'Key press (code: 40) simulated successfully.' }],
310 |         isError: false,
311 |       });
312 |     });
313 | 
314 |     it('should return success for key press with duration', async () => {
315 |       const mockExecutor = createMockExecutor({
316 |         success: true,
317 |         output: 'key press completed',
318 |         error: '',
319 |       });
320 | 
321 |       const mockAxeHelpers = {
322 |         getAxePath: () => '/usr/local/bin/axe',
323 |         getBundledAxeEnvironment: () => ({}),
324 |         createAxeNotAvailableResponse: () => ({
325 |           content: [
326 |             {
327 |               type: 'text',
328 |               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.',
329 |             },
330 |           ],
331 |           isError: true,
332 |         }),
333 |       };
334 | 
335 |       const result = await key_pressLogic(
336 |         {
337 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
338 |           keyCode: 42,
339 |           duration: 1.5,
340 |         },
341 |         mockExecutor,
342 |         mockAxeHelpers,
343 |       );
344 | 
345 |       expect(result).toEqual({
346 |         content: [{ type: 'text', text: 'Key press (code: 42) simulated successfully.' }],
347 |         isError: false,
348 |       });
349 |     });
350 | 
351 |     it('should handle DependencyError when axe is not available', async () => {
352 |       const mockAxeHelpers = {
353 |         getAxePath: () => null,
354 |         getBundledAxeEnvironment: () => ({}),
355 |         createAxeNotAvailableResponse: () => ({
356 |           content: [
357 |             {
358 |               type: 'text',
359 |               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.',
360 |             },
361 |           ],
362 |           isError: true,
363 |         }),
364 |       };
365 | 
366 |       const result = await key_pressLogic(
367 |         {
368 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
369 |           keyCode: 40,
370 |         },
371 |         createNoopExecutor(),
372 |         mockAxeHelpers,
373 |       );
374 | 
375 |       expect(result).toEqual({
376 |         content: [
377 |           {
378 |             type: 'text',
379 |             text:
380 |               'Bundled axe tool not found. UI automation features are not available.\n\n' +
381 |               'This is likely an installation issue with the npm package.\n' +
382 |               'Please reinstall xcodebuildmcp or report this issue.',
383 |           },
384 |         ],
385 |         isError: true,
386 |       });
387 |     });
388 | 
389 |     it('should handle AxeError from failed command execution', async () => {
390 |       const mockExecutor = createMockExecutor({
391 |         success: false,
392 |         output: '',
393 |         error: 'axe command failed',
394 |       });
395 | 
396 |       const mockAxeHelpers = {
397 |         getAxePath: () => '/usr/local/bin/axe',
398 |         getBundledAxeEnvironment: () => ({}),
399 |         createAxeNotAvailableResponse: () => ({
400 |           content: [
401 |             {
402 |               type: 'text',
403 |               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.',
404 |             },
405 |           ],
406 |           isError: true,
407 |         }),
408 |       };
409 | 
410 |       const result = await key_pressLogic(
411 |         {
412 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
413 |           keyCode: 40,
414 |         },
415 |         mockExecutor,
416 |         mockAxeHelpers,
417 |       );
418 | 
419 |       expect(result).toEqual({
420 |         content: [
421 |           {
422 |             type: 'text',
423 |             text: "Error: Failed to simulate key press (code: 40): axe command 'key' failed.\nDetails: axe command failed",
424 |           },
425 |         ],
426 |         isError: true,
427 |       });
428 |     });
429 | 
430 |     it('should handle SystemError from command execution', async () => {
431 |       const mockExecutor = () => {
432 |         throw new Error('System error occurred');
433 |       };
434 | 
435 |       const mockAxeHelpers = {
436 |         getAxePath: () => '/usr/local/bin/axe',
437 |         getBundledAxeEnvironment: () => ({}),
438 |         createAxeNotAvailableResponse: () => ({
439 |           content: [
440 |             {
441 |               type: 'text',
442 |               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.',
443 |             },
444 |           ],
445 |           isError: true,
446 |         }),
447 |       };
448 | 
449 |       const result = await key_pressLogic(
450 |         {
451 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
452 |           keyCode: 40,
453 |         },
454 |         mockExecutor,
455 |         mockAxeHelpers,
456 |       );
457 | 
458 |       expect(result.isError).toBe(true);
459 |       expect(result.content[0].text).toContain(
460 |         'Error: System error executing axe: Failed to execute axe command: System error occurred',
461 |       );
462 |     });
463 | 
464 |     it('should handle unexpected Error objects', async () => {
465 |       const mockExecutor = () => {
466 |         throw new Error('Unexpected error');
467 |       };
468 | 
469 |       const mockAxeHelpers = {
470 |         getAxePath: () => '/usr/local/bin/axe',
471 |         getBundledAxeEnvironment: () => ({}),
472 |         createAxeNotAvailableResponse: () => ({
473 |           content: [
474 |             {
475 |               type: 'text',
476 |               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.',
477 |             },
478 |           ],
479 |           isError: true,
480 |         }),
481 |       };
482 | 
483 |       const result = await key_pressLogic(
484 |         {
485 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
486 |           keyCode: 40,
487 |         },
488 |         mockExecutor,
489 |         mockAxeHelpers,
490 |       );
491 | 
492 |       expect(result.isError).toBe(true);
493 |       expect(result.content[0].text).toContain(
494 |         'Error: System error executing axe: Failed to execute axe command: Unexpected error',
495 |       );
496 |     });
497 | 
498 |     it('should handle unexpected string errors', async () => {
499 |       const mockExecutor = () => {
500 |         throw 'String error';
501 |       };
502 | 
503 |       const mockAxeHelpers = {
504 |         getAxePath: () => '/usr/local/bin/axe',
505 |         getBundledAxeEnvironment: () => ({}),
506 |         createAxeNotAvailableResponse: () => ({
507 |           content: [
508 |             {
509 |               type: 'text',
510 |               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.',
511 |             },
512 |           ],
513 |           isError: true,
514 |         }),
515 |       };
516 | 
517 |       const result = await key_pressLogic(
518 |         {
519 |           simulatorUuid: '12345678-1234-1234-1234-123456789012',
520 |           keyCode: 40,
521 |         },
522 |         mockExecutor,
523 |         mockAxeHelpers,
524 |       );
525 | 
526 |       expect(result).toEqual({
527 |         content: [
528 |           {
529 |             type: 'text',
530 |             text: 'Error: System error executing axe: Failed to execute axe command: String error',
531 |           },
532 |         ],
533 |         isError: true,
534 |       });
535 |     });
536 |   });
537 | });
538 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/screenshot.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for screenshot plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using pure dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import { z } from 'zod';
  9 | import {
 10 |   createMockExecutor,
 11 |   createMockFileSystemExecutor,
 12 |   createCommandMatchingMockExecutor,
 13 | } from '../../../../test-utils/mock-executors.ts';
 14 | import { SystemError } from '../../../../utils/responses/index.ts';
 15 | import screenshotPlugin, { screenshotLogic } from '../../ui-testing/screenshot.ts';
 16 | 
 17 | describe('screenshot plugin', () => {
 18 |   // No mocks to clear since we use pure dependency injection
 19 | 
 20 |   describe('Export Field Validation (Literal)', () => {
 21 |     it('should have correct name field', () => {
 22 |       expect(screenshotPlugin.name).toBe('screenshot');
 23 |     });
 24 | 
 25 |     it('should have correct description field', () => {
 26 |       expect(screenshotPlugin.description).toBe(
 27 |         "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).",
 28 |       );
 29 |     });
 30 | 
 31 |     it('should have handler function', () => {
 32 |       expect(typeof screenshotPlugin.handler).toBe('function');
 33 |     });
 34 | 
 35 |     it('should have correct schema validation', () => {
 36 |       const schema = z.object(screenshotPlugin.schema);
 37 | 
 38 |       expect(
 39 |         schema.safeParse({
 40 |           simulatorUuid: '550e8400-e29b-41d4-a716-446655440000',
 41 |         }).success,
 42 |       ).toBe(true);
 43 | 
 44 |       expect(
 45 |         schema.safeParse({
 46 |           simulatorUuid: 123,
 47 |         }).success,
 48 |       ).toBe(false);
 49 | 
 50 |       expect(schema.safeParse({}).success).toBe(false);
 51 |     });
 52 |   });
 53 | 
 54 |   describe('Command Generation', () => {
 55 |     it('should generate correct simctl and sips commands', async () => {
 56 |       const capturedCommands: string[][] = [];
 57 | 
 58 |       const mockExecutor = createCommandMatchingMockExecutor({
 59 |         'xcrun simctl': { success: true, output: 'Screenshot saved' },
 60 |         sips: { success: true, output: 'Image optimized' },
 61 |       });
 62 | 
 63 |       // Wrap to capture both commands
 64 |       const capturingExecutor = async (command: string[], ...args: any[]) => {
 65 |         capturedCommands.push(command);
 66 |         return mockExecutor(command, ...args);
 67 |       };
 68 | 
 69 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
 70 |         readFile: async () => 'fake-image-data',
 71 |       });
 72 | 
 73 |       const mockPathDeps = {
 74 |         tmpdir: () => '/tmp',
 75 |         join: (...paths: string[]) => paths.join('/'),
 76 |       };
 77 | 
 78 |       const mockUuidDeps = {
 79 |         v4: () => 'mock-uuid-123',
 80 |       };
 81 | 
 82 |       await screenshotLogic(
 83 |         {
 84 |           simulatorUuid: 'test-uuid',
 85 |         },
 86 |         capturingExecutor,
 87 |         mockFileSystemExecutor,
 88 |         mockPathDeps,
 89 |         mockUuidDeps,
 90 |       );
 91 | 
 92 |       // Should execute both commands in sequence
 93 |       expect(capturedCommands).toHaveLength(2);
 94 | 
 95 |       // First command: xcrun simctl screenshot
 96 |       expect(capturedCommands[0]).toEqual([
 97 |         'xcrun',
 98 |         'simctl',
 99 |         'io',
100 |         'test-uuid',
101 |         'screenshot',
102 |         '/tmp/screenshot_mock-uuid-123.png',
103 |       ]);
104 | 
105 |       // Second command: sips optimization
106 |       expect(capturedCommands[1]).toEqual([
107 |         'sips',
108 |         '-Z',
109 |         '800',
110 |         '-s',
111 |         'format',
112 |         'jpeg',
113 |         '-s',
114 |         'formatOptions',
115 |         '75',
116 |         '/tmp/screenshot_mock-uuid-123.png',
117 |         '--out',
118 |         '/tmp/screenshot_optimized_mock-uuid-123.jpg',
119 |       ]);
120 |     });
121 | 
122 |     it('should generate correct path with different uuid', async () => {
123 |       const capturedCommands: string[][] = [];
124 | 
125 |       const mockExecutor = createCommandMatchingMockExecutor({
126 |         'xcrun simctl': { success: true, output: 'Screenshot saved' },
127 |         sips: { success: true, output: 'Image optimized' },
128 |       });
129 | 
130 |       // Wrap to capture both commands
131 |       const capturingExecutor = async (command: string[], ...args: any[]) => {
132 |         capturedCommands.push(command);
133 |         return mockExecutor(command, ...args);
134 |       };
135 | 
136 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
137 |         readFile: async () => 'fake-image-data',
138 |       });
139 | 
140 |       const mockPathDeps = {
141 |         tmpdir: () => '/tmp',
142 |         join: (...paths: string[]) => paths.join('/'),
143 |       };
144 | 
145 |       const mockUuidDeps = {
146 |         v4: () => 'different-uuid-456',
147 |       };
148 | 
149 |       await screenshotLogic(
150 |         {
151 |           simulatorUuid: 'another-uuid',
152 |         },
153 |         capturingExecutor,
154 |         mockFileSystemExecutor,
155 |         mockPathDeps,
156 |         mockUuidDeps,
157 |       );
158 | 
159 |       // Should execute both commands in sequence
160 |       expect(capturedCommands).toHaveLength(2);
161 | 
162 |       // First command: xcrun simctl screenshot
163 |       expect(capturedCommands[0]).toEqual([
164 |         'xcrun',
165 |         'simctl',
166 |         'io',
167 |         'another-uuid',
168 |         'screenshot',
169 |         '/tmp/screenshot_different-uuid-456.png',
170 |       ]);
171 | 
172 |       // Second command: sips optimization
173 |       expect(capturedCommands[1]).toEqual([
174 |         'sips',
175 |         '-Z',
176 |         '800',
177 |         '-s',
178 |         'format',
179 |         'jpeg',
180 |         '-s',
181 |         'formatOptions',
182 |         '75',
183 |         '/tmp/screenshot_different-uuid-456.png',
184 |         '--out',
185 |         '/tmp/screenshot_optimized_different-uuid-456.jpg',
186 |       ]);
187 |     });
188 | 
189 |     it('should use default dependencies when not provided', async () => {
190 |       const capturedCommands: string[][] = [];
191 | 
192 |       const mockExecutor = createCommandMatchingMockExecutor({
193 |         'xcrun simctl': { success: true, output: 'Screenshot saved' },
194 |         sips: { success: true, output: 'Image optimized' },
195 |       });
196 | 
197 |       // Wrap to capture both commands
198 |       const capturingExecutor = async (command: string[], ...args: any[]) => {
199 |         capturedCommands.push(command);
200 |         return mockExecutor(command, ...args);
201 |       };
202 | 
203 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
204 |         readFile: async () => 'fake-image-data',
205 |       });
206 | 
207 |       await screenshotLogic(
208 |         {
209 |           simulatorUuid: 'test-uuid',
210 |         },
211 |         capturingExecutor,
212 |         mockFileSystemExecutor,
213 |       );
214 | 
215 |       // Should execute both commands in sequence
216 |       expect(capturedCommands).toHaveLength(2);
217 | 
218 |       // First command should be generated with real os.tmpdir, path.join, and uuidv4
219 |       const firstCommand = capturedCommands[0];
220 |       expect(firstCommand).toHaveLength(6);
221 |       expect(firstCommand[0]).toBe('xcrun');
222 |       expect(firstCommand[1]).toBe('simctl');
223 |       expect(firstCommand[2]).toBe('io');
224 |       expect(firstCommand[3]).toBe('test-uuid');
225 |       expect(firstCommand[4]).toBe('screenshot');
226 |       expect(firstCommand[5]).toMatch(/\/.*\/screenshot_.*\.png/);
227 | 
228 |       // Second command should be sips optimization
229 |       const secondCommand = capturedCommands[1];
230 |       expect(secondCommand[0]).toBe('sips');
231 |       expect(secondCommand[1]).toBe('-Z');
232 |       expect(secondCommand[2]).toBe('800');
233 |       // Should have proper PNG input and JPG output paths
234 |       expect(secondCommand[secondCommand.length - 3]).toMatch(/\/.*\/screenshot_.*\.png/);
235 |       expect(secondCommand[secondCommand.length - 1]).toMatch(/\/.*\/screenshot_optimized_.*\.jpg/);
236 |     });
237 |   });
238 | 
239 |   describe('Response Processing', () => {
240 |     it('should capture screenshot successfully', async () => {
241 |       const mockImageBuffer = Buffer.from('fake-image-data');
242 | 
243 |       // Mock both commands: screenshot + optimization
244 |       const mockExecutor = createCommandMatchingMockExecutor({
245 |         'xcrun simctl': { success: true, output: 'Screenshot saved' },
246 |         sips: { success: true, output: 'Image optimized' },
247 |       });
248 | 
249 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
250 |         readFile: async () => mockImageBuffer.toString('base64'), // Return base64 directly
251 |       });
252 | 
253 |       const mockPathDeps = {
254 |         tmpdir: () => '/tmp',
255 |         join: (...paths: string[]) => paths.join('/'),
256 |       };
257 | 
258 |       const mockUuidDeps = {
259 |         v4: () => 'mock-uuid-123',
260 |       };
261 | 
262 |       const result = await screenshotLogic(
263 |         {
264 |           simulatorUuid: 'test-uuid',
265 |         },
266 |         mockExecutor,
267 |         mockFileSystemExecutor,
268 |         mockPathDeps,
269 |         mockUuidDeps,
270 |       );
271 | 
272 |       expect(result).toEqual({
273 |         content: [
274 |           {
275 |             type: 'image',
276 |             data: mockImageBuffer.toString('base64'),
277 |             mimeType: 'image/jpeg', // Now JPEG after optimization
278 |           },
279 |         ],
280 |         isError: false,
281 |       });
282 |     });
283 | 
284 |     it('should handle missing simulatorUuid via handler', async () => {
285 |       // Test Zod validation by calling the handler with invalid params
286 |       const result = await screenshotPlugin.handler({});
287 | 
288 |       expect(result).toEqual({
289 |         content: [
290 |           {
291 |             type: 'text',
292 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nsimulatorUuid: Required',
293 |           },
294 |         ],
295 |         isError: true,
296 |       });
297 |     });
298 | 
299 |     it('should handle command failure', async () => {
300 |       const mockExecutor = createMockExecutor({
301 |         success: false,
302 |         output: '',
303 |         error: 'Command failed',
304 |       });
305 | 
306 |       const mockPathDeps = {
307 |         tmpdir: () => '/tmp',
308 |         join: (...paths: string[]) => paths.join('/'),
309 |       };
310 | 
311 |       const mockUuidDeps = {
312 |         v4: () => 'mock-uuid-123',
313 |       };
314 | 
315 |       const result = await screenshotLogic(
316 |         {
317 |           simulatorUuid: 'test-uuid',
318 |         },
319 |         mockExecutor,
320 |         createMockFileSystemExecutor(),
321 |         mockPathDeps,
322 |         mockUuidDeps,
323 |       );
324 | 
325 |       expect(result).toEqual({
326 |         content: [
327 |           {
328 |             type: 'text',
329 |             text: 'Error: System error executing screenshot: Failed to capture screenshot: Command failed',
330 |           },
331 |         ],
332 |         isError: true,
333 |       });
334 |     });
335 | 
336 |     it('should handle file read failure', async () => {
337 |       const mockExecutor = createMockExecutor({
338 |         success: true,
339 |         output: '',
340 |         error: undefined,
341 |       });
342 | 
343 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
344 |         readFile: async () => {
345 |           throw new Error('File not found');
346 |         },
347 |       });
348 | 
349 |       const mockPathDeps = {
350 |         tmpdir: () => '/tmp',
351 |         join: (...paths: string[]) => paths.join('/'),
352 |       };
353 | 
354 |       const mockUuidDeps = {
355 |         v4: () => 'mock-uuid-123',
356 |       };
357 | 
358 |       const result = await screenshotLogic(
359 |         {
360 |           simulatorUuid: 'test-uuid',
361 |         },
362 |         mockExecutor,
363 |         mockFileSystemExecutor,
364 |         mockPathDeps,
365 |         mockUuidDeps,
366 |       );
367 | 
368 |       expect(result).toEqual({
369 |         content: [
370 |           {
371 |             type: 'text',
372 |             text: 'Error: Screenshot captured but failed to process image file: File not found',
373 |           },
374 |         ],
375 |         isError: true,
376 |       });
377 |     });
378 | 
379 |     it('should call correct command with direct execution', async () => {
380 |       const capturedArgs: any[][] = [];
381 | 
382 |       const mockExecutor = createCommandMatchingMockExecutor({
383 |         'xcrun simctl': { success: true, output: 'Screenshot saved' },
384 |         sips: { success: true, output: 'Image optimized' },
385 |       });
386 | 
387 |       // Wrap to capture both command executions
388 |       const capturingExecutor = async (...args: any[]) => {
389 |         capturedArgs.push(args);
390 |         return mockExecutor(...args);
391 |       };
392 | 
393 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
394 |         readFile: async () => 'fake-image-data',
395 |       });
396 | 
397 |       const mockPathDeps = {
398 |         tmpdir: () => '/tmp',
399 |         join: (...paths: string[]) => paths.join('/'),
400 |       };
401 | 
402 |       const mockUuidDeps = {
403 |         v4: () => 'mock-uuid-123',
404 |       };
405 | 
406 |       await screenshotLogic(
407 |         {
408 |           simulatorUuid: 'test-uuid',
409 |         },
410 |         capturingExecutor,
411 |         mockFileSystemExecutor,
412 |         mockPathDeps,
413 |         mockUuidDeps,
414 |       );
415 | 
416 |       // Should capture both command executions
417 |       expect(capturedArgs).toHaveLength(2);
418 | 
419 |       // First call: xcrun simctl screenshot (3 args: command, logPrefix, useShell)
420 |       expect(capturedArgs[0]).toEqual([
421 |         ['xcrun', 'simctl', 'io', 'test-uuid', 'screenshot', '/tmp/screenshot_mock-uuid-123.png'],
422 |         '[Screenshot]: screenshot',
423 |         false,
424 |       ]);
425 | 
426 |       // Second call: sips optimization (3 args: command, logPrefix, useShell)
427 |       expect(capturedArgs[1]).toEqual([
428 |         [
429 |           'sips',
430 |           '-Z',
431 |           '800',
432 |           '-s',
433 |           'format',
434 |           'jpeg',
435 |           '-s',
436 |           'formatOptions',
437 |           '75',
438 |           '/tmp/screenshot_mock-uuid-123.png',
439 |           '--out',
440 |           '/tmp/screenshot_optimized_mock-uuid-123.jpg',
441 |         ],
442 |         '[Screenshot]: optimize image',
443 |         false,
444 |       ]);
445 |     });
446 | 
447 |     it('should handle SystemError exceptions', async () => {
448 |       const mockExecutor = createMockExecutor(new SystemError('System error occurred'));
449 | 
450 |       const mockPathDeps = {
451 |         tmpdir: () => '/tmp',
452 |         join: (...paths: string[]) => paths.join('/'),
453 |       };
454 | 
455 |       const mockUuidDeps = {
456 |         v4: () => 'mock-uuid-123',
457 |       };
458 | 
459 |       const result = await screenshotLogic(
460 |         {
461 |           simulatorUuid: 'test-uuid',
462 |         },
463 |         mockExecutor,
464 |         createMockFileSystemExecutor(),
465 |         mockPathDeps,
466 |         mockUuidDeps,
467 |       );
468 | 
469 |       expect(result).toEqual({
470 |         content: [
471 |           {
472 |             type: 'text',
473 |             text: 'Error: System error executing screenshot: System error occurred',
474 |           },
475 |         ],
476 |         isError: true,
477 |       });
478 |     });
479 | 
480 |     it('should handle unexpected Error objects', async () => {
481 |       const mockExecutor = createMockExecutor(new Error('Unexpected error'));
482 | 
483 |       const mockPathDeps = {
484 |         tmpdir: () => '/tmp',
485 |         join: (...paths: string[]) => paths.join('/'),
486 |       };
487 | 
488 |       const mockUuidDeps = {
489 |         v4: () => 'mock-uuid-123',
490 |       };
491 | 
492 |       const result = await screenshotLogic(
493 |         {
494 |           simulatorUuid: 'test-uuid',
495 |         },
496 |         mockExecutor,
497 |         createMockFileSystemExecutor(),
498 |         mockPathDeps,
499 |         mockUuidDeps,
500 |       );
501 | 
502 |       expect(result).toEqual({
503 |         content: [
504 |           {
505 |             type: 'text',
506 |             text: 'Error: An unexpected error occurred: Unexpected error',
507 |           },
508 |         ],
509 |         isError: true,
510 |       });
511 |     });
512 | 
513 |     it('should handle unexpected string errors', async () => {
514 |       const mockExecutor = createMockExecutor('String error');
515 | 
516 |       const mockPathDeps = {
517 |         tmpdir: () => '/tmp',
518 |         join: (...paths: string[]) => paths.join('/'),
519 |       };
520 | 
521 |       const mockUuidDeps = {
522 |         v4: () => 'mock-uuid-123',
523 |       };
524 | 
525 |       const result = await screenshotLogic(
526 |         {
527 |           simulatorUuid: 'test-uuid',
528 |         },
529 |         mockExecutor,
530 |         createMockFileSystemExecutor(),
531 |         mockPathDeps,
532 |         mockUuidDeps,
533 |       );
534 | 
535 |       expect(result).toEqual({
536 |         content: [
537 |           {
538 |             type: 'text',
539 |             text: 'Error: An unexpected error occurred: String error',
540 |           },
541 |         ],
542 |         isError: true,
543 |       });
544 |     });
545 | 
546 |     it('should handle file read error with fileSystemExecutor', async () => {
547 |       const mockExecutor = createMockExecutor({
548 |         success: true,
549 |         output: '',
550 |         error: undefined,
551 |       });
552 | 
553 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
554 |         readFile: async () => {
555 |           throw 'File system error';
556 |         },
557 |       });
558 | 
559 |       const mockPathDeps = {
560 |         tmpdir: () => '/tmp',
561 |         join: (...paths: string[]) => paths.join('/'),
562 |       };
563 | 
564 |       const mockUuidDeps = {
565 |         v4: () => 'mock-uuid-123',
566 |       };
567 | 
568 |       const result = await screenshotLogic(
569 |         {
570 |           simulatorUuid: 'test-uuid',
571 |         },
572 |         mockExecutor,
573 |         mockFileSystemExecutor,
574 |         mockPathDeps,
575 |         mockUuidDeps,
576 |       );
577 | 
578 |       expect(result).toEqual({
579 |         content: [
580 |           {
581 |             type: 'text',
582 |             text: 'Error: Screenshot captured but failed to process image file: File system error',
583 |           },
584 |         ],
585 |         isError: true,
586 |       });
587 |     });
588 |   });
589 | });
590 | 
```

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

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

--------------------------------------------------------------------------------
/docs/RELOADEROO_XCODEBUILDMCP_PRIMER.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Reloaderoo + XcodeBuildMCP: Curated CLI Primer
  2 | 
  3 | Use this primer to drive XcodeBuildMCP entirely through Reloaderoo—treating it like a CLI. It is designed to be included in your agent’s context to show exactly how to invoke the specific tools your project needs.
  4 | 
  5 | Why this file:
  6 | - XcodeBuildMCP exposes many tools. Dumping the full tool surface into the context wastes tokens.
  7 | - Instead, copy this file into your project and delete everything you don’t need. Keep only the commands relevant to your workflow (e.g., just Simulator tools).
  8 | - Your trimmed version becomes a small, project‑specific reference that tells your agent precisely which Reloaderoo tool calls to make.
  9 | 
 10 | How to use this primer:
 11 | 1. Copy this file into your repo (e.g., docs/xcodebuildmcp_primer.md or AGENTS.md).
 12 | 2. Remove all sections and commands you don’t use. Keep it minimal.
 13 | 3. Replace placeholders with your real values (paths, schemes, simulator UUIDs/Names, bundle IDs, etc.).
 14 | 4. Use the quiet (-q) examples to reduce noise; pipe output to jq when you only need the content.
 15 | 5. Include your curated file in the agent context whenever you want it to call XcodeBuildMCP via Reloaderoo.
 16 | 
 17 | Conventions in the examples:
 18 | - Calls use: npx reloaderoo@latest inspect … -q -- npx xcodebuildmcp@latest
 19 | - Parameters are passed as JSON via --params.
 20 | - Resources are read with read-resource (e.g., xcodebuildmcp://simulators).
 21 | - Use jq -r '.contents[].text' to extract the textual results when needed.
 22 | 
 23 | Keep it small. The smaller your curated primer, the less context your agent needs—and the cheaper, faster, and more reliable your interactions will be.
 24 | 
 25 | ## Installation
 26 | 
 27 | Reloaderoo is available via npm and can be used with npx for universal compatibility.
 28 | 
 29 | ```bash
 30 | # Use npx to run reloaderoo
 31 | npx reloaderoo@latest --help
 32 | ```
 33 | 
 34 | ## Hint
 35 | 
 36 | Use jq to parse the output to get just the content response:
 37 | 
 38 | ```bash
 39 |  npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -q -- npx xcodebuildmcp@latest | jq -r '.contents[].text'
 40 |  ```
 41 | 
 42 | **Example Tool Calls:**
 43 | 
 44 | ## Dynamic Tool Discovery
 45 | 
 46 | - **`discover_tools`**: Analyzes a task description to enable relevant tools.
 47 |   ```bash
 48 |   npx reloaderoo@latest inspect call-tool discover_tools --params '{"task_description": "I want to build and run my iOS app on a simulator."}' -q -- npx xcodebuildmcp@latest
 49 |   ```
 50 | 
 51 | ## iOS Device Development
 52 | 
 53 | - **`build_device`**: Builds an app for a physical device.
 54 |   ```bash
 55 |   npx reloaderoo@latest inspect -q call-tool build_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest
 56 |   ```
 57 | - **`get_device_app_path`**: Gets the `.app` bundle path for a device build.
 58 |   ```bash
 59 |   npx reloaderoo@latest inspect call-tool get_device_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest
 60 |   ```
 61 | - **`install_app_device`**: Installs an app on a physical device.
 62 |   ```bash
 63 |   npx reloaderoo@latest inspect call-tool install_app_device --params '{"deviceId": "DEVICE_UDID", "appPath": "/path/to/MyApp.app"}' -q -- npx xcodebuildmcp@latest
 64 |   ```
 65 | - **`launch_app_device`**: Launches an app on a physical device.
 66 |   ```bash
 67 |   npx reloaderoo@latest inspect call-tool launch_app_device --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest
 68 |   ```
 69 | - **`list_devices`**: Lists connected physical devices.
 70 |   ```bash
 71 |   npx reloaderoo@latest inspect call-tool list_devices --params '{}' -q -- npx xcodebuildmcp@latest
 72 |   ```
 73 | - **`stop_app_device`**: Stops an app on a physical device.
 74 |   ```bash
 75 |   npx reloaderoo@latest inspect call-tool stop_app_device --params '{"deviceId": "DEVICE_UDID", "processId": 12345}' -q -- npx xcodebuildmcp@latest
 76 |   ```
 77 | - **`test_device`**: Runs tests on a physical device.
 78 |   ```bash
 79 |   npx reloaderoo@latest inspect call-tool test_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "deviceId": "DEVICE_UDID"}' -q -- npx xcodebuildmcp@latest
 80 |   ```
 81 | 
 82 | ## iOS Simulator Development
 83 | 
 84 | - **`boot_sim`**: Boots a simulator.
 85 |   ```bash
 86 |   npx reloaderoo@latest inspect call-tool boot_sim --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest
 87 |   ```
 88 | - **`build_run_sim`**: Builds and runs an app on a simulator.
 89 |   ```bash
 90 |   npx reloaderoo@latest inspect call-tool build_run_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest
 91 |   ```
 92 | - **`build_sim`**: Builds an app for a simulator.
 93 |   ```bash
 94 |   npx reloaderoo@latest inspect call-tool build_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest
 95 |   ```
 96 | - **`get_sim_app_path`**: Gets the `.app` bundle path for a simulator build.
 97 |   ```bash
 98 |   npx reloaderoo@latest inspect call-tool get_sim_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "platform": "iOS Simulator", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest
 99 |   ```
100 | - **`install_app_sim`**: Installs an app on a simulator.
101 |   ```bash
102 |   npx reloaderoo@latest inspect call-tool install_app_sim --params '{"simulatorUuid": "SIMULATOR_UUID", "appPath": "/path/to/MyApp.app"}' -q -- npx xcodebuildmcp@latest
103 |   ```
104 | - **`launch_app_logs_sim`**: Launches an app on a simulator with log capture.
105 |   ```bash
106 |   npx reloaderoo@latest inspect call-tool launch_app_logs_sim --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest
107 |   ```
108 | - **`launch_app_sim`**: Launches an app on a simulator.
109 |   ```bash
110 |   npx reloaderoo@latest inspect call-tool launch_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest
111 |   ```
112 | - **`list_sims`**: Lists available simulators.
113 |   ```bash
114 |   npx reloaderoo@latest inspect call-tool list_sims --params '{}' -q -- npx xcodebuildmcp@latest
115 |   ```
116 | - **`open_sim`**: Opens the Simulator application.
117 |   ```bash
118 |   npx reloaderoo@latest inspect call-tool open_sim --params '{}' -q -- npx xcodebuildmcp@latest
119 |   ```
120 | - **`stop_app_sim`**: Stops an app on a simulator.
121 |   ```bash
122 |   npx reloaderoo@latest inspect call-tool stop_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest
123 |   ```
124 | - **`test_sim`**: Runs tests on a simulator.
125 |   ```bash
126 |   npx reloaderoo@latest inspect call-tool test_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest
127 |   ```
128 | 
129 | ## Log Capture & Management
130 | 
131 | - **`start_device_log_cap`**: Starts log capture for a physical device.
132 |   ```bash
133 |   npx reloaderoo@latest inspect call-tool start_device_log_cap --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest
134 |   ```
135 | - **`start_sim_log_cap`**: Starts log capture for a simulator.
136 |   ```bash
137 |   npx reloaderoo@latest inspect call-tool start_sim_log_cap --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest
138 |   ```
139 | - **`stop_device_log_cap`**: Stops log capture for a physical device.
140 |   ```bash
141 |   npx reloaderoo@latest inspect call-tool stop_device_log_cap --params '{"logSessionId": "SESSION_ID"}' -q -- npx xcodebuildmcp@latest
142 |   ```
143 | - **`stop_sim_log_cap`**: Stops log capture for a simulator.
144 |   ```bash
145 |   npx reloaderoo@latest inspect call-tool stop_sim_log_cap --params '{"logSessionId": "SESSION_ID"}' -q -- npx xcodebuildmcp@latest
146 |   ```
147 | 
148 | ## macOS Development
149 | 
150 | - **`build_macos`**: Builds a macOS app.
151 |   ```bash
152 |   npx reloaderoo@latest inspect call-tool build_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest
153 |   ```
154 | - **`build_run_macos`**: Builds and runs a macOS app.
155 |   ```bash
156 |   npx reloaderoo@latest inspect call-tool build_run_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest
157 |   ```
158 | - **`get_mac_app_path`**: Gets the `.app` bundle path for a macOS build.
159 |   ```bash
160 |   npx reloaderoo@latest inspect call-tool get_mac_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest
161 |   ```
162 | - **`launch_mac_app`**: Launches a macOS app.
163 |   ```bash
164 |   npx reloaderoo@latest inspect call-tool launch_mac_app --params '{"appPath": "/Applications/Calculator.app"}' -q -- npx xcodebuildmcp@latest
165 |   ```
166 | - **`stop_mac_app`**: Stops a macOS app.
167 |   ```bash
168 |   npx reloaderoo@latest inspect call-tool stop_mac_app --params '{"appName": "Calculator"}' -q -- npx xcodebuildmcp@latest
169 |   ```
170 | - **`test_macos`**: Runs tests for a macOS project.
171 |   ```bash
172 |   npx reloaderoo@latest inspect call-tool test_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest
173 |   ```
174 | 
175 | ## Project Discovery
176 | 
177 | - **`discover_projs`**: Discovers Xcode projects and workspaces.
178 |   ```bash
179 |   npx reloaderoo@latest inspect call-tool discover_projs --params '{"workspaceRoot": "/path/to/workspace"}' -q -- npx xcodebuildmcp@latest
180 |   ```
181 | - **`get_app_bundle_id`**: Gets an app's bundle identifier.
182 |   ```bash
183 |   npx reloaderoo@latest inspect call-tool get_app_bundle_id --params '{"appPath": "/path/to/MyApp.app"}' -q -- npx xcodebuildmcp@latest
184 |   ```
185 | - **`get_mac_bundle_id`**: Gets a macOS app's bundle identifier.
186 |   ```bash
187 |   npx reloaderoo@latest inspect call-tool get_mac_bundle_id --params '{"appPath": "/Applications/Calculator.app"}' -q -- npx xcodebuildmcp@latest
188 |   ```
189 | - **`list_schemes`**: Lists schemes in a project or workspace.
190 |   ```bash
191 |   npx reloaderoo@latest inspect call-tool list_schemes --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -q -- npx xcodebuildmcp@latest
192 |   ```
193 | - **`show_build_settings`**: Shows build settings for a scheme.
194 |   ```bash
195 |   npx reloaderoo@latest inspect call-tool show_build_settings --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest
196 |   ```
197 | 
198 | ## Project Scaffolding
199 | 
200 | - **`scaffold_ios_project`**: Scaffolds a new iOS project.
201 |   ```bash
202 |   npx reloaderoo@latest inspect call-tool scaffold_ios_project --params '{"projectName": "MyNewApp", "outputPath": "/path/to/projects"}' -q -- npx xcodebuildmcp@latest
203 |   ```
204 | - **`scaffold_macos_project`**: Scaffolds a new macOS project.
205 |   ```bash
206 |   npx reloaderoo@latest inspect call-tool scaffold_macos_project --params '{"projectName": "MyNewMacApp", "outputPath": "/path/to/projects"}' -q -- npx xcodebuildmcp@latest
207 |   ```
208 | 
209 | ## Project Utilities
210 | 
211 | - **`clean`**: Cleans build artifacts.
212 |   ```bash
213 |   # For a project
214 |   npx reloaderoo@latest inspect call-tool clean --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -q -- npx xcodebuildmcp@latest
215 |   # For a workspace
216 |   npx reloaderoo@latest inspect call-tool clean --params '{"workspacePath": "/path/to/MyWorkspace.xcworkspace", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest
217 |   ```
218 | 
219 | ## Simulator Management
220 | 
221 | - **`reset_sim_location`**: Resets a simulator's location.
222 |   ```bash
223 |   npx reloaderoo@latest inspect call-tool reset_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest
224 |   ```
225 | - **`set_sim_appearance`**: Sets a simulator's appearance (dark/light mode).
226 |   ```bash
227 |   npx reloaderoo@latest inspect call-tool set_sim_appearance --params '{"simulatorUuid": "SIMULATOR_UUID", "mode": "dark"}' -q -- npx xcodebuildmcp@latest
228 |   ```
229 | - **`set_sim_location`**: Sets a simulator's GPS location.
230 |   ```bash
231 |   npx reloaderoo@latest inspect call-tool set_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID", "latitude": 37.7749, "longitude": -122.4194}' -q -- npx xcodebuildmcp@latest
232 |   ```
233 | - **`sim_statusbar`**: Overrides a simulator's status bar.
234 |   ```bash
235 |   npx reloaderoo@latest inspect call-tool sim_statusbar --params '{"simulatorUuid": "SIMULATOR_UUID", "dataNetwork": "wifi"}' -q -- npx xcodebuildmcp@latest
236 |   ```
237 | 
238 | ## Swift Package Manager
239 | 
240 | - **`swift_package_build`**: Builds a Swift package.
241 |   ```bash
242 |   npx reloaderoo@latest inspect call-tool swift_package_build --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest
243 |   ```
244 | - **`swift_package_clean`**: Cleans a Swift package.
245 |   ```bash
246 |   npx reloaderoo@latest inspect call-tool swift_package_clean --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest
247 |   ```
248 | - **`swift_package_list`**: Lists running Swift package processes.
249 |   ```bash
250 |   npx reloaderoo@latest inspect call-tool swift_package_list --params '{}' -q -- npx xcodebuildmcp@latest
251 |   ```
252 | - **`swift_package_run`**: Runs a Swift package executable.
253 |   ```bash
254 |   npx reloaderoo@latest inspect call-tool swift_package_run --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest
255 |   ```
256 | - **`swift_package_stop`**: Stops a running Swift package process.
257 |   ```bash
258 |   npx reloaderoo@latest inspect call-tool swift_package_stop --params '{"pid": 12345}' -q -- npx xcodebuildmcp@latest
259 |   ```
260 | - **`swift_package_test`**: Tests a Swift package.
261 |   ```bash
262 |   npx reloaderoo@latest inspect call-tool swift_package_test --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest
263 |   ```
264 | 
265 | ## System Doctor
266 | 
267 | - **`doctor`**: Runs system diagnostics.
268 |   ```bash
269 |   npx reloaderoo@latest inspect call-tool doctor --params '{}' -q -- npx xcodebuildmcp@latest
270 |   ```
271 | 
272 | ## UI Testing & Automation
273 | 
274 | - **`button`**: Simulates a hardware button press.
275 |   ```bash
276 |   npx reloaderoo@latest inspect call-tool button --params '{"simulatorUuid": "SIMULATOR_UUID", "buttonType": "home"}' -q -- npx xcodebuildmcp@latest
277 |   ```
278 | - **`describe_ui`**: Gets the UI hierarchy of the current screen.
279 |   ```bash
280 |   npx reloaderoo@latest inspect call-tool describe_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest
281 |   ```
282 | - **`gesture`**: Performs a pre-defined gesture.
283 |   ```bash
284 |   npx reloaderoo@latest inspect call-tool gesture --params '{"simulatorUuid": "SIMULATOR_UUID", "preset": "scroll-up"}' -q -- npx xcodebuildmcp@latest
285 |   ```
286 | - **`key_press`**: Simulates a key press.
287 |   ```bash
288 |   npx reloaderoo@latest inspect call-tool key_press --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCode": 40}' -q -- npx xcodebuildmcp@latest
289 |   ```
290 | - **`key_sequence`**: Simulates a sequence of key presses.
291 |   ```bash
292 |   npx reloaderoo@latest inspect call-tool key_sequence --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCodes": [40, 42, 44]}' -q -- npx xcodebuildmcp@latest
293 |   ```
294 | - **`long_press`**: Performs a long press at coordinates.
295 |   ```bash
296 |   npx reloaderoo@latest inspect call-tool long_press --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "duration": 1500}' -q -- npx xcodebuildmcp@latest
297 |   ```
298 | - **`screenshot`**: Takes a screenshot.
299 |   ```bash
300 |   npx reloaderoo@latest inspect call-tool screenshot --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest
301 |   ```
302 | - **`swipe`**: Performs a swipe gesture.
303 |   ```bash
304 |   npx reloaderoo@latest inspect call-tool swipe --params '{"simulatorUuid": "SIMULATOR_UUID", "x1": 100, "y1": 200, "x2": 100, "y2": 400}' -q -- npx xcodebuildmcp@latest
305 |   ```
306 | - **`tap`**: Performs a tap at coordinates.
307 |   ```bash
308 |   npx reloaderoo@latest inspect call-tool tap --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200}' -q -- npx xcodebuildmcp@latest
309 |   ```
310 | - **`touch`**: Simulates a touch down or up event.
311 |   ```bash
312 |   npx reloaderoo@latest inspect call-tool touch --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "down": true}' -q -- npx xcodebuildmcp@latest
313 |   ```
314 | - **`type_text`**: Types text into the focused element.
315 |   ```bash
316 |   npx reloaderoo@latest inspect call-tool type_text --params '{"simulatorUuid": "SIMULATOR_UUID", "text": "Hello, World!"}' -q -- npx xcodebuildmcp@latest
317 |   ```
318 | 
319 | ## Resources
320 | 
321 | - **Read devices resource**:
322 |   ```bash
323 |   npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -q -- npx xcodebuildmcp@latest
324 |   ```
325 | - **Read simulators resource**:
326 |   ```bash
327 |   npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -q -- npx xcodebuildmcp@latest
328 |   ```
329 | - **Read doctor resource**:
330 |   ```bash
331 |   npx reloaderoo@latest inspect read-resource "xcodebuildmcp://doctor" -q -- npx xcodebuildmcp@latest
332 |   ```
333 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/device/list_devices.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Device Workspace Plugin: List Devices
  3 |  *
  4 |  * Lists connected physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro)
  5 |  * with their UUIDs, names, and connection status. Use this to discover physical devices for testing.
  6 |  */
  7 | 
  8 | import { z } from 'zod';
  9 | import type { ToolResponse } from '../../../types/common.ts';
 10 | import { log } from '../../../utils/logging/index.ts';
 11 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
 12 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
 13 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
 14 | import { promises as fs } from 'fs';
 15 | import { tmpdir } from 'os';
 16 | import { join } from 'path';
 17 | 
 18 | // Define schema as ZodObject (empty schema since this tool takes no parameters)
 19 | const listDevicesSchema = z.object({});
 20 | 
 21 | // Use z.infer for type safety
 22 | type ListDevicesParams = z.infer<typeof listDevicesSchema>;
 23 | 
 24 | /**
 25 |  * Business logic for listing connected devices
 26 |  */
 27 | export async function list_devicesLogic(
 28 |   params: ListDevicesParams,
 29 |   executor: CommandExecutor,
 30 |   pathDeps?: { tmpdir?: () => string; join?: (...paths: string[]) => string },
 31 |   fsDeps?: {
 32 |     readFile?: (path: string, encoding?: string) => Promise<string>;
 33 |     unlink?: (path: string) => Promise<void>;
 34 |   },
 35 | ): Promise<ToolResponse> {
 36 |   log('info', 'Starting device discovery');
 37 | 
 38 |   try {
 39 |     // Try modern devicectl with JSON output first (iOS 17+, Xcode 15+)
 40 |     const tempDir = pathDeps?.tmpdir ? pathDeps.tmpdir() : tmpdir();
 41 |     const timestamp = pathDeps?.join ? '123' : Date.now(); // Use fixed timestamp for tests
 42 |     const tempJsonPath = pathDeps?.join
 43 |       ? pathDeps.join(tempDir, `devicectl-${timestamp}.json`)
 44 |       : join(tempDir, `devicectl-${timestamp}.json`);
 45 |     const devices = [];
 46 |     let useDevicectl = false;
 47 | 
 48 |     try {
 49 |       const result = await executor(
 50 |         ['xcrun', 'devicectl', 'list', 'devices', '--json-output', tempJsonPath],
 51 |         'List Devices (devicectl with JSON)',
 52 |         true,
 53 |         undefined,
 54 |       );
 55 | 
 56 |       if (result.success) {
 57 |         useDevicectl = true;
 58 |         // Read and parse the JSON file
 59 |         const jsonContent = fsDeps?.readFile
 60 |           ? await fsDeps.readFile(tempJsonPath, 'utf8')
 61 |           : await fs.readFile(tempJsonPath, 'utf8');
 62 |         const deviceCtlData: unknown = JSON.parse(jsonContent);
 63 | 
 64 |         // Type guard to validate the device data structure
 65 |         const isValidDeviceData = (data: unknown): data is { result?: { devices?: unknown[] } } => {
 66 |           return (
 67 |             typeof data === 'object' &&
 68 |             data !== null &&
 69 |             'result' in data &&
 70 |             typeof (data as { result?: unknown }).result === 'object' &&
 71 |             (data as { result?: unknown }).result !== null &&
 72 |             'devices' in ((data as { result?: unknown }).result as { devices?: unknown }) &&
 73 |             Array.isArray(
 74 |               ((data as { result?: unknown }).result as { devices?: unknown[] }).devices,
 75 |             )
 76 |           );
 77 |         };
 78 | 
 79 |         if (isValidDeviceData(deviceCtlData) && deviceCtlData.result?.devices) {
 80 |           for (const deviceRaw of deviceCtlData.result.devices) {
 81 |             // Type guard for device object
 82 |             const isValidDevice = (
 83 |               device: unknown,
 84 |             ): device is {
 85 |               visibilityClass?: string;
 86 |               connectionProperties?: {
 87 |                 pairingState?: string;
 88 |                 tunnelState?: string;
 89 |                 transportType?: string;
 90 |               };
 91 |               deviceProperties?: {
 92 |                 platformIdentifier?: string;
 93 |                 name?: string;
 94 |                 osVersionNumber?: string;
 95 |                 developerModeStatus?: string;
 96 |                 marketingName?: string;
 97 |               };
 98 |               hardwareProperties?: {
 99 |                 productType?: string;
100 |                 cpuType?: { name?: string };
101 |               };
102 |               identifier?: string;
103 |             } => {
104 |               if (typeof device !== 'object' || device === null) {
105 |                 return false;
106 |               }
107 | 
108 |               const dev = device as Record<string, unknown>;
109 | 
110 |               // Check if identifier exists and is a string (most critical property)
111 |               if (typeof dev.identifier !== 'string' && dev.identifier !== undefined) {
112 |                 return false;
113 |               }
114 | 
115 |               // Check visibilityClass if present
116 |               if (dev.visibilityClass !== undefined && typeof dev.visibilityClass !== 'string') {
117 |                 return false;
118 |               }
119 | 
120 |               // Check connectionProperties structure if present
121 |               if (dev.connectionProperties !== undefined) {
122 |                 if (
123 |                   typeof dev.connectionProperties !== 'object' ||
124 |                   dev.connectionProperties === null
125 |                 ) {
126 |                   return false;
127 |                 }
128 |                 const connProps = dev.connectionProperties as Record<string, unknown>;
129 |                 if (
130 |                   connProps.pairingState !== undefined &&
131 |                   typeof connProps.pairingState !== 'string'
132 |                 ) {
133 |                   return false;
134 |                 }
135 |                 if (
136 |                   connProps.tunnelState !== undefined &&
137 |                   typeof connProps.tunnelState !== 'string'
138 |                 ) {
139 |                   return false;
140 |                 }
141 |                 if (
142 |                   connProps.transportType !== undefined &&
143 |                   typeof connProps.transportType !== 'string'
144 |                 ) {
145 |                   return false;
146 |                 }
147 |               }
148 | 
149 |               // Check deviceProperties structure if present
150 |               if (dev.deviceProperties !== undefined) {
151 |                 if (typeof dev.deviceProperties !== 'object' || dev.deviceProperties === null) {
152 |                   return false;
153 |                 }
154 |                 const devProps = dev.deviceProperties as Record<string, unknown>;
155 |                 if (
156 |                   devProps.platformIdentifier !== undefined &&
157 |                   typeof devProps.platformIdentifier !== 'string'
158 |                 ) {
159 |                   return false;
160 |                 }
161 |                 if (devProps.name !== undefined && typeof devProps.name !== 'string') {
162 |                   return false;
163 |                 }
164 |                 if (
165 |                   devProps.osVersionNumber !== undefined &&
166 |                   typeof devProps.osVersionNumber !== 'string'
167 |                 ) {
168 |                   return false;
169 |                 }
170 |                 if (
171 |                   devProps.developerModeStatus !== undefined &&
172 |                   typeof devProps.developerModeStatus !== 'string'
173 |                 ) {
174 |                   return false;
175 |                 }
176 |                 if (
177 |                   devProps.marketingName !== undefined &&
178 |                   typeof devProps.marketingName !== 'string'
179 |                 ) {
180 |                   return false;
181 |                 }
182 |               }
183 | 
184 |               // Check hardwareProperties structure if present
185 |               if (dev.hardwareProperties !== undefined) {
186 |                 if (typeof dev.hardwareProperties !== 'object' || dev.hardwareProperties === null) {
187 |                   return false;
188 |                 }
189 |                 const hwProps = dev.hardwareProperties as Record<string, unknown>;
190 |                 if (hwProps.productType !== undefined && typeof hwProps.productType !== 'string') {
191 |                   return false;
192 |                 }
193 |                 if (hwProps.cpuType !== undefined) {
194 |                   if (typeof hwProps.cpuType !== 'object' || hwProps.cpuType === null) {
195 |                     return false;
196 |                   }
197 |                   const cpuType = hwProps.cpuType as Record<string, unknown>;
198 |                   if (cpuType.name !== undefined && typeof cpuType.name !== 'string') {
199 |                     return false;
200 |                   }
201 |                 }
202 |               }
203 | 
204 |               return true;
205 |             };
206 | 
207 |             if (!isValidDevice(deviceRaw)) continue;
208 | 
209 |             const device = deviceRaw;
210 | 
211 |             // Skip simulators or unavailable devices
212 |             if (
213 |               device.visibilityClass === 'Simulator' ||
214 |               !device.connectionProperties?.pairingState
215 |             ) {
216 |               continue;
217 |             }
218 | 
219 |             // Determine platform from platformIdentifier
220 |             let platform = 'Unknown';
221 |             const platformId = device.deviceProperties?.platformIdentifier?.toLowerCase() ?? '';
222 |             if (typeof platformId === 'string') {
223 |               if (platformId.includes('ios') || platformId.includes('iphone')) {
224 |                 platform = 'iOS';
225 |               } else if (platformId.includes('ipad')) {
226 |                 platform = 'iPadOS';
227 |               } else if (platformId.includes('watch')) {
228 |                 platform = 'watchOS';
229 |               } else if (platformId.includes('tv') || platformId.includes('apple tv')) {
230 |                 platform = 'tvOS';
231 |               } else if (platformId.includes('vision')) {
232 |                 platform = 'visionOS';
233 |               }
234 |             }
235 | 
236 |             // Determine connection state
237 |             const pairingState = device.connectionProperties?.pairingState ?? '';
238 |             const tunnelState = device.connectionProperties?.tunnelState ?? '';
239 |             const transportType = device.connectionProperties?.transportType ?? '';
240 | 
241 |             let state = 'Unknown';
242 |             // Consider a device available if it's paired, regardless of tunnel state
243 |             // This allows WiFi-connected devices to be used even if tunnelState isn't "connected"
244 |             if (pairingState === 'paired') {
245 |               if (tunnelState === 'connected') {
246 |                 state = 'Available';
247 |               } else {
248 |                 // Device is paired but tunnel state may be different for WiFi connections
249 |                 // Still mark as available since devicectl commands can work with paired devices
250 |                 state = 'Available (WiFi)';
251 |               }
252 |             } else {
253 |               state = 'Unpaired';
254 |             }
255 | 
256 |             devices.push({
257 |               name: device.deviceProperties?.name ?? 'Unknown Device',
258 |               identifier: device.identifier ?? 'Unknown',
259 |               platform: platform,
260 |               model:
261 |                 device.deviceProperties?.marketingName ?? device.hardwareProperties?.productType,
262 |               osVersion: device.deviceProperties?.osVersionNumber,
263 |               state: state,
264 |               connectionType: transportType,
265 |               trustState: pairingState,
266 |               developerModeStatus: device.deviceProperties?.developerModeStatus,
267 |               productType: device.hardwareProperties?.productType,
268 |               cpuArchitecture: device.hardwareProperties?.cpuType?.name,
269 |             });
270 |           }
271 |         }
272 |       }
273 |     } catch {
274 |       log('info', 'devicectl with JSON failed, trying xctrace fallback');
275 |     } finally {
276 |       // Clean up temp file
277 |       try {
278 |         if (fsDeps?.unlink) {
279 |           await fsDeps.unlink(tempJsonPath);
280 |         } else {
281 |           await fs.unlink(tempJsonPath);
282 |         }
283 |       } catch {
284 |         // Ignore cleanup errors
285 |       }
286 |     }
287 | 
288 |     // If devicectl failed or returned no devices, fallback to xctrace
289 |     if (!useDevicectl || devices.length === 0) {
290 |       const result = await executor(
291 |         ['xcrun', 'xctrace', 'list', 'devices'],
292 |         'List Devices (xctrace)',
293 |         true,
294 |         undefined,
295 |       );
296 | 
297 |       if (!result.success) {
298 |         return {
299 |           content: [
300 |             {
301 |               type: 'text',
302 |               text: `Failed to list devices: ${result.error}\n\nMake sure Xcode is installed and devices are connected and trusted.`,
303 |             },
304 |           ],
305 |           isError: true,
306 |         };
307 |       }
308 | 
309 |       // Return raw xctrace output without parsing
310 |       return {
311 |         content: [
312 |           {
313 |             type: 'text',
314 |             text: `Device listing (xctrace output):\n\n${result.output}\n\nNote: For better device information, please upgrade to Xcode 15 or later which supports the modern devicectl command.`,
315 |           },
316 |         ],
317 |       };
318 |     }
319 | 
320 |     // Format the response
321 |     let responseText = 'Connected Devices:\n\n';
322 | 
323 |     // Filter out duplicates
324 |     const uniqueDevices = devices.filter(
325 |       (device, index, self) => index === self.findIndex((d) => d.identifier === device.identifier),
326 |     );
327 | 
328 |     if (uniqueDevices.length === 0) {
329 |       responseText += 'No physical Apple devices found.\n\n';
330 |       responseText += 'Make sure:\n';
331 |       responseText += '1. Devices are connected via USB or WiFi\n';
332 |       responseText += '2. Devices are unlocked and trusted\n';
333 |       responseText += '3. "Trust this computer" has been accepted on the device\n';
334 |       responseText += '4. Developer mode is enabled on the device (iOS 16+)\n';
335 |       responseText += '5. Xcode is properly installed\n\n';
336 |       responseText += 'For simulators, use the list_sims tool instead.\n';
337 |     } else {
338 |       // Group devices by availability status
339 |       const availableDevices = uniqueDevices.filter(
340 |         (d) => d.state === 'Available' || d.state === 'Available (WiFi)' || d.state === 'Connected',
341 |       );
342 |       const pairedDevices = uniqueDevices.filter((d) => d.state === 'Paired (not connected)');
343 |       const unpairedDevices = uniqueDevices.filter((d) => d.state === 'Unpaired');
344 | 
345 |       if (availableDevices.length > 0) {
346 |         responseText += '✅ Available Devices:\n';
347 |         for (const device of availableDevices) {
348 |           responseText += `\n📱 ${device.name}\n`;
349 |           responseText += `   UDID: ${device.identifier}\n`;
350 |           responseText += `   Model: ${device.model ?? 'Unknown'}\n`;
351 |           if (device.productType) {
352 |             responseText += `   Product Type: ${device.productType}\n`;
353 |           }
354 |           responseText += `   Platform: ${device.platform} ${device.osVersion ?? ''}\n`;
355 |           if (device.cpuArchitecture) {
356 |             responseText += `   CPU Architecture: ${device.cpuArchitecture}\n`;
357 |           }
358 |           responseText += `   Connection: ${device.connectionType ?? 'Unknown'}\n`;
359 |           if (device.developerModeStatus) {
360 |             responseText += `   Developer Mode: ${device.developerModeStatus}\n`;
361 |           }
362 |         }
363 |         responseText += '\n';
364 |       }
365 | 
366 |       if (pairedDevices.length > 0) {
367 |         responseText += '🔗 Paired but Not Connected:\n';
368 |         for (const device of pairedDevices) {
369 |           responseText += `\n📱 ${device.name}\n`;
370 |           responseText += `   UDID: ${device.identifier}\n`;
371 |           responseText += `   Model: ${device.model ?? 'Unknown'}\n`;
372 |           responseText += `   Platform: ${device.platform} ${device.osVersion ?? ''}\n`;
373 |         }
374 |         responseText += '\n';
375 |       }
376 | 
377 |       if (unpairedDevices.length > 0) {
378 |         responseText += '❌ Unpaired Devices:\n';
379 |         for (const device of unpairedDevices) {
380 |           responseText += `- ${device.name} (${device.identifier})\n`;
381 |         }
382 |         responseText += '\n';
383 |       }
384 |     }
385 | 
386 |     // Add next steps
387 |     const availableDevicesExist = uniqueDevices.some(
388 |       (d) => d.state === 'Available' || d.state === 'Available (WiFi)' || d.state === 'Connected',
389 |     );
390 | 
391 |     if (availableDevicesExist) {
392 |       responseText += 'Next Steps:\n';
393 |       responseText +=
394 |         "1. Build for device: build_device({ scheme: 'SCHEME', deviceId: 'DEVICE_UDID' })\n";
395 |       responseText += "2. Run tests: test_device({ scheme: 'SCHEME', deviceId: 'DEVICE_UDID' })\n";
396 |       responseText += "3. Get app path: get_device_app_path({ scheme: 'SCHEME' })\n\n";
397 |       responseText += 'Note: Use the device ID/UDID from above when required by other tools.\n';
398 |     } else if (uniqueDevices.length > 0) {
399 |       responseText +=
400 |         'Note: No devices are currently available for testing. Make sure devices are:\n';
401 |       responseText += '- Connected via USB\n';
402 |       responseText += '- Unlocked and trusted\n';
403 |       responseText += '- Have developer mode enabled (iOS 16+)\n';
404 |     }
405 | 
406 |     return {
407 |       content: [
408 |         {
409 |           type: 'text',
410 |           text: responseText,
411 |         },
412 |       ],
413 |     };
414 |   } catch (error) {
415 |     const errorMessage = error instanceof Error ? error.message : String(error);
416 |     log('error', `Error listing devices: ${errorMessage}`);
417 |     return {
418 |       content: [
419 |         {
420 |           type: 'text',
421 |           text: `Failed to list devices: ${errorMessage}`,
422 |         },
423 |       ],
424 |       isError: true,
425 |     };
426 |   }
427 | }
428 | 
429 | export default {
430 |   name: 'list_devices',
431 |   description:
432 |     'Lists connected physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) with their UUIDs, names, and connection status. Use this to discover physical devices for testing.',
433 |   schema: listDevicesSchema.shape, // MCP SDK compatibility
434 |   handler: createTypedTool(listDevicesSchema, list_devicesLogic, getDefaultCommandExecutor),
435 | };
436 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for start_device_log_cap plugin
  3 |  * Following CLAUDE.md testing standards with pure dependency injection
  4 |  */
  5 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  6 | import { EventEmitter } from 'events';
  7 | import type { ChildProcess } from 'child_process';
  8 | import { z } from 'zod';
  9 | import {
 10 |   createMockExecutor,
 11 |   createMockFileSystemExecutor,
 12 | } from '../../../../test-utils/mock-executors.ts';
 13 | import plugin, {
 14 |   start_device_log_capLogic,
 15 |   activeDeviceLogSessions,
 16 | } from '../start_device_log_cap.ts';
 17 | import { sessionStore } from '../../../../utils/session-store.ts';
 18 | 
 19 | describe('start_device_log_cap plugin', () => {
 20 |   // Mock state tracking
 21 |   let commandCalls: Array<{
 22 |     command: string[];
 23 |     logPrefix?: string;
 24 |     useShell?: boolean;
 25 |     env?: Record<string, string>;
 26 |   }> = [];
 27 |   let mkdirCalls: string[] = [];
 28 |   let writeFileCalls: Array<{ path: string; content: string }> = [];
 29 | 
 30 |   // Reset state
 31 |   commandCalls = [];
 32 |   mkdirCalls = [];
 33 |   writeFileCalls = [];
 34 | 
 35 |   const originalJsonWaitEnv = process.env.XBMCP_LAUNCH_JSON_WAIT_MS;
 36 | 
 37 |   beforeEach(() => {
 38 |     sessionStore.clear();
 39 |     activeDeviceLogSessions.clear();
 40 |     process.env.XBMCP_LAUNCH_JSON_WAIT_MS = '25';
 41 |   });
 42 | 
 43 |   afterEach(() => {
 44 |     if (originalJsonWaitEnv === undefined) {
 45 |       delete process.env.XBMCP_LAUNCH_JSON_WAIT_MS;
 46 |     } else {
 47 |       process.env.XBMCP_LAUNCH_JSON_WAIT_MS = originalJsonWaitEnv;
 48 |     }
 49 |   });
 50 | 
 51 |   describe('Plugin Structure', () => {
 52 |     it('should export an object with required properties', () => {
 53 |       expect(plugin).toHaveProperty('name');
 54 |       expect(plugin).toHaveProperty('description');
 55 |       expect(plugin).toHaveProperty('schema');
 56 |       expect(plugin).toHaveProperty('handler');
 57 |     });
 58 | 
 59 |     it('should have correct tool name', () => {
 60 |       expect(plugin.name).toBe('start_device_log_cap');
 61 |     });
 62 | 
 63 |     it('should have correct description', () => {
 64 |       expect(plugin.description).toBe('Starts log capture on a connected device.');
 65 |     });
 66 | 
 67 |     it('should have correct schema structure', () => {
 68 |       // Schema should be a plain object for MCP protocol compliance
 69 |       expect(typeof plugin.schema).toBe('object');
 70 |       expect(Object.keys(plugin.schema)).toEqual(['bundleId']);
 71 | 
 72 |       // Validate that schema fields are Zod types that can be used for validation
 73 |       const schema = z.object(plugin.schema).strict();
 74 |       expect(schema.safeParse({ bundleId: 'com.test.app' }).success).toBe(true);
 75 |       expect(schema.safeParse({}).success).toBe(false);
 76 |     });
 77 | 
 78 |     it('should have handler as a function', () => {
 79 |       expect(typeof plugin.handler).toBe('function');
 80 |     });
 81 |   });
 82 | 
 83 |   describe('Handler Requirements', () => {
 84 |     it('should require deviceId when not provided', async () => {
 85 |       const result = await plugin.handler({ bundleId: 'com.example.MyApp' });
 86 | 
 87 |       expect(result.isError).toBe(true);
 88 |       expect(result.content[0].text).toContain('deviceId is required');
 89 |     });
 90 |   });
 91 | 
 92 |   describe('Handler Functionality', () => {
 93 |     it('should start log capture successfully', async () => {
 94 |       // Mock successful command execution
 95 |       const mockExecutor = createMockExecutor({
 96 |         success: true,
 97 |         output: 'App launched successfully',
 98 |       });
 99 | 
100 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
101 |         mkdir: async (path: string) => {
102 |           mkdirCalls.push(path);
103 |         },
104 |         writeFile: async (path: string, content: string) => {
105 |           writeFileCalls.push({ path, content });
106 |         },
107 |       });
108 | 
109 |       const result = await start_device_log_capLogic(
110 |         {
111 |           deviceId: '00008110-001A2C3D4E5F',
112 |           bundleId: 'com.example.MyApp',
113 |         },
114 |         mockExecutor,
115 |         mockFileSystemExecutor,
116 |       );
117 | 
118 |       expect(result.content[0].text).toMatch(/✅ Device log capture started successfully/);
119 |       expect(result.content[0].text).toMatch(/Session ID: [a-f0-9-]{36}/);
120 |       expect(result.isError ?? false).toBe(false);
121 |     });
122 | 
123 |     it('should include next steps in success response', async () => {
124 |       // Mock successful command execution
125 |       const mockExecutor = createMockExecutor({
126 |         success: true,
127 |         output: 'App launched successfully',
128 |       });
129 | 
130 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
131 |         mkdir: async (path: string) => {
132 |           mkdirCalls.push(path);
133 |         },
134 |         writeFile: async (path: string, content: string) => {
135 |           writeFileCalls.push({ path, content });
136 |         },
137 |       });
138 | 
139 |       const result = await start_device_log_capLogic(
140 |         {
141 |           deviceId: '00008110-001A2C3D4E5F',
142 |           bundleId: 'com.example.MyApp',
143 |         },
144 |         mockExecutor,
145 |         mockFileSystemExecutor,
146 |       );
147 | 
148 |       expect(result.content[0].text).toContain('Next Steps:');
149 |       expect(result.content[0].text).toContain('Use stop_device_log_cap');
150 |     });
151 | 
152 |     it('should surface early launch failures when process exits immediately', async () => {
153 |       const failingProcess = new EventEmitter() as unknown as ChildProcess & {
154 |         exitCode: number | null;
155 |         killed: boolean;
156 |         kill(signal?: string): boolean;
157 |         stdout: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
158 |         stderr: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
159 |       };
160 | 
161 |       const stubOutput = new EventEmitter() as NodeJS.ReadableStream & {
162 |         setEncoding?: (encoding: string) => void;
163 |       };
164 |       stubOutput.setEncoding = () => {};
165 |       const stubError = new EventEmitter() as NodeJS.ReadableStream & {
166 |         setEncoding?: (encoding: string) => void;
167 |       };
168 |       stubError.setEncoding = () => {};
169 | 
170 |       failingProcess.stdout = stubOutput;
171 |       failingProcess.stderr = stubError;
172 |       failingProcess.exitCode = null;
173 |       failingProcess.killed = false;
174 |       failingProcess.kill = () => {
175 |         failingProcess.killed = true;
176 |         failingProcess.exitCode = 0;
177 |         failingProcess.emit('close', 0, null);
178 |         return true;
179 |       };
180 | 
181 |       const mockExecutor = createMockExecutor({
182 |         success: true,
183 |         output: '',
184 |         process: failingProcess,
185 |       });
186 | 
187 |       let createdLogPath = '';
188 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
189 |         mkdir: async () => {},
190 |         writeFile: async (path: string, content: string) => {
191 |           createdLogPath = path;
192 |           writeFileCalls.push({ path, content });
193 |         },
194 |       });
195 | 
196 |       const resultPromise = start_device_log_capLogic(
197 |         {
198 |           deviceId: '00008110-001A2C3D4E5F',
199 |           bundleId: 'com.invalid.App',
200 |         },
201 |         mockExecutor,
202 |         mockFileSystemExecutor,
203 |       );
204 | 
205 |       setTimeout(() => {
206 |         stubError.emit(
207 |           'data',
208 |           'ERROR: The application failed to launch. (com.apple.dt.CoreDeviceError error 10002)\nNSLocalizedRecoverySuggestion = Provide a valid bundle identifier.\n',
209 |         );
210 |         failingProcess.exitCode = 70;
211 |         failingProcess.emit('close', 70, null);
212 |       }, 10);
213 | 
214 |       const result = await resultPromise;
215 | 
216 |       expect(result.isError).toBe(true);
217 |       expect(result.content[0].text).toContain('Provide a valid bundle identifier');
218 |       expect(activeDeviceLogSessions.size).toBe(0);
219 |       expect(createdLogPath).not.toBe('');
220 |     });
221 | 
222 |     it('should surface JSON-reported failures when launch cannot start', async () => {
223 |       const jsonFailure = {
224 |         error: {
225 |           domain: 'com.apple.dt.CoreDeviceError',
226 |           code: 10002,
227 |           localizedDescription: 'The application failed to launch.',
228 |           userInfo: {
229 |             NSLocalizedRecoverySuggestion: 'Provide a valid bundle identifier.',
230 |             NSLocalizedFailureReason: 'The requested application com.invalid.App is not installed.',
231 |             BundleIdentifier: 'com.invalid.App',
232 |           },
233 |         },
234 |       };
235 | 
236 |       const failingProcess = new EventEmitter() as unknown as ChildProcess & {
237 |         exitCode: number | null;
238 |         killed: boolean;
239 |         kill(signal?: string): boolean;
240 |         stdout: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
241 |         stderr: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
242 |       };
243 | 
244 |       const stubOutput = new EventEmitter() as NodeJS.ReadableStream & {
245 |         setEncoding?: (encoding: string) => void;
246 |       };
247 |       stubOutput.setEncoding = () => {};
248 |       const stubError = new EventEmitter() as NodeJS.ReadableStream & {
249 |         setEncoding?: (encoding: string) => void;
250 |       };
251 |       stubError.setEncoding = () => {};
252 | 
253 |       failingProcess.stdout = stubOutput;
254 |       failingProcess.stderr = stubError;
255 |       failingProcess.exitCode = null;
256 |       failingProcess.killed = false;
257 |       failingProcess.kill = () => {
258 |         failingProcess.killed = true;
259 |         return true;
260 |       };
261 | 
262 |       const mockExecutor = createMockExecutor({
263 |         success: true,
264 |         output: '',
265 |         process: failingProcess,
266 |       });
267 | 
268 |       let jsonPathSeen = '';
269 |       let removedJsonPath = '';
270 | 
271 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
272 |         mkdir: async () => {},
273 |         writeFile: async () => {},
274 |         existsSync: (filePath: string): boolean => {
275 |           if (filePath.includes('devicectl-launch-')) {
276 |             jsonPathSeen = filePath;
277 |             return true;
278 |           }
279 |           return false;
280 |         },
281 |         readFile: async (filePath: string): Promise<string> => {
282 |           if (filePath.includes('devicectl-launch-')) {
283 |             jsonPathSeen = filePath;
284 |             return JSON.stringify(jsonFailure);
285 |           }
286 |           return '';
287 |         },
288 |         rm: async (filePath: string) => {
289 |           if (filePath.includes('devicectl-launch-')) {
290 |             removedJsonPath = filePath;
291 |           }
292 |         },
293 |       });
294 | 
295 |       setTimeout(() => {
296 |         failingProcess.exitCode = 0;
297 |         failingProcess.emit('close', 0, null);
298 |       }, 5);
299 | 
300 |       const result = await start_device_log_capLogic(
301 |         {
302 |           deviceId: '00008110-001A2C3D4E5F',
303 |           bundleId: 'com.invalid.App',
304 |         },
305 |         mockExecutor,
306 |         mockFileSystemExecutor,
307 |       );
308 | 
309 |       expect(result.isError).toBe(true);
310 |       expect(result.content[0].text).toContain('Provide a valid bundle identifier');
311 |       expect(jsonPathSeen).not.toBe('');
312 |       expect(removedJsonPath).toBe(jsonPathSeen);
313 |       expect(activeDeviceLogSessions.size).toBe(0);
314 |       expect(failingProcess.killed).toBe(true);
315 |     });
316 | 
317 |     it('should treat JSON success payload as confirmation of launch', async () => {
318 |       const jsonSuccess = {
319 |         result: {
320 |           process: {
321 |             processIdentifier: 4321,
322 |           },
323 |         },
324 |       };
325 | 
326 |       const runningProcess = new EventEmitter() as unknown as ChildProcess & {
327 |         exitCode: number | null;
328 |         killed: boolean;
329 |         kill(signal?: string): boolean;
330 |         stdout: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
331 |         stderr: NodeJS.ReadableStream & { setEncoding?: (encoding: string) => void };
332 |       };
333 | 
334 |       const stubOutput = new EventEmitter() as NodeJS.ReadableStream & {
335 |         setEncoding?: (encoding: string) => void;
336 |       };
337 |       stubOutput.setEncoding = () => {};
338 |       const stubError = new EventEmitter() as NodeJS.ReadableStream & {
339 |         setEncoding?: (encoding: string) => void;
340 |       };
341 |       stubError.setEncoding = () => {};
342 | 
343 |       runningProcess.stdout = stubOutput;
344 |       runningProcess.stderr = stubError;
345 |       runningProcess.exitCode = null;
346 |       runningProcess.killed = false;
347 |       runningProcess.kill = () => {
348 |         runningProcess.killed = true;
349 |         runningProcess.emit('close', 0, null);
350 |         return true;
351 |       };
352 | 
353 |       const mockExecutor = createMockExecutor({
354 |         success: true,
355 |         output: '',
356 |         process: runningProcess,
357 |       });
358 | 
359 |       let jsonPathSeen = '';
360 |       let removedJsonPath = '';
361 |       let jsonRemoved = false;
362 | 
363 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
364 |         mkdir: async () => {},
365 |         writeFile: async () => {},
366 |         existsSync: (filePath: string): boolean => {
367 |           if (filePath.includes('devicectl-launch-')) {
368 |             jsonPathSeen = filePath;
369 |             return !jsonRemoved;
370 |           }
371 |           return false;
372 |         },
373 |         readFile: async (filePath: string): Promise<string> => {
374 |           if (filePath.includes('devicectl-launch-')) {
375 |             jsonPathSeen = filePath;
376 |             return JSON.stringify(jsonSuccess);
377 |           }
378 |           return '';
379 |         },
380 |         rm: async (filePath: string) => {
381 |           if (filePath.includes('devicectl-launch-')) {
382 |             jsonRemoved = true;
383 |             removedJsonPath = filePath;
384 |           }
385 |         },
386 |       });
387 | 
388 |       setTimeout(() => {
389 |         runningProcess.emit('close', 0, null);
390 |       }, 5);
391 | 
392 |       const result = await start_device_log_capLogic(
393 |         {
394 |           deviceId: '00008110-001A2C3D4E5F',
395 |           bundleId: 'com.example.MyApp',
396 |         },
397 |         mockExecutor,
398 |         mockFileSystemExecutor,
399 |       );
400 | 
401 |       expect(result.content[0].text).toContain('Device log capture started successfully');
402 |       expect(result.isError ?? false).toBe(false);
403 |       expect(jsonPathSeen).not.toBe('');
404 |       expect(removedJsonPath).toBe(jsonPathSeen);
405 |       expect(activeDeviceLogSessions.size).toBe(1);
406 |     });
407 | 
408 |     it('should handle directory creation failure', async () => {
409 |       // Mock mkdir to fail
410 |       const mockExecutor = createMockExecutor({
411 |         success: false,
412 |         output: '',
413 |         error: 'Command failed',
414 |       });
415 | 
416 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
417 |         mkdir: async (path: string) => {
418 |           mkdirCalls.push(path);
419 |           throw new Error('Permission denied');
420 |         },
421 |       });
422 | 
423 |       const result = await start_device_log_capLogic(
424 |         {
425 |           deviceId: '00008110-001A2C3D4E5F',
426 |           bundleId: 'com.example.MyApp',
427 |         },
428 |         mockExecutor,
429 |         mockFileSystemExecutor,
430 |       );
431 | 
432 |       expect(result).toEqual({
433 |         content: [
434 |           {
435 |             type: 'text',
436 |             text: 'Failed to start device log capture: Permission denied',
437 |           },
438 |         ],
439 |         isError: true,
440 |       });
441 |     });
442 | 
443 |     it('should handle file write failure', async () => {
444 |       // Mock writeFile to fail
445 |       const mockExecutor = createMockExecutor({
446 |         success: false,
447 |         output: '',
448 |         error: 'Command failed',
449 |       });
450 | 
451 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
452 |         mkdir: async (path: string) => {
453 |           mkdirCalls.push(path);
454 |         },
455 |         writeFile: async (path: string, content: string) => {
456 |           writeFileCalls.push({ path, content });
457 |           throw new Error('Disk full');
458 |         },
459 |       });
460 | 
461 |       const result = await start_device_log_capLogic(
462 |         {
463 |           deviceId: '00008110-001A2C3D4E5F',
464 |           bundleId: 'com.example.MyApp',
465 |         },
466 |         mockExecutor,
467 |         mockFileSystemExecutor,
468 |       );
469 | 
470 |       expect(result).toEqual({
471 |         content: [
472 |           {
473 |             type: 'text',
474 |             text: 'Failed to start device log capture: Disk full',
475 |           },
476 |         ],
477 |         isError: true,
478 |       });
479 |     });
480 | 
481 |     it('should handle spawn process error', async () => {
482 |       // Mock spawn to throw error
483 |       const mockExecutor = createMockExecutor(new Error('Command not found'));
484 | 
485 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
486 |         mkdir: async (path: string) => {
487 |           mkdirCalls.push(path);
488 |         },
489 |         writeFile: async (path: string, content: string) => {
490 |           writeFileCalls.push({ path, content });
491 |         },
492 |       });
493 | 
494 |       const result = await start_device_log_capLogic(
495 |         {
496 |           deviceId: '00008110-001A2C3D4E5F',
497 |           bundleId: 'com.example.MyApp',
498 |         },
499 |         mockExecutor,
500 |         mockFileSystemExecutor,
501 |       );
502 | 
503 |       expect(result).toEqual({
504 |         content: [
505 |           {
506 |             type: 'text',
507 |             text: 'Failed to start device log capture: Command not found',
508 |           },
509 |         ],
510 |         isError: true,
511 |       });
512 |     });
513 | 
514 |     it('should handle string error objects', async () => {
515 |       // Mock mkdir to fail with string error
516 |       const mockExecutor = createMockExecutor('String error message');
517 | 
518 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
519 |         mkdir: async (path: string) => {
520 |           mkdirCalls.push(path);
521 |         },
522 |         writeFile: async (path: string, content: string) => {
523 |           writeFileCalls.push({ path, content });
524 |         },
525 |       });
526 | 
527 |       const result = await start_device_log_capLogic(
528 |         {
529 |           deviceId: '00008110-001A2C3D4E5F',
530 |           bundleId: 'com.example.MyApp',
531 |         },
532 |         mockExecutor,
533 |         mockFileSystemExecutor,
534 |       );
535 | 
536 |       expect(result).toEqual({
537 |         content: [
538 |           {
539 |             type: 'text',
540 |             text: 'Failed to start device log capture: String error message',
541 |           },
542 |         ],
543 |         isError: true,
544 |       });
545 |     });
546 |   });
547 | });
548 | 
```
Page 11/14FirstPrevNextLast