#
tokens: 47069/50000 9/393 files (page 11/16)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 11 of 16. 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
│       ├── README.md
│       ├── release.yml
│       ├── sentry.yml
│       └── stale.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── .vscode
│   ├── extensions.json
│   ├── launch.json
│   ├── mcp.json
│   ├── settings.json
│   └── tasks.json
├── AGENTS.md
├── banner.png
├── build-plugins
│   ├── plugin-discovery.js
│   ├── plugin-discovery.ts
│   └── tsconfig.json
├── CHANGELOG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── docs
│   ├── CONFIGURATION.md
│   ├── DAP_BACKEND_IMPLEMENTATION_PLAN.md
│   ├── DEBUGGING_ARCHITECTURE.md
│   ├── DEMOS.md
│   ├── dev
│   │   ├── ARCHITECTURE.md
│   │   ├── CODE_QUALITY.md
│   │   ├── CONTRIBUTING.md
│   │   ├── ESLINT_TYPE_SAFETY.md
│   │   ├── MANUAL_TESTING.md
│   │   ├── NODEJS_2025.md
│   │   ├── PLUGIN_DEVELOPMENT.md
│   │   ├── README.md
│   │   ├── RELEASE_PROCESS.md
│   │   ├── RELOADEROO_FOR_XCODEBUILDMCP.md
│   │   ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md
│   │   ├── RELOADEROO.md
│   │   ├── session_management_plan.md
│   │   ├── session-aware-migration-todo.md
│   │   ├── SMITHERY.md
│   │   ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md
│   │   ├── TESTING.md
│   │   └── ZOD_MIGRATION_GUIDE.md
│   ├── DEVICE_CODE_SIGNING.md
│   ├── GETTING_STARTED.md
│   ├── investigations
│   │   ├── issue-154-screenshot-downscaling.md
│   │   ├── issue-163.md
│   │   ├── issue-debugger-attach-stopped.md
│   │   └── issue-describe-ui-empty-after-debugger-resume.md
│   ├── OVERVIEW.md
│   ├── PRIVACY.md
│   ├── README.md
│   ├── SESSION_DEFAULTS.md
│   ├── TOOLS.md
│   └── TROUBLESHOOTING.md
├── eslint.config.js
├── example_projects
│   ├── .vscode
│   │   └── launch.json
│   ├── iOS
│   │   ├── .cursor
│   │   │   └── rules
│   │   │       └── errors.mdc
│   │   ├── .vscode
│   │   │   └── settings.json
│   │   ├── Makefile
│   │   ├── MCPTest
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── MCPTestApp.swift
│   │   │   └── Preview Content
│   │   │       └── Preview Assets.xcassets
│   │   │           └── Contents.json
│   │   ├── MCPTest.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── xcshareddata
│   │   │       └── xcschemes
│   │   │           └── MCPTest.xcscheme
│   │   └── MCPTestUITests
│   │       └── MCPTestUITests.swift
│   ├── iOS_Calculator
│   │   ├── .gitignore
│   │   ├── CalculatorApp
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── CalculatorApp.swift
│   │   │   └── CalculatorApp.xctestplan
│   │   ├── CalculatorApp.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── xcshareddata
│   │   │       └── xcschemes
│   │   │           └── CalculatorApp.xcscheme
│   │   ├── CalculatorApp.xcworkspace
│   │   │   └── contents.xcworkspacedata
│   │   ├── CalculatorAppPackage
│   │   │   ├── .gitignore
│   │   │   ├── Package.swift
│   │   │   ├── Sources
│   │   │   │   └── CalculatorAppFeature
│   │   │   │       ├── BackgroundEffect.swift
│   │   │   │       ├── CalculatorButton.swift
│   │   │   │       ├── CalculatorDisplay.swift
│   │   │   │       ├── CalculatorInputHandler.swift
│   │   │   │       ├── CalculatorService.swift
│   │   │   │       └── ContentView.swift
│   │   │   └── Tests
│   │   │       └── CalculatorAppFeatureTests
│   │   │           └── CalculatorServiceTests.swift
│   │   ├── CalculatorAppTests
│   │   │   └── CalculatorAppTests.swift
│   │   └── Config
│   │       ├── Debug.xcconfig
│   │       ├── Release.xcconfig
│   │       ├── Shared.xcconfig
│   │       └── Tests.xcconfig
│   ├── macOS
│   │   ├── MCPTest
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── MCPTest.entitlements
│   │   │   ├── MCPTestApp.swift
│   │   │   └── Preview Content
│   │   │       └── Preview Assets.xcassets
│   │   │           └── Contents.json
│   │   ├── MCPTest.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── xcshareddata
│   │   │       └── xcschemes
│   │   │           └── MCPTest.xcscheme
│   │   └── MCPTestTests
│   │       └── MCPTestTests.swift
│   └── spm
│       ├── .gitignore
│       ├── Package.resolved
│       ├── Package.swift
│       ├── Sources
│       │   ├── long-server
│       │   │   └── main.swift
│       │   ├── quick-task
│       │   │   └── main.swift
│       │   ├── spm
│       │   │   └── main.swift
│       │   └── TestLib
│       │       └── TaskManager.swift
│       └── Tests
│           └── TestLibTests
│               └── SimpleTests.swift
├── LICENSE
├── mcp-install-dark.png
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── analysis
│   │   └── tools-analysis.ts
│   ├── bundle-axe.sh
│   ├── check-code-patterns.js
│   ├── generate-loaders.ts
│   ├── generate-version.ts
│   ├── release.sh
│   ├── tools-cli.ts
│   ├── update-tools-docs.ts
│   └── verify-smithery-bundle.sh
├── server.json
├── smithery.config.js
├── smithery.yaml
├── src
│   ├── core
│   │   ├── __tests__
│   │   │   └── resources.test.ts
│   │   ├── generated-plugins.ts
│   │   ├── generated-resources.ts
│   │   ├── plugin-registry.ts
│   │   ├── plugin-types.ts
│   │   └── resources.ts
│   ├── doctor-cli.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── resources
│   │   │   ├── __tests__
│   │   │   │   ├── devices.test.ts
│   │   │   │   ├── doctor.test.ts
│   │   │   │   ├── session-status.test.ts
│   │   │   │   └── simulators.test.ts
│   │   │   ├── devices.ts
│   │   │   ├── doctor.ts
│   │   │   ├── session-status.ts
│   │   │   └── simulators.ts
│   │   └── tools
│   │       ├── debugging
│   │       │   ├── debug_attach_sim.ts
│   │       │   ├── debug_breakpoint_add.ts
│   │       │   ├── debug_breakpoint_remove.ts
│   │       │   ├── debug_continue.ts
│   │       │   ├── debug_detach.ts
│   │       │   ├── debug_lldb_command.ts
│   │       │   ├── debug_stack.ts
│   │       │   ├── debug_variables.ts
│   │       │   └── index.ts
│   │       ├── device
│   │       │   ├── __tests__
│   │       │   │   ├── build_device.test.ts
│   │       │   │   ├── get_device_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── install_app_device.test.ts
│   │       │   │   ├── launch_app_device.test.ts
│   │       │   │   ├── list_devices.test.ts
│   │       │   │   ├── re-exports.test.ts
│   │       │   │   ├── stop_app_device.test.ts
│   │       │   │   └── test_device.test.ts
│   │       │   ├── build_device.ts
│   │       │   ├── clean.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_device_app_path.ts
│   │       │   ├── index.ts
│   │       │   ├── install_app_device.ts
│   │       │   ├── launch_app_device.ts
│   │       │   ├── list_devices.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── start_device_log_cap.ts
│   │       │   ├── stop_app_device.ts
│   │       │   ├── stop_device_log_cap.ts
│   │       │   └── test_device.ts
│   │       ├── doctor
│   │       │   ├── __tests__
│   │       │   │   ├── doctor.test.ts
│   │       │   │   └── index.test.ts
│   │       │   ├── doctor.ts
│   │       │   ├── index.ts
│   │       │   └── lib
│   │       │       └── doctor.deps.ts
│   │       ├── logging
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── start_device_log_cap.test.ts
│   │       │   │   ├── start_sim_log_cap.test.ts
│   │       │   │   ├── stop_device_log_cap.test.ts
│   │       │   │   └── stop_sim_log_cap.test.ts
│   │       │   ├── index.ts
│   │       │   ├── start_device_log_cap.ts
│   │       │   ├── start_sim_log_cap.ts
│   │       │   ├── stop_device_log_cap.ts
│   │       │   └── stop_sim_log_cap.ts
│   │       ├── macos
│   │       │   ├── __tests__
│   │       │   │   ├── build_macos.test.ts
│   │       │   │   ├── build_run_macos.test.ts
│   │       │   │   ├── get_mac_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── launch_mac_app.test.ts
│   │       │   │   ├── re-exports.test.ts
│   │       │   │   ├── stop_mac_app.test.ts
│   │       │   │   └── test_macos.test.ts
│   │       │   ├── build_macos.ts
│   │       │   ├── build_run_macos.ts
│   │       │   ├── clean.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_mac_app_path.ts
│   │       │   ├── get_mac_bundle_id.ts
│   │       │   ├── index.ts
│   │       │   ├── launch_mac_app.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── stop_mac_app.ts
│   │       │   └── test_macos.ts
│   │       ├── project-discovery
│   │       │   ├── __tests__
│   │       │   │   ├── discover_projs.test.ts
│   │       │   │   ├── get_app_bundle_id.test.ts
│   │       │   │   ├── get_mac_bundle_id.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── list_schemes.test.ts
│   │       │   │   └── show_build_settings.test.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_mac_bundle_id.ts
│   │       │   ├── index.ts
│   │       │   ├── list_schemes.ts
│   │       │   └── show_build_settings.ts
│   │       ├── project-scaffolding
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── scaffold_ios_project.test.ts
│   │       │   │   └── scaffold_macos_project.test.ts
│   │       │   ├── index.ts
│   │       │   ├── scaffold_ios_project.ts
│   │       │   └── scaffold_macos_project.ts
│   │       ├── session-management
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── session_clear_defaults.test.ts
│   │       │   │   ├── session_set_defaults.test.ts
│   │       │   │   └── session_show_defaults.test.ts
│   │       │   ├── index.ts
│   │       │   ├── session_clear_defaults.ts
│   │       │   ├── session_set_defaults.ts
│   │       │   └── session_show_defaults.ts
│   │       ├── simulator
│   │       │   ├── __tests__
│   │       │   │   ├── boot_sim.test.ts
│   │       │   │   ├── build_run_sim.test.ts
│   │       │   │   ├── build_sim.test.ts
│   │       │   │   ├── get_sim_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── install_app_sim.test.ts
│   │       │   │   ├── launch_app_logs_sim.test.ts
│   │       │   │   ├── launch_app_sim.test.ts
│   │       │   │   ├── list_sims.test.ts
│   │       │   │   ├── open_sim.test.ts
│   │       │   │   ├── record_sim_video.test.ts
│   │       │   │   ├── screenshot.test.ts
│   │       │   │   ├── stop_app_sim.test.ts
│   │       │   │   └── test_sim.test.ts
│   │       │   ├── boot_sim.ts
│   │       │   ├── build_run_sim.ts
│   │       │   ├── build_sim.ts
│   │       │   ├── clean.ts
│   │       │   ├── describe_ui.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_sim_app_path.ts
│   │       │   ├── index.ts
│   │       │   ├── install_app_sim.ts
│   │       │   ├── launch_app_logs_sim.ts
│   │       │   ├── launch_app_sim.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── list_sims.ts
│   │       │   ├── open_sim.ts
│   │       │   ├── record_sim_video.ts
│   │       │   ├── screenshot.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── stop_app_sim.ts
│   │       │   └── test_sim.ts
│   │       ├── simulator-management
│   │       │   ├── __tests__
│   │       │   │   ├── erase_sims.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── reset_sim_location.test.ts
│   │       │   │   ├── set_sim_appearance.test.ts
│   │       │   │   ├── set_sim_location.test.ts
│   │       │   │   └── sim_statusbar.test.ts
│   │       │   ├── boot_sim.ts
│   │       │   ├── erase_sims.ts
│   │       │   ├── index.ts
│   │       │   ├── list_sims.ts
│   │       │   ├── open_sim.ts
│   │       │   ├── reset_sim_location.ts
│   │       │   ├── set_sim_appearance.ts
│   │       │   ├── set_sim_location.ts
│   │       │   └── sim_statusbar.ts
│   │       ├── swift-package
│   │       │   ├── __tests__
│   │       │   │   ├── active-processes.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── swift_package_build.test.ts
│   │       │   │   ├── swift_package_clean.test.ts
│   │       │   │   ├── swift_package_list.test.ts
│   │       │   │   ├── swift_package_run.test.ts
│   │       │   │   ├── swift_package_stop.test.ts
│   │       │   │   └── swift_package_test.test.ts
│   │       │   ├── active-processes.ts
│   │       │   ├── index.ts
│   │       │   ├── swift_package_build.ts
│   │       │   ├── swift_package_clean.ts
│   │       │   ├── swift_package_list.ts
│   │       │   ├── swift_package_run.ts
│   │       │   ├── swift_package_stop.ts
│   │       │   └── swift_package_test.ts
│   │       ├── ui-testing
│   │       │   ├── __tests__
│   │       │   │   ├── button.test.ts
│   │       │   │   ├── describe_ui.test.ts
│   │       │   │   ├── gesture.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── key_press.test.ts
│   │       │   │   ├── key_sequence.test.ts
│   │       │   │   ├── long_press.test.ts
│   │       │   │   ├── screenshot.test.ts
│   │       │   │   ├── swipe.test.ts
│   │       │   │   ├── tap.test.ts
│   │       │   │   ├── touch.test.ts
│   │       │   │   └── type_text.test.ts
│   │       │   ├── button.ts
│   │       │   ├── describe_ui.ts
│   │       │   ├── gesture.ts
│   │       │   ├── index.ts
│   │       │   ├── key_press.ts
│   │       │   ├── key_sequence.ts
│   │       │   ├── long_press.ts
│   │       │   ├── screenshot.ts
│   │       │   ├── swipe.ts
│   │       │   ├── tap.ts
│   │       │   ├── touch.ts
│   │       │   └── type_text.ts
│   │       └── utilities
│   │           ├── __tests__
│   │           │   ├── clean.test.ts
│   │           │   └── index.test.ts
│   │           ├── clean.ts
│   │           └── index.ts
│   ├── server
│   │   ├── bootstrap.ts
│   │   └── server.ts
│   ├── smithery.ts
│   ├── test-utils
│   │   └── mock-executors.ts
│   ├── types
│   │   └── common.ts
│   ├── utils
│   │   ├── __tests__
│   │   │   ├── build-utils-suppress-warnings.test.ts
│   │   │   ├── build-utils.test.ts
│   │   │   ├── debugger-simctl.test.ts
│   │   │   ├── environment.test.ts
│   │   │   ├── session-aware-tool-factory.test.ts
│   │   │   ├── session-store.test.ts
│   │   │   ├── simulator-utils.test.ts
│   │   │   ├── test-runner-env-integration.test.ts
│   │   │   ├── typed-tool-factory.test.ts
│   │   │   └── workflow-selection.test.ts
│   │   ├── axe
│   │   │   └── index.ts
│   │   ├── axe-helpers.ts
│   │   ├── build
│   │   │   └── index.ts
│   │   ├── build-utils.ts
│   │   ├── capabilities.ts
│   │   ├── command.ts
│   │   ├── CommandExecutor.ts
│   │   ├── debugger
│   │   │   ├── __tests__
│   │   │   │   └── debugger-manager-dap.test.ts
│   │   │   ├── backends
│   │   │   │   ├── __tests__
│   │   │   │   │   └── dap-backend.test.ts
│   │   │   │   ├── dap-backend.ts
│   │   │   │   ├── DebuggerBackend.ts
│   │   │   │   └── lldb-cli-backend.ts
│   │   │   ├── dap
│   │   │   │   ├── __tests__
│   │   │   │   │   └── transport-framing.test.ts
│   │   │   │   ├── adapter-discovery.ts
│   │   │   │   ├── transport.ts
│   │   │   │   └── types.ts
│   │   │   ├── debugger-manager.ts
│   │   │   ├── index.ts
│   │   │   ├── simctl.ts
│   │   │   ├── tool-context.ts
│   │   │   ├── types.ts
│   │   │   └── ui-automation-guard.ts
│   │   ├── environment.ts
│   │   ├── errors.ts
│   │   ├── execution
│   │   │   ├── index.ts
│   │   │   └── interactive-process.ts
│   │   ├── FileSystemExecutor.ts
│   │   ├── log_capture.ts
│   │   ├── log-capture
│   │   │   ├── device-log-sessions.ts
│   │   │   └── index.ts
│   │   ├── logger.ts
│   │   ├── logging
│   │   │   └── index.ts
│   │   ├── plugin-registry
│   │   │   └── index.ts
│   │   ├── responses
│   │   │   └── index.ts
│   │   ├── runtime-registry.ts
│   │   ├── schema-helpers.ts
│   │   ├── sentry.ts
│   │   ├── session-status.ts
│   │   ├── session-store.ts
│   │   ├── simulator-utils.ts
│   │   ├── template
│   │   │   └── index.ts
│   │   ├── template-manager.ts
│   │   ├── test
│   │   │   └── index.ts
│   │   ├── test-common.ts
│   │   ├── tool-registry.ts
│   │   ├── typed-tool-factory.ts
│   │   ├── validation
│   │   │   └── index.ts
│   │   ├── validation.ts
│   │   ├── version
│   │   │   └── index.ts
│   │   ├── video_capture.ts
│   │   ├── video-capture
│   │   │   └── index.ts
│   │   ├── workflow-selection.ts
│   │   ├── xcode.ts
│   │   ├── xcodemake
│   │   │   └── index.ts
│   │   └── xcodemake.ts
│   └── version.ts
├── tsconfig.json
├── tsconfig.test.json
├── tsconfig.tests.json
├── tsup.config.ts
├── vitest.config.ts
└── XcodeBuildMCP.code-workspace
```

# Files

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

```swift
  1 | import Testing
  2 | import Foundation
  3 | @testable import CalculatorAppFeature
  4 | 
  5 | // MARK: - Calculator Basic Tests
  6 | @Suite("Calculator Basic Functionality")
  7 | struct CalculatorBasicTests {
  8 |     
  9 |     @Test("Calculator initializes with correct default values")
 10 |     func testInitialState() {
 11 |         let calculator = CalculatorService()
 12 |         #expect(calculator.display == "0")
 13 |         #expect(calculator.currentValue == 0)
 14 |         #expect(calculator.previousValue == 0)
 15 |         #expect(calculator.currentOperation == nil)
 16 |         #expect(calculator.willResetDisplay == false)
 17 |     }
 18 |     
 19 |     @Test("Clear function resets calculator to initial state")
 20 |     func testClear() {
 21 |         let calculator = CalculatorService()
 22 |         calculator.inputNumber("5")
 23 |         calculator.setOperation(.add)
 24 |         calculator.inputNumber("3")
 25 |         
 26 |         calculator.clear()
 27 |         
 28 |         #expect(calculator.display == "0")
 29 |         #expect(calculator.currentValue == 0)
 30 |         #expect(calculator.previousValue == 0)
 31 |     }
 32 |     
 33 |     @Test("This test should fail to verify error reporting")
 34 |     func testIntentionalFailure() {
 35 |         let calculator = CalculatorService()
 36 |         // This test is designed to fail to test error reporting
 37 |         #expect(calculator.display == "999", "This should fail - display should be 0, not 999")
 38 |         #expect(calculator.currentOperation == nil)
 39 |         #expect(calculator.willResetDisplay == false)
 40 |     }
 41 | }
 42 | 
 43 | // MARK: - Number Input Tests
 44 | @Suite("Number Input")
 45 | struct NumberInputTests {
 46 |     
 47 |     @Test("Adding single digit numbers")
 48 |     func testSingleDigitInput() {
 49 |         let calculator = CalculatorService()
 50 |         
 51 |         calculator.inputNumber("5")
 52 |         #expect(calculator.display == "5")
 53 |         #expect(calculator.currentValue == 5)
 54 |     }
 55 |     
 56 |     @Test("Adding multiple digit numbers")
 57 |     func testMultipleDigitInput() {
 58 |         let calculator = CalculatorService()
 59 |         
 60 |         calculator.inputNumber("1")
 61 |         calculator.inputNumber("2")
 62 |         calculator.inputNumber("3")
 63 |         
 64 |         #expect(calculator.display == "123")
 65 |         #expect(calculator.currentValue == 123)
 66 |     }
 67 |     
 68 |     @Test("Adding decimal numbers")
 69 |     func testDecimalInput() {
 70 |         let calculator = CalculatorService()
 71 |         
 72 |         calculator.inputNumber("1")
 73 |         calculator.inputDecimal()
 74 |         calculator.inputNumber("5")
 75 |         
 76 |         #expect(calculator.display == "1.5")
 77 |         #expect(calculator.currentValue == 1.5)
 78 |     }
 79 |     
 80 |     @Test("Multiple decimal points should be ignored")
 81 |     func testMultipleDecimalPoints() {
 82 |         let calculator = CalculatorService()
 83 |         
 84 |         calculator.inputNumber("1")
 85 |         calculator.inputDecimal()
 86 |         calculator.inputNumber("5")
 87 |         calculator.inputDecimal() // This should be ignored
 88 |         calculator.inputNumber("2")
 89 |         
 90 |         #expect(calculator.display == "1.52")
 91 |         #expect(calculator.currentValue == 1.52)
 92 |     }
 93 |     
 94 |     @Test("Decimal point at start creates 0.")
 95 |     func testDecimalAtStart() {
 96 |         let calculator = CalculatorService()
 97 |         
 98 |         calculator.inputDecimal()
 99 |         calculator.inputNumber("5")
100 |         
101 |         #expect(calculator.display == "0.5")
102 |         #expect(calculator.currentValue == 0.5)
103 |     }
104 | }
105 | 
106 | // MARK: - Operation Tests
107 | @Suite("Mathematical Operations")
108 | struct OperationTests {
109 |     
110 |     @Test("Addition operation", arguments: [
111 |         (5.0, 3.0, 8.0),
112 |         (10.0, -2.0, 8.0),
113 |         (0.0, 5.0, 5.0),
114 |         (-3.0, -7.0, -10.0)
115 |     ])
116 |     func testAddition(a: Double, b: Double, expected: Double) {
117 |         let result = CalculatorService.Operation.add.calculate(a, b)
118 |         #expect(result == expected)
119 |     }
120 |     
121 |     @Test("Subtraction operation", arguments: [
122 |         (10.0, 3.0, 7.0),
123 |         (5.0, 8.0, -3.0),
124 |         (0.0, 5.0, -5.0),
125 |         (-3.0, -7.0, 4.0)
126 |     ])
127 |     func testSubtraction(a: Double, b: Double, expected: Double) {
128 |         let result = CalculatorService.Operation.subtract.calculate(a, b)
129 |         #expect(result == expected)
130 |     }
131 |     
132 |     @Test("Multiplication operation", arguments: [
133 |         (5.0, 3.0, 15.0),
134 |         (4.0, -2.0, -8.0),
135 |         (0.0, 5.0, 0.0),
136 |         (-3.0, -7.0, 21.0)
137 |     ])
138 |     func testMultiplication(a: Double, b: Double, expected: Double) {
139 |         let result = CalculatorService.Operation.multiply.calculate(a, b)
140 |         #expect(result == expected)
141 |     }
142 |     
143 |     @Test("Division operation", arguments: [
144 |         (10.0, 2.0, 5.0),
145 |         (15.0, 3.0, 5.0),
146 |         (-8.0, 2.0, -4.0),
147 |         (7.0, 2.0, 3.5)
148 |     ])
149 |     func testDivision(a: Double, b: Double, expected: Double) {
150 |         let result = CalculatorService.Operation.divide.calculate(a, b)
151 |         #expect(result == expected)
152 |     }
153 |     
154 |     @Test("Division by zero returns zero")
155 |     func testDivisionByZero() {
156 |         let result = CalculatorService.Operation.divide.calculate(10.0, 0.0)
157 |         #expect(result == 0.0)
158 |     }
159 | }
160 | 
161 | // MARK: - Calculator Integration Tests
162 | @Suite("Calculator Integration Tests")
163 | struct CalculatorIntegrationTests {
164 |     
165 |     @Test("Simple addition calculation")
166 |     func testSimpleAddition() {
167 |         let calculator = CalculatorService()
168 |         
169 |         calculator.inputNumber("5")
170 |         calculator.setOperation(.add)
171 |         calculator.inputNumber("3")
172 |         calculator.calculate()
173 |         
174 |         #expect(calculator.display == "8")
175 |         #expect(calculator.currentValue == 8)
176 |     }
177 |     
178 |     @Test("Chain calculations")
179 |     func testChainCalculations() {
180 |         let calculator = CalculatorService()
181 |         
182 |         calculator.inputNumber("5")
183 |         calculator.setOperation(.add)
184 |         calculator.inputNumber("3")
185 |         calculator.setOperation(.multiply) // Should calculate 5+3=8 first
186 |         calculator.inputNumber("2")
187 |         calculator.calculate()
188 |         
189 |         #expect(calculator.currentValue == 16) // (5+3) * 2 = 16
190 |     }
191 |     
192 |     @Test("Complex calculation sequence")
193 |     func testComplexCalculation() {
194 |         let calculator = CalculatorService()
195 |         
196 |         // Calculate: 10 + 5 * 2 - 3
197 |         calculator.inputNumber("1")
198 |         calculator.inputNumber("0")
199 |         calculator.setOperation(.add)
200 |         calculator.inputNumber("5")
201 |         calculator.setOperation(.multiply)
202 |         calculator.inputNumber("2")
203 |         calculator.setOperation(.subtract)
204 |         calculator.inputNumber("3")
205 |         calculator.calculate()
206 |         
207 |         #expect(calculator.currentValue == 27) // ((10+5)*2)-3 = 27
208 |     }
209 | 
210 |     @Test("Repetitive equals press repeats last operation")
211 |     func testRepetitiveEquals() {
212 |         let calculator = CalculatorService()
213 | 
214 |         calculator.inputNumber("5")
215 |         calculator.setOperation(.add)
216 |         calculator.inputNumber("3")
217 |         calculator.calculate() // 5 + 3 = 8
218 | 
219 |         #expect(calculator.currentValue == 8)
220 | 
221 |         calculator.calculate() // Should be 8 + 3 = 11
222 |         #expect(calculator.currentValue == 11)
223 | 
224 |         calculator.calculate() // Should be 11 + 3 = 14
225 |         #expect(calculator.currentValue == 14)
226 |     }
227 | 
228 |     @Test("Expression display updates correctly")
229 |     func testExpressionDisplay() {
230 |         let calculator = CalculatorService()
231 | 
232 |         calculator.inputNumber("1")
233 |         calculator.inputNumber("2")
234 |         #expect(calculator.expressionDisplay == "")
235 | 
236 |         calculator.setOperation(.add)
237 |         #expect(calculator.expressionDisplay == "12 +")
238 | 
239 |         calculator.inputNumber("3")
240 |         #expect(calculator.expressionDisplay == "12 +") 
241 | 
242 |         calculator.calculate()
243 |         #expect(calculator.expressionDisplay == "12 + 3 =")
244 |     }
245 | }
246 | 
247 | // MARK: - Special Functions Tests
248 | @Suite("Special Functions")
249 | struct SpecialFunctionsTests {
250 |     
251 |     @Test("Toggle sign on positive number")
252 |     func testToggleSignPositive() {
253 |         let calculator = CalculatorService()
254 |         
255 |         calculator.inputNumber("5")
256 |         calculator.toggleSign()
257 |         
258 |         #expect(calculator.display == "-5")
259 |         #expect(calculator.currentValue == -5)
260 |     }
261 |     
262 |     @Test("Toggle sign on negative number")
263 |     func testToggleSignNegative() {
264 |         let calculator = CalculatorService()
265 |         
266 |         calculator.inputNumber("5")
267 |         calculator.toggleSign()
268 |         calculator.toggleSign()
269 |         
270 |         #expect(calculator.display == "5")
271 |         #expect(calculator.currentValue == 5)
272 |     }
273 |     
274 |     @Test("Toggle sign on zero has no effect")
275 |     func testToggleSignZero() {
276 |         let calculator = CalculatorService()
277 |         
278 |         calculator.toggleSign()
279 |         
280 |         #expect(calculator.display == "0")
281 |         #expect(calculator.currentValue == 0)
282 |     }
283 |     
284 |     @Test("Percentage calculation", arguments: [
285 |         ("100", 1.0),
286 |         ("50", 0.5),
287 |         ("25", 0.25),
288 |         ("200", 2.0)
289 |     ])
290 |     func testPercentage(input: String, expected: Double) {
291 |         let calculator = CalculatorService()
292 |         
293 |         calculator.inputNumber(input)
294 |         calculator.percentage()
295 |         
296 |         #expect(calculator.currentValue == expected)
297 |     }
298 | }
299 | 
300 | // MARK: - Input Handler Tests
301 | @Suite("Input Handler Integration")
302 | struct InputHandlerTests {
303 |     
304 |     @Test("Number input through handler")
305 |     func testNumberInputThroughHandler() {
306 |         let calculator = CalculatorService()
307 |         let handler = CalculatorInputHandler(service: calculator)
308 |         
309 |         handler.handleInput("1")
310 |         handler.handleInput("2")
311 |         handler.handleInput("3")
312 |         
313 |         #expect(calculator.display == "123")
314 |     }
315 |     
316 |     @Test("Operation input through handler")
317 |     func testOperationInputThroughHandler() {
318 |         let calculator = CalculatorService()
319 |         let handler = CalculatorInputHandler(service: calculator)
320 |         
321 |         handler.handleInput("5")
322 |         handler.handleInput("+")
323 |         handler.handleInput("3")
324 |         handler.handleInput("=")
325 |         
326 |         #expect(calculator.currentValue == 8)
327 |     }
328 |     
329 |     @Test("Clear input through handler")
330 |     func testClearInputThroughHandler() {
331 |         let calculator = CalculatorService()
332 |         let handler = CalculatorInputHandler(service: calculator)
333 |         
334 |         handler.handleInput("5")
335 |         handler.handleInput("+")
336 |         handler.handleInput("3")
337 |         handler.handleInput("C")
338 |         
339 |         #expect(calculator.display == "0")
340 |         #expect(calculator.currentValue == 0)
341 |     }
342 |     
343 |     @Test("Decimal input through handler")
344 |     func testDecimalInputThroughHandler() {
345 |         let calculator = CalculatorService()
346 |         let handler = CalculatorInputHandler(service: calculator)
347 |         
348 |         handler.handleInput("1")
349 |         handler.handleInput(".")
350 |         handler.handleInput("5")
351 |         
352 |         #expect(calculator.display == "1.5")
353 |     }
354 | }
355 | 
356 | // MARK: - Edge Cases Tests
357 | @Suite("Edge Cases")
358 | struct EdgeCaseTests {
359 |     
360 |     @Test("Calculate without setting operation")
361 |     func testCalculateWithoutOperation() {
362 |         let calculator = CalculatorService()
363 |         
364 |         calculator.inputNumber("5")
365 |         calculator.calculate()
366 |         
367 |         #expect(calculator.currentValue == 5) // Should remain unchanged
368 |     }
369 |     
370 |     @Test("Setting operation without previous number")
371 |     func testOperationWithoutPreviousNumber() {
372 |         let calculator = CalculatorService()
373 |         
374 |         calculator.setOperation(.add)
375 |         calculator.inputNumber("5")
376 |         calculator.calculate()
377 |         
378 |         #expect(calculator.currentValue == 5) // 0 + 5 = 5
379 |     }
380 |     
381 |     @Test("Multiple equals presses")
382 |     func testMultipleEquals() {
383 |         let calculator = CalculatorService()
384 |         
385 |         calculator.inputNumber("5")
386 |         calculator.setOperation(.add)
387 |         calculator.inputNumber("3")
388 |         calculator.calculate()
389 |         
390 |         let firstResult = calculator.currentValue
391 |         calculator.calculate() // Second equals press
392 |         
393 |         #expect(firstResult == 8)
394 |         #expect(calculator.currentValue == 11) // Should repeat last operation: 8 + 3 = 11
395 |     }
396 | }
397 | 
398 | // MARK: - Error Handling Tests
399 | @Suite("Error Handling")
400 | struct ErrorHandlingTests {
401 |     
402 |     @Test("Calculator handles invalid input gracefully")
403 |     func testInvalidInputHandling() {
404 |         let calculator = CalculatorService()
405 |         let handler = CalculatorInputHandler(service: calculator)
406 |         
407 |         // Test pressing operation without any number
408 |         handler.handleInput("+")
409 |         handler.handleInput("5")
410 |         handler.handleInput("=")
411 |         
412 |         #expect(calculator.currentValue == 5) // Should be 0 + 5 = 5
413 |     }
414 |     
415 |     @Test("Calculator state after multiple clears")
416 |     func testMultipleClearOperations() {
417 |         let calculator = CalculatorService()
418 |         
419 |         calculator.inputNumber("123")
420 |         calculator.setOperation(.add)
421 |         calculator.inputNumber("456")
422 |         
423 |         // Multiple clear operations
424 |         calculator.clear()
425 |         calculator.clear()
426 |         calculator.clear()
427 |         
428 |         #expect(calculator.display == "0")
429 |         #expect(calculator.currentValue == 0)
430 |         #expect(calculator.currentOperation == nil)
431 |     }
432 |     
433 |     @Test("Large number error handling")
434 |     func testLargeNumberError() {
435 |         let calculator = CalculatorService()
436 |         calculator.inputNumber("1000000000000") // 1e12
437 |         calculator.setOperation(.multiply)
438 |         calculator.inputNumber("2")
439 |         calculator.calculate()
440 | 
441 |         #expect(calculator.hasError == true)
442 |         #expect(calculator.display == "Error")
443 |         #expect(calculator.expressionDisplay == "Number too large")
444 |     }
445 | }
446 | 
447 | // MARK: - Decimal Edge Cases
448 | @Suite("Decimal Edge Cases")
449 | struct DecimalEdgeCaseTests {
450 |     
451 |     @Test("Very small decimal numbers")
452 |     func testVerySmallDecimals() {
453 |         let calculator = CalculatorService()
454 |         
455 |         calculator.inputNumber("0")
456 |         calculator.inputDecimal()
457 |         calculator.inputNumber("0")
458 |         calculator.inputNumber("0")
459 |         calculator.inputNumber("1")
460 |         
461 |         #expect(calculator.display == "0.001")
462 |         #expect(calculator.currentValue == 0.001)
463 |     }
464 |     
465 |     @Test("Decimal operations precision")
466 |     func testDecimalPrecision() {
467 |         let calculator = CalculatorService()
468 |         
469 |         calculator.inputNumber("0")
470 |         calculator.inputDecimal()
471 |         calculator.inputNumber("1")
472 |         calculator.setOperation(.add)
473 |         calculator.inputNumber("0")
474 |         calculator.inputDecimal()
475 |         calculator.inputNumber("2")
476 |         calculator.calculate()
477 |         
478 |         // 0.1 + 0.2 should equal 0.3 (within floating point precision)
479 |         #expect(abs(calculator.currentValue - 0.3) < 0.0001)
480 |     }
481 | }
482 | 
```

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

```markdown
  1 | # Reloaderoo Usage Guide for XcodeBuildMCP
  2 | 
  3 | This guide explains how to use Reloaderoo for interacting with XcodeBuildMCP as a CLI to save context window space.
  4 | 
  5 | You can use this guide to prompt your agent, but providing the entire document will give you no actual benefits. You will end up using more context than just using MCP server directly. So it's recommended that you curate this document by removing the example commands that you don't need and just keeping the ones that are right for your project. You'll then want to keep this file within your project workspace and then include it in the context window when you need to interact your agent to use XcodeBuildMCP tools.
  6 | 
  7 | > [!IMPORTANT]
  8 | > Please remove this introduction before you prompt your agent with this file or any derrived version of it.
  9 | 
 10 | ## Installation
 11 | 
 12 | Reloaderoo is available via npm and can be used with npx for universal compatibility.
 13 | 
 14 | ```bash
 15 | # Use npx to run reloaderoo
 16 | npx reloaderoo@latest --help
 17 | ```
 18 | 
 19 | **Example Tool Calls:**
 20 | 
 21 | ### iOS Device Development
 22 | 
 23 | - **`build_device`**: Builds an app for a physical device.
 24 |   ```bash
 25 |   npx reloaderoo@latest inspect call-tool build_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
 26 |   ```
 27 | - **`get_device_app_path`**: Gets the `.app` bundle path for a device build.
 28 |   ```bash
 29 |   npx reloaderoo@latest inspect call-tool get_device_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
 30 |   ```
 31 | - **`install_app_device`**: Installs an app on a physical device.
 32 |   ```bash
 33 |   npx reloaderoo@latest inspect call-tool install_app_device --params '{"deviceId": "DEVICE_UDID", "appPath": "/path/to/MyApp.app"}' -- node build/index.js
 34 |   ```
 35 | - **`launch_app_device`**: Launches an app on a physical device.
 36 |   ```bash
 37 |   npx reloaderoo@latest inspect call-tool launch_app_device --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -- node build/index.js
 38 |   ```
 39 | - **`list_devices`**: Lists connected physical devices.
 40 |   ```bash
 41 |   npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
 42 |   ```
 43 | - **`stop_app_device`**: Stops an app on a physical device.
 44 |   ```bash
 45 |   npx reloaderoo@latest inspect call-tool stop_app_device --params '{"deviceId": "DEVICE_UDID", "processId": 12345}' -- node build/index.js
 46 |   ```
 47 | - **`test_device`**: Runs tests on a physical device.
 48 |   ```bash
 49 |   npx reloaderoo@latest inspect call-tool test_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "deviceId": "DEVICE_UDID"}' -- node build/index.js
 50 |   ```
 51 | 
 52 | ### iOS Simulator Development
 53 | 
 54 | - **`boot_sim`**: Boots a simulator.
 55 |   ```bash
 56 |   npx reloaderoo@latest inspect call-tool boot_sim --params '{"simulatorId": "SIMULATOR_UUID"}' -- node build/index.js
 57 |   ```
 58 | - **`build_run_sim`**: Builds and runs an app on a simulator.
 59 |   ```bash
 60 |   npx reloaderoo@latest inspect call-tool build_run_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js
 61 |   ```
 62 | - **`build_sim`**: Builds an app for a simulator.
 63 |   ```bash
 64 |   npx reloaderoo@latest inspect call-tool build_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js
 65 |   ```
 66 | - **`get_sim_app_path`**: Gets the `.app` bundle path for a simulator build.
 67 |   ```bash
 68 |   npx reloaderoo@latest inspect call-tool get_sim_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "platform": "iOS Simulator", "simulatorName": "iPhone 16"}' -- node build/index.js
 69 |   ```
 70 | - **`install_app_sim`**: Installs an app on a simulator.
 71 |   ```bash
 72 |   npx reloaderoo@latest inspect call-tool install_app_sim --params '{"simulatorId": "SIMULATOR_UUID", "appPath": "/path/to/MyApp.app"}' -- node build/index.js
 73 |   ```
 74 | - **`launch_app_logs_sim`**: Launches an app on a simulator with log capture.
 75 |   ```bash
 76 |   npx reloaderoo@latest inspect call-tool launch_app_logs_sim --params '{"simulatorId": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -- node build/index.js
 77 |   ```
 78 | - **`launch_app_sim`**: Launches an app on a simulator.
 79 |   ```bash
 80 |   npx reloaderoo@latest inspect call-tool launch_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -- node build/index.js
 81 |   ```
 82 | - **`list_sims`**: Lists available simulators.
 83 |   ```bash
 84 |   npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/index.js
 85 |   ```
 86 | - **`open_sim`**: Opens the Simulator application.
 87 |   ```bash
 88 |   npx reloaderoo@latest inspect call-tool open_sim --params '{}' -- node build/index.js
 89 |   ```
 90 | - **`stop_app_sim`**: Stops an app on a simulator.
 91 |   ```bash
 92 |   npx reloaderoo@latest inspect call-tool stop_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -- node build/index.js
 93 |   ```
 94 | - **`test_sim`**: Runs tests on a simulator.
 95 |   ```bash
 96 |   npx reloaderoo@latest inspect call-tool test_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/index.js
 97 |   ```
 98 | 
 99 | ### Log Capture & Management
100 | 
101 | - **`start_device_log_cap`**: Starts log capture for a physical device.
102 |   ```bash
103 |   npx reloaderoo@latest inspect call-tool start_device_log_cap --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -- node build/index.js
104 |   ```
105 | - **`start_sim_log_cap`**: Starts log capture for a simulator.
106 |   ```bash
107 |   npx reloaderoo@latest inspect call-tool start_sim_log_cap --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -- node build/index.js
108 |   ```
109 | - **`stop_device_log_cap`**: Stops log capture for a physical device.
110 |   ```bash
111 |   npx reloaderoo@latest inspect call-tool stop_device_log_cap --params '{"logSessionId": "SESSION_ID"}' -- node build/index.js
112 |   ```
113 | - **`stop_sim_log_cap`**: Stops log capture for a simulator.
114 |   ```bash
115 |   npx reloaderoo@latest inspect call-tool stop_sim_log_cap --params '{"logSessionId": "SESSION_ID"}' -- node build/index.js
116 |   ```
117 | 
118 | ### macOS Development
119 | 
120 | - **`build_macos`**: Builds a macOS app.
121 |   ```bash
122 |   npx reloaderoo@latest inspect call-tool build_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
123 |   ```
124 | - **`build_run_macos`**: Builds and runs a macOS app.
125 |   ```bash
126 |   npx reloaderoo@latest inspect call-tool build_run_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
127 |   ```
128 | - **`get_mac_app_path`**: Gets the `.app` bundle path for a macOS build.
129 |   ```bash
130 |   npx reloaderoo@latest inspect call-tool get_mac_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
131 |   ```
132 | - **`launch_mac_app`**: Launches a macOS app.
133 |   ```bash
134 |   npx reloaderoo@latest inspect call-tool launch_mac_app --params '{"appPath": "/Applications/Calculator.app"}' -- node build/index.js
135 |   ```
136 | - **`stop_mac_app`**: Stops a macOS app.
137 |   ```bash
138 |   npx reloaderoo@latest inspect call-tool stop_mac_app --params '{"appName": "Calculator"}' -- node build/index.js
139 |   ```
140 | - **`test_macos`**: Runs tests for a macOS project.
141 |   ```bash
142 |   npx reloaderoo@latest inspect call-tool test_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
143 |   ```
144 | 
145 | ### Project Discovery
146 | 
147 | - **`discover_projs`**: Discovers Xcode projects and workspaces.
148 |   ```bash
149 |   npx reloaderoo@latest inspect call-tool discover_projs --params '{"workspaceRoot": "/path/to/workspace"}' -- node build/index.js
150 |   ```
151 | - **`get_app_bundle_id`**: Gets an app's bundle identifier.
152 |   ```bash
153 |   npx reloaderoo@latest inspect call-tool get_app_bundle_id --params '{"appPath": "/path/to/MyApp.app"}' -- node build/index.js
154 |   ```
155 | - **`get_mac_bundle_id`**: Gets a macOS app's bundle identifier.
156 |   ```bash
157 |   npx reloaderoo@latest inspect call-tool get_mac_bundle_id --params '{"appPath": "/Applications/Calculator.app"}' -- node build/index.js
158 |   ```
159 | - **`list_schemes`**: Lists schemes in a project or workspace.
160 |   ```bash
161 |   npx reloaderoo@latest inspect call-tool list_schemes --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -- node build/index.js
162 |   ```
163 | - **`show_build_settings`**: Shows build settings for a scheme.
164 |   ```bash
165 |   npx reloaderoo@latest inspect call-tool show_build_settings --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/index.js
166 |   ```
167 | 
168 | ### Project Scaffolding
169 | 
170 | - **`scaffold_ios_project`**: Scaffolds a new iOS project.
171 |   ```bash
172 |   npx reloaderoo@latest inspect call-tool scaffold_ios_project --params '{"projectName": "MyNewApp", "outputPath": "/path/to/projects"}' -- node build/index.js
173 |   ```
174 | - **`scaffold_macos_project`**: Scaffolds a new macOS project.
175 |   ```bash
176 |   npx reloaderoo@latest inspect call-tool scaffold_macos_project --params '{"projectName": "MyNewMacApp", "outputPath": "/path/to/projects"}' -- node build/index.js
177 |   ```
178 | 
179 | ### Project Utilities
180 | 
181 | - **`clean`**: Cleans build artifacts.
182 |   ```bash
183 |   # For a project
184 |   npx reloaderoo@latest inspect call-tool clean --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -- node build/index.js
185 |   # For a workspace
186 |   npx reloaderoo@latest inspect call-tool clean --params '{"workspacePath": "/path/to/MyWorkspace.xcworkspace", "scheme": "MyScheme"}' -- node build/index.js
187 |   ```
188 | 
189 | ### Simulator Management
190 | 
191 | - **`reset_sim_location`**: Resets a simulator's location.
192 |   ```bash
193 |   npx reloaderoo@latest inspect call-tool reset_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js
194 |   ```
195 | - **`set_sim_appearance`**: Sets a simulator's appearance (dark/light mode).
196 |   ```bash
197 |   npx reloaderoo@latest inspect call-tool set_sim_appearance --params '{"simulatorUuid": "SIMULATOR_UUID", "mode": "dark"}' -- node build/index.js
198 |   ```
199 | - **`set_sim_location`**: Sets a simulator's GPS location.
200 |   ```bash
201 |   npx reloaderoo@latest inspect call-tool set_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID", "latitude": 37.7749, "longitude": -122.4194}' -- node build/index.js
202 |   ```
203 | - **`sim_statusbar`**: Overrides a simulator's status bar.
204 |   ```bash
205 |   npx reloaderoo@latest inspect call-tool sim_statusbar --params '{"simulatorUuid": "SIMULATOR_UUID", "dataNetwork": "wifi"}' -- node build/index.js
206 |   ```
207 | 
208 | ### Swift Package Manager
209 | 
210 | - **`swift_package_build`**: Builds a Swift package.
211 |   ```bash
212 |   npx reloaderoo@latest inspect call-tool swift_package_build --params '{"packagePath": "/path/to/package"}' -- node build/index.js
213 |   ```
214 | - **`swift_package_clean`**: Cleans a Swift package.
215 |   ```bash
216 |   npx reloaderoo@latest inspect call-tool swift_package_clean --params '{"packagePath": "/path/to/package"}' -- node build/index.js
217 |   ```
218 | - **`swift_package_list`**: Lists running Swift package processes.
219 |   ```bash
220 |   npx reloaderoo@latest inspect call-tool swift_package_list --params '{}' -- node build/index.js
221 |   ```
222 | - **`swift_package_run`**: Runs a Swift package executable.
223 |   ```bash
224 |   npx reloaderoo@latest inspect call-tool swift_package_run --params '{"packagePath": "/path/to/package"}' -- node build/index.js
225 |   ```
226 | - **`swift_package_stop`**: Stops a running Swift package process.
227 |   ```bash
228 |   npx reloaderoo@latest inspect call-tool swift_package_stop --params '{"pid": 12345}' -- node build/index.js
229 |   ```
230 | - **`swift_package_test`**: Tests a Swift package.
231 |   ```bash
232 |   npx reloaderoo@latest inspect call-tool swift_package_test --params '{"packagePath": "/path/to/package"}' -- node build/index.js
233 |   ```
234 | 
235 | ### System Doctor
236 | 
237 | - **`doctor`**: Runs system diagnostics.
238 |   ```bash
239 |   npx reloaderoo@latest inspect call-tool doctor --params '{}' -- node build/index.js
240 |   ```
241 | 
242 | ### UI Testing & Automation
243 | 
244 | - **`button`**: Simulates a hardware button press.
245 |   ```bash
246 |   npx reloaderoo@latest inspect call-tool button --params '{"simulatorUuid": "SIMULATOR_UUID", "buttonType": "home"}' -- node build/index.js
247 |   ```
248 | - **`describe_ui`**: Gets the UI hierarchy of the current screen.
249 |   ```bash
250 |   npx reloaderoo@latest inspect call-tool describe_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js
251 |   ```
252 | - **`gesture`**: Performs a pre-defined gesture.
253 |   ```bash
254 |   npx reloaderoo@latest inspect call-tool gesture --params '{"simulatorUuid": "SIMULATOR_UUID", "preset": "scroll-up"}' -- node build/index.js
255 |   ```
256 | - **`key_press`**: Simulates a key press.
257 |   ```bash
258 |   npx reloaderoo@latest inspect call-tool key_press --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCode": 40}' -- node build/index.js
259 |   ```
260 | - **`key_sequence`**: Simulates a sequence of key presses.
261 |   ```bash
262 |   npx reloaderoo@latest inspect call-tool key_sequence --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCodes": [40, 42, 44]}' -- node build/index.js
263 |   ```
264 | - **`long_press`**: Performs a long press at coordinates.
265 |   ```bash
266 |   npx reloaderoo@latest inspect call-tool long_press --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "duration": 1500}' -- node build/index.js
267 |   ```
268 | - **`screenshot`**: Takes a screenshot.
269 |   ```bash
270 |   npx reloaderoo@latest inspect call-tool screenshot --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js
271 |   ```
272 | - **`swipe`**: Performs a swipe gesture.
273 |   ```bash
274 |   npx reloaderoo@latest inspect call-tool swipe --params '{"simulatorUuid": "SIMULATOR_UUID", "x1": 100, "y1": 200, "x2": 100, "y2": 400}' -- node build/index.js
275 |   ```
276 | - **`tap`**: Performs a tap at coordinates.
277 |   ```bash
278 |   npx reloaderoo@latest inspect call-tool tap --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200}' -- node build/index.js
279 |   ```
280 | - **`touch`**: Simulates a touch down or up event.
281 |   ```bash
282 |   npx reloaderoo@latest inspect call-tool touch --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "down": true}' -- node build/index.js
283 |   ```
284 | - **`type_text`**: Types text into the focused element.
285 |   ```bash
286 |   npx reloaderoo@latest inspect call-tool type_text --params '{"simulatorUuid": "SIMULATOR_UUID", "text": "Hello, World!"}' -- node build/index.js
287 |   ```
288 | 
289 | ### Resources
290 | 
291 | - **Read devices resource**:
292 |   ```bash
293 |   npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -- node build/index.js
294 |   ```
295 | - **Read simulators resource**:
296 |   ```bash
297 |   npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -- node build/index.js
298 |   ```
299 | - **Read doctor resource**:
300 |   ```bash
301 |   npx reloaderoo@latest inspect read-resource "xcodebuildmcp://doctor" -- node build/index.js
302 |   ```
303 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for build_macos plugin (unified)
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using pure dependency injection for deterministic testing
  5 |  * NO VITEST MOCKING ALLOWED - Only createMockExecutor and createMockFileSystemExecutor
  6 |  */
  7 | 
  8 | import { describe, it, expect, beforeEach } from 'vitest';
  9 | import * as z from 'zod';
 10 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
 11 | import { sessionStore } from '../../../../utils/session-store.ts';
 12 | import buildMacOS, { buildMacOSLogic } from '../build_macos.ts';
 13 | 
 14 | describe('build_macos plugin', () => {
 15 |   beforeEach(() => {
 16 |     sessionStore.clear();
 17 |   });
 18 | 
 19 |   describe('Export Field Validation (Literal)', () => {
 20 |     it('should have correct name', () => {
 21 |       expect(buildMacOS.name).toBe('build_macos');
 22 |     });
 23 | 
 24 |     it('should have correct description', () => {
 25 |       expect(buildMacOS.description).toBe('Builds a macOS app.');
 26 |     });
 27 | 
 28 |     it('should have handler function', () => {
 29 |       expect(typeof buildMacOS.handler).toBe('function');
 30 |     });
 31 | 
 32 |     it('should validate schema correctly', () => {
 33 |       const schema = z.object(buildMacOS.schema);
 34 | 
 35 |       expect(schema.safeParse({}).success).toBe(true);
 36 |       expect(
 37 |         schema.safeParse({
 38 |           derivedDataPath: '/path/to/derived-data',
 39 |           extraArgs: ['--arg1', '--arg2'],
 40 |           preferXcodebuild: true,
 41 |         }).success,
 42 |       ).toBe(true);
 43 | 
 44 |       expect(schema.safeParse({ derivedDataPath: 42 }).success).toBe(false);
 45 |       expect(schema.safeParse({ extraArgs: ['--ok', 1] }).success).toBe(false);
 46 |       expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);
 47 | 
 48 |       const schemaKeys = Object.keys(buildMacOS.schema).sort();
 49 |       expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort());
 50 |     });
 51 |   });
 52 | 
 53 |   describe('Handler Requirements', () => {
 54 |     it('should require scheme when no defaults provided', async () => {
 55 |       const result = await buildMacOS.handler({});
 56 | 
 57 |       expect(result.isError).toBe(true);
 58 |       expect(result.content[0].text).toContain('scheme is required');
 59 |       expect(result.content[0].text).toContain('session-set-defaults');
 60 |     });
 61 | 
 62 |     it('should require project or workspace once scheme default exists', async () => {
 63 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 64 | 
 65 |       const result = await buildMacOS.handler({});
 66 | 
 67 |       expect(result.isError).toBe(true);
 68 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 69 |     });
 70 | 
 71 |     it('should reject when both projectPath and workspacePath provided explicitly', async () => {
 72 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 73 | 
 74 |       const result = await buildMacOS.handler({
 75 |         projectPath: '/path/to/project.xcodeproj',
 76 |         workspacePath: '/path/to/workspace.xcworkspace',
 77 |       });
 78 | 
 79 |       expect(result.isError).toBe(true);
 80 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 81 |       expect(result.content[0].text).toContain('projectPath');
 82 |       expect(result.content[0].text).toContain('workspacePath');
 83 |     });
 84 |   });
 85 | 
 86 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 87 |     it('should return exact successful build response', async () => {
 88 |       const mockExecutor = createMockExecutor({
 89 |         success: true,
 90 |         output: 'BUILD SUCCEEDED',
 91 |       });
 92 | 
 93 |       const result = await buildMacOSLogic(
 94 |         {
 95 |           projectPath: '/path/to/MyProject.xcodeproj',
 96 |           scheme: 'MyScheme',
 97 |         },
 98 |         mockExecutor,
 99 |       );
100 | 
101 |       expect(result).toEqual({
102 |         content: [
103 |           {
104 |             type: 'text',
105 |             text: '✅ macOS Build build succeeded for scheme MyScheme.',
106 |           },
107 |           {
108 |             type: 'text',
109 |             text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyScheme' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })",
110 |           },
111 |         ],
112 |       });
113 |     });
114 | 
115 |     it('should return exact build failure response', async () => {
116 |       const mockExecutor = createMockExecutor({
117 |         success: false,
118 |         error: 'error: Compilation error in main.swift',
119 |       });
120 | 
121 |       const result = await buildMacOSLogic(
122 |         {
123 |           projectPath: '/path/to/MyProject.xcodeproj',
124 |           scheme: 'MyScheme',
125 |         },
126 |         mockExecutor,
127 |       );
128 | 
129 |       expect(result).toEqual({
130 |         content: [
131 |           {
132 |             type: 'text',
133 |             text: '❌ [stderr] error: Compilation error in main.swift',
134 |           },
135 |           {
136 |             type: 'text',
137 |             text: '❌ macOS Build build failed for scheme MyScheme.',
138 |           },
139 |         ],
140 |         isError: true,
141 |       });
142 |     });
143 | 
144 |     it('should return exact successful build response with optional parameters', async () => {
145 |       const mockExecutor = createMockExecutor({
146 |         success: true,
147 |         output: 'BUILD SUCCEEDED',
148 |       });
149 | 
150 |       const result = await buildMacOSLogic(
151 |         {
152 |           projectPath: '/path/to/MyProject.xcodeproj',
153 |           scheme: 'MyScheme',
154 |           configuration: 'Release',
155 |           arch: 'arm64',
156 |           derivedDataPath: '/path/to/derived-data',
157 |           extraArgs: ['--verbose'],
158 |           preferXcodebuild: true,
159 |         },
160 |         mockExecutor,
161 |       );
162 | 
163 |       expect(result).toEqual({
164 |         content: [
165 |           {
166 |             type: 'text',
167 |             text: '✅ macOS Build build succeeded for scheme MyScheme.',
168 |           },
169 |           {
170 |             type: 'text',
171 |             text: "Next Steps:\n1. Get app path: get_mac_app_path({ scheme: 'MyScheme' })\n2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })",
172 |           },
173 |         ],
174 |       });
175 |     });
176 | 
177 |     it('should return exact exception handling response', async () => {
178 |       // Create executor that throws error during command execution
179 |       // This will be caught by executeXcodeBuildCommand's try-catch block
180 |       const mockExecutor = async () => {
181 |         throw new Error('Network error');
182 |       };
183 | 
184 |       const result = await buildMacOSLogic(
185 |         {
186 |           projectPath: '/path/to/MyProject.xcodeproj',
187 |           scheme: 'MyScheme',
188 |         },
189 |         mockExecutor,
190 |       );
191 | 
192 |       expect(result).toEqual({
193 |         content: [
194 |           {
195 |             type: 'text',
196 |             text: 'Error during macOS Build build: Network error',
197 |           },
198 |         ],
199 |         isError: true,
200 |       });
201 |     });
202 | 
203 |     it('should return exact spawn error handling response', async () => {
204 |       // Create executor that throws spawn error during command execution
205 |       // This will be caught by executeXcodeBuildCommand's try-catch block
206 |       const mockExecutor = async () => {
207 |         throw new Error('Spawn error');
208 |       };
209 | 
210 |       const result = await buildMacOSLogic(
211 |         {
212 |           projectPath: '/path/to/MyProject.xcodeproj',
213 |           scheme: 'MyScheme',
214 |         },
215 |         mockExecutor,
216 |       );
217 | 
218 |       expect(result).toEqual({
219 |         content: [
220 |           {
221 |             type: 'text',
222 |             text: 'Error during macOS Build build: Spawn error',
223 |           },
224 |         ],
225 |         isError: true,
226 |       });
227 |     });
228 |   });
229 | 
230 |   describe('Command Generation', () => {
231 |     it('should generate correct xcodebuild command with minimal parameters', async () => {
232 |       let capturedCommand: string[] = [];
233 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
234 | 
235 |       // Override the executor to capture the command
236 |       const spyExecutor = async (command: string[]) => {
237 |         capturedCommand = command;
238 |         return mockExecutor(command);
239 |       };
240 | 
241 |       const result = await buildMacOSLogic(
242 |         {
243 |           projectPath: '/path/to/project.xcodeproj',
244 |           scheme: 'MyScheme',
245 |         },
246 |         spyExecutor,
247 |       );
248 | 
249 |       expect(capturedCommand).toEqual([
250 |         'xcodebuild',
251 |         '-project',
252 |         '/path/to/project.xcodeproj',
253 |         '-scheme',
254 |         'MyScheme',
255 |         '-configuration',
256 |         'Debug',
257 |         '-skipMacroValidation',
258 |         '-destination',
259 |         'platform=macOS',
260 |         'build',
261 |       ]);
262 |     });
263 | 
264 |     it('should generate correct xcodebuild command with all parameters', async () => {
265 |       let capturedCommand: string[] = [];
266 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
267 | 
268 |       // Override the executor to capture the command
269 |       const spyExecutor = async (command: string[]) => {
270 |         capturedCommand = command;
271 |         return mockExecutor(command);
272 |       };
273 | 
274 |       const result = await buildMacOSLogic(
275 |         {
276 |           projectPath: '/path/to/project.xcodeproj',
277 |           scheme: 'MyScheme',
278 |           configuration: 'Release',
279 |           arch: 'x86_64',
280 |           derivedDataPath: '/custom/derived',
281 |           extraArgs: ['--verbose'],
282 |           preferXcodebuild: true,
283 |         },
284 |         spyExecutor,
285 |       );
286 | 
287 |       expect(capturedCommand).toEqual([
288 |         'xcodebuild',
289 |         '-project',
290 |         '/path/to/project.xcodeproj',
291 |         '-scheme',
292 |         'MyScheme',
293 |         '-configuration',
294 |         'Release',
295 |         '-skipMacroValidation',
296 |         '-destination',
297 |         'platform=macOS,arch=x86_64',
298 |         '-derivedDataPath',
299 |         '/custom/derived',
300 |         '--verbose',
301 |         'build',
302 |       ]);
303 |     });
304 | 
305 |     it('should generate correct xcodebuild command with only derivedDataPath', async () => {
306 |       let capturedCommand: string[] = [];
307 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
308 | 
309 |       // Override the executor to capture the command
310 |       const spyExecutor = async (command: string[]) => {
311 |         capturedCommand = command;
312 |         return mockExecutor(command);
313 |       };
314 | 
315 |       const result = await buildMacOSLogic(
316 |         {
317 |           projectPath: '/path/to/project.xcodeproj',
318 |           scheme: 'MyScheme',
319 |           derivedDataPath: '/custom/derived/data',
320 |         },
321 |         spyExecutor,
322 |       );
323 | 
324 |       expect(capturedCommand).toEqual([
325 |         'xcodebuild',
326 |         '-project',
327 |         '/path/to/project.xcodeproj',
328 |         '-scheme',
329 |         'MyScheme',
330 |         '-configuration',
331 |         'Debug',
332 |         '-skipMacroValidation',
333 |         '-destination',
334 |         'platform=macOS',
335 |         '-derivedDataPath',
336 |         '/custom/derived/data',
337 |         'build',
338 |       ]);
339 |     });
340 | 
341 |     it('should generate correct xcodebuild command with arm64 architecture only', async () => {
342 |       let capturedCommand: string[] = [];
343 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
344 | 
345 |       // Override the executor to capture the command
346 |       const spyExecutor = async (command: string[]) => {
347 |         capturedCommand = command;
348 |         return mockExecutor(command);
349 |       };
350 | 
351 |       const result = await buildMacOSLogic(
352 |         {
353 |           projectPath: '/path/to/project.xcodeproj',
354 |           scheme: 'MyScheme',
355 |           arch: 'arm64',
356 |         },
357 |         spyExecutor,
358 |       );
359 | 
360 |       expect(capturedCommand).toEqual([
361 |         'xcodebuild',
362 |         '-project',
363 |         '/path/to/project.xcodeproj',
364 |         '-scheme',
365 |         'MyScheme',
366 |         '-configuration',
367 |         'Debug',
368 |         '-skipMacroValidation',
369 |         '-destination',
370 |         'platform=macOS,arch=arm64',
371 |         'build',
372 |       ]);
373 |     });
374 | 
375 |     it('should handle paths with spaces in command generation', async () => {
376 |       let capturedCommand: string[] = [];
377 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
378 | 
379 |       // Override the executor to capture the command
380 |       const spyExecutor = async (command: string[]) => {
381 |         capturedCommand = command;
382 |         return mockExecutor(command);
383 |       };
384 | 
385 |       const result = await buildMacOSLogic(
386 |         {
387 |           projectPath: '/Users/dev/My Project/MyProject.xcodeproj',
388 |           scheme: 'MyScheme',
389 |         },
390 |         spyExecutor,
391 |       );
392 | 
393 |       expect(capturedCommand).toEqual([
394 |         'xcodebuild',
395 |         '-project',
396 |         '/Users/dev/My Project/MyProject.xcodeproj',
397 |         '-scheme',
398 |         'MyScheme',
399 |         '-configuration',
400 |         'Debug',
401 |         '-skipMacroValidation',
402 |         '-destination',
403 |         'platform=macOS',
404 |         'build',
405 |       ]);
406 |     });
407 | 
408 |     it('should generate correct xcodebuild workspace command with minimal parameters', async () => {
409 |       let capturedCommand: string[] = [];
410 |       const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });
411 | 
412 |       // Override the executor to capture the command
413 |       const spyExecutor = async (command: string[]) => {
414 |         capturedCommand = command;
415 |         return mockExecutor(command);
416 |       };
417 | 
418 |       const result = await buildMacOSLogic(
419 |         {
420 |           workspacePath: '/path/to/workspace.xcworkspace',
421 |           scheme: 'MyScheme',
422 |         },
423 |         spyExecutor,
424 |       );
425 | 
426 |       expect(capturedCommand).toEqual([
427 |         'xcodebuild',
428 |         '-workspace',
429 |         '/path/to/workspace.xcworkspace',
430 |         '-scheme',
431 |         'MyScheme',
432 |         '-configuration',
433 |         'Debug',
434 |         '-skipMacroValidation',
435 |         '-destination',
436 |         'platform=macOS',
437 |         'build',
438 |       ]);
439 |     });
440 |   });
441 | 
442 |   describe('XOR Validation', () => {
443 |     it('should error when neither projectPath nor workspacePath provided', async () => {
444 |       const result = await buildMacOS.handler({ scheme: 'MyScheme' });
445 |       expect(result.isError).toBe(true);
446 |       expect(result.content[0].text).toContain('Provide a project or workspace');
447 |     });
448 | 
449 |     it('should error when both projectPath and workspacePath provided', async () => {
450 |       const result = await buildMacOS.handler({
451 |         projectPath: '/path/to/project.xcodeproj',
452 |         workspacePath: '/path/to/workspace.xcworkspace',
453 |         scheme: 'MyScheme',
454 |       });
455 |       expect(result.isError).toBe(true);
456 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
457 |     });
458 | 
459 |     it('should succeed with valid projectPath', async () => {
460 |       const mockExecutor = createMockExecutor({
461 |         success: true,
462 |         output: 'BUILD SUCCEEDED',
463 |       });
464 | 
465 |       const result = await buildMacOSLogic(
466 |         {
467 |           projectPath: '/path/to/project.xcodeproj',
468 |           scheme: 'MyScheme',
469 |         },
470 |         mockExecutor,
471 |       );
472 | 
473 |       expect(result.isError).toBeUndefined();
474 |     });
475 | 
476 |     it('should succeed with valid workspacePath', async () => {
477 |       const mockExecutor = createMockExecutor({
478 |         success: true,
479 |         output: 'BUILD SUCCEEDED',
480 |       });
481 | 
482 |       const result = await buildMacOSLogic(
483 |         {
484 |           workspacePath: '/path/to/workspace.xcworkspace',
485 |           scheme: 'MyScheme',
486 |         },
487 |         mockExecutor,
488 |       );
489 | 
490 |       expect(result.isError).toBeUndefined();
491 |     });
492 |   });
493 | });
494 | 
```

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

```typescript
  1 | /**
  2 |  * Test for scaffold_macos_project plugin - Dependency Injection Architecture
  3 |  *
  4 |  * Tests the plugin structure and exported components for scaffold_macos_project tool.
  5 |  * Uses pure dependency injection with createMockFileSystemExecutor.
  6 |  * NO VITEST MOCKING ALLOWED - Only createMockExecutor/createMockFileSystemExecutor
  7 |  *
  8 |  * Plugin location: plugins/utilities/scaffold_macos_project.js
  9 |  */
 10 | 
 11 | import { describe, it, expect, beforeEach } from 'vitest';
 12 | import * as z from 'zod';
 13 | import {
 14 |   createMockFileSystemExecutor,
 15 |   createNoopExecutor,
 16 |   createMockExecutor,
 17 |   createMockCommandResponse,
 18 | } from '../../../../test-utils/mock-executors.ts';
 19 | import plugin, { scaffold_macos_projectLogic } from '../scaffold_macos_project.ts';
 20 | import { TemplateManager } from '../../../../utils/template/index.ts';
 21 | 
 22 | // ONLY ALLOWED MOCKING: createMockFileSystemExecutor
 23 | 
 24 | describe('scaffold_macos_project plugin', () => {
 25 |   let mockFileSystemExecutor: ReturnType<typeof createMockFileSystemExecutor>;
 26 |   let templateManagerStub: {
 27 |     getTemplatePath: (
 28 |       platform: string,
 29 |       commandExecutor?: unknown,
 30 |       fileSystemExecutor?: unknown,
 31 |     ) => Promise<string>;
 32 |     cleanup: (path: string) => Promise<void>;
 33 |     setError: (error: Error | string | null) => void;
 34 |     getCalls: () => string;
 35 |     resetCalls: () => void;
 36 |   };
 37 | 
 38 |   beforeEach(async () => {
 39 |     // Create template manager stub using pure JavaScript approach
 40 |     let templateManagerCall = '';
 41 |     let templateManagerError: Error | string | null = null;
 42 | 
 43 |     templateManagerStub = {
 44 |       getTemplatePath: async (
 45 |         platform: string,
 46 |         commandExecutor?: unknown,
 47 |         fileSystemExecutor?: unknown,
 48 |       ) => {
 49 |         templateManagerCall = `getTemplatePath(${platform})`;
 50 |         if (templateManagerError) {
 51 |           throw templateManagerError;
 52 |         }
 53 |         return '/tmp/test-templates/macos';
 54 |       },
 55 |       cleanup: async (path: string) => {
 56 |         templateManagerCall += `,cleanup(${path})`;
 57 |         return undefined;
 58 |       },
 59 |       // Test helpers
 60 |       setError: (error: Error | string | null) => {
 61 |         templateManagerError = error;
 62 |       },
 63 |       getCalls: () => templateManagerCall,
 64 |       resetCalls: () => {
 65 |         templateManagerCall = '';
 66 |       },
 67 |     };
 68 | 
 69 |     // Create fresh mock file system executor for each test
 70 |     mockFileSystemExecutor = createMockFileSystemExecutor({
 71 |       existsSync: () => false,
 72 |       mkdir: async () => {},
 73 |       cp: async () => {},
 74 |       readFile: async () => 'template content with MyProject placeholder',
 75 |       writeFile: async () => {},
 76 |       readdir: async () => [
 77 |         { name: 'Package.swift', isDirectory: () => false, isFile: () => true },
 78 |         { name: 'MyProject.swift', isDirectory: () => false, isFile: () => true },
 79 |       ],
 80 |     });
 81 | 
 82 |     // Replace the real TemplateManager with our stub for most tests
 83 |     (TemplateManager as any).getTemplatePath = templateManagerStub.getTemplatePath;
 84 |     (TemplateManager as any).cleanup = templateManagerStub.cleanup;
 85 |   });
 86 | 
 87 |   describe('Export Field Validation (Literal)', () => {
 88 |     it('should have correct name field', () => {
 89 |       expect(plugin.name).toBe('scaffold_macos_project');
 90 |     });
 91 | 
 92 |     it('should have correct description field', () => {
 93 |       expect(plugin.description).toBe(
 94 |         'Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration.',
 95 |       );
 96 |     });
 97 | 
 98 |     it('should have handler as function', () => {
 99 |       expect(typeof plugin.handler).toBe('function');
100 |     });
101 | 
102 |     it('should have valid schema with required fields', () => {
103 |       // Test the schema object exists
104 |       expect(plugin.schema).toBeDefined();
105 |       expect(plugin.schema.projectName).toBeDefined();
106 |       expect(plugin.schema.outputPath).toBeDefined();
107 |       expect(plugin.schema.bundleIdentifier).toBeDefined();
108 |       expect(plugin.schema.customizeNames).toBeDefined();
109 |       expect(plugin.schema.deploymentTarget).toBeDefined();
110 |     });
111 |   });
112 | 
113 |   describe('Command Generation', () => {
114 |     it('should generate correct curl command for macOS template download', async () => {
115 |       // This test validates that the curl command would be generated correctly
116 |       // by verifying the URL construction logic
117 |       const expectedUrl =
118 |         'https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/';
119 | 
120 |       // The curl command should be structured correctly for macOS template
121 |       expect(expectedUrl).toContain('XcodeBuildMCP-macOS-Template');
122 |       expect(expectedUrl).toContain('releases/download');
123 | 
124 |       // The template zip file should follow the expected pattern
125 |       const expectedFilename = 'template.zip';
126 |       expect(expectedFilename).toMatch(/template\.zip$/);
127 | 
128 |       // The curl command flags should be correct
129 |       const expectedCurlFlags = ['-L', '-f', '-o'];
130 |       expect(expectedCurlFlags).toContain('-L'); // Follow redirects
131 |       expect(expectedCurlFlags).toContain('-f'); // Fail on HTTP errors
132 |       expect(expectedCurlFlags).toContain('-o'); // Output to file
133 |     });
134 | 
135 |     it('should generate correct unzip command for template extraction', async () => {
136 |       // This test validates that the unzip command would be generated correctly
137 |       // by verifying the command structure
138 |       const expectedUnzipCommand = ['unzip', '-q', 'template.zip'];
139 | 
140 |       // The unzip command should use the quiet flag
141 |       expect(expectedUnzipCommand).toContain('-q');
142 | 
143 |       // The unzip command should target the template zip file
144 |       expect(expectedUnzipCommand).toContain('template.zip');
145 | 
146 |       // The unzip command should be structured correctly
147 |       expect(expectedUnzipCommand[0]).toBe('unzip');
148 |       expect(expectedUnzipCommand[1]).toBe('-q');
149 |       expect(expectedUnzipCommand[2]).toMatch(/template\.zip$/);
150 |     });
151 | 
152 |     it('should generate correct commands for template with version', async () => {
153 |       // This test validates that the curl command would be generated correctly with version
154 |       const testVersion = 'v1.0.0';
155 |       const expectedUrlWithVersion = `https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/${testVersion}/`;
156 | 
157 |       // The URL should contain the specific version
158 |       expect(expectedUrlWithVersion).toContain(testVersion);
159 |       expect(expectedUrlWithVersion).toContain('XcodeBuildMCP-macOS-Template');
160 |       expect(expectedUrlWithVersion).toContain('releases/download');
161 | 
162 |       // The version should be in the correct format
163 |       expect(testVersion).toMatch(/^v\d+\.\d+\.\d+$/);
164 | 
165 |       // The full URL should be correctly constructed
166 |       expect(expectedUrlWithVersion).toBe(
167 |         `https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/${testVersion}/`,
168 |       );
169 |     });
170 | 
171 |     it('should not generate commands when using local template path', async () => {
172 |       let capturedCommands: string[][] = [];
173 |       const trackingExecutor = async (command: string[]) => {
174 |         capturedCommands.push(command);
175 |         return createMockCommandResponse({
176 |           success: true,
177 |           output: 'Command successful',
178 |         });
179 |       };
180 | 
181 |       // Store original environment variable
182 |       const originalEnv = process.env.XCODEBUILDMCP_MACOS_TEMPLATE_PATH;
183 | 
184 |       // Mock local template path exists
185 |       mockFileSystemExecutor.existsSync = (path: string) => {
186 |         return path === '/local/template/path' || path === '/local/template/path/template';
187 |       };
188 | 
189 |       // Set environment variable for local template path
190 |       process.env.XCODEBUILDMCP_MACOS_TEMPLATE_PATH = '/local/template/path';
191 | 
192 |       // Restore original TemplateManager for command generation tests
193 |       const { TemplateManager: OriginalTemplateManager } = await import(
194 |         '../../../../utils/template/index.ts'
195 |       );
196 |       (TemplateManager as any).getTemplatePath = OriginalTemplateManager.getTemplatePath;
197 |       (TemplateManager as any).cleanup = OriginalTemplateManager.cleanup;
198 | 
199 |       await scaffold_macos_projectLogic(
200 |         {
201 |           projectName: 'TestMacApp',
202 |           customizeNames: true,
203 |           outputPath: '/tmp/test-projects',
204 |         },
205 |         trackingExecutor,
206 |         mockFileSystemExecutor,
207 |       );
208 | 
209 |       // Should not generate any curl or unzip commands when using local template
210 |       expect(capturedCommands).not.toContainEqual(
211 |         expect.arrayContaining(['curl', expect.anything(), expect.anything()]),
212 |       );
213 |       expect(capturedCommands).not.toContainEqual(
214 |         expect.arrayContaining(['unzip', expect.anything(), expect.anything()]),
215 |       );
216 | 
217 |       // Clean up environment variable
218 |       process.env.XCODEBUILDMCP_MACOS_TEMPLATE_PATH = originalEnv;
219 | 
220 |       // Restore stub after test
221 |       (TemplateManager as any).getTemplatePath = templateManagerStub.getTemplatePath;
222 |       (TemplateManager as any).cleanup = templateManagerStub.cleanup;
223 |     });
224 |   });
225 | 
226 |   describe('Handler Behavior (Complete Literal Returns)', () => {
227 |     it('should return success response for valid scaffold macOS project request', async () => {
228 |       const result = await scaffold_macos_projectLogic(
229 |         {
230 |           projectName: 'TestMacApp',
231 |           customizeNames: true,
232 |           outputPath: '/tmp/test-projects',
233 |           bundleIdentifier: 'com.test.macapp',
234 |         },
235 |         createNoopExecutor(),
236 |         mockFileSystemExecutor,
237 |       );
238 | 
239 |       expect(result).toEqual({
240 |         content: [
241 |           {
242 |             type: 'text',
243 |             text: JSON.stringify(
244 |               {
245 |                 success: true,
246 |                 projectPath: '/tmp/test-projects',
247 |                 platform: 'macOS',
248 |                 message: 'Successfully scaffolded macOS project "TestMacApp" in /tmp/test-projects',
249 |                 nextSteps: [
250 |                   'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
251 |                   'Build for macOS: build_macos({ workspacePath: "/tmp/test-projects/TestMacApp.xcworkspace", scheme: "TestMacApp" })',
252 |                   'Build & Run on macOS: build_run_macos({ workspacePath: "/tmp/test-projects/TestMacApp.xcworkspace", scheme: "TestMacApp" })',
253 |                 ],
254 |               },
255 |               null,
256 |               2,
257 |             ),
258 |           },
259 |         ],
260 |       });
261 | 
262 |       // Verify template manager calls using manual tracking
263 |       expect(templateManagerStub.getCalls()).toBe(
264 |         'getTemplatePath(macOS),cleanup(/tmp/test-templates/macos)',
265 |       );
266 |     });
267 | 
268 |     it('should return success response with customizeNames false', async () => {
269 |       const result = await scaffold_macos_projectLogic(
270 |         {
271 |           projectName: 'TestMacApp',
272 |           outputPath: '/tmp/test-projects',
273 |           customizeNames: false,
274 |         },
275 |         createNoopExecutor(),
276 |         mockFileSystemExecutor,
277 |       );
278 | 
279 |       expect(result).toEqual({
280 |         content: [
281 |           {
282 |             type: 'text',
283 |             text: JSON.stringify(
284 |               {
285 |                 success: true,
286 |                 projectPath: '/tmp/test-projects',
287 |                 platform: 'macOS',
288 |                 message: 'Successfully scaffolded macOS project "TestMacApp" in /tmp/test-projects',
289 |                 nextSteps: [
290 |                   'Important: Before working on the project make sure to read the README.md file in the workspace root directory.',
291 |                   'Build for macOS: build_macos({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject" })',
292 |                   'Build & Run on macOS: build_run_macos({ workspacePath: "/tmp/test-projects/MyProject.xcworkspace", scheme: "MyProject" })',
293 |                 ],
294 |               },
295 |               null,
296 |               2,
297 |             ),
298 |           },
299 |         ],
300 |       });
301 |     });
302 | 
303 |     it('should return error response for invalid project name', async () => {
304 |       const result = await scaffold_macos_projectLogic(
305 |         {
306 |           projectName: '123InvalidName',
307 |           customizeNames: true,
308 |           outputPath: '/tmp/test-projects',
309 |         },
310 |         createNoopExecutor(),
311 |         mockFileSystemExecutor,
312 |       );
313 | 
314 |       expect(result).toEqual({
315 |         content: [
316 |           {
317 |             type: 'text',
318 |             text: JSON.stringify(
319 |               {
320 |                 success: false,
321 |                 error:
322 |                   'Project name must start with a letter and contain only letters, numbers, and underscores',
323 |               },
324 |               null,
325 |               2,
326 |             ),
327 |           },
328 |         ],
329 |         isError: true,
330 |       });
331 |     });
332 | 
333 |     it('should return error response for existing project files', async () => {
334 |       // Override existsSync to return true for workspace file
335 |       mockFileSystemExecutor.existsSync = () => true;
336 | 
337 |       const result = await scaffold_macos_projectLogic(
338 |         {
339 |           projectName: 'TestMacApp',
340 |           customizeNames: true,
341 |           outputPath: '/tmp/test-projects',
342 |         },
343 |         createNoopExecutor(),
344 |         mockFileSystemExecutor,
345 |       );
346 | 
347 |       expect(result).toEqual({
348 |         content: [
349 |           {
350 |             type: 'text',
351 |             text: JSON.stringify(
352 |               {
353 |                 success: false,
354 |                 error: 'Xcode project files already exist in /tmp/test-projects',
355 |               },
356 |               null,
357 |               2,
358 |             ),
359 |           },
360 |         ],
361 |         isError: true,
362 |       });
363 |     });
364 | 
365 |     it('should return error response for template manager failure', async () => {
366 |       templateManagerStub.setError(new Error('Template not found'));
367 | 
368 |       const result = await scaffold_macos_projectLogic(
369 |         {
370 |           projectName: 'TestMacApp',
371 |           customizeNames: true,
372 |           outputPath: '/tmp/test-projects',
373 |         },
374 |         createNoopExecutor(),
375 |         mockFileSystemExecutor,
376 |       );
377 | 
378 |       expect(result).toEqual({
379 |         content: [
380 |           {
381 |             type: 'text',
382 |             text: JSON.stringify(
383 |               {
384 |                 success: false,
385 |                 error: 'Failed to get template for macOS: Template not found',
386 |               },
387 |               null,
388 |               2,
389 |             ),
390 |           },
391 |         ],
392 |         isError: true,
393 |       });
394 |     });
395 |   });
396 | 
397 |   describe('File System Operations', () => {
398 |     it('should create directories and process files correctly', async () => {
399 |       await scaffold_macos_projectLogic(
400 |         {
401 |           projectName: 'TestApp',
402 |           customizeNames: true,
403 |           outputPath: '/tmp/test',
404 |         },
405 |         createNoopExecutor(),
406 |         mockFileSystemExecutor,
407 |       );
408 | 
409 |       // Verify template manager calls using manual tracking
410 |       expect(templateManagerStub.getCalls()).toBe(
411 |         'getTemplatePath(macOS),cleanup(/tmp/test-templates/macos)',
412 |       );
413 | 
414 |       // File system operations are called by the mock implementation
415 |       // but we can't verify them without vitest mocking patterns
416 |       // This test validates the integration works correctly
417 |     });
418 |   });
419 | });
420 | 
```

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

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

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

```typescript
  1 | /**
  2 |  * Mock Executors for Testing - Dependency Injection Architecture
  3 |  *
  4 |  * This module provides mock implementations of CommandExecutor and FileSystemExecutor
  5 |  * for testing purposes. These mocks are completely isolated from production dependencies
  6 |  * to avoid import chains that could trigger native module loading issues in test environments.
  7 |  *
  8 |  * IMPORTANT: These are EXACT copies of the mock functions originally in utils/command.js
  9 |  * to ensure zero behavioral changes during the file reorganization.
 10 |  *
 11 |  * Responsibilities:
 12 |  * - Providing mock command execution for tests
 13 |  * - Providing mock file system operations for tests
 14 |  * - Maintaining exact behavior compatibility with original implementations
 15 |  * - Avoiding any dependencies on production logging or instrumentation
 16 |  */
 17 | 
 18 | import { ChildProcess } from 'child_process';
 19 | import type { WriteStream } from 'fs';
 20 | import { EventEmitter } from 'node:events';
 21 | import { PassThrough } from 'node:stream';
 22 | import { CommandExecutor, type CommandResponse } from '../utils/CommandExecutor.ts';
 23 | import { FileSystemExecutor } from '../utils/FileSystemExecutor.ts';
 24 | import type { InteractiveProcess, InteractiveSpawner } from '../utils/execution/index.ts';
 25 | 
 26 | export type { CommandExecutor, FileSystemExecutor };
 27 | 
 28 | export const mockProcess = { pid: 12345 } as unknown as ChildProcess;
 29 | 
 30 | export function createMockCommandResponse(
 31 |   overrides: Partial<CommandResponse> = {},
 32 | ): CommandResponse {
 33 |   return {
 34 |     success: overrides.success ?? true,
 35 |     output: overrides.output ?? '',
 36 |     error: overrides.error,
 37 |     process: overrides.process ?? mockProcess,
 38 |     exitCode: overrides.exitCode ?? (overrides.success === false ? 1 : 0),
 39 |   };
 40 | }
 41 | 
 42 | /**
 43 |  * Create a mock executor for testing
 44 |  * @param result Mock command result or error to throw
 45 |  * @returns Mock executor function
 46 |  */
 47 | export function createMockExecutor(
 48 |   result:
 49 |     | {
 50 |         success?: boolean;
 51 |         output?: string;
 52 |         error?: string;
 53 |         process?: unknown;
 54 |         exitCode?: number;
 55 |         shouldThrow?: Error;
 56 |         onExecute?: (
 57 |           command: string[],
 58 |           logPrefix?: string,
 59 |           useShell?: boolean,
 60 |           opts?: { env?: Record<string, string>; cwd?: string },
 61 |           detached?: boolean,
 62 |         ) => void;
 63 |       }
 64 |     | Error
 65 |     | string,
 66 | ): CommandExecutor {
 67 |   // If result is Error or string, return executor that rejects
 68 |   if (result instanceof Error || typeof result === 'string') {
 69 |     return async () => {
 70 |       throw result;
 71 |     };
 72 |   }
 73 | 
 74 |   // If shouldThrow is specified, return executor that rejects with that error
 75 |   if (result.shouldThrow) {
 76 |     return async () => {
 77 |       throw result.shouldThrow;
 78 |     };
 79 |   }
 80 | 
 81 |   const mockProcess = {
 82 |     pid: 12345,
 83 |     stdout: null,
 84 |     stderr: null,
 85 |     stdin: null,
 86 |     stdio: [null, null, null],
 87 |     killed: false,
 88 |     connected: false,
 89 |     exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
 90 |     signalCode: null,
 91 |     spawnargs: [],
 92 |     spawnfile: 'sh',
 93 |   } as unknown as ChildProcess;
 94 | 
 95 |   return async (command, logPrefix, useShell, opts, detached) => {
 96 |     // Call onExecute callback if provided
 97 |     if (result.onExecute) {
 98 |       result.onExecute(command, logPrefix, useShell, opts, detached);
 99 |     }
100 | 
101 |     return {
102 |       success: result.success ?? true,
103 |       output: result.output ?? '',
104 |       error: result.error,
105 |       process: (result.process ?? mockProcess) as ChildProcess,
106 |       exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
107 |     };
108 |   };
109 | }
110 | 
111 | /**
112 |  * Create a no-op executor that throws an error if called
113 |  * Use this for tests where an executor is required but should never be called
114 |  * @returns CommandExecutor that throws on invocation
115 |  */
116 | export function createNoopExecutor(): CommandExecutor {
117 |   return async (command) => {
118 |     throw new Error(
119 |       `🚨 NOOP EXECUTOR CALLED! 🚨\n` +
120 |         `Command: ${command.join(' ')}\n` +
121 |         `This executor should never be called in this test context.\n` +
122 |         `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
123 |         `Either fix the test to avoid this code path, or use createMockExecutor() instead.`,
124 |     );
125 |   };
126 | }
127 | 
128 | /**
129 |  * Create a command-matching mock executor for testing multi-command scenarios
130 |  * Perfect for tools that execute multiple commands (like screenshot: simctl + sips)
131 |  *
132 |  * @param commandMap - Map of command patterns to their mock responses
133 |  * @returns CommandExecutor that matches commands and returns appropriate responses
134 |  *
135 |  * @example
136 |  * ```typescript
137 |  * const mockExecutor = createCommandMatchingMockExecutor({
138 |  *   'xcrun simctl': { output: 'Screenshot saved' },
139 |  *   'sips': { output: 'Image optimized' }
140 |  * });
141 |  * ```
142 |  */
143 | export function createCommandMatchingMockExecutor(
144 |   commandMap: Record<
145 |     string,
146 |     {
147 |       success?: boolean;
148 |       output?: string;
149 |       error?: string;
150 |       process?: unknown;
151 |       exitCode?: number;
152 |     }
153 |   >,
154 | ): CommandExecutor {
155 |   return async (command) => {
156 |     const commandStr = command.join(' ');
157 | 
158 |     // Find matching command pattern
159 |     const matchedKey = Object.keys(commandMap).find((key) => commandStr.includes(key));
160 | 
161 |     if (!matchedKey) {
162 |       throw new Error(
163 |         `🚨 UNEXPECTED COMMAND! 🚨\n` +
164 |           `Command: ${commandStr}\n` +
165 |           `Expected one of: ${Object.keys(commandMap).join(', ')}\n` +
166 |           `Available patterns: ${JSON.stringify(Object.keys(commandMap), null, 2)}`,
167 |       );
168 |     }
169 | 
170 |     const result = commandMap[matchedKey];
171 | 
172 |     const mockProcess = {
173 |       pid: 12345,
174 |       stdout: null,
175 |       stderr: null,
176 |       stdin: null,
177 |       stdio: [null, null, null],
178 |       killed: false,
179 |       connected: false,
180 |       exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
181 |       signalCode: null,
182 |       spawnargs: [],
183 |       spawnfile: 'sh',
184 |     } as unknown as ChildProcess;
185 | 
186 |     return {
187 |       success: result.success ?? true, // Success by default (as discussed)
188 |       output: result.output ?? '',
189 |       error: result.error,
190 |       process: (result.process ?? mockProcess) as ChildProcess,
191 |       exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
192 |     };
193 |   };
194 | }
195 | 
196 | export type MockInteractiveSession = {
197 |   stdout: PassThrough;
198 |   stderr: PassThrough;
199 |   stdin: PassThrough;
200 |   emitExit: (code?: number | null, signal?: NodeJS.Signals | null) => void;
201 |   emitError: (error: Error) => void;
202 | };
203 | 
204 | export type MockInteractiveSpawnerScript = {
205 |   onSpawn?: (session: MockInteractiveSession) => void;
206 |   onWrite?: (data: string, session: MockInteractiveSession) => void;
207 |   onKill?: (signal: NodeJS.Signals | undefined, session: MockInteractiveSession) => void;
208 |   onDispose?: (session: MockInteractiveSession) => void;
209 | };
210 | 
211 | export function createMockInteractiveSpawner(
212 |   script: MockInteractiveSpawnerScript = {},
213 | ): InteractiveSpawner {
214 |   return (): InteractiveProcess => {
215 |     const stdout = new PassThrough();
216 |     const stderr = new PassThrough();
217 |     const stdin = new PassThrough();
218 |     const emitter = new EventEmitter();
219 |     const mockProcess = emitter as unknown as ChildProcess;
220 |     const mutableProcess = mockProcess as unknown as {
221 |       stdout: PassThrough | null;
222 |       stderr: PassThrough | null;
223 |       stdin: PassThrough | null;
224 |       killed: boolean;
225 |       exitCode: number | null;
226 |       signalCode: NodeJS.Signals | null;
227 |       spawnargs: string[];
228 |       spawnfile: string;
229 |       pid: number;
230 |     };
231 | 
232 |     mutableProcess.stdout = stdout;
233 |     mutableProcess.stderr = stderr;
234 |     mutableProcess.stdin = stdin;
235 |     mutableProcess.killed = false;
236 |     mutableProcess.exitCode = null;
237 |     mutableProcess.signalCode = null;
238 |     mutableProcess.spawnargs = [];
239 |     mutableProcess.spawnfile = 'mock';
240 |     mutableProcess.pid = 12345;
241 |     mockProcess.kill = ((signal?: NodeJS.Signals): boolean => {
242 |       mutableProcess.killed = true;
243 |       emitter.emit('exit', 0, signal ?? null);
244 |       return true;
245 |     }) as ChildProcess['kill'];
246 | 
247 |     const session: MockInteractiveSession = {
248 |       stdout,
249 |       stderr,
250 |       stdin,
251 |       emitExit: (code = 0, signal = null) => {
252 |         emitter.emit('exit', code, signal);
253 |       },
254 |       emitError: (error) => {
255 |         emitter.emit('error', error);
256 |       },
257 |     };
258 | 
259 |     script.onSpawn?.(session);
260 | 
261 |     let disposed = false;
262 | 
263 |     return {
264 |       process: mockProcess,
265 |       write(data: string): void {
266 |         if (disposed) {
267 |           throw new Error('Mock interactive process disposed');
268 |         }
269 |         script.onWrite?.(data, session);
270 |       },
271 |       kill(signal?: NodeJS.Signals): void {
272 |         if (disposed) return;
273 |         mutableProcess.killed = true;
274 |         script.onKill?.(signal, session);
275 |         emitter.emit('exit', 0, signal ?? null);
276 |       },
277 |       dispose(): void {
278 |         if (disposed) return;
279 |         disposed = true;
280 |         script.onDispose?.(session);
281 |         stdout.end();
282 |         stderr.end();
283 |         stdin.end();
284 |         emitter.removeAllListeners();
285 |       },
286 |     };
287 |   };
288 | }
289 | 
290 | /**
291 |  * Create a mock file system executor for testing
292 |  */
293 | export function createMockFileSystemExecutor(
294 |   overrides?: Partial<FileSystemExecutor>,
295 | ): FileSystemExecutor {
296 |   const mockWriteStream = ((): WriteStream => {
297 |     const emitter = new EventEmitter();
298 |     const stream = Object.assign(emitter, {
299 |       destroyed: false,
300 |       write: () => true,
301 |       end: () => {
302 |         stream.destroyed = true;
303 |         emitter.emit('close');
304 |       },
305 |     }) as unknown as WriteStream;
306 |     return stream;
307 |   })();
308 | 
309 |   return {
310 |     mkdir: async (): Promise<void> => {},
311 |     readFile: async (): Promise<string> => 'mock file content',
312 |     writeFile: async (): Promise<void> => {},
313 |     createWriteStream: () => mockWriteStream,
314 |     cp: async (): Promise<void> => {},
315 |     readdir: async (): Promise<unknown[]> => [],
316 |     rm: async (): Promise<void> => {},
317 |     existsSync: (): boolean => false,
318 |     stat: async (): Promise<{ isDirectory(): boolean; mtimeMs: number }> => ({
319 |       isDirectory: (): boolean => true,
320 |       mtimeMs: Date.now(),
321 |     }),
322 |     mkdtemp: async (): Promise<string> => '/tmp/mock-temp-123456',
323 |     tmpdir: (): string => '/tmp',
324 |     ...overrides,
325 |   };
326 | }
327 | 
328 | /**
329 |  * Create a no-op file system executor that throws an error if called
330 |  * Use this for tests where an executor is required but should never be called
331 |  * @returns CommandExecutor that throws on invocation
332 |  */
333 | export function createNoopFileSystemExecutor(): FileSystemExecutor {
334 |   return {
335 |     mkdir: async (): Promise<void> => {
336 |       throw new Error(
337 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
338 |           `This executor should never be called in this test context.\n` +
339 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
340 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
341 |       );
342 |     },
343 |     readFile: async (): Promise<string> => {
344 |       throw new Error(
345 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
346 |           `This executor should never be called in this test context.\n` +
347 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
348 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
349 |       );
350 |     },
351 |     writeFile: async (): Promise<void> => {
352 |       throw new Error(
353 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
354 |           `This executor should never be called in this test context.\n` +
355 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
356 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
357 |       );
358 |     },
359 |     createWriteStream: (): WriteStream => {
360 |       throw new Error(
361 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
362 |           `This executor should never be called in this test context.\n` +
363 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
364 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
365 |       );
366 |     },
367 |     cp: async (): Promise<void> => {
368 |       throw new Error(
369 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
370 |           `This executor should never be called in this test context.\n` +
371 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
372 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
373 |       );
374 |     },
375 |     readdir: async (): Promise<unknown[]> => {
376 |       throw new Error(
377 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
378 |           `This executor should never be called in this test context.\n` +
379 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
380 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
381 |       );
382 |     },
383 |     rm: async (): Promise<void> => {
384 |       throw new Error(
385 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
386 |           `This executor should never be called in this test context.\n` +
387 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
388 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
389 |       );
390 |     },
391 |     existsSync: (): boolean => {
392 |       throw new Error(
393 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
394 |           `This executor should never be called in this test context.\n` +
395 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
396 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
397 |       );
398 |     },
399 |     stat: async (): Promise<{ isDirectory(): boolean; mtimeMs: number }> => {
400 |       throw new Error(
401 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
402 |           `This executor should never be called in this test context.\n` +
403 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
404 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
405 |       );
406 |     },
407 |     mkdtemp: async (): Promise<string> => {
408 |       throw new Error(
409 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
410 |           `This executor should never be called in this test context.\n` +
411 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
412 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
413 |       );
414 |     },
415 |     tmpdir: (): string => '/tmp',
416 |   };
417 | }
418 | 
419 | /**
420 |  * Create a mock environment detector for testing
421 |  * @param options Mock options for environment detection
422 |  * @returns Mock environment detector
423 |  */
424 | export function createMockEnvironmentDetector(
425 |   options: {
426 |     isRunningUnderClaudeCode?: boolean;
427 |   } = {},
428 | ): import('../utils/environment.js').EnvironmentDetector {
429 |   return {
430 |     isRunningUnderClaudeCode: () => options.isRunningUnderClaudeCode ?? false,
431 |   };
432 | }
433 | 
```

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

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

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

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

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

```typescript
  1 | /**
  2 |  * Tests for get_mac_app_path plugin (unified project/workspace)
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | import { describe, it, expect, beforeEach } from 'vitest';
  7 | import * as z from 'zod';
  8 | import {
  9 |   createMockCommandResponse,
 10 |   createMockExecutor,
 11 |   type CommandExecutor,
 12 | } from '../../../../test-utils/mock-executors.ts';
 13 | import { sessionStore } from '../../../../utils/session-store.ts';
 14 | import getMacAppPath, { get_mac_app_pathLogic } from '../get_mac_app_path.ts';
 15 | 
 16 | describe('get_mac_app_path plugin', () => {
 17 |   beforeEach(() => {
 18 |     sessionStore.clear();
 19 |   });
 20 | 
 21 |   describe('Export Field Validation (Literal)', () => {
 22 |     it('should have correct name', () => {
 23 |       expect(getMacAppPath.name).toBe('get_mac_app_path');
 24 |     });
 25 | 
 26 |     it('should have correct description', () => {
 27 |       expect(getMacAppPath.description).toBe('Retrieves the built macOS app bundle path.');
 28 |     });
 29 | 
 30 |     it('should have handler function', () => {
 31 |       expect(typeof getMacAppPath.handler).toBe('function');
 32 |     });
 33 | 
 34 |     it('should validate schema correctly', () => {
 35 |       const schema = z.object(getMacAppPath.schema);
 36 | 
 37 |       expect(schema.safeParse({}).success).toBe(true);
 38 |       expect(
 39 |         schema.safeParse({
 40 |           derivedDataPath: '/path/to/derived',
 41 |           extraArgs: ['--verbose'],
 42 |         }).success,
 43 |       ).toBe(true);
 44 | 
 45 |       expect(schema.safeParse({ derivedDataPath: 7 }).success).toBe(false);
 46 |       expect(schema.safeParse({ extraArgs: ['--bad', 1] }).success).toBe(false);
 47 | 
 48 |       const schemaKeys = Object.keys(getMacAppPath.schema).sort();
 49 |       expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs'].sort());
 50 |     });
 51 |   });
 52 | 
 53 |   describe('Handler Requirements', () => {
 54 |     it('should require scheme before running', async () => {
 55 |       const result = await getMacAppPath.handler({});
 56 | 
 57 |       expect(result.isError).toBe(true);
 58 |       expect(result.content[0].text).toContain('scheme is required');
 59 |     });
 60 | 
 61 |     it('should require project or workspace when scheme default exists', async () => {
 62 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 63 | 
 64 |       const result = await getMacAppPath.handler({});
 65 | 
 66 |       expect(result.isError).toBe(true);
 67 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 68 |     });
 69 | 
 70 |     it('should reject when both projectPath and workspacePath provided explicitly', async () => {
 71 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 72 | 
 73 |       const result = await getMacAppPath.handler({
 74 |         projectPath: '/path/to/project.xcodeproj',
 75 |         workspacePath: '/path/to/workspace.xcworkspace',
 76 |       });
 77 | 
 78 |       expect(result.isError).toBe(true);
 79 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 80 |     });
 81 |   });
 82 | 
 83 |   describe('XOR Validation', () => {
 84 |     it('should error when neither projectPath nor workspacePath provided', async () => {
 85 |       const result = await getMacAppPath.handler({
 86 |         scheme: 'MyScheme',
 87 |       });
 88 | 
 89 |       expect(result.isError).toBe(true);
 90 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 91 |     });
 92 | 
 93 |     it('should error when both projectPath and workspacePath provided', async () => {
 94 |       const result = await getMacAppPath.handler({
 95 |         projectPath: '/path/to/project.xcodeproj',
 96 |         workspacePath: '/path/to/workspace.xcworkspace',
 97 |         scheme: 'MyScheme',
 98 |       });
 99 | 
100 |       expect(result.isError).toBe(true);
101 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
102 |     });
103 |   });
104 | 
105 |   describe('Command Generation', () => {
106 |     it('should generate correct command with workspace minimal parameters', async () => {
107 |       // Manual call tracking for command verification
108 |       const calls: any[] = [];
109 |       const mockExecutor: CommandExecutor = async (...args) => {
110 |         calls.push(args);
111 |         return createMockCommandResponse({
112 |           success: true,
113 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
114 |           error: undefined,
115 |         });
116 |       };
117 | 
118 |       const args = {
119 |         workspacePath: '/path/to/MyProject.xcworkspace',
120 |         scheme: 'MyScheme',
121 |       };
122 | 
123 |       await get_mac_app_pathLogic(args, mockExecutor);
124 | 
125 |       // Verify command generation with manual call tracking
126 |       expect(calls).toHaveLength(1);
127 |       expect(calls[0]).toEqual([
128 |         [
129 |           'xcodebuild',
130 |           '-showBuildSettings',
131 |           '-workspace',
132 |           '/path/to/MyProject.xcworkspace',
133 |           '-scheme',
134 |           'MyScheme',
135 |           '-configuration',
136 |           'Debug',
137 |         ],
138 |         'Get App Path',
139 |         true,
140 |         undefined,
141 |       ]);
142 |     });
143 | 
144 |     it('should generate correct command with project minimal parameters', async () => {
145 |       // Manual call tracking for command verification
146 |       const calls: any[] = [];
147 |       const mockExecutor: CommandExecutor = async (...args) => {
148 |         calls.push(args);
149 |         return createMockCommandResponse({
150 |           success: true,
151 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
152 |           error: undefined,
153 |         });
154 |       };
155 | 
156 |       const args = {
157 |         projectPath: '/path/to/MyProject.xcodeproj',
158 |         scheme: 'MyScheme',
159 |       };
160 | 
161 |       await get_mac_app_pathLogic(args, mockExecutor);
162 | 
163 |       // Verify command generation with manual call tracking
164 |       expect(calls).toHaveLength(1);
165 |       expect(calls[0]).toEqual([
166 |         [
167 |           'xcodebuild',
168 |           '-showBuildSettings',
169 |           '-project',
170 |           '/path/to/MyProject.xcodeproj',
171 |           '-scheme',
172 |           'MyScheme',
173 |           '-configuration',
174 |           'Debug',
175 |         ],
176 |         'Get App Path',
177 |         true,
178 |         undefined,
179 |       ]);
180 |     });
181 | 
182 |     it('should generate correct command with workspace all parameters', async () => {
183 |       // Manual call tracking for command verification
184 |       const calls: any[] = [];
185 |       const mockExecutor: CommandExecutor = async (...args) => {
186 |         calls.push(args);
187 |         return createMockCommandResponse({
188 |           success: true,
189 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
190 |           error: undefined,
191 |         });
192 |       };
193 | 
194 |       const args = {
195 |         workspacePath: '/path/to/MyProject.xcworkspace',
196 |         scheme: 'MyScheme',
197 |         configuration: 'Release',
198 |         arch: 'arm64' as const,
199 |       };
200 | 
201 |       await get_mac_app_pathLogic(args, mockExecutor);
202 | 
203 |       // Verify command generation with manual call tracking
204 |       expect(calls).toHaveLength(1);
205 |       expect(calls[0]).toEqual([
206 |         [
207 |           'xcodebuild',
208 |           '-showBuildSettings',
209 |           '-workspace',
210 |           '/path/to/MyProject.xcworkspace',
211 |           '-scheme',
212 |           'MyScheme',
213 |           '-configuration',
214 |           'Release',
215 |           '-destination',
216 |           'platform=macOS,arch=arm64',
217 |         ],
218 |         'Get App Path',
219 |         true,
220 |         undefined,
221 |       ]);
222 |     });
223 | 
224 |     it('should generate correct command with x86_64 architecture', async () => {
225 |       // Manual call tracking for command verification
226 |       const calls: any[] = [];
227 |       const mockExecutor: CommandExecutor = async (...args) => {
228 |         calls.push(args);
229 |         return createMockCommandResponse({
230 |           success: true,
231 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
232 |           error: undefined,
233 |         });
234 |       };
235 | 
236 |       const args = {
237 |         workspacePath: '/path/to/MyProject.xcworkspace',
238 |         scheme: 'MyScheme',
239 |         configuration: 'Debug',
240 |         arch: 'x86_64' as const,
241 |       };
242 | 
243 |       await get_mac_app_pathLogic(args, mockExecutor);
244 | 
245 |       // Verify command generation with manual call tracking
246 |       expect(calls).toHaveLength(1);
247 |       expect(calls[0]).toEqual([
248 |         [
249 |           'xcodebuild',
250 |           '-showBuildSettings',
251 |           '-workspace',
252 |           '/path/to/MyProject.xcworkspace',
253 |           '-scheme',
254 |           'MyScheme',
255 |           '-configuration',
256 |           'Debug',
257 |           '-destination',
258 |           'platform=macOS,arch=x86_64',
259 |         ],
260 |         'Get App Path',
261 |         true,
262 |         undefined,
263 |       ]);
264 |     });
265 | 
266 |     it('should generate correct command with project all parameters', async () => {
267 |       // Manual call tracking for command verification
268 |       const calls: any[] = [];
269 |       const mockExecutor: CommandExecutor = async (...args) => {
270 |         calls.push(args);
271 |         return createMockCommandResponse({
272 |           success: true,
273 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
274 |           error: undefined,
275 |         });
276 |       };
277 | 
278 |       const args = {
279 |         projectPath: '/path/to/MyProject.xcodeproj',
280 |         scheme: 'MyScheme',
281 |         configuration: 'Release',
282 |         derivedDataPath: '/path/to/derived',
283 |         extraArgs: ['--verbose'],
284 |       };
285 | 
286 |       await get_mac_app_pathLogic(args, mockExecutor);
287 | 
288 |       // Verify command generation with manual call tracking
289 |       expect(calls).toHaveLength(1);
290 |       expect(calls[0]).toEqual([
291 |         [
292 |           'xcodebuild',
293 |           '-showBuildSettings',
294 |           '-project',
295 |           '/path/to/MyProject.xcodeproj',
296 |           '-scheme',
297 |           'MyScheme',
298 |           '-configuration',
299 |           'Release',
300 |           '-derivedDataPath',
301 |           '/path/to/derived',
302 |           '--verbose',
303 |         ],
304 |         'Get App Path',
305 |         true,
306 |         undefined,
307 |       ]);
308 |     });
309 | 
310 |     it('should use default configuration when not provided', async () => {
311 |       // Manual call tracking for command verification
312 |       const calls: any[] = [];
313 |       const mockExecutor: CommandExecutor = async (...args) => {
314 |         calls.push(args);
315 |         return createMockCommandResponse({
316 |           success: true,
317 |           output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app',
318 |           error: undefined,
319 |         });
320 |       };
321 | 
322 |       const args = {
323 |         workspacePath: '/path/to/MyProject.xcworkspace',
324 |         scheme: 'MyScheme',
325 |         arch: 'arm64' as const,
326 |       };
327 | 
328 |       await get_mac_app_pathLogic(args, mockExecutor);
329 | 
330 |       // Verify command generation with manual call tracking
331 |       expect(calls).toHaveLength(1);
332 |       expect(calls[0]).toEqual([
333 |         [
334 |           'xcodebuild',
335 |           '-showBuildSettings',
336 |           '-workspace',
337 |           '/path/to/MyProject.xcworkspace',
338 |           '-scheme',
339 |           'MyScheme',
340 |           '-configuration',
341 |           'Debug',
342 |           '-destination',
343 |           'platform=macOS,arch=arm64',
344 |         ],
345 |         'Get App Path',
346 |         true,
347 |         undefined,
348 |       ]);
349 |     });
350 |   });
351 | 
352 |   describe('Handler Behavior (Complete Literal Returns)', () => {
353 |     it('should return Zod validation error for missing scheme', async () => {
354 |       const result = await getMacAppPath.handler({
355 |         workspacePath: '/path/to/MyProject.xcworkspace',
356 |       });
357 | 
358 |       expect(result.isError).toBe(true);
359 |       expect(result.content[0].text).toContain('scheme is required');
360 |       expect(result.content[0].text).toContain('session-set-defaults');
361 |     });
362 | 
363 |     it('should return exact successful app path response with workspace', async () => {
364 |       const mockExecutor = createMockExecutor({
365 |         success: true,
366 |         output: `
367 | BUILT_PRODUCTS_DIR = /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug
368 | FULL_PRODUCT_NAME = MyApp.app
369 |         `,
370 |       });
371 | 
372 |       const result = await get_mac_app_pathLogic(
373 |         {
374 |           workspacePath: '/path/to/MyProject.xcworkspace',
375 |           scheme: 'MyScheme',
376 |         },
377 |         mockExecutor,
378 |       );
379 | 
380 |       expect(result).toEqual({
381 |         content: [
382 |           {
383 |             type: 'text',
384 |             text: '✅ App path retrieved successfully: /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app',
385 |           },
386 |           {
387 |             type: 'text',
388 |             text: 'Next Steps:\n1. Get bundle ID: get_app_bundle_id({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })\n2. Launch app: launch_mac_app({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })',
389 |           },
390 |         ],
391 |       });
392 |     });
393 | 
394 |     it('should return exact successful app path response with project', async () => {
395 |       const mockExecutor = createMockExecutor({
396 |         success: true,
397 |         output: `
398 | BUILT_PRODUCTS_DIR = /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug
399 | FULL_PRODUCT_NAME = MyApp.app
400 |         `,
401 |       });
402 | 
403 |       const result = await get_mac_app_pathLogic(
404 |         {
405 |           projectPath: '/path/to/MyProject.xcodeproj',
406 |           scheme: 'MyScheme',
407 |         },
408 |         mockExecutor,
409 |       );
410 | 
411 |       expect(result).toEqual({
412 |         content: [
413 |           {
414 |             type: 'text',
415 |             text: '✅ App path retrieved successfully: /Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app',
416 |           },
417 |           {
418 |             type: 'text',
419 |             text: 'Next Steps:\n1. Get bundle ID: get_app_bundle_id({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })\n2. Launch app: launch_mac_app({ appPath: "/Users/test/Library/Developer/Xcode/DerivedData/MyApp-abc123/Build/Products/Debug/MyApp.app" })',
420 |           },
421 |         ],
422 |       });
423 |     });
424 | 
425 |     it('should return exact build settings failure response', async () => {
426 |       const mockExecutor = createMockExecutor({
427 |         success: false,
428 |         error: 'error: No such scheme',
429 |       });
430 | 
431 |       const result = await get_mac_app_pathLogic(
432 |         {
433 |           workspacePath: '/path/to/MyProject.xcworkspace',
434 |           scheme: 'MyScheme',
435 |         },
436 |         mockExecutor,
437 |       );
438 | 
439 |       expect(result).toEqual({
440 |         content: [
441 |           {
442 |             type: 'text',
443 |             text: 'Error: Failed to get macOS app path\nDetails: error: No such scheme',
444 |           },
445 |         ],
446 |         isError: true,
447 |       });
448 |     });
449 | 
450 |     it('should return exact missing build settings response', async () => {
451 |       const mockExecutor = createMockExecutor({
452 |         success: true,
453 |         output: 'OTHER_SETTING = value',
454 |       });
455 | 
456 |       const result = await get_mac_app_pathLogic(
457 |         {
458 |           workspacePath: '/path/to/MyProject.xcworkspace',
459 |           scheme: 'MyScheme',
460 |         },
461 |         mockExecutor,
462 |       );
463 | 
464 |       expect(result).toEqual({
465 |         content: [
466 |           {
467 |             type: 'text',
468 |             text: 'Error: Failed to get macOS app path\nDetails: Could not extract app path from build settings',
469 |           },
470 |         ],
471 |         isError: true,
472 |       });
473 |     });
474 | 
475 |     it('should return exact exception handling response', async () => {
476 |       const mockExecutor = async () => {
477 |         throw new Error('Network error');
478 |       };
479 | 
480 |       const result = await get_mac_app_pathLogic(
481 |         {
482 |           workspacePath: '/path/to/MyProject.xcworkspace',
483 |           scheme: 'MyScheme',
484 |         },
485 |         mockExecutor,
486 |       );
487 | 
488 |       expect(result).toEqual({
489 |         content: [
490 |           {
491 |             type: 'text',
492 |             text: 'Error: Failed to get macOS app path\nDetails: Network error',
493 |           },
494 |         ],
495 |         isError: true,
496 |       });
497 |     });
498 |   });
499 | });
500 | 
```
Page 11/16FirstPrevNextLast