#
tokens: 46586/50000 11/393 files (page 9/16)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 9 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

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

```typescript
  1 | /**
  2 |  * Tests for list_devices plugin (device-shared)
  3 |  * This tests the re-exported plugin from device-workspace
  4 |  * Following CLAUDE.md testing standards with literal validation
  5 |  *
  6 |  * Note: This is a re-export test. Comprehensive handler tests are in device-workspace/list_devices.test.ts
  7 |  */
  8 | 
  9 | import { describe, it, expect } from 'vitest';
 10 | import {
 11 |   createMockCommandResponse,
 12 |   createMockExecutor,
 13 | } from '../../../../test-utils/mock-executors.ts';
 14 | 
 15 | // Import the logic function and re-export
 16 | import listDevices, { list_devicesLogic } from '../list_devices.ts';
 17 | 
 18 | describe('list_devices plugin (device-shared)', () => {
 19 |   describe('Export Field Validation (Literal)', () => {
 20 |     it('should export list_devicesLogic function', () => {
 21 |       expect(typeof list_devicesLogic).toBe('function');
 22 |     });
 23 | 
 24 |     it('should have correct name', () => {
 25 |       expect(listDevices.name).toBe('list_devices');
 26 |     });
 27 | 
 28 |     it('should have correct description', () => {
 29 |       expect(listDevices.description).toBe(
 30 |         'Lists connected physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) with their UUIDs, names, and connection status. Use this to discover physical devices for testing.',
 31 |       );
 32 |     });
 33 | 
 34 |     it('should have handler function', () => {
 35 |       expect(typeof listDevices.handler).toBe('function');
 36 |     });
 37 | 
 38 |     it('should have empty schema', () => {
 39 |       expect(listDevices.schema).toEqual({});
 40 |     });
 41 |   });
 42 | 
 43 |   describe('Command Generation Tests', () => {
 44 |     it('should generate correct devicectl command', async () => {
 45 |       const devicectlJson = {
 46 |         result: {
 47 |           devices: [
 48 |             {
 49 |               identifier: 'test-device-123',
 50 |               visibilityClass: 'Default',
 51 |               connectionProperties: {
 52 |                 pairingState: 'paired',
 53 |                 tunnelState: 'connected',
 54 |                 transportType: 'USB',
 55 |               },
 56 |               deviceProperties: {
 57 |                 name: 'Test iPhone',
 58 |                 platformIdentifier: 'com.apple.platform.iphoneos',
 59 |                 osVersionNumber: '17.0',
 60 |               },
 61 |               hardwareProperties: {
 62 |                 productType: 'iPhone15,2',
 63 |               },
 64 |             },
 65 |           ],
 66 |         },
 67 |       };
 68 | 
 69 |       // Track command calls
 70 |       const commandCalls: Array<{
 71 |         command: string[];
 72 |         logPrefix?: string;
 73 |         useShell?: boolean;
 74 |         env?: Record<string, string>;
 75 |       }> = [];
 76 | 
 77 |       // Create mock executor
 78 |       const mockExecutor = createMockExecutor({
 79 |         success: true,
 80 |         output: '',
 81 |       });
 82 | 
 83 |       // Wrap to track calls
 84 |       const trackingExecutor = async (
 85 |         command: string[],
 86 |         logPrefix?: string,
 87 |         useShell?: boolean,
 88 |         opts?: { env?: Record<string, string> },
 89 |         _detached?: boolean,
 90 |       ) => {
 91 |         commandCalls.push({ command, logPrefix, useShell, env: opts?.env });
 92 |         return mockExecutor(command, logPrefix, useShell, opts, _detached);
 93 |       };
 94 | 
 95 |       // Create mock path dependencies
 96 |       const mockPathDeps = {
 97 |         tmpdir: () => '/tmp',
 98 |         join: (...paths: string[]) => paths.join('/'),
 99 |       };
100 | 
101 |       // Create mock filesystem with specific behavior
102 |       const mockFsDeps = {
103 |         readFile: async (_path: string, _encoding?: string) => JSON.stringify(devicectlJson),
104 |         unlink: async () => {},
105 |       };
106 | 
107 |       await list_devicesLogic({}, trackingExecutor, mockPathDeps, mockFsDeps);
108 | 
109 |       expect(commandCalls).toHaveLength(1);
110 |       expect(commandCalls[0].command).toEqual([
111 |         'xcrun',
112 |         'devicectl',
113 |         'list',
114 |         'devices',
115 |         '--json-output',
116 |         '/tmp/devicectl-123.json',
117 |       ]);
118 |       expect(commandCalls[0].logPrefix).toBe('List Devices (devicectl with JSON)');
119 |       expect(commandCalls[0].useShell).toBe(true);
120 |       expect(commandCalls[0].env).toBeUndefined();
121 |     });
122 | 
123 |     it('should generate correct xctrace fallback command', async () => {
124 |       // Track command calls
125 |       const commandCalls: Array<{
126 |         command: string[];
127 |         logPrefix?: string;
128 |         useShell?: boolean;
129 |         env?: Record<string, string>;
130 |       }> = [];
131 | 
132 |       // Create tracking executor with call count behavior
133 |       let callCount = 0;
134 |       const trackingExecutor = async (
135 |         command: string[],
136 |         logPrefix?: string,
137 |         useShell?: boolean,
138 |         opts?: { env?: Record<string, string> },
139 |         _detached?: boolean,
140 |       ) => {
141 |         callCount++;
142 |         commandCalls.push({ command, logPrefix, useShell, env: opts?.env });
143 | 
144 |         if (callCount === 1) {
145 |           // First call fails (devicectl)
146 |           return createMockCommandResponse({
147 |             success: false,
148 |             output: '',
149 |             error: 'devicectl failed',
150 |           });
151 |         } else {
152 |           // Second call succeeds (xctrace)
153 |           return createMockCommandResponse({
154 |             success: true,
155 |             output: 'iPhone 15 (12345678-1234-1234-1234-123456789012)',
156 |             error: undefined,
157 |           });
158 |         }
159 |       };
160 | 
161 |       // Create mock path dependencies
162 |       const mockPathDeps = {
163 |         tmpdir: () => '/tmp',
164 |         join: (...paths: string[]) => paths.join('/'),
165 |       };
166 | 
167 |       // Create mock filesystem that throws for readFile
168 |       const mockFsDeps = {
169 |         readFile: async () => {
170 |           throw new Error('File not found');
171 |         },
172 |         unlink: async () => {},
173 |       };
174 | 
175 |       await list_devicesLogic({}, trackingExecutor, mockPathDeps, mockFsDeps);
176 | 
177 |       expect(commandCalls).toHaveLength(2);
178 |       expect(commandCalls[1].command).toEqual(['xcrun', 'xctrace', 'list', 'devices']);
179 |       expect(commandCalls[1].logPrefix).toBe('List Devices (xctrace)');
180 |       expect(commandCalls[1].useShell).toBe(true);
181 |       expect(commandCalls[1].env).toBeUndefined();
182 |     });
183 |   });
184 | 
185 |   describe('Success Path Tests', () => {
186 |     it('should return successful devicectl response with parsed devices', async () => {
187 |       const devicectlJson = {
188 |         result: {
189 |           devices: [
190 |             {
191 |               identifier: 'test-device-123',
192 |               visibilityClass: 'Default',
193 |               connectionProperties: {
194 |                 pairingState: 'paired',
195 |                 tunnelState: 'connected',
196 |                 transportType: 'USB',
197 |               },
198 |               deviceProperties: {
199 |                 name: 'Test iPhone',
200 |                 platformIdentifier: 'com.apple.platform.iphoneos',
201 |                 osVersionNumber: '17.0',
202 |               },
203 |               hardwareProperties: {
204 |                 productType: 'iPhone15,2',
205 |               },
206 |             },
207 |           ],
208 |         },
209 |       };
210 | 
211 |       const mockExecutor = createMockExecutor({
212 |         success: true,
213 |         output: '',
214 |       });
215 | 
216 |       // Create mock path dependencies
217 |       const mockPathDeps = {
218 |         tmpdir: () => '/tmp',
219 |         join: (...paths: string[]) => paths.join('/'),
220 |       };
221 | 
222 |       // Create mock filesystem with specific behavior
223 |       const mockFsDeps = {
224 |         readFile: async (_path: string, _encoding?: string) => JSON.stringify(devicectlJson),
225 |         unlink: async () => {},
226 |       };
227 | 
228 |       const result = await list_devicesLogic({}, mockExecutor, mockPathDeps, mockFsDeps);
229 | 
230 |       expect(result).toEqual({
231 |         content: [
232 |           {
233 |             type: 'text',
234 |             text: "Connected Devices:\n\n✅ Available Devices:\n\n📱 Test iPhone\n   UDID: test-device-123\n   Model: iPhone15,2\n   Product Type: iPhone15,2\n   Platform: iOS 17.0\n   Connection: USB\n\nNext Steps:\n1. Build for device: build_device({ scheme: 'SCHEME', deviceId: 'DEVICE_UDID' })\n2. Run tests: test_device({ scheme: 'SCHEME', deviceId: 'DEVICE_UDID' })\n3. Get app path: get_device_app_path({ scheme: 'SCHEME' })\n\nNote: Use the device ID/UDID from above when required by other tools.\nHint: Save a default device with session-set-defaults { deviceId: 'DEVICE_UDID' }.\n",
235 |           },
236 |         ],
237 |       });
238 |     });
239 | 
240 |     it('should return successful xctrace fallback response', async () => {
241 |       // Create executor with call count behavior
242 |       let callCount = 0;
243 |       const mockExecutor = async (
244 |         _command: string[],
245 |         _logPrefix?: string,
246 |         _useShell?: boolean,
247 |         _opts?: { env?: Record<string, string> },
248 |         _detached?: boolean,
249 |       ) => {
250 |         callCount++;
251 |         if (callCount === 1) {
252 |           // First call fails (devicectl)
253 |           return createMockCommandResponse({
254 |             success: false,
255 |             output: '',
256 |             error: 'devicectl failed',
257 |           });
258 |         } else {
259 |           // Second call succeeds (xctrace)
260 |           return createMockCommandResponse({
261 |             success: true,
262 |             output: 'iPhone 15 (12345678-1234-1234-1234-123456789012)',
263 |             error: undefined,
264 |           });
265 |         }
266 |       };
267 | 
268 |       // Create mock path dependencies
269 |       const mockPathDeps = {
270 |         tmpdir: () => '/tmp',
271 |         join: (...paths: string[]) => paths.join('/'),
272 |       };
273 | 
274 |       // Create mock filesystem that throws for readFile
275 |       const mockFsDeps = {
276 |         readFile: async () => {
277 |           throw new Error('File not found');
278 |         },
279 |         unlink: async () => {},
280 |       };
281 | 
282 |       const result = await list_devicesLogic({}, mockExecutor, mockPathDeps, mockFsDeps);
283 | 
284 |       expect(result).toEqual({
285 |         content: [
286 |           {
287 |             type: 'text',
288 |             text: 'Device listing (xctrace output):\n\niPhone 15 (12345678-1234-1234-1234-123456789012)\n\nNote: For better device information, please upgrade to Xcode 15 or later which supports the modern devicectl command.',
289 |           },
290 |         ],
291 |       });
292 |     });
293 | 
294 |     it('should return successful no devices found response', async () => {
295 |       const devicectlJson = {
296 |         result: {
297 |           devices: [],
298 |         },
299 |       };
300 | 
301 |       // Create executor with call count behavior
302 |       let callCount = 0;
303 |       const mockExecutor = async (
304 |         _command: string[],
305 |         _logPrefix?: string,
306 |         _useShell?: boolean,
307 |         _opts?: { env?: Record<string, string> },
308 |         _detached?: boolean,
309 |       ) => {
310 |         callCount++;
311 |         if (callCount === 1) {
312 |           // First call succeeds (devicectl)
313 |           return createMockCommandResponse({
314 |             success: true,
315 |             output: '',
316 |             error: undefined,
317 |           });
318 |         } else {
319 |           // Second call succeeds (xctrace) with empty output
320 |           return createMockCommandResponse({
321 |             success: true,
322 |             output: '',
323 |             error: undefined,
324 |           });
325 |         }
326 |       };
327 | 
328 |       // Create mock path dependencies
329 |       const mockPathDeps = {
330 |         tmpdir: () => '/tmp',
331 |         join: (...paths: string[]) => paths.join('/'),
332 |       };
333 | 
334 |       // Create mock filesystem with empty devices response
335 |       const mockFsDeps = {
336 |         readFile: async (_path: string, _encoding?: string) => JSON.stringify(devicectlJson),
337 |         unlink: async () => {},
338 |       };
339 | 
340 |       const result = await list_devicesLogic({}, mockExecutor, mockPathDeps, mockFsDeps);
341 | 
342 |       expect(result).toEqual({
343 |         content: [
344 |           {
345 |             type: 'text',
346 |             text: 'Device listing (xctrace output):\n\n\n\nNote: For better device information, please upgrade to Xcode 15 or later which supports the modern devicectl command.',
347 |           },
348 |         ],
349 |       });
350 |     });
351 |   });
352 | 
353 |   // Note: Handler functionality is thoroughly tested in device-workspace/list_devices.test.ts
354 |   // This test file only verifies the re-export works correctly
355 | });
356 | 
```

--------------------------------------------------------------------------------
/.claude/agents/xcodebuild-mcp-qa-tester.md:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | name: xcodebuild-mcp-qa-tester
  3 | description: Use this agent when you need comprehensive black box testing of the XcodeBuildMCP server using Reloaderoo. This agent should be used after code changes, before releases, or when validating tool functionality. Examples:\n\n- <example>\n  Context: The user has made changes to XcodeBuildMCP tools and wants to validate everything works correctly.\n  user: "I've updated the simulator tools and need to make sure they all work properly"\n  assistant: "I'll use the xcodebuild-mcp-qa-tester agent to perform comprehensive black box testing of all simulator tools using Reloaderoo"\n  <commentary>\n  Since the user needs thorough testing of XcodeBuildMCP functionality, use the xcodebuild-mcp-qa-tester agent to systematically validate all tools and resources.\n  </commentary>\n</example>\n\n- <example>\n  Context: The user is preparing for a release and needs full QA validation.\n  user: "We're about to release version 2.1.0 and need complete testing coverage"\n  assistant: "I'll launch the xcodebuild-mcp-qa-tester agent to perform thorough black box testing of all XcodeBuildMCP tools and resources following the manual testing procedures"\n  <commentary>\n  For release validation, the QA tester agent should perform comprehensive testing to ensure all functionality works as expected.\n  </commentary>\n</example>
  4 | tools: Task, Bash, Glob, Grep, LS, ExitPlanMode, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, ListMcpResourcesTool, ReadMcpResourceTool
  5 | color: purple
  6 | ---
  7 | 
  8 | You are a senior quality assurance software engineer specializing in black box testing of the XcodeBuildMCP server. Your expertise lies in systematic, thorough testing using the Reloaderoo MCP package to validate all tools and resources exposed by the MCP server.
  9 | 
 10 | ## Your Core Responsibilities
 11 | 
 12 | 1. **Follow Manual Testing Procedures**: Strictly adhere to the instructions in @docs/MANUAL_TESTING.md for systematic test execution
 13 | 2. **Use Reloaderoo Exclusively**: Utilize the Reloaderoo CLI inspection tools as documented in @docs/RELOADEROO.md for all testing activities
 14 | 3. **Comprehensive Coverage**: Test ALL tools and resources - never skip or assume functionality works
 15 | 4. **Black Box Approach**: Test from the user perspective without knowledge of internal implementation details
 16 | 5. **Live Documentation**: Create and continuously update a markdown test report showing real-time progress
 17 | 6. **MANDATORY COMPLETION**: Continue testing until EVERY SINGLE tool and resource has been tested - DO NOT STOP until 100% completion is achieved
 18 | 
 19 | ## MANDATORY Test Report Creation and Updates
 20 | 
 21 | ### Step 1: Create Initial Test Report (IMMEDIATELY)
 22 | **BEFORE TESTING BEGINS**, you MUST:
 23 | 
 24 | 1. **Create Test Report File**: Generate a markdown file in the workspace root named `TESTING_REPORT_<YYYY-MM-DD>_<HH-MM>.md`
 25 | 2. **Include Report Header**: Date, time, environment information, and testing scope
 26 | 3. **Discovery Phase**: Run `list-tools` and `list-resources` to get complete inventory
 27 | 4. **Create Checkbox Lists**: Add unchecked markdown checkboxes for every single tool and resource discovered
 28 | 
 29 | ### Test Report Initial Structure
 30 | ```markdown
 31 | # XcodeBuildMCP Testing Report
 32 | **Date:** YYYY-MM-DD HH:MM:SS  
 33 | **Environment:** [System details]  
 34 | **Testing Scope:** Comprehensive black box testing of all tools and resources
 35 | 
 36 | ## Test Summary
 37 | - **Total Tools:** [X]
 38 | - **Total Resources:** [Y]
 39 | - **Tests Completed:** 0/[X+Y]
 40 | - **Tests Passed:** 0
 41 | - **Tests Failed:** 0
 42 | 
 43 | ## Tools Testing Checklist
 44 | - [ ] Tool: tool_name_1 - Test with valid parameters
 45 | - [ ] Tool: tool_name_2 - Test with valid parameters
 46 | [... all tools discovered ...]
 47 | 
 48 | ## Resources Testing Checklist  
 49 | - [ ] Resource: resource_uri_1 - Validate content and accessibility
 50 | - [ ] Resource: resource_uri_2 - Validate content and accessibility
 51 | [... all resources discovered ...]
 52 | 
 53 | ## Detailed Test Results
 54 | [Updated as tests are completed]
 55 | 
 56 | ## Failed Tests
 57 | [Updated if any failures occur]
 58 | ```
 59 | 
 60 | ### Step 2: Continuous Updates (AFTER EACH TEST)
 61 | **IMMEDIATELY after completing each test**, you MUST update the test report with:
 62 | 
 63 | 1. **Check the box**: Change `- [ ]` to `- [x]` for the completed test
 64 | 2. **Update test summary counts**: Increment completed/passed/failed counters
 65 | 3. **Add detailed result**: Append to "Detailed Test Results" section with:
 66 |    - Test command used
 67 |    - Verification method
 68 |    - Validation summary
 69 |    - Pass/fail status
 70 | 
 71 | ### Live Update Example
 72 | After testing `list_sims` tool, update the report:
 73 | ```markdown
 74 | - [x] Tool: list_sims - Test with valid parameters ✅ PASSED
 75 | 
 76 | ## Detailed Test Results
 77 | 
 78 | ### Tool: list_sims ✅ PASSED
 79 | **Command:** `npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/index.js`
 80 | **Verification:** Command returned JSON array with 6 simulator objects
 81 | **Validation Summary:** Successfully discovered 6 available simulators with UUIDs, names, and boot status
 82 | **Timestamp:** 2025-01-29 14:30:15
 83 | ```
 84 | 
 85 | ## Testing Methodology
 86 | 
 87 | ### Pre-Testing Setup
 88 | - Always start by building the project: `npm run build`
 89 | - Verify Reloaderoo is available: `npx reloaderoo@latest --help`
 90 | - Check server connectivity: `npx reloaderoo@latest inspect ping -- node build/index.js`
 91 | - Get server information: `npx reloaderoo@latest inspect server-info -- node build/index.js`
 92 | 
 93 | ### Systematic Testing Workflow
 94 | 1. **Create Initial Report**: Generate test report with all checkboxes unchecked
 95 | 2. **Individual Testing**: Test each tool/resource systematically
 96 | 3. **Live Updates**: Update report immediately after each test completion
 97 | 4. **Continuous Tracking**: Report serves as real-time progress tracker
 98 | 5. **CONTINUOUS EXECUTION**: Never stop until ALL tools and resources are tested (100% completion)
 99 | 6. **Progress Monitoring**: Check total tested vs total available - continue if any remain untested
100 | 7. **Final Review**: Ensure all checkboxes are marked and results documented
101 | 
102 | ### CRITICAL: NO EARLY TERMINATION
103 | - **NEVER STOP** testing until every single tool and resource has been tested
104 | - If you have tested X out of Y items, IMMEDIATELY continue testing the remaining Y-X items
105 | - The only acceptable completion state is 100% coverage (all checkboxes checked)
106 | - Do not summarize or conclude until literally every tool and resource has been individually tested
107 | - Use the test report checkbox count as your progress indicator - if any boxes remain unchecked, CONTINUE TESTING
108 | 
109 | ### Tool Testing Process
110 | For each tool:
111 | 1. Execute test with `npx reloaderoo@latest inspect call-tool <tool_name> --params '<json>' -- node build/index.js`
112 | 2. Verify response format and content
113 | 3. **IMMEDIATELY** update test report with result
114 | 4. Check the box and add detailed verification summary
115 | 5. Move to next tool
116 | 
117 | ### Resource Testing Process
118 | For each resource:
119 | 1. Execute test with `npx reloaderoo@latest inspect read-resource "<uri>" -- node build/index.js`
120 | 2. Verify resource accessibility and content format
121 | 3. **IMMEDIATELY** update test report with result
122 | 4. Check the box and add detailed verification summary
123 | 5. Move to next resource
124 | 
125 | ## Quality Standards
126 | 
127 | ### Thoroughness Over Speed
128 | - **NEVER rush testing** - take time to be comprehensive
129 | - Test every single tool and resource without exception
130 | - Update the test report after every single test - no batching
131 | - The markdown report is the single source of truth for progress
132 | 
133 | ### Test Documentation Requirements
134 | - Record the exact command used for each test
135 | - Document expected vs actual results
136 | - Note any warnings, errors, or unexpected behavior
137 | - Include full JSON responses for failed tests
138 | - Categorize issues by severity (critical, major, minor)
139 | - **MANDATORY**: Update test report immediately after each test completion
140 | 
141 | ### Validation Criteria
142 | - All tools must respond without errors for valid inputs
143 | - Error messages must be clear and actionable for invalid inputs
144 | - JSON responses must be properly formatted
145 | - Resource URIs must be accessible and return valid data
146 | - Tool descriptions must accurately reflect functionality
147 | 
148 | ## Testing Environment Considerations
149 | 
150 | ### Prerequisites Validation
151 | - Verify Xcode is installed and accessible
152 | - Check for required simulators and devices
153 | - Validate development environment setup
154 | - Ensure all dependencies are available
155 | 
156 | ### Platform-Specific Testing
157 | - Test iOS simulator tools with actual simulators
158 | - Validate device tools (when devices are available)
159 | - Test macOS-specific functionality
160 | - Verify Swift Package Manager integration
161 | 
162 | ## Test Report Management
163 | 
164 | ### File Naming Convention
165 | - Format: `TESTING_REPORT_<YYYY-MM-DD>_<HH-MM>.md`
166 | - Location: Workspace root directory
167 | - Example: `TESTING_REPORT_2025-01-29_14-30.md`
168 | 
169 | ### Update Requirements
170 | - **Real-time updates**: Update after every single test completion
171 | - **No batching**: Never wait to update multiple tests at once
172 | - **Checkbox tracking**: Visual progress through checked/unchecked boxes
173 | - **Detailed results**: Each test gets a dedicated result section
174 | - **Summary statistics**: Keep running totals updated
175 | 
176 | ### Verification Summary Requirements
177 | Every test result MUST answer: "How did you know this test passed?"
178 | 
179 | Examples of strong verification summaries:
180 | - `Successfully discovered 84 tools in server response`
181 | - `Returned valid app bundle path: /path/to/MyApp.app`
182 | - `Listed 6 simulators with expected UUID format and boot status`
183 | - `Resource returned JSON array with 4 device objects containing UDID and name fields`
184 | - `Tool correctly rejected invalid parameters with clear error message`
185 | 
186 | ## Error Investigation Protocol
187 | 
188 | 1. **Reproduce Consistently**: Ensure errors can be reproduced reliably
189 | 2. **Isolate Variables**: Test with minimal parameters to isolate issues
190 | 3. **Check Prerequisites**: Verify all required tools and environments are available
191 | 4. **Document Context**: Include system information, versions, and environment details
192 | 5. **Update Report**: Document failures immediately in the test report
193 | 
194 | ## Critical Success Criteria
195 | 
196 | - ✅ Test report created BEFORE any testing begins with all checkboxes unchecked
197 | - ✅ Every single tool has its own checkbox and detailed result section
198 | - ✅ Every single resource has its own checkbox and detailed result section
199 | - ✅ Report updated IMMEDIATELY after each individual test completion
200 | - ✅ No tool or resource is skipped or grouped together
201 | - ✅ Each verification summary clearly explains how success was determined
202 | - ✅ Real-time progress tracking through checkbox completion
203 | - ✅ Test report serves as the single source of truth for all testing progress
204 | - ✅ **100% COMPLETION MANDATORY**: All checkboxes must be checked before considering testing complete
205 | 
206 | ## ABSOLUTE COMPLETION REQUIREMENT
207 | 
208 | **YOU MUST NOT STOP TESTING UNTIL:**
209 | - Every single tool discovered by `list-tools` has been individually tested
210 | - Every single resource discovered by `list-resources` has been individually tested  
211 | - All checkboxes in your test report are marked as complete
212 | - The test summary shows X/X completion (100%)
213 | 
214 | **IF TESTING IS NOT 100% COMPLETE:**
215 | - Immediately identify which tools/resources remain untested
216 | - Continue systematic testing of the remaining items
217 | - Update the test report after each additional test
218 | - Do not provide final summaries or conclusions until literally everything is tested
219 | 
220 | Remember: Your role is to be the final quality gate before release. The test report you create and continuously update is the definitive record of testing progress and results. Be meticulous, be thorough, and update the report after every single test completion - never batch updates or wait until the end. **NEVER CONCLUDE TESTING UNTIL 100% COMPLETION IS ACHIEVED.**
221 | 
```

--------------------------------------------------------------------------------
/docs/DEBUGGING_ARCHITECTURE.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Debugging Architecture
  2 | 
  3 | This document describes how the simulator debugging tools are wired, how sessions are managed,
  4 | and how external tools (simctl, Simulator, LLDB, xcodebuild) are invoked.
  5 | 
  6 | ## Scope
  7 | 
  8 | - Tools: `src/mcp/tools/debugging/*`
  9 | - Debugger subsystem: `src/utils/debugger/*`
 10 | - Execution and tool wiring: `src/utils/typed-tool-factory.ts`, `src/utils/execution/*`
 11 | - External invocation: `xcrun simctl`, `xcrun lldb`, `xcodebuild`
 12 | 
 13 | ## Registration and Wiring
 14 | 
 15 | - Workflow discovery is automatic: `src/core/plugin-registry.ts` loads debugging tools via the
 16 |   generated workflow loaders (`src/core/generated-plugins.ts`).
 17 | - Tool handlers are created with the typed tool factory:
 18 |   - `createTypedToolWithContext` for standard tools (Zod validation + dependency injection).
 19 |   - `createSessionAwareToolWithContext` for session-aware tools (merges session defaults and
 20 |     validates requirements).
 21 | - Debugging tools inject a `DebuggerToolContext` that provides:
 22 |   - `executor`: a `CommandExecutor` used for simctl and other command execution.
 23 |   - `debugger`: a shared `DebuggerManager` instance.
 24 | 
 25 | ## Session Defaults and Validation
 26 | 
 27 | - Session defaults live in `src/utils/session-store.ts` and are merged with user args by the
 28 |   session-aware tool factory.
 29 | - `debug_attach_sim` is session-aware; it can omit `simulatorId`/`simulatorName` in the public
 30 |   schema and rely on session defaults.
 31 | - The `XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS` env flag exposes legacy schemas that include all
 32 |   parameters (no session default hiding).
 33 | 
 34 | ## Debug Session Lifecycle
 35 | 
 36 | `DebuggerManager` owns lifecycle, state, and backend routing:
 37 | 
 38 | Backend selection happens inside `DebuggerManager.createSession`:
 39 | 
 40 | - Selection order: explicit `backend` argument -> `XCODEBUILDMCP_DEBUGGER_BACKEND` -> default `lldb-cli`.
 41 | - Env values: `lldb-cli`/`lldb` -> `lldb-cli`, `dap` -> `dap`, anything else throws.
 42 | - Backend factory: `defaultBackendFactory` maps `lldb-cli` to `createLldbCliBackend` and `dap` to
 43 |   `createDapBackend`. A custom factory can be injected for tests or extensions.
 44 | 
 45 | 1. `debug_attach_sim` resolves simulator UUID and PID, then calls
 46 |    `DebuggerManager.createSession`.
 47 | 2. `DebuggerManager` creates a backend (default `lldb-cli`), attaches to the process, and stores
 48 |    session metadata (id, simulatorId, pid, timestamps).
 49 | 3. Debugging tools (`debug_lldb_command`, `debug_stack`, `debug_variables`,
 50 |    `debug_breakpoint_add/remove`) look up the session (explicit id or current) and route commands
 51 |    to the backend.
 52 | 4. `debug_detach` calls `DebuggerManager.detachSession` to detach and dispose the backend.
 53 | 
 54 | ## Debug Session + Command Execution Flow
 55 | 
 56 | Session lifecycle flow (text):
 57 | 
 58 | 1. Client calls `debug_attach_sim`.
 59 | 2. `debug_attach_sim` resolves simulator UUID and PID, then calls `DebuggerManager.createSession`.
 60 | 3. `DebuggerManager.createSession` resolves backend kind (explicit/env/default), instantiates the
 61 |    backend, and calls `backend.attach`.
 62 | 4. Command tools (`debug_lldb_command`, `debug_stack`, `debug_variables`) call
 63 |    `DebuggerManager.runCommand`/`getStack`/`getVariables`, which route to the backend.
 64 | 5. `debug_detach` calls `DebuggerManager.detachSession`, which invokes `backend.detach` and
 65 |    `backend.dispose`.
 66 | 
 67 | `LldbCliBackend.runCommand()` flow (text):
 68 | 
 69 | 1. Enqueue the command to serialize LLDB access.
 70 | 2. Await backend readiness (`initialize` completed).
 71 | 3. Write the command to the interactive process.
 72 | 4. Write `script print("__XCODEBUILDMCP_DONE__")` to emit the sentinel marker.
 73 | 5. Buffer stdout/stderr until the sentinel is detected.
 74 | 6. Trim the buffer to the next prompt, sanitize output, and return the result.
 75 | 
 76 | <details>
 77 | <summary>Sequence diagrams (Mermaid)</summary>
 78 | 
 79 | ```mermaid
 80 | sequenceDiagram
 81 |   participant U as User/Client
 82 |   participant A as debug_attach_sim
 83 |   participant M as DebuggerManager
 84 |   participant F as backendFactory
 85 |   participant B as DebuggerBackend (lldb-cli|dap)
 86 |   participant L as LldbCliBackend
 87 |   participant P as InteractiveProcess (xcrun lldb)
 88 | 
 89 |   U->>A: debug_attach_sim(simulator*, bundleId|pid)
 90 |   A->>A: determineSimulatorUuid(...)
 91 |   A->>A: resolveSimulatorAppPid(...) (if bundleId)
 92 |   A->>M: createSession({simulatorId, pid, waitFor})
 93 |   M->>M: resolveBackendKind(explicit/env/default)
 94 |   M->>F: create backend(kind)
 95 |   F-->>M: backend instance
 96 |   M->>B: attach({pid, simulatorId, waitFor})
 97 |   alt kind == lldb-cli
 98 |     B-->>L: (is LldbCliBackend)
 99 |     L->>P: spawn xcrun lldb + initialize prompt/sentinel
100 |   else kind == dap
101 |     B-->>M: throws DAP_ERROR_MESSAGE
102 |   end
103 |   M-->>A: DebugSessionInfo {id, backend, ...}
104 |   A->>M: setCurrentSession(id) (optional)
105 |   U->>M: runCommand(id?, "thread backtrace")
106 |   M->>B: runCommand(...)
107 |   U->>M: detachSession(id?)
108 |   M->>B: detach()
109 |   M->>B: dispose()
110 | ```
111 | 
112 | ```mermaid
113 | sequenceDiagram
114 |   participant T as debug_lldb_command
115 |   participant M as DebuggerManager
116 |   participant L as LldbCliBackend
117 |   participant P as InteractiveProcess
118 |   participant S as stdout/stderr buffer
119 | 
120 |   T->>M: runCommand(sessionId?, command, {timeoutMs?})
121 |   M->>L: runCommand(command)
122 |   L->>L: enqueue(work)
123 |   L->>L: await ready (initialize())
124 |   L->>P: write(command + "\n")
125 |   L->>P: write('script print("__XCODEBUILDMCP_DONE__")\n')
126 |   P-->>S: stdout/stderr chunks
127 |   S-->>L: handleData() appends to buffer
128 |   L->>L: checkPending() finds sentinel
129 |   L->>L: slice output up to sentinel
130 |   L->>L: trim buffer to next prompt (if present)
131 |   L->>L: sanitizeOutput() + trimEnd()
132 |   L-->>M: output string
133 |   M-->>T: output string
134 | ```
135 | 
136 | </details>
137 | 
138 | ## LLDB CLI Backend (Default)
139 | 
140 | - Backend implementation: `src/utils/debugger/backends/lldb-cli-backend.ts`.
141 | - Uses `InteractiveSpawner` from `src/utils/execution/interactive-process.ts` to keep a single
142 |   long-lived `xcrun lldb` process alive.
143 | - Keeps LLDB state (breakpoints, selected frames, target) across tool calls without reattaching.
144 | 
145 | ### Internals: interactive process model
146 | 
147 | - The backend spawns `xcrun lldb --no-lldbinit -o "settings set prompt <prompt>"`.
148 | - `InteractiveProcess.write()` is used to send commands; stdout and stderr are merged into a single
149 |   parse buffer.
150 | - `InteractiveProcess.dispose()` closes stdin, removes listeners, and kills the process.
151 | 
152 | ### Prompt and sentinel protocol
153 | 
154 | The backend uses a prompt + sentinel protocol to detect command completion reliably:
155 | 
156 | - `LLDB_PROMPT = "XCODEBUILDMCP_LLDB> "`
157 | - `COMMAND_SENTINEL = "__XCODEBUILDMCP_DONE__"`
158 | 
159 | Definitions:
160 | 
161 | - Prompt: the LLDB REPL prompt string that indicates LLDB is ready to accept the next command.
162 | - Sentinel: a unique marker explicitly printed after each command to mark the end of that
163 |   command's output.
164 | 
165 | Protocol flow:
166 | 
167 | 1. Startup: write `script print("__XCODEBUILDMCP_DONE__")` to prime the prompt parser.
168 | 2. For each command:
169 |    - Write the command.
170 |    - Write `script print("__XCODEBUILDMCP_DONE__")`.
171 |    - Read until the sentinel is observed, then trim up to the next prompt.
172 | 
173 | The sentinel marks command completion, while the prompt indicates the REPL is ready for the next
174 | command.
175 | 
176 | Why both a prompt and a sentinel?
177 | 
178 | - The sentinel is the explicit end-of-output marker; LLDB does not provide a reliable boundary for
179 |   arbitrary command output otherwise.
180 | - The prompt is used to cleanly align the buffer for the next command after the sentinel is seen.
181 | 
182 | Annotated example (simplified):
183 | 
184 | 1. Backend writes:
185 |    - `thread backtrace`
186 |    - `script print("__XCODEBUILDMCP_DONE__")`
187 | 2. LLDB emits (illustrative):
188 |    - `... thread backtrace output ...`
189 |    - `__XCODEBUILDMCP_DONE__`
190 |    - `XCODEBUILDMCP_LLDB> `
191 | 3. Parser behavior:
192 |    - Sentinel marks the end of the command output payload.
193 |    - Prompt is used to trim the buffer, so the next command starts cleanly.
194 | 
195 | ### Output parsing and sanitization
196 | 
197 | - `handleData()` appends to an internal buffer, and `checkPending()` scans for the sentinel regex
198 |   `/(^|\\r?\\n)__XCODEBUILDMCP_DONE__(\\r?\\n)/`.
199 | - Output is the buffer up to the sentinel. The remainder is trimmed to the next prompt, if present.
200 | - `sanitizeOutput()` removes prompt echoes, sentinel lines, the `script print(...)` lines, and empty
201 |   lines, then `runCommand()` returns `trimEnd()` output.
202 | 
203 | ### Concurrency model (queueing)
204 | 
205 | - Commands are serialized through a promise queue to avoid interleaved output.
206 | - `waitForSentinel()` rejects if a pending command exists, acting as a safety check.
207 | 
208 | ### Timeouts, errors, and disposal
209 | 
210 | - Startup timeout: `DEFAULT_STARTUP_TIMEOUT_MS = 10_000`.
211 | - Per-command timeout: `DEFAULT_COMMAND_TIMEOUT_MS = 30_000` (override via `runCommand` opts).
212 | - Timeout failure clears the pending command and rejects the promise.
213 | - `assertNoLldbError()` throws if `/error:/i` appears in output (simple heuristic).
214 | - Process exit triggers `failPending(new Error(...))` so in-flight calls fail promptly.
215 | - `runCommand()` rejects immediately if the backend is already disposed.
216 | 
217 | ### Testing and injection
218 | 
219 | `getDefaultInteractiveSpawner()` throws in test environments to prevent spawning real interactive
220 | processes. Tests should inject a mock `InteractiveSpawner` into `createLldbCliBackend()` or a custom
221 | `DebuggerManager` backend factory.
222 | 
223 | ## DAP Backend (lldb-dap)
224 | 
225 | - Implementation: `src/utils/debugger/backends/dap-backend.ts`, with protocol support in
226 |   `src/utils/debugger/dap/transport.ts`, `src/utils/debugger/dap/types.ts`, and adapter discovery in
227 |   `src/utils/debugger/dap/adapter-discovery.ts`.
228 | - Selected via backend selection (explicit `backend`, `XCODEBUILDMCP_DEBUGGER_BACKEND=dap`, or default when unset).
229 | - Adapter discovery uses `xcrun --find lldb-dap`; missing adapters raise a clear dependency error.
230 | - One `lldb-dap` process is spawned per session; DAP framing and request correlation are handled
231 |   by `DapTransport`.
232 | - Session handshake: `initialize` → `attach` → `configurationDone`.
233 | - Breakpoints are stateful: adding/removing re-issues `setBreakpoints` or
234 |   `setFunctionBreakpoints` with the remaining list. Conditions are passed in the request body.
235 | - Stack/variables typically require a stopped thread; the backend returns guidance if the process
236 |   is still running.
237 | 
238 | ## External Tool Invocation
239 | 
240 | ### simctl and Simulator
241 | 
242 | - Simulator UUID resolution uses `xcrun simctl list devices available -j`
243 |   (`determineSimulatorUuid` in `src/utils/simulator-utils.ts`).
244 | - PID lookup uses `xcrun simctl spawn <simulatorId> launchctl list`
245 |   (`resolveSimulatorAppPid` in `src/utils/debugger/simctl.ts`).
246 | 
247 | ### LLDB
248 | 
249 | - Attachment uses `xcrun lldb --no-lldbinit` in the interactive backend.
250 | - Breakpoint conditions are applied internally by the LLDB CLI backend using
251 |   `breakpoint modify -c "<condition>" <id>` after creation.
252 | 
253 | ### xcodebuild (Build/Launch Context)
254 | 
255 | - Debugging assumes a running simulator app.
256 | - The typical flow is to build and launch via simulator tools (for example `build_sim`),
257 |   which uses `executeXcodeBuildCommand` to invoke `xcodebuild` (or `xcodemake` when enabled).
258 | - After launch, `debug_attach_sim` attaches LLDB to the simulator process.
259 | 
260 | ## Typical Tool Flow
261 | 
262 | 1. Build and launch the app on a simulator (`build_sim`, `launch_app_sim`).
263 | 2. Attach LLDB (`debug_attach_sim`) using session defaults or explicit simulator + bundle ID.
264 | 3. Set breakpoints (`debug_breakpoint_add`), inspect stack/variables (`debug_stack`,
265 |    `debug_variables`), and issue arbitrary LLDB commands (`debug_lldb_command`).
266 | 4. Detach when done (`debug_detach`).
267 | 
268 | ## Integration Points Summary
269 | 
270 | - Tool entrypoints: `src/mcp/tools/debugging/*`
271 | - Session defaults: `src/utils/session-store.ts`
272 | - Debug session manager: `src/utils/debugger/debugger-manager.ts`
273 | - Backends: `src/utils/debugger/backends/lldb-cli-backend.ts` (default),
274 |   `src/utils/debugger/backends/dap-backend.ts`
275 | - Interactive execution: `src/utils/execution/interactive-process.ts` (used by LLDB CLI backend)
276 | - External commands: `xcrun simctl`, `xcrun lldb`, `xcodebuild`
277 | 
```

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

```typescript
  1 | /**
  2 |  * stop_sim_log_cap Plugin Tests - Test coverage for stop_sim_log_cap plugin
  3 |  *
  4 |  * This test file provides complete coverage for the stop_sim_log_cap plugin:
  5 |  * - Plugin structure validation
  6 |  * - Handler functionality (stop log capture session and retrieve captured logs)
  7 |  * - Error handling for validation and log capture failures
  8 |  *
  9 |  * Tests follow the canonical testing patterns from CLAUDE.md with deterministic
 10 |  * response validation and comprehensive parameter testing.
 11 |  * Converted to pure dependency injection without vitest mocking.
 12 |  */
 13 | 
 14 | import { describe, it, expect, beforeEach } from 'vitest';
 15 | import * as z from 'zod';
 16 | import stopSimLogCap, { stop_sim_log_capLogic } from '../stop_sim_log_cap.ts';
 17 | import { activeLogSessions } from '../../../../utils/log_capture.ts';
 18 | import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts';
 19 | 
 20 | describe('stop_sim_log_cap plugin', () => {
 21 |   beforeEach(() => {
 22 |     // Clear any active sessions before each test
 23 |     activeLogSessions.clear();
 24 |   });
 25 | 
 26 |   // Helper function to create a test log session
 27 |   async function createTestLogSession(sessionId: string, logContent: string = '') {
 28 |     const mockProcess = {
 29 |       pid: 12345,
 30 |       killed: false,
 31 |       exitCode: null,
 32 |       kill: () => {},
 33 |     };
 34 | 
 35 |     const logFilePath = `/tmp/xcodemcp_sim_log_test_${sessionId}.log`;
 36 |     const fileSystem = createMockFileSystemExecutor({
 37 |       existsSync: (path) => path === logFilePath,
 38 |       readFile: async (path, _encoding) => {
 39 |         if (path !== logFilePath) {
 40 |           throw new Error(`ENOENT: no such file or directory, open '${path}'`);
 41 |         }
 42 |         return logContent;
 43 |       },
 44 |     });
 45 | 
 46 |     activeLogSessions.set(sessionId, {
 47 |       processes: [mockProcess as any],
 48 |       logFilePath: logFilePath,
 49 |       simulatorUuid: 'test-simulator-uuid',
 50 |       bundleId: 'com.example.TestApp',
 51 |     });
 52 | 
 53 |     return { fileSystem, logFilePath };
 54 |   }
 55 | 
 56 |   describe('Export Field Validation (Literal)', () => {
 57 |     it('should have correct plugin structure', () => {
 58 |       expect(stopSimLogCap).toHaveProperty('name');
 59 |       expect(stopSimLogCap).toHaveProperty('description');
 60 |       expect(stopSimLogCap).toHaveProperty('schema');
 61 |       expect(stopSimLogCap).toHaveProperty('handler');
 62 | 
 63 |       expect(stopSimLogCap.name).toBe('stop_sim_log_cap');
 64 |       expect(stopSimLogCap.description).toBe(
 65 |         'Stops an active simulator log capture session and returns the captured logs.',
 66 |       );
 67 |       expect(typeof stopSimLogCap.handler).toBe('function');
 68 |       expect(typeof stopSimLogCap.schema).toBe('object');
 69 |     });
 70 | 
 71 |     it('should have correct schema structure', () => {
 72 |       // Schema should be a plain object for MCP protocol compliance
 73 |       expect(typeof stopSimLogCap.schema).toBe('object');
 74 |       expect(stopSimLogCap.schema).toHaveProperty('logSessionId');
 75 | 
 76 |       // Validate that schema fields are Zod types that can be used for validation
 77 |       const schema = z.object(stopSimLogCap.schema);
 78 |       expect(schema.safeParse({ logSessionId: 'test-session-id' }).success).toBe(true);
 79 |       expect(schema.safeParse({ logSessionId: 123 }).success).toBe(false);
 80 |     });
 81 | 
 82 |     it('should validate schema with valid parameters', () => {
 83 |       expect(stopSimLogCap.schema.logSessionId.safeParse('test-session-id').success).toBe(true);
 84 |     });
 85 | 
 86 |     it('should reject invalid schema parameters', () => {
 87 |       expect(stopSimLogCap.schema.logSessionId.safeParse(null).success).toBe(false);
 88 |       expect(stopSimLogCap.schema.logSessionId.safeParse(undefined).success).toBe(false);
 89 |       expect(stopSimLogCap.schema.logSessionId.safeParse(123).success).toBe(false);
 90 |       expect(stopSimLogCap.schema.logSessionId.safeParse(true).success).toBe(false);
 91 |     });
 92 |   });
 93 | 
 94 |   describe('Input Validation', () => {
 95 |     it('should handle null logSessionId (validation handled by framework)', async () => {
 96 |       // With typed tool factory, invalid params won't reach the logic function
 97 |       // This test now validates that the logic function works with valid empty strings
 98 |       const { fileSystem } = await createTestLogSession('', 'Log content for empty session');
 99 | 
100 |       const result = await stop_sim_log_capLogic(
101 |         {
102 |           logSessionId: '',
103 |         },
104 |         fileSystem,
105 |       );
106 | 
107 |       expect(result.isError).toBeUndefined();
108 |       expect(result.content[0].text).toBe(
109 |         'Log capture session  stopped successfully. Log content follows:\n\nLog content for empty session',
110 |       );
111 |     });
112 | 
113 |     it('should handle undefined logSessionId (validation handled by framework)', async () => {
114 |       // With typed tool factory, invalid params won't reach the logic function
115 |       // This test now validates that the logic function works with valid empty strings
116 |       const { fileSystem } = await createTestLogSession('', 'Log content for empty session');
117 | 
118 |       const result = await stop_sim_log_capLogic(
119 |         {
120 |           logSessionId: '',
121 |         },
122 |         fileSystem,
123 |       );
124 | 
125 |       expect(result.isError).toBeUndefined();
126 |       expect(result.content[0].text).toBe(
127 |         'Log capture session  stopped successfully. Log content follows:\n\nLog content for empty session',
128 |       );
129 |     });
130 | 
131 |     it('should handle empty string logSessionId', async () => {
132 |       const { fileSystem } = await createTestLogSession('', 'Log content for empty session');
133 | 
134 |       const result = await stop_sim_log_capLogic(
135 |         {
136 |           logSessionId: '',
137 |         },
138 |         fileSystem,
139 |       );
140 | 
141 |       expect(result.isError).toBeUndefined();
142 |       expect(result.content[0].text).toBe(
143 |         'Log capture session  stopped successfully. Log content follows:\n\nLog content for empty session',
144 |       );
145 |     });
146 |   });
147 | 
148 |   describe('Function Call Generation', () => {
149 |     it('should call stopLogCapture with correct parameters', async () => {
150 |       const { fileSystem } = await createTestLogSession(
151 |         'test-session-id',
152 |         'Mock log content from file',
153 |       );
154 | 
155 |       const result = await stop_sim_log_capLogic(
156 |         {
157 |           logSessionId: 'test-session-id',
158 |         },
159 |         fileSystem,
160 |       );
161 | 
162 |       expect(result.isError).toBeUndefined();
163 |       expect(result.content[0].text).toBe(
164 |         'Log capture session test-session-id stopped successfully. Log content follows:\n\nMock log content from file',
165 |       );
166 |     });
167 | 
168 |     it('should call stopLogCapture with different session ID', async () => {
169 |       const { fileSystem } = await createTestLogSession(
170 |         'different-session-id',
171 |         'Different log content',
172 |       );
173 | 
174 |       const result = await stop_sim_log_capLogic(
175 |         {
176 |           logSessionId: 'different-session-id',
177 |         },
178 |         fileSystem,
179 |       );
180 | 
181 |       expect(result.isError).toBeUndefined();
182 |       expect(result.content[0].text).toBe(
183 |         'Log capture session different-session-id stopped successfully. Log content follows:\n\nDifferent log content',
184 |       );
185 |     });
186 |   });
187 | 
188 |   describe('Response Processing', () => {
189 |     it('should handle successful log capture stop', async () => {
190 |       const { fileSystem } = await createTestLogSession(
191 |         'test-session-id',
192 |         'Mock log content from file',
193 |       );
194 | 
195 |       const result = await stop_sim_log_capLogic(
196 |         {
197 |           logSessionId: 'test-session-id',
198 |         },
199 |         fileSystem,
200 |       );
201 | 
202 |       expect(result.isError).toBeUndefined();
203 |       expect(result.content[0].text).toBe(
204 |         'Log capture session test-session-id stopped successfully. Log content follows:\n\nMock log content from file',
205 |       );
206 |     });
207 | 
208 |     it('should handle empty log content', async () => {
209 |       const { fileSystem } = await createTestLogSession('test-session-id', '');
210 | 
211 |       const result = await stop_sim_log_capLogic(
212 |         {
213 |           logSessionId: 'test-session-id',
214 |         },
215 |         fileSystem,
216 |       );
217 | 
218 |       expect(result.isError).toBeUndefined();
219 |       expect(result.content[0].text).toBe(
220 |         'Log capture session test-session-id stopped successfully. Log content follows:\n\n',
221 |       );
222 |     });
223 | 
224 |     it('should handle multiline log content', async () => {
225 |       const { fileSystem } = await createTestLogSession(
226 |         'test-session-id',
227 |         'Line 1\nLine 2\nLine 3',
228 |       );
229 | 
230 |       const result = await stop_sim_log_capLogic(
231 |         {
232 |           logSessionId: 'test-session-id',
233 |         },
234 |         fileSystem,
235 |       );
236 | 
237 |       expect(result.isError).toBeUndefined();
238 |       expect(result.content[0].text).toBe(
239 |         'Log capture session test-session-id stopped successfully. Log content follows:\n\nLine 1\nLine 2\nLine 3',
240 |       );
241 |     });
242 | 
243 |     it('should handle log capture stop errors for non-existent session', async () => {
244 |       const result = await stop_sim_log_capLogic(
245 |         {
246 |           logSessionId: 'non-existent-session',
247 |         },
248 |         createMockFileSystemExecutor(),
249 |       );
250 | 
251 |       expect(result.isError).toBe(true);
252 |       expect(result.content[0].text).toBe(
253 |         'Error stopping log capture session non-existent-session: Log capture session not found: non-existent-session',
254 |       );
255 |     });
256 | 
257 |     it('should handle file read errors', async () => {
258 |       // Create session but make file reading fail in the log_capture utility
259 |       const mockProcess = {
260 |         pid: 12345,
261 |         killed: false,
262 |         exitCode: null,
263 |         kill: () => {},
264 |       };
265 | 
266 |       activeLogSessions.set('test-session-id', {
267 |         processes: [mockProcess as any],
268 |         logFilePath: `/tmp/test_file_not_found.log`,
269 |         simulatorUuid: 'test-simulator-uuid',
270 |         bundleId: 'com.example.TestApp',
271 |       });
272 | 
273 |       const result = await stop_sim_log_capLogic(
274 |         { logSessionId: 'test-session-id' },
275 |         createMockFileSystemExecutor({
276 |           existsSync: () => false,
277 |         }),
278 |       );
279 | 
280 |       expect(result.isError).toBe(true);
281 |       expect(result.content[0].text).toContain(
282 |         'Error stopping log capture session test-session-id:',
283 |       );
284 |     });
285 | 
286 |     it('should handle permission errors', async () => {
287 |       // Create session but make file reading fail in the log_capture utility
288 |       const mockProcess = {
289 |         pid: 12345,
290 |         killed: false,
291 |         exitCode: null,
292 |         kill: () => {},
293 |       };
294 | 
295 |       activeLogSessions.set('test-session-id', {
296 |         processes: [mockProcess as any],
297 |         logFilePath: `/tmp/test_permission_denied.log`,
298 |         simulatorUuid: 'test-simulator-uuid',
299 |         bundleId: 'com.example.TestApp',
300 |       });
301 | 
302 |       const result = await stop_sim_log_capLogic(
303 |         { logSessionId: 'test-session-id' },
304 |         createMockFileSystemExecutor({
305 |           existsSync: () => true,
306 |           readFile: async () => {
307 |             throw new Error('Permission denied');
308 |           },
309 |         }),
310 |       );
311 | 
312 |       expect(result.isError).toBe(true);
313 |       expect(result.content[0].text).toContain(
314 |         'Error stopping log capture session test-session-id:',
315 |       );
316 |     });
317 | 
318 |     it('should handle various error types', async () => {
319 |       // Create session but make file reading fail in the log_capture utility
320 |       const mockProcess = {
321 |         pid: 12345,
322 |         killed: false,
323 |         exitCode: null,
324 |         kill: () => {},
325 |       };
326 | 
327 |       activeLogSessions.set('test-session-id', {
328 |         processes: [mockProcess as any],
329 |         logFilePath: `/tmp/test_generic_error.log`,
330 |         simulatorUuid: 'test-simulator-uuid',
331 |         bundleId: 'com.example.TestApp',
332 |       });
333 | 
334 |       const result = await stop_sim_log_capLogic(
335 |         { logSessionId: 'test-session-id' },
336 |         createMockFileSystemExecutor({
337 |           existsSync: () => true,
338 |           readFile: async () => {
339 |             throw new Error('Something went wrong');
340 |           },
341 |         }),
342 |       );
343 | 
344 |       expect(result.isError).toBe(true);
345 |       expect(result.content[0].text).toContain(
346 |         'Error stopping log capture session test-session-id:',
347 |       );
348 |     });
349 |   });
350 | });
351 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/project-discovery/__tests__/discover_projs.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Pure dependency injection test for discover_projs plugin
  3 |  *
  4 |  * Tests the plugin structure and project discovery functionality
  5 |  * including parameter validation, file system operations, and response formatting.
  6 |  *
  7 |  * Uses createMockFileSystemExecutor for file system operations.
  8 |  */
  9 | 
 10 | import { describe, it, expect, beforeEach } from 'vitest';
 11 | import * as z from 'zod';
 12 | import plugin, { discover_projsLogic } from '../discover_projs.ts';
 13 | import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts';
 14 | 
 15 | describe('discover_projs plugin', () => {
 16 |   let mockFileSystemExecutor: any;
 17 | 
 18 |   // Create mock file system executor
 19 |   mockFileSystemExecutor = createMockFileSystemExecutor({
 20 |     stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
 21 |     readdir: async () => [],
 22 |   });
 23 | 
 24 |   describe('Export Field Validation (Literal)', () => {
 25 |     it('should have correct name', () => {
 26 |       expect(plugin.name).toBe('discover_projs');
 27 |     });
 28 | 
 29 |     it('should have correct description', () => {
 30 |       expect(plugin.description).toBe(
 31 |         'Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.',
 32 |       );
 33 |     });
 34 | 
 35 |     it('should have handler function', () => {
 36 |       expect(typeof plugin.handler).toBe('function');
 37 |     });
 38 | 
 39 |     it('should validate schema with valid inputs', () => {
 40 |       const schema = z.object(plugin.schema);
 41 |       expect(schema.safeParse({ workspaceRoot: '/path/to/workspace' }).success).toBe(true);
 42 |       expect(
 43 |         schema.safeParse({ workspaceRoot: '/path/to/workspace', scanPath: 'subdir' }).success,
 44 |       ).toBe(true);
 45 |       expect(schema.safeParse({ workspaceRoot: '/path/to/workspace', maxDepth: 3 }).success).toBe(
 46 |         true,
 47 |       );
 48 |       expect(
 49 |         schema.safeParse({
 50 |           workspaceRoot: '/path/to/workspace',
 51 |           scanPath: 'subdir',
 52 |           maxDepth: 5,
 53 |         }).success,
 54 |       ).toBe(true);
 55 |     });
 56 | 
 57 |     it('should validate schema with invalid inputs', () => {
 58 |       const schema = z.object(plugin.schema);
 59 |       expect(schema.safeParse({}).success).toBe(false);
 60 |       expect(schema.safeParse({ workspaceRoot: 123 }).success).toBe(false);
 61 |       expect(schema.safeParse({ workspaceRoot: '/path', scanPath: 123 }).success).toBe(false);
 62 |       expect(schema.safeParse({ workspaceRoot: '/path', maxDepth: 'invalid' }).success).toBe(false);
 63 |       expect(schema.safeParse({ workspaceRoot: '/path', maxDepth: -1 }).success).toBe(false);
 64 |       expect(schema.safeParse({ workspaceRoot: '/path', maxDepth: 1.5 }).success).toBe(false);
 65 |     });
 66 |   });
 67 | 
 68 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 69 |     it('should handle workspaceRoot parameter correctly when provided', async () => {
 70 |       mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 });
 71 |       mockFileSystemExecutor.readdir = async () => [];
 72 | 
 73 |       const result = await discover_projsLogic(
 74 |         { workspaceRoot: '/workspace' },
 75 |         mockFileSystemExecutor,
 76 |       );
 77 | 
 78 |       expect(result).toEqual({
 79 |         content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
 80 |         isError: false,
 81 |       });
 82 |     });
 83 | 
 84 |     it('should return error when scan path does not exist', async () => {
 85 |       mockFileSystemExecutor.stat = async () => {
 86 |         throw new Error('ENOENT: no such file or directory');
 87 |       };
 88 | 
 89 |       const result = await discover_projsLogic(
 90 |         {
 91 |           workspaceRoot: '/workspace',
 92 |           scanPath: '.',
 93 |           maxDepth: 5,
 94 |         },
 95 |         mockFileSystemExecutor,
 96 |       );
 97 | 
 98 |       expect(result).toEqual({
 99 |         content: [
100 |           {
101 |             type: 'text',
102 |             text: 'Failed to access scan path: /workspace. Error: ENOENT: no such file or directory',
103 |           },
104 |         ],
105 |         isError: true,
106 |       });
107 |     });
108 | 
109 |     it('should return error when scan path is not a directory', async () => {
110 |       mockFileSystemExecutor.stat = async () => ({ isDirectory: () => false, mtimeMs: 0 });
111 | 
112 |       const result = await discover_projsLogic(
113 |         {
114 |           workspaceRoot: '/workspace',
115 |           scanPath: '.',
116 |           maxDepth: 5,
117 |         },
118 |         mockFileSystemExecutor,
119 |       );
120 | 
121 |       expect(result).toEqual({
122 |         content: [{ type: 'text', text: 'Scan path is not a directory: /workspace' }],
123 |         isError: true,
124 |       });
125 |     });
126 | 
127 |     it('should return success with no projects found', async () => {
128 |       mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 });
129 |       mockFileSystemExecutor.readdir = async () => [];
130 | 
131 |       const result = await discover_projsLogic(
132 |         {
133 |           workspaceRoot: '/workspace',
134 |           scanPath: '.',
135 |           maxDepth: 5,
136 |         },
137 |         mockFileSystemExecutor,
138 |       );
139 | 
140 |       expect(result).toEqual({
141 |         content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
142 |         isError: false,
143 |       });
144 |     });
145 | 
146 |     it('should return success with projects found', async () => {
147 |       mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 });
148 |       mockFileSystemExecutor.readdir = async () => [
149 |         { name: 'MyApp.xcodeproj', isDirectory: () => true, isSymbolicLink: () => false },
150 |         { name: 'MyWorkspace.xcworkspace', isDirectory: () => true, isSymbolicLink: () => false },
151 |       ];
152 | 
153 |       const result = await discover_projsLogic(
154 |         {
155 |           workspaceRoot: '/workspace',
156 |           scanPath: '.',
157 |           maxDepth: 5,
158 |         },
159 |         mockFileSystemExecutor,
160 |       );
161 | 
162 |       expect(result).toEqual({
163 |         content: [
164 |           { type: 'text', text: 'Discovery finished. Found 1 projects and 1 workspaces.' },
165 |           { type: 'text', text: 'Projects found:\n - /workspace/MyApp.xcodeproj' },
166 |           { type: 'text', text: 'Workspaces found:\n - /workspace/MyWorkspace.xcworkspace' },
167 |           {
168 |             type: 'text',
169 |             text: "Hint: Save a default with session-set-defaults { projectPath: '...' } or { workspacePath: '...' }.",
170 |           },
171 |         ],
172 |         isError: false,
173 |       });
174 |     });
175 | 
176 |     it('should handle fs error with code', async () => {
177 |       const error = new Error('Permission denied');
178 |       (error as any).code = 'EACCES';
179 |       mockFileSystemExecutor.stat = async () => {
180 |         throw error;
181 |       };
182 | 
183 |       const result = await discover_projsLogic(
184 |         {
185 |           workspaceRoot: '/workspace',
186 |           scanPath: '.',
187 |           maxDepth: 5,
188 |         },
189 |         mockFileSystemExecutor,
190 |       );
191 | 
192 |       expect(result).toEqual({
193 |         content: [
194 |           {
195 |             type: 'text',
196 |             text: 'Failed to access scan path: /workspace. Error: Permission denied',
197 |           },
198 |         ],
199 |         isError: true,
200 |       });
201 |     });
202 | 
203 |     it('should handle string error', async () => {
204 |       mockFileSystemExecutor.stat = async () => {
205 |         throw 'String error';
206 |       };
207 | 
208 |       const result = await discover_projsLogic(
209 |         {
210 |           workspaceRoot: '/workspace',
211 |           scanPath: '.',
212 |           maxDepth: 5,
213 |         },
214 |         mockFileSystemExecutor,
215 |       );
216 | 
217 |       expect(result).toEqual({
218 |         content: [
219 |           { type: 'text', text: 'Failed to access scan path: /workspace. Error: String error' },
220 |         ],
221 |         isError: true,
222 |       });
223 |     });
224 | 
225 |     it('should handle workspaceRoot parameter correctly', async () => {
226 |       mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 });
227 |       mockFileSystemExecutor.readdir = async () => [];
228 | 
229 |       const result = await discover_projsLogic(
230 |         {
231 |           workspaceRoot: '/workspace',
232 |         },
233 |         mockFileSystemExecutor,
234 |       );
235 | 
236 |       expect(result).toEqual({
237 |         content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
238 |         isError: false,
239 |       });
240 |     });
241 | 
242 |     it('should handle scan path outside workspace root', async () => {
243 |       // Mock path normalization to simulate path outside workspace root
244 |       mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 });
245 |       mockFileSystemExecutor.readdir = async () => [];
246 | 
247 |       const result = await discover_projsLogic(
248 |         {
249 |           workspaceRoot: '/workspace',
250 |           scanPath: '../outside',
251 |           maxDepth: 5,
252 |         },
253 |         mockFileSystemExecutor,
254 |       );
255 | 
256 |       expect(result).toEqual({
257 |         content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
258 |         isError: false,
259 |       });
260 |     });
261 | 
262 |     it('should handle error with object containing message and code properties', async () => {
263 |       const errorObject = {
264 |         message: 'Access denied',
265 |         code: 'EACCES',
266 |       };
267 |       mockFileSystemExecutor.stat = async () => {
268 |         throw errorObject;
269 |       };
270 | 
271 |       const result = await discover_projsLogic(
272 |         {
273 |           workspaceRoot: '/workspace',
274 |           scanPath: '.',
275 |           maxDepth: 5,
276 |         },
277 |         mockFileSystemExecutor,
278 |       );
279 | 
280 |       expect(result).toEqual({
281 |         content: [
282 |           { type: 'text', text: 'Failed to access scan path: /workspace. Error: Access denied' },
283 |         ],
284 |         isError: true,
285 |       });
286 |     });
287 | 
288 |     it('should handle max depth reached during recursive scan', async () => {
289 |       let readdirCallCount = 0;
290 | 
291 |       mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 });
292 |       mockFileSystemExecutor.readdir = async () => {
293 |         readdirCallCount++;
294 |         if (readdirCallCount <= 3) {
295 |           return [
296 |             {
297 |               name: `subdir${readdirCallCount}`,
298 |               isDirectory: () => true,
299 |               isSymbolicLink: () => false,
300 |             },
301 |           ];
302 |         }
303 |         return [];
304 |       };
305 | 
306 |       const result = await discover_projsLogic(
307 |         {
308 |           workspaceRoot: '/workspace',
309 |           scanPath: '.',
310 |           maxDepth: 3,
311 |         },
312 |         mockFileSystemExecutor,
313 |       );
314 | 
315 |       expect(result).toEqual({
316 |         content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
317 |         isError: false,
318 |       });
319 |     });
320 | 
321 |     it('should handle skipped directory types during scan', async () => {
322 |       mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 });
323 |       mockFileSystemExecutor.readdir = async () => [
324 |         { name: 'build', isDirectory: () => true, isSymbolicLink: () => false },
325 |         { name: 'DerivedData', isDirectory: () => true, isSymbolicLink: () => false },
326 |         { name: 'symlink', isDirectory: () => true, isSymbolicLink: () => true },
327 |         { name: 'regular.txt', isDirectory: () => false, isSymbolicLink: () => false },
328 |       ];
329 | 
330 |       const result = await discover_projsLogic(
331 |         {
332 |           workspaceRoot: '/workspace',
333 |           scanPath: '.',
334 |           maxDepth: 5,
335 |         },
336 |         mockFileSystemExecutor,
337 |       );
338 | 
339 |       // Test that skipped directories and files are correctly filtered out
340 |       expect(result).toEqual({
341 |         content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
342 |         isError: false,
343 |       });
344 |     });
345 | 
346 |     it('should handle error during recursive directory reading', async () => {
347 |       mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true, mtimeMs: 0 });
348 |       mockFileSystemExecutor.readdir = async () => {
349 |         const readError = new Error('Permission denied');
350 |         (readError as any).code = 'EACCES';
351 |         throw readError;
352 |       };
353 | 
354 |       const result = await discover_projsLogic(
355 |         {
356 |           workspaceRoot: '/workspace',
357 |           scanPath: '.',
358 |           maxDepth: 5,
359 |         },
360 |         mockFileSystemExecutor,
361 |       );
362 | 
363 |       // The function should handle the error gracefully and continue
364 |       expect(result).toEqual({
365 |         content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
366 |         isError: false,
367 |       });
368 |     });
369 |   });
370 | });
371 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/get_sim_app_path.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Simulator Get App Path Plugin: Get Simulator App Path (Unified)
  3 |  *
  4 |  * Gets the app bundle path for a simulator by UUID or name using either a project or workspace file.
  5 |  * Accepts mutually exclusive `projectPath` or `workspacePath`.
  6 |  * Accepts mutually exclusive `simulatorId` or `simulatorName`.
  7 |  */
  8 | 
  9 | import * as z from 'zod';
 10 | import { log } from '../../../utils/logging/index.ts';
 11 | import { createTextResponse } from '../../../utils/responses/index.ts';
 12 | import type { CommandExecutor } from '../../../utils/execution/index.ts';
 13 | import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
 14 | import { ToolResponse } from '../../../types/common.ts';
 15 | import {
 16 |   createSessionAwareTool,
 17 |   getSessionAwareToolSchemaShape,
 18 | } from '../../../utils/typed-tool-factory.ts';
 19 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
 20 | 
 21 | const XcodePlatform = {
 22 |   macOS: 'macOS',
 23 |   iOS: 'iOS',
 24 |   iOSSimulator: 'iOS Simulator',
 25 |   watchOS: 'watchOS',
 26 |   watchOSSimulator: 'watchOS Simulator',
 27 |   tvOS: 'tvOS',
 28 |   tvOSSimulator: 'tvOS Simulator',
 29 |   visionOS: 'visionOS',
 30 |   visionOSSimulator: 'visionOS Simulator',
 31 | };
 32 | 
 33 | function constructDestinationString(
 34 |   platform: string,
 35 |   simulatorName: string,
 36 |   simulatorId: string,
 37 |   useLatest: boolean = true,
 38 |   arch?: string,
 39 | ): string {
 40 |   const isSimulatorPlatform = [
 41 |     XcodePlatform.iOSSimulator,
 42 |     XcodePlatform.watchOSSimulator,
 43 |     XcodePlatform.tvOSSimulator,
 44 |     XcodePlatform.visionOSSimulator,
 45 |   ].includes(platform);
 46 | 
 47 |   // If ID is provided for a simulator, it takes precedence and uniquely identifies it.
 48 |   if (isSimulatorPlatform && simulatorId) {
 49 |     return `platform=${platform},id=${simulatorId}`;
 50 |   }
 51 | 
 52 |   // If name is provided for a simulator
 53 |   if (isSimulatorPlatform && simulatorName) {
 54 |     return `platform=${platform},name=${simulatorName}${useLatest ? ',OS=latest' : ''}`;
 55 |   }
 56 | 
 57 |   // If it's a simulator platform but neither ID nor name is provided (should be prevented by callers now)
 58 |   if (isSimulatorPlatform && !simulatorId && !simulatorName) {
 59 |     log(
 60 |       'warning',
 61 |       `Constructing generic destination for ${platform} without name or ID. This might not be specific enough.`,
 62 |     );
 63 |     throw new Error(`Simulator name or ID is required for specific ${platform} operations`);
 64 |   }
 65 | 
 66 |   // Handle non-simulator platforms
 67 |   switch (platform) {
 68 |     case XcodePlatform.macOS:
 69 |       return arch ? `platform=macOS,arch=${arch}` : 'platform=macOS';
 70 |     case XcodePlatform.iOS:
 71 |       return 'generic/platform=iOS';
 72 |     case XcodePlatform.watchOS:
 73 |       return 'generic/platform=watchOS';
 74 |     case XcodePlatform.tvOS:
 75 |       return 'generic/platform=tvOS';
 76 |     case XcodePlatform.visionOS:
 77 |       return 'generic/platform=visionOS';
 78 |   }
 79 |   // Fallback just in case (shouldn't be reached with enum)
 80 |   log('error', `Reached unexpected point in constructDestinationString for platform: ${platform}`);
 81 |   return `platform=${platform}`;
 82 | }
 83 | 
 84 | // Define base schema
 85 | const baseGetSimulatorAppPathSchema = z.object({
 86 |   projectPath: z
 87 |     .string()
 88 |     .optional()
 89 |     .describe('Path to .xcodeproj file. Provide EITHER this OR workspacePath, not both'),
 90 |   workspacePath: z
 91 |     .string()
 92 |     .optional()
 93 |     .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'),
 94 |   scheme: z.string().describe('The scheme to use (Required)'),
 95 |   platform: z
 96 |     .enum(['iOS Simulator', 'watchOS Simulator', 'tvOS Simulator', 'visionOS Simulator'])
 97 |     .describe('Target simulator platform (Required)'),
 98 |   simulatorId: z
 99 |     .string()
100 |     .optional()
101 |     .describe(
102 |       'UUID of the simulator (from list_sims). Provide EITHER this OR simulatorName, not both',
103 |     ),
104 |   simulatorName: z
105 |     .string()
106 |     .optional()
107 |     .describe(
108 |       "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
109 |     ),
110 |   configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
111 |   useLatestOS: z
112 |     .boolean()
113 |     .optional()
114 |     .describe('Whether to use the latest OS version for the named simulator'),
115 |   arch: z.string().optional().describe('Optional architecture'),
116 | });
117 | 
118 | // Add XOR validation with preprocessing
119 | const getSimulatorAppPathSchema = z.preprocess(
120 |   nullifyEmptyStrings,
121 |   baseGetSimulatorAppPathSchema
122 |     .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
123 |       message: 'Either projectPath or workspacePath is required.',
124 |     })
125 |     .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
126 |       message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
127 |     })
128 |     .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
129 |       message: 'Either simulatorId or simulatorName is required.',
130 |     })
131 |     .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
132 |       message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
133 |     }),
134 | );
135 | 
136 | // Use z.infer for type safety
137 | type GetSimulatorAppPathParams = z.infer<typeof getSimulatorAppPathSchema>;
138 | 
139 | /**
140 |  * Exported business logic function for getting app path
141 |  */
142 | export async function get_sim_app_pathLogic(
143 |   params: GetSimulatorAppPathParams,
144 |   executor: CommandExecutor,
145 | ): Promise<ToolResponse> {
146 |   // Set defaults - Zod validation already ensures required params are present
147 |   const projectPath = params.projectPath;
148 |   const workspacePath = params.workspacePath;
149 |   const scheme = params.scheme;
150 |   const platform = params.platform;
151 |   const simulatorId = params.simulatorId;
152 |   const simulatorName = params.simulatorName;
153 |   const configuration = params.configuration ?? 'Debug';
154 |   const useLatestOS = params.useLatestOS ?? true;
155 |   const arch = params.arch;
156 | 
157 |   // Log warning if useLatestOS is provided with simulatorId
158 |   if (simulatorId && params.useLatestOS !== undefined) {
159 |     log(
160 |       'warning',
161 |       `useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)`,
162 |     );
163 |   }
164 | 
165 |   log('info', `Getting app path for scheme ${scheme} on platform ${platform}`);
166 | 
167 |   try {
168 |     // Create the command array for xcodebuild with -showBuildSettings option
169 |     const command = ['xcodebuild', '-showBuildSettings'];
170 | 
171 |     // Add the workspace or project (XOR validation ensures exactly one is provided)
172 |     if (workspacePath) {
173 |       command.push('-workspace', workspacePath);
174 |     } else if (projectPath) {
175 |       command.push('-project', projectPath);
176 |     }
177 | 
178 |     // Add the scheme and configuration
179 |     command.push('-scheme', scheme);
180 |     command.push('-configuration', configuration);
181 | 
182 |     // Handle destination based on platform
183 |     const isSimulatorPlatform = [
184 |       XcodePlatform.iOSSimulator,
185 |       XcodePlatform.watchOSSimulator,
186 |       XcodePlatform.tvOSSimulator,
187 |       XcodePlatform.visionOSSimulator,
188 |     ].includes(platform);
189 | 
190 |     let destinationString = '';
191 | 
192 |     if (isSimulatorPlatform) {
193 |       if (simulatorId) {
194 |         destinationString = `platform=${platform},id=${simulatorId}`;
195 |       } else if (simulatorName) {
196 |         destinationString = `platform=${platform},name=${simulatorName}${(simulatorId ? false : useLatestOS) ? ',OS=latest' : ''}`;
197 |       } else {
198 |         return createTextResponse(
199 |           `For ${platform} platform, either simulatorId or simulatorName must be provided`,
200 |           true,
201 |         );
202 |       }
203 |     } else if (platform === XcodePlatform.macOS) {
204 |       destinationString = constructDestinationString(platform, '', '', false, arch);
205 |     } else if (platform === XcodePlatform.iOS) {
206 |       destinationString = 'generic/platform=iOS';
207 |     } else if (platform === XcodePlatform.watchOS) {
208 |       destinationString = 'generic/platform=watchOS';
209 |     } else if (platform === XcodePlatform.tvOS) {
210 |       destinationString = 'generic/platform=tvOS';
211 |     } else if (platform === XcodePlatform.visionOS) {
212 |       destinationString = 'generic/platform=visionOS';
213 |     } else {
214 |       return createTextResponse(`Unsupported platform: ${platform}`, true);
215 |     }
216 | 
217 |     command.push('-destination', destinationString);
218 | 
219 |     // Execute the command directly
220 |     const result = await executor(command, 'Get App Path', true, undefined);
221 | 
222 |     if (!result.success) {
223 |       return createTextResponse(`Failed to get app path: ${result.error}`, true);
224 |     }
225 | 
226 |     if (!result.output) {
227 |       return createTextResponse('Failed to extract build settings output from the result.', true);
228 |     }
229 | 
230 |     const buildSettingsOutput = result.output;
231 |     const builtProductsDirMatch = buildSettingsOutput.match(/^\s*BUILT_PRODUCTS_DIR\s*=\s*(.+)$/m);
232 |     const fullProductNameMatch = buildSettingsOutput.match(/^\s*FULL_PRODUCT_NAME\s*=\s*(.+)$/m);
233 | 
234 |     if (!builtProductsDirMatch || !fullProductNameMatch) {
235 |       return createTextResponse(
236 |         'Failed to extract app path from build settings. Make sure the app has been built first.',
237 |         true,
238 |       );
239 |     }
240 | 
241 |     const builtProductsDir = builtProductsDirMatch[1].trim();
242 |     const fullProductName = fullProductNameMatch[1].trim();
243 |     const appPath = `${builtProductsDir}/${fullProductName}`;
244 | 
245 |     let nextStepsText = '';
246 |     if (platform === XcodePlatform.macOS) {
247 |       nextStepsText = `Next Steps:
248 | 1. Get bundle ID: get_mac_bundle_id({ appPath: "${appPath}" })
249 | 2. Launch the app: launch_mac_app({ appPath: "${appPath}" })`;
250 |     } else if (isSimulatorPlatform) {
251 |       nextStepsText = `Next Steps:
252 | 1. Get bundle ID: get_app_bundle_id({ appPath: "${appPath}" })
253 | 2. Boot simulator: boot_sim({ simulatorId: "SIMULATOR_UUID" })
254 | 3. Install app: install_app_sim({ simulatorId: "SIMULATOR_UUID", appPath: "${appPath}" })
255 | 4. Launch app: launch_app_sim({ simulatorId: "SIMULATOR_UUID", bundleId: "BUNDLE_ID" })`;
256 |     } else if (
257 |       [
258 |         XcodePlatform.iOS,
259 |         XcodePlatform.watchOS,
260 |         XcodePlatform.tvOS,
261 |         XcodePlatform.visionOS,
262 |       ].includes(platform)
263 |     ) {
264 |       nextStepsText = `Next Steps:
265 | 1. Get bundle ID: get_app_bundle_id({ appPath: "${appPath}" })
266 | 2. Install app on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "${appPath}" })
267 | 3. Launch app on device: launch_app_device({ deviceId: "DEVICE_UDID", bundleId: "BUNDLE_ID" })`;
268 |     } else {
269 |       // For other platforms
270 |       nextStepsText = `Next Steps:
271 | 1. The app has been built for ${platform}
272 | 2. Use platform-specific deployment tools to install and run the app`;
273 |     }
274 | 
275 |     return {
276 |       content: [
277 |         {
278 |           type: 'text',
279 |           text: `✅ App path retrieved successfully: ${appPath}`,
280 |         },
281 |         {
282 |           type: 'text',
283 |           text: nextStepsText,
284 |         },
285 |       ],
286 |       isError: false,
287 |     };
288 |   } catch (error) {
289 |     const errorMessage = error instanceof Error ? error.message : String(error);
290 |     log('error', `Error retrieving app path: ${errorMessage}`);
291 |     return createTextResponse(`Error retrieving app path: ${errorMessage}`, true);
292 |   }
293 | }
294 | 
295 | const publicSchemaObject = baseGetSimulatorAppPathSchema.omit({
296 |   projectPath: true,
297 |   workspacePath: true,
298 |   scheme: true,
299 |   simulatorId: true,
300 |   simulatorName: true,
301 |   configuration: true,
302 |   useLatestOS: true,
303 |   arch: true,
304 | } as const);
305 | 
306 | export default {
307 |   name: 'get_sim_app_path',
308 |   description: 'Retrieves the built app path for an iOS simulator.',
309 |   schema: getSessionAwareToolSchemaShape({
310 |     sessionAware: publicSchemaObject,
311 |     legacy: baseGetSimulatorAppPathSchema,
312 |   }),
313 |   annotations: {
314 |     title: 'Get Simulator App Path',
315 |     readOnlyHint: true,
316 |   },
317 |   handler: createSessionAwareTool<GetSimulatorAppPathParams>({
318 |     internalSchema: getSimulatorAppPathSchema as unknown as z.ZodType<GetSimulatorAppPathParams>,
319 |     logicFunction: get_sim_app_pathLogic,
320 |     getExecutor: getDefaultCommandExecutor,
321 |     requirements: [
322 |       { allOf: ['scheme'], message: 'scheme is required' },
323 |       { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
324 |       { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
325 |     ],
326 |     exclusivePairs: [
327 |       ['projectPath', 'workspacePath'],
328 |       ['simulatorId', 'simulatorName'],
329 |     ],
330 |   }),
331 | };
332 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach } from 'vitest';
  2 | import * as z from 'zod';
  3 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
  4 | import { sessionStore } from '../../../../utils/session-store.ts';
  5 | import launchAppSim, { launch_app_simLogic } from '../launch_app_sim.ts';
  6 | 
  7 | describe('launch_app_sim tool', () => {
  8 |   beforeEach(() => {
  9 |     sessionStore.clear();
 10 |   });
 11 | 
 12 |   describe('Export Field Validation (Literal)', () => {
 13 |     it('should expose correct name and description', () => {
 14 |       expect(launchAppSim.name).toBe('launch_app_sim');
 15 |       expect(launchAppSim.description).toBe('Launches an app in an iOS simulator.');
 16 |     });
 17 | 
 18 |     it('should expose only non-session fields in public schema', () => {
 19 |       const schema = z.object(launchAppSim.schema);
 20 | 
 21 |       expect(
 22 |         schema.safeParse({
 23 |           bundleId: 'com.example.testapp',
 24 |         }).success,
 25 |       ).toBe(true);
 26 | 
 27 |       expect(
 28 |         schema.safeParse({
 29 |           bundleId: 'com.example.testapp',
 30 |           args: ['--debug'],
 31 |         }).success,
 32 |       ).toBe(true);
 33 | 
 34 |       expect(schema.safeParse({}).success).toBe(false);
 35 |       expect(schema.safeParse({ bundleId: 123 }).success).toBe(false);
 36 |       expect(schema.safeParse({ args: ['--debug'] }).success).toBe(false);
 37 | 
 38 |       expect(Object.keys(launchAppSim.schema).sort()).toEqual(['args', 'bundleId'].sort());
 39 | 
 40 |       const withSimDefaults = schema.safeParse({
 41 |         simulatorId: 'sim-default',
 42 |         simulatorName: 'iPhone 16',
 43 |         bundleId: 'com.example.testapp',
 44 |       });
 45 |       expect(withSimDefaults.success).toBe(true);
 46 |       const parsed = withSimDefaults.data as Record<string, unknown>;
 47 |       expect(parsed.simulatorId).toBeUndefined();
 48 |       expect(parsed.simulatorName).toBeUndefined();
 49 |     });
 50 |   });
 51 | 
 52 |   describe('Handler Requirements', () => {
 53 |     it('should require simulator identifier when not provided', async () => {
 54 |       const result = await launchAppSim.handler({ bundleId: 'com.example.testapp' });
 55 | 
 56 |       expect(result.isError).toBe(true);
 57 |       expect(result.content[0].text).toContain('Missing required session defaults');
 58 |       expect(result.content[0].text).toContain('Provide simulatorId or simulatorName');
 59 |       expect(result.content[0].text).toContain('session-set-defaults');
 60 |     });
 61 | 
 62 |     it('should validate bundleId when simulatorId default exists', async () => {
 63 |       sessionStore.setDefaults({ simulatorId: 'SIM-UUID' });
 64 | 
 65 |       const result = await launchAppSim.handler({});
 66 | 
 67 |       expect(result.isError).toBe(true);
 68 |       expect(result.content[0].text).toContain('Parameter validation failed');
 69 |       expect(result.content[0].text).toContain(
 70 |         'bundleId: Invalid input: expected string, received undefined',
 71 |       );
 72 |     });
 73 | 
 74 |     it('should reject when both simulatorId and simulatorName provided explicitly', async () => {
 75 |       const result = await launchAppSim.handler({
 76 |         simulatorId: 'SIM-UUID',
 77 |         simulatorName: 'iPhone 16',
 78 |         bundleId: 'com.example.testapp',
 79 |       });
 80 | 
 81 |       expect(result.isError).toBe(true);
 82 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 83 |       expect(result.content[0].text).toContain('simulatorId');
 84 |       expect(result.content[0].text).toContain('simulatorName');
 85 |     });
 86 |   });
 87 | 
 88 |   describe('Logic Behavior (Literal Returns)', () => {
 89 |     it('should launch app successfully with simulatorId', async () => {
 90 |       let callCount = 0;
 91 |       const sequencedExecutor = async (command: string[]) => {
 92 |         callCount++;
 93 |         if (callCount === 1) {
 94 |           return {
 95 |             success: true,
 96 |             output: '/path/to/app/container',
 97 |             error: '',
 98 |             process: {} as any,
 99 |           };
100 |         }
101 |         return {
102 |           success: true,
103 |           output: 'App launched successfully',
104 |           error: '',
105 |           process: {} as any,
106 |         };
107 |       };
108 | 
109 |       const result = await launch_app_simLogic(
110 |         {
111 |           simulatorId: 'test-uuid-123',
112 |           bundleId: 'com.example.testapp',
113 |         },
114 |         sequencedExecutor,
115 |       );
116 | 
117 |       expect(result).toEqual({
118 |         content: [
119 |           {
120 |             type: 'text',
121 |             text: `✅ App launched successfully in simulator test-uuid-123.\n\nNext Steps:\n1. To see simulator: open_sim()\n2. Log capture: start_sim_log_cap({ simulatorId: "test-uuid-123", bundleId: "com.example.testapp" })\n   With console: start_sim_log_cap({ simulatorId: "test-uuid-123", bundleId: "com.example.testapp", captureConsole: true })\n3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`,
122 |           },
123 |         ],
124 |       });
125 |     });
126 | 
127 |     it('should append additional arguments when provided', async () => {
128 |       let callCount = 0;
129 |       const commands: string[][] = [];
130 | 
131 |       const sequencedExecutor = async (command: string[]) => {
132 |         callCount++;
133 |         commands.push(command);
134 |         if (callCount === 1) {
135 |           return {
136 |             success: true,
137 |             output: '/path/to/app/container',
138 |             error: '',
139 |             process: {} as any,
140 |           };
141 |         }
142 |         return {
143 |           success: true,
144 |           output: 'App launched successfully',
145 |           error: '',
146 |           process: {} as any,
147 |         };
148 |       };
149 | 
150 |       await launch_app_simLogic(
151 |         {
152 |           simulatorId: 'test-uuid-123',
153 |           bundleId: 'com.example.testapp',
154 |           args: ['--debug', '--verbose'],
155 |         },
156 |         sequencedExecutor,
157 |       );
158 | 
159 |       expect(commands).toEqual([
160 |         ['xcrun', 'simctl', 'get_app_container', 'test-uuid-123', 'com.example.testapp', 'app'],
161 |         [
162 |           'xcrun',
163 |           'simctl',
164 |           'launch',
165 |           'test-uuid-123',
166 |           'com.example.testapp',
167 |           '--debug',
168 |           '--verbose',
169 |         ],
170 |       ]);
171 |     });
172 | 
173 |     it('should surface error when simulatorId missing after lookup', async () => {
174 |       const result = await launch_app_simLogic(
175 |         {
176 |           simulatorId: undefined,
177 |           bundleId: 'com.example.testapp',
178 |         } as any,
179 |         async () => ({
180 |           success: true,
181 |           output: '',
182 |           error: '',
183 |           process: {} as any,
184 |         }),
185 |       );
186 | 
187 |       expect(result).toEqual({
188 |         content: [
189 |           {
190 |             type: 'text',
191 |             text: 'No simulator identifier provided',
192 |           },
193 |         ],
194 |         isError: true,
195 |       });
196 |     });
197 | 
198 |     it('should detect missing app container on install check', async () => {
199 |       const mockExecutor = async (command: string[]) => {
200 |         if (command.includes('get_app_container')) {
201 |           return {
202 |             success: false,
203 |             output: '',
204 |             error: 'App container not found',
205 |             process: {} as any,
206 |           };
207 |         }
208 |         return {
209 |           success: true,
210 |           output: '',
211 |           error: '',
212 |           process: {} as any,
213 |         };
214 |       };
215 | 
216 |       const result = await launch_app_simLogic(
217 |         {
218 |           simulatorId: 'test-uuid-123',
219 |           bundleId: 'com.example.testapp',
220 |         },
221 |         mockExecutor,
222 |       );
223 | 
224 |       expect(result).toEqual({
225 |         content: [
226 |           {
227 |             type: 'text',
228 |             text: `App is not installed on the simulator. Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.`,
229 |           },
230 |         ],
231 |         isError: true,
232 |       });
233 |     });
234 | 
235 |     it('should return error when install check throws', async () => {
236 |       const mockExecutor = async (command: string[]) => {
237 |         if (command.includes('get_app_container')) {
238 |           throw new Error('Simctl command failed');
239 |         }
240 |         return {
241 |           success: true,
242 |           output: '',
243 |           error: '',
244 |           process: {} as any,
245 |         };
246 |       };
247 | 
248 |       const result = await launch_app_simLogic(
249 |         {
250 |           simulatorId: 'test-uuid-123',
251 |           bundleId: 'com.example.testapp',
252 |         },
253 |         mockExecutor,
254 |       );
255 | 
256 |       expect(result).toEqual({
257 |         content: [
258 |           {
259 |             type: 'text',
260 |             text: `App is not installed on the simulator (check failed). Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.`,
261 |           },
262 |         ],
263 |         isError: true,
264 |       });
265 |     });
266 | 
267 |     it('should handle launch failure', async () => {
268 |       let callCount = 0;
269 |       const mockExecutor = async (command: string[]) => {
270 |         callCount++;
271 |         if (callCount === 1) {
272 |           return {
273 |             success: true,
274 |             output: '/path/to/app/container',
275 |             error: '',
276 |             process: {} as any,
277 |           };
278 |         }
279 |         return {
280 |           success: false,
281 |           output: '',
282 |           error: 'Launch failed',
283 |           process: {} as any,
284 |         };
285 |       };
286 | 
287 |       const result = await launch_app_simLogic(
288 |         {
289 |           simulatorId: 'test-uuid-123',
290 |           bundleId: 'com.example.testapp',
291 |         },
292 |         mockExecutor,
293 |       );
294 | 
295 |       expect(result).toEqual({
296 |         content: [
297 |           {
298 |             type: 'text',
299 |             text: 'Launch app in simulator operation failed: Launch failed',
300 |           },
301 |         ],
302 |       });
303 |     });
304 | 
305 |     it('should launch using simulatorName by resolving UUID', async () => {
306 |       let callCount = 0;
307 |       const sequencedExecutor = async (command: string[]) => {
308 |         callCount++;
309 |         if (callCount === 1) {
310 |           return {
311 |             success: true,
312 |             output: JSON.stringify({
313 |               devices: {
314 |                 'iOS 17.0': [
315 |                   {
316 |                     name: 'iPhone 16',
317 |                     udid: 'resolved-uuid',
318 |                     isAvailable: true,
319 |                     state: 'Shutdown',
320 |                   },
321 |                 ],
322 |               },
323 |             }),
324 |             error: '',
325 |             process: {} as any,
326 |           };
327 |         }
328 |         if (callCount === 2) {
329 |           return {
330 |             success: true,
331 |             output: '/path/to/app/container',
332 |             error: '',
333 |             process: {} as any,
334 |           };
335 |         }
336 |         return {
337 |           success: true,
338 |           output: 'App launched successfully',
339 |           error: '',
340 |           process: {} as any,
341 |         };
342 |       };
343 | 
344 |       const result = await launch_app_simLogic(
345 |         {
346 |           simulatorName: 'iPhone 16',
347 |           bundleId: 'com.example.testapp',
348 |         },
349 |         sequencedExecutor,
350 |       );
351 | 
352 |       expect(result).toEqual({
353 |         content: [
354 |           {
355 |             type: 'text',
356 |             text: `✅ App launched successfully in simulator "iPhone 16" (resolved-uuid).\n\nNext Steps:\n1. To see simulator: open_sim()\n2. Log capture: start_sim_log_cap({ simulatorName: "iPhone 16", bundleId: "com.example.testapp" })\n   With console: start_sim_log_cap({ simulatorName: "iPhone 16", bundleId: "com.example.testapp", captureConsole: true })\n3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`,
357 |           },
358 |         ],
359 |       });
360 |     });
361 | 
362 |     it('should return error when simulator name is not found', async () => {
363 |       const mockListExecutor = async () => ({
364 |         success: true,
365 |         output: JSON.stringify({ devices: {} }),
366 |         error: '',
367 |         process: {} as any,
368 |       });
369 | 
370 |       const result = await launch_app_simLogic(
371 |         {
372 |           simulatorName: 'Missing Simulator',
373 |           bundleId: 'com.example.testapp',
374 |         },
375 |         mockListExecutor,
376 |       );
377 | 
378 |       expect(result).toEqual({
379 |         content: [
380 |           {
381 |             type: 'text',
382 |             text: 'Simulator named "Missing Simulator" not found. Use list_sims to see available simulators.',
383 |           },
384 |         ],
385 |         isError: true,
386 |       });
387 |     });
388 | 
389 |     it('should return error when simctl list fails', async () => {
390 |       const mockExecutor = createMockExecutor({
391 |         success: false,
392 |         output: '',
393 |         error: 'simctl list failed',
394 |       });
395 | 
396 |       const result = await launch_app_simLogic(
397 |         {
398 |           simulatorName: 'iPhone 16',
399 |           bundleId: 'com.example.testapp',
400 |         },
401 |         mockExecutor,
402 |       );
403 | 
404 |       expect(result).toEqual({
405 |         content: [
406 |           {
407 |             type: 'text',
408 |             text: 'Failed to list simulators: simctl list failed',
409 |           },
410 |         ],
411 |         isError: true,
412 |       });
413 |     });
414 |   });
415 | });
416 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/macos/test_macos.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * macOS Shared Plugin: Test macOS (Unified)
  3 |  *
  4 |  * Runs tests for a macOS project or workspace using xcodebuild test and parses xcresult output.
  5 |  * Accepts mutually exclusive `projectPath` or `workspacePath`.
  6 |  */
  7 | 
  8 | import * as z from 'zod';
  9 | import { join } from 'path';
 10 | import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
 11 | import { log } from '../../../utils/logging/index.ts';
 12 | import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
 13 | import { createTextResponse } from '../../../utils/responses/index.ts';
 14 | import { normalizeTestRunnerEnv } from '../../../utils/environment.ts';
 15 | import type {
 16 |   CommandExecutor,
 17 |   FileSystemExecutor,
 18 |   CommandExecOptions,
 19 | } from '../../../utils/execution/index.ts';
 20 | import {
 21 |   getDefaultCommandExecutor,
 22 |   getDefaultFileSystemExecutor,
 23 | } from '../../../utils/execution/index.ts';
 24 | import {
 25 |   createSessionAwareTool,
 26 |   getSessionAwareToolSchemaShape,
 27 | } from '../../../utils/typed-tool-factory.ts';
 28 | import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';
 29 | 
 30 | // Unified schema: XOR between projectPath and workspacePath
 31 | const baseSchemaObject = z.object({
 32 |   projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
 33 |   workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
 34 |   scheme: z.string().describe('The scheme to use'),
 35 |   configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
 36 |   derivedDataPath: z
 37 |     .string()
 38 |     .optional()
 39 |     .describe('Path where build products and other derived data will go'),
 40 |   extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
 41 |   preferXcodebuild: z
 42 |     .boolean()
 43 |     .optional()
 44 |     .describe('If true, prefers xcodebuild over the experimental incremental build system'),
 45 |   testRunnerEnv: z
 46 |     .record(z.string(), z.string())
 47 |     .optional()
 48 |     .describe(
 49 |       'Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)',
 50 |     ),
 51 | });
 52 | 
 53 | const publicSchemaObject = baseSchemaObject.omit({
 54 |   projectPath: true,
 55 |   workspacePath: true,
 56 |   scheme: true,
 57 |   configuration: true,
 58 | } as const);
 59 | 
 60 | const testMacosSchema = z.preprocess(
 61 |   nullifyEmptyStrings,
 62 |   baseSchemaObject
 63 |     .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
 64 |       message: 'Either projectPath or workspacePath is required.',
 65 |     })
 66 |     .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
 67 |       message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
 68 |     }),
 69 | );
 70 | 
 71 | export type TestMacosParams = z.infer<typeof testMacosSchema>;
 72 | 
 73 | /**
 74 |  * Type definition for test summary structure from xcresulttool
 75 |  * @typedef {Object} TestSummary
 76 |  * @property {string} [title]
 77 |  * @property {string} [result]
 78 |  * @property {number} [totalTestCount]
 79 |  * @property {number} [passedTests]
 80 |  * @property {number} [failedTests]
 81 |  * @property {number} [skippedTests]
 82 |  * @property {number} [expectedFailures]
 83 |  * @property {string} [environmentDescription]
 84 |  * @property {Array<Object>} [devicesAndConfigurations]
 85 |  * @property {Array<Object>} [testFailures]
 86 |  * @property {Array<Object>} [topInsights]
 87 |  */
 88 | 
 89 | /**
 90 |  * Parse xcresult bundle using xcrun xcresulttool
 91 |  */
 92 | async function parseXcresultBundle(
 93 |   resultBundlePath: string,
 94 |   executor: CommandExecutor = getDefaultCommandExecutor(),
 95 | ): Promise<string> {
 96 |   try {
 97 |     const result = await executor(
 98 |       ['xcrun', 'xcresulttool', 'get', 'test-results', 'summary', '--path', resultBundlePath],
 99 |       'Parse xcresult bundle',
100 |       true,
101 |     );
102 | 
103 |     if (!result.success) {
104 |       throw new Error(result.error ?? 'Failed to parse xcresult bundle');
105 |     }
106 | 
107 |     // Parse JSON response and format as human-readable
108 |     let summary: unknown;
109 |     try {
110 |       summary = JSON.parse(result.output || '{}');
111 |     } catch (parseError) {
112 |       throw new Error(`Failed to parse JSON output: ${parseError}`);
113 |     }
114 | 
115 |     if (typeof summary !== 'object' || summary === null) {
116 |       throw new Error('Invalid JSON output: expected object');
117 |     }
118 | 
119 |     return formatTestSummary(summary as Record<string, unknown>);
120 |   } catch (error) {
121 |     const errorMessage = error instanceof Error ? error.message : String(error);
122 |     log('error', `Error parsing xcresult bundle: ${errorMessage}`);
123 |     throw error;
124 |   }
125 | }
126 | 
127 | /**
128 |  * Format test summary JSON into human-readable text
129 |  */
130 | function formatTestSummary(summary: Record<string, unknown>): string {
131 |   const lines = [];
132 | 
133 |   lines.push(`Test Summary: ${summary.title ?? 'Unknown'}`);
134 |   lines.push(`Overall Result: ${summary.result ?? 'Unknown'}`);
135 |   lines.push('');
136 | 
137 |   lines.push('Test Counts:');
138 |   lines.push(`  Total: ${summary.totalTestCount ?? 0}`);
139 |   lines.push(`  Passed: ${summary.passedTests ?? 0}`);
140 |   lines.push(`  Failed: ${summary.failedTests ?? 0}`);
141 |   lines.push(`  Skipped: ${summary.skippedTests ?? 0}`);
142 |   lines.push(`  Expected Failures: ${summary.expectedFailures ?? 0}`);
143 |   lines.push('');
144 | 
145 |   if (summary.environmentDescription) {
146 |     lines.push(`Environment: ${summary.environmentDescription}`);
147 |     lines.push('');
148 |   }
149 | 
150 |   if (
151 |     summary.devicesAndConfigurations &&
152 |     Array.isArray(summary.devicesAndConfigurations) &&
153 |     summary.devicesAndConfigurations.length > 0
154 |   ) {
155 |     const firstDeviceConfig: unknown = summary.devicesAndConfigurations[0];
156 |     if (
157 |       typeof firstDeviceConfig === 'object' &&
158 |       firstDeviceConfig !== null &&
159 |       'device' in firstDeviceConfig
160 |     ) {
161 |       const device: unknown = (firstDeviceConfig as Record<string, unknown>).device;
162 |       if (typeof device === 'object' && device !== null) {
163 |         const deviceRecord = device as Record<string, unknown>;
164 |         const deviceName =
165 |           'deviceName' in deviceRecord && typeof deviceRecord.deviceName === 'string'
166 |             ? deviceRecord.deviceName
167 |             : 'Unknown';
168 |         const platform =
169 |           'platform' in deviceRecord && typeof deviceRecord.platform === 'string'
170 |             ? deviceRecord.platform
171 |             : 'Unknown';
172 |         const osVersion =
173 |           'osVersion' in deviceRecord && typeof deviceRecord.osVersion === 'string'
174 |             ? deviceRecord.osVersion
175 |             : 'Unknown';
176 | 
177 |         lines.push(`Device: ${deviceName} (${platform} ${osVersion})`);
178 |         lines.push('');
179 |       }
180 |     }
181 |   }
182 | 
183 |   if (
184 |     summary.testFailures &&
185 |     Array.isArray(summary.testFailures) &&
186 |     summary.testFailures.length > 0
187 |   ) {
188 |     lines.push('Test Failures:');
189 |     summary.testFailures.forEach((failure: unknown, index: number) => {
190 |       if (typeof failure === 'object' && failure !== null) {
191 |         const failureRecord = failure as Record<string, unknown>;
192 |         const testName =
193 |           'testName' in failureRecord && typeof failureRecord.testName === 'string'
194 |             ? failureRecord.testName
195 |             : 'Unknown Test';
196 |         const targetName =
197 |           'targetName' in failureRecord && typeof failureRecord.targetName === 'string'
198 |             ? failureRecord.targetName
199 |             : 'Unknown Target';
200 | 
201 |         lines.push(`  ${index + 1}. ${testName} (${targetName})`);
202 | 
203 |         if ('failureText' in failureRecord && typeof failureRecord.failureText === 'string') {
204 |           lines.push(`     ${failureRecord.failureText}`);
205 |         }
206 |       }
207 |     });
208 |     lines.push('');
209 |   }
210 | 
211 |   if (summary.topInsights && Array.isArray(summary.topInsights) && summary.topInsights.length > 0) {
212 |     lines.push('Insights:');
213 |     summary.topInsights.forEach((insight: unknown, index: number) => {
214 |       if (typeof insight === 'object' && insight !== null) {
215 |         const insightRecord = insight as Record<string, unknown>;
216 |         const impact =
217 |           'impact' in insightRecord && typeof insightRecord.impact === 'string'
218 |             ? insightRecord.impact
219 |             : 'Unknown';
220 |         const text =
221 |           'text' in insightRecord && typeof insightRecord.text === 'string'
222 |             ? insightRecord.text
223 |             : 'No description';
224 | 
225 |         lines.push(`  ${index + 1}. [${impact}] ${text}`);
226 |       }
227 |     });
228 |   }
229 | 
230 |   return lines.join('\n');
231 | }
232 | 
233 | /**
234 |  * Business logic for testing a macOS project or workspace.
235 |  * Exported for direct testing and reuse.
236 |  */
237 | export async function testMacosLogic(
238 |   params: TestMacosParams,
239 |   executor: CommandExecutor = getDefaultCommandExecutor(),
240 |   fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
241 | ): Promise<ToolResponse> {
242 |   log('info', `Starting test run for scheme ${params.scheme} on platform macOS (internal)`);
243 | 
244 |   try {
245 |     // Create temporary directory for xcresult bundle
246 |     const tempDir = await fileSystemExecutor.mkdtemp(
247 |       join(fileSystemExecutor.tmpdir(), 'xcodebuild-test-'),
248 |     );
249 |     const resultBundlePath = join(tempDir, 'TestResults.xcresult');
250 | 
251 |     // Add resultBundlePath to extraArgs
252 |     const extraArgs = [...(params.extraArgs ?? []), `-resultBundlePath`, resultBundlePath];
253 | 
254 |     // Prepare execution options with TEST_RUNNER_ environment variables
255 |     const execOpts: CommandExecOptions | undefined = params.testRunnerEnv
256 |       ? { env: normalizeTestRunnerEnv(params.testRunnerEnv) }
257 |       : undefined;
258 | 
259 |     // Run the test command
260 |     const testResult = await executeXcodeBuildCommand(
261 |       {
262 |         projectPath: params.projectPath,
263 |         workspacePath: params.workspacePath,
264 |         scheme: params.scheme,
265 |         configuration: params.configuration ?? 'Debug',
266 |         derivedDataPath: params.derivedDataPath,
267 |         extraArgs,
268 |       },
269 |       {
270 |         platform: XcodePlatform.macOS,
271 |         logPrefix: 'Test Run',
272 |       },
273 |       params.preferXcodebuild ?? false,
274 |       'test',
275 |       executor,
276 |       execOpts,
277 |     );
278 | 
279 |     // Parse xcresult bundle if it exists, regardless of whether tests passed or failed
280 |     // Test failures are expected and should not prevent xcresult parsing
281 |     try {
282 |       log('info', `Attempting to parse xcresult bundle at: ${resultBundlePath}`);
283 | 
284 |       // Check if the file exists
285 |       try {
286 |         await fileSystemExecutor.stat(resultBundlePath);
287 |         log('info', `xcresult bundle exists at: ${resultBundlePath}`);
288 |       } catch {
289 |         log('warn', `xcresult bundle does not exist at: ${resultBundlePath}`);
290 |         throw new Error(`xcresult bundle not found at ${resultBundlePath}`);
291 |       }
292 | 
293 |       const testSummary = await parseXcresultBundle(resultBundlePath, executor);
294 |       log('info', 'Successfully parsed xcresult bundle');
295 | 
296 |       // Clean up temporary directory
297 |       await fileSystemExecutor.rm(tempDir, { recursive: true, force: true });
298 | 
299 |       // Return combined result - preserve isError from testResult (test failures should be marked as errors)
300 |       return {
301 |         content: [
302 |           ...(testResult.content ?? []),
303 |           {
304 |             type: 'text',
305 |             text: '\nTest Results Summary:\n' + testSummary,
306 |           },
307 |         ],
308 |         isError: testResult.isError,
309 |       };
310 |     } catch (parseError) {
311 |       // If parsing fails, return original test result
312 |       log('warn', `Failed to parse xcresult bundle: ${parseError}`);
313 | 
314 |       // Clean up temporary directory even if parsing fails
315 |       try {
316 |         await fileSystemExecutor.rm(tempDir, { recursive: true, force: true });
317 |       } catch (cleanupError) {
318 |         log('warn', `Failed to clean up temporary directory: ${cleanupError}`);
319 |       }
320 | 
321 |       return testResult;
322 |     }
323 |   } catch (error) {
324 |     const errorMessage = error instanceof Error ? error.message : String(error);
325 |     log('error', `Error during test run: ${errorMessage}`);
326 |     return createTextResponse(`Error during test run: ${errorMessage}`, true);
327 |   }
328 | }
329 | 
330 | export default {
331 |   name: 'test_macos',
332 |   description: 'Runs tests for a macOS target.',
333 |   schema: getSessionAwareToolSchemaShape({
334 |     sessionAware: publicSchemaObject,
335 |     legacy: baseSchemaObject,
336 |   }),
337 |   annotations: {
338 |     title: 'Test macOS',
339 |     destructiveHint: true,
340 |   },
341 |   handler: createSessionAwareTool<TestMacosParams>({
342 |     internalSchema: testMacosSchema as unknown as z.ZodType<TestMacosParams, unknown>,
343 |     logicFunction: (params, executor) =>
344 |       testMacosLogic(params, executor, getDefaultFileSystemExecutor()),
345 |     getExecutor: getDefaultCommandExecutor,
346 |     requirements: [
347 |       { allOf: ['scheme'], message: 'scheme is required' },
348 |       { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
349 |     ],
350 |     exclusivePairs: [['projectPath', 'workspacePath']],
351 |   }),
352 | };
353 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for swift_package_run plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Integration tests using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import * as z from 'zod';
  9 | import {
 10 |   createMockExecutor,
 11 |   createNoopExecutor,
 12 |   createMockCommandResponse,
 13 | } from '../../../../test-utils/mock-executors.ts';
 14 | import swiftPackageRun, { swift_package_runLogic } from '../swift_package_run.ts';
 15 | import type { CommandExecutor } from '../../../../utils/execution/index.ts';
 16 | 
 17 | describe('swift_package_run plugin', () => {
 18 |   describe('Export Field Validation (Literal)', () => {
 19 |     it('should have correct name', () => {
 20 |       expect(swiftPackageRun.name).toBe('swift_package_run');
 21 |     });
 22 | 
 23 |     it('should have correct description', () => {
 24 |       expect(swiftPackageRun.description).toBe(
 25 |         'Runs an executable target from a Swift Package with swift run',
 26 |       );
 27 |     });
 28 | 
 29 |     it('should have handler function', () => {
 30 |       expect(typeof swiftPackageRun.handler).toBe('function');
 31 |     });
 32 | 
 33 |     it('should validate schema correctly', () => {
 34 |       // Test packagePath (required string)
 35 |       expect(swiftPackageRun.schema.packagePath.safeParse('valid/path').success).toBe(true);
 36 |       expect(swiftPackageRun.schema.packagePath.safeParse(null).success).toBe(false);
 37 | 
 38 |       // Test executableName (optional string)
 39 |       expect(swiftPackageRun.schema.executableName.safeParse('MyExecutable').success).toBe(true);
 40 |       expect(swiftPackageRun.schema.executableName.safeParse(undefined).success).toBe(true);
 41 |       expect(swiftPackageRun.schema.executableName.safeParse(123).success).toBe(false);
 42 | 
 43 |       // Test arguments (optional array of strings)
 44 |       expect(swiftPackageRun.schema.arguments.safeParse(['arg1', 'arg2']).success).toBe(true);
 45 |       expect(swiftPackageRun.schema.arguments.safeParse(undefined).success).toBe(true);
 46 |       expect(swiftPackageRun.schema.arguments.safeParse(['arg1', 123]).success).toBe(false);
 47 | 
 48 |       // Test configuration (optional enum)
 49 |       expect(swiftPackageRun.schema.configuration.safeParse('debug').success).toBe(true);
 50 |       expect(swiftPackageRun.schema.configuration.safeParse('release').success).toBe(true);
 51 |       expect(swiftPackageRun.schema.configuration.safeParse(undefined).success).toBe(true);
 52 |       expect(swiftPackageRun.schema.configuration.safeParse('invalid').success).toBe(false);
 53 | 
 54 |       // Test timeout (optional number)
 55 |       expect(swiftPackageRun.schema.timeout.safeParse(30).success).toBe(true);
 56 |       expect(swiftPackageRun.schema.timeout.safeParse(undefined).success).toBe(true);
 57 |       expect(swiftPackageRun.schema.timeout.safeParse('30').success).toBe(false);
 58 | 
 59 |       // Test background (optional boolean)
 60 |       expect(swiftPackageRun.schema.background.safeParse(true).success).toBe(true);
 61 |       expect(swiftPackageRun.schema.background.safeParse(false).success).toBe(true);
 62 |       expect(swiftPackageRun.schema.background.safeParse(undefined).success).toBe(true);
 63 |       expect(swiftPackageRun.schema.background.safeParse('true').success).toBe(false);
 64 | 
 65 |       // Test parseAsLibrary (optional boolean)
 66 |       expect(swiftPackageRun.schema.parseAsLibrary.safeParse(true).success).toBe(true);
 67 |       expect(swiftPackageRun.schema.parseAsLibrary.safeParse(false).success).toBe(true);
 68 |       expect(swiftPackageRun.schema.parseAsLibrary.safeParse(undefined).success).toBe(true);
 69 |       expect(swiftPackageRun.schema.parseAsLibrary.safeParse('true').success).toBe(false);
 70 |     });
 71 |   });
 72 | 
 73 |   let executorCalls: any[] = [];
 74 | 
 75 |   beforeEach(() => {
 76 |     executorCalls = [];
 77 |   });
 78 | 
 79 |   describe('Command Generation Testing', () => {
 80 |     it('should build correct command for basic run (foreground mode)', async () => {
 81 |       const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => {
 82 |         executorCalls.push({ command, logPrefix, useShell, opts });
 83 |         return Promise.resolve(
 84 |           createMockCommandResponse({
 85 |             success: true,
 86 |             output: 'Process completed',
 87 |             error: undefined,
 88 |           }),
 89 |         );
 90 |       };
 91 | 
 92 |       await swift_package_runLogic(
 93 |         {
 94 |           packagePath: '/test/package',
 95 |         },
 96 |         mockExecutor,
 97 |       );
 98 | 
 99 |       expect(executorCalls[0]).toEqual({
100 |         command: ['swift', 'run', '--package-path', '/test/package'],
101 |         logPrefix: 'Swift Package Run',
102 |         useShell: true,
103 |         opts: undefined,
104 |       });
105 |     });
106 | 
107 |     it('should build correct command with release configuration', async () => {
108 |       const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => {
109 |         executorCalls.push({ command, logPrefix, useShell, opts });
110 |         return Promise.resolve(
111 |           createMockCommandResponse({
112 |             success: true,
113 |             output: 'Process completed',
114 |             error: undefined,
115 |           }),
116 |         );
117 |       };
118 | 
119 |       await swift_package_runLogic(
120 |         {
121 |           packagePath: '/test/package',
122 |           configuration: 'release',
123 |         },
124 |         mockExecutor,
125 |       );
126 | 
127 |       expect(executorCalls[0]).toEqual({
128 |         command: ['swift', 'run', '--package-path', '/test/package', '-c', 'release'],
129 |         logPrefix: 'Swift Package Run',
130 |         useShell: true,
131 |         opts: undefined,
132 |       });
133 |     });
134 | 
135 |     it('should build correct command with executable name', async () => {
136 |       const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => {
137 |         executorCalls.push({ command, logPrefix, useShell, opts });
138 |         return Promise.resolve(
139 |           createMockCommandResponse({
140 |             success: true,
141 |             output: 'Process completed',
142 |             error: undefined,
143 |           }),
144 |         );
145 |       };
146 | 
147 |       await swift_package_runLogic(
148 |         {
149 |           packagePath: '/test/package',
150 |           executableName: 'MyApp',
151 |         },
152 |         mockExecutor,
153 |       );
154 | 
155 |       expect(executorCalls[0]).toEqual({
156 |         command: ['swift', 'run', '--package-path', '/test/package', 'MyApp'],
157 |         logPrefix: 'Swift Package Run',
158 |         useShell: true,
159 |         opts: undefined,
160 |       });
161 |     });
162 | 
163 |     it('should build correct command with arguments', async () => {
164 |       const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => {
165 |         executorCalls.push({ command, logPrefix, useShell, opts });
166 |         return Promise.resolve(
167 |           createMockCommandResponse({
168 |             success: true,
169 |             output: 'Process completed',
170 |             error: undefined,
171 |           }),
172 |         );
173 |       };
174 | 
175 |       await swift_package_runLogic(
176 |         {
177 |           packagePath: '/test/package',
178 |           arguments: ['arg1', 'arg2'],
179 |         },
180 |         mockExecutor,
181 |       );
182 | 
183 |       expect(executorCalls[0]).toEqual({
184 |         command: ['swift', 'run', '--package-path', '/test/package', '--', 'arg1', 'arg2'],
185 |         logPrefix: 'Swift Package Run',
186 |         useShell: true,
187 |         opts: undefined,
188 |       });
189 |     });
190 | 
191 |     it('should build correct command with parseAsLibrary flag', async () => {
192 |       const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => {
193 |         executorCalls.push({ command, logPrefix, useShell, opts });
194 |         return Promise.resolve(
195 |           createMockCommandResponse({
196 |             success: true,
197 |             output: 'Process completed',
198 |             error: undefined,
199 |           }),
200 |         );
201 |       };
202 | 
203 |       await swift_package_runLogic(
204 |         {
205 |           packagePath: '/test/package',
206 |           parseAsLibrary: true,
207 |         },
208 |         mockExecutor,
209 |       );
210 | 
211 |       expect(executorCalls[0]).toEqual({
212 |         command: [
213 |           'swift',
214 |           'run',
215 |           '--package-path',
216 |           '/test/package',
217 |           '-Xswiftc',
218 |           '-parse-as-library',
219 |         ],
220 |         logPrefix: 'Swift Package Run',
221 |         useShell: true,
222 |         opts: undefined,
223 |       });
224 |     });
225 | 
226 |     it('should build correct command with all parameters', async () => {
227 |       const mockExecutor: CommandExecutor = (command, logPrefix, useShell, opts) => {
228 |         executorCalls.push({ command, logPrefix, useShell, opts });
229 |         return Promise.resolve(
230 |           createMockCommandResponse({
231 |             success: true,
232 |             output: 'Process completed',
233 |             error: undefined,
234 |           }),
235 |         );
236 |       };
237 | 
238 |       await swift_package_runLogic(
239 |         {
240 |           packagePath: '/test/package',
241 |           executableName: 'MyApp',
242 |           configuration: 'release',
243 |           arguments: ['arg1'],
244 |           parseAsLibrary: true,
245 |         },
246 |         mockExecutor,
247 |       );
248 | 
249 |       expect(executorCalls[0]).toEqual({
250 |         command: [
251 |           'swift',
252 |           'run',
253 |           '--package-path',
254 |           '/test/package',
255 |           '-c',
256 |           'release',
257 |           '-Xswiftc',
258 |           '-parse-as-library',
259 |           'MyApp',
260 |           '--',
261 |           'arg1',
262 |         ],
263 |         logPrefix: 'Swift Package Run',
264 |         useShell: true,
265 |         opts: undefined,
266 |       });
267 |     });
268 | 
269 |     it('should not call executor for background mode', async () => {
270 |       // For background mode, no executor should be called since it uses direct spawn
271 |       const mockExecutor = createNoopExecutor();
272 | 
273 |       const result = await swift_package_runLogic(
274 |         {
275 |           packagePath: '/test/package',
276 |           background: true,
277 |         },
278 |         mockExecutor,
279 |       );
280 | 
281 |       // Should return success without calling executor
282 |       expect(result.content[0].text).toContain('🚀 Started executable in background');
283 |     });
284 |   });
285 | 
286 |   describe('Response Logic Testing', () => {
287 |     it('should return validation error for missing packagePath', async () => {
288 |       // Since the tool now uses createTypedTool, Zod validation happens at the handler level
289 |       // Test the handler directly to see Zod validation
290 |       const result = await swiftPackageRun.handler({});
291 | 
292 |       expect(result).toEqual({
293 |         content: [
294 |           {
295 |             type: 'text',
296 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\npackagePath: Invalid input: expected string, received undefined',
297 |           },
298 |         ],
299 |         isError: true,
300 |       });
301 |     });
302 | 
303 |     it('should return success response for background mode', async () => {
304 |       const mockExecutor = createNoopExecutor();
305 |       const result = await swift_package_runLogic(
306 |         {
307 |           packagePath: '/test/package',
308 |           background: true,
309 |         },
310 |         mockExecutor,
311 |       );
312 | 
313 |       expect(result.content[0].text).toContain('🚀 Started executable in background');
314 |       expect(result.content[0].text).toContain('💡 Process is running independently');
315 |     });
316 | 
317 |     it('should return success response for successful execution', async () => {
318 |       const mockExecutor = createMockExecutor({
319 |         success: true,
320 |         output: 'Hello, World!',
321 |       });
322 | 
323 |       const result = await swift_package_runLogic(
324 |         {
325 |           packagePath: '/test/package',
326 |         },
327 |         mockExecutor,
328 |       );
329 | 
330 |       expect(result).toEqual({
331 |         content: [
332 |           { type: 'text', text: '✅ Swift executable completed successfully.' },
333 |           { type: 'text', text: '💡 Process finished cleanly. Check output for results.' },
334 |           { type: 'text', text: 'Hello, World!' },
335 |         ],
336 |       });
337 |     });
338 | 
339 |     it('should return error response for failed execution', async () => {
340 |       const mockExecutor = createMockExecutor({
341 |         success: false,
342 |         output: '',
343 |         error: 'Compilation failed',
344 |       });
345 | 
346 |       const result = await swift_package_runLogic(
347 |         {
348 |           packagePath: '/test/package',
349 |         },
350 |         mockExecutor,
351 |       );
352 | 
353 |       expect(result).toEqual({
354 |         content: [
355 |           { type: 'text', text: '❌ Swift executable failed.' },
356 |           { type: 'text', text: '(no output)' },
357 |           { type: 'text', text: 'Errors:\nCompilation failed' },
358 |         ],
359 |       });
360 |     });
361 | 
362 |     it('should handle executor error', async () => {
363 |       const mockExecutor = createMockExecutor(new Error('Command not found'));
364 | 
365 |       const result = await swift_package_runLogic(
366 |         {
367 |           packagePath: '/test/package',
368 |         },
369 |         mockExecutor,
370 |       );
371 | 
372 |       expect(result).toEqual({
373 |         content: [
374 |           {
375 |             type: 'text',
376 |             text: 'Error: Failed to execute swift run\nDetails: Command not found',
377 |           },
378 |         ],
379 |         isError: true,
380 |       });
381 |     });
382 |   });
383 | });
384 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for test_device plugin
  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 manual stubs
  6 |  */
  7 | 
  8 | import { describe, it, expect, beforeEach } from 'vitest';
  9 | import * as z from 'zod';
 10 | import {
 11 |   createMockCommandResponse,
 12 |   createMockExecutor,
 13 |   createMockFileSystemExecutor,
 14 | } from '../../../../test-utils/mock-executors.ts';
 15 | import testDevice, { testDeviceLogic } from '../test_device.ts';
 16 | import { sessionStore } from '../../../../utils/session-store.ts';
 17 | 
 18 | describe('test_device plugin', () => {
 19 |   beforeEach(() => {
 20 |     sessionStore.clear();
 21 |   });
 22 | 
 23 |   describe('Export Field Validation (Literal)', () => {
 24 |     it('should have correct name', () => {
 25 |       expect(testDevice.name).toBe('test_device');
 26 |     });
 27 | 
 28 |     it('should have correct description', () => {
 29 |       expect(testDevice.description).toBe('Runs tests on a physical Apple device.');
 30 |     });
 31 | 
 32 |     it('should have handler function', () => {
 33 |       expect(typeof testDevice.handler).toBe('function');
 34 |     });
 35 | 
 36 |     it('should expose only session-free fields in public schema', () => {
 37 |       const schema = z.strictObject(testDevice.schema);
 38 |       expect(
 39 |         schema.safeParse({
 40 |           derivedDataPath: '/path/to/derived-data',
 41 |           extraArgs: ['--arg1'],
 42 |           preferXcodebuild: true,
 43 |           platform: 'iOS',
 44 |           testRunnerEnv: { FOO: 'bar' },
 45 |         }).success,
 46 |       ).toBe(true);
 47 |       expect(schema.safeParse({}).success).toBe(true);
 48 |       expect(schema.safeParse({ projectPath: '/path/to/project.xcodeproj' }).success).toBe(false);
 49 | 
 50 |       const schemaKeys = Object.keys(testDevice.schema).sort();
 51 |       expect(schemaKeys).toEqual([
 52 |         'derivedDataPath',
 53 |         'extraArgs',
 54 |         'platform',
 55 |         'preferXcodebuild',
 56 |         'testRunnerEnv',
 57 |       ]);
 58 |     });
 59 | 
 60 |     it('should validate XOR between projectPath and workspacePath', async () => {
 61 |       // This would be validated at the schema level via createTypedTool
 62 |       // We test the schema validation through successful logic calls instead
 63 |       const mockExecutor = createMockExecutor({
 64 |         success: true,
 65 |         output: JSON.stringify({
 66 |           title: 'Test Schema',
 67 |           result: 'SUCCESS',
 68 |           totalTestCount: 1,
 69 |           passedTests: 1,
 70 |           failedTests: 0,
 71 |           skippedTests: 0,
 72 |           expectedFailures: 0,
 73 |         }),
 74 |       });
 75 | 
 76 |       // Valid: project path only
 77 |       const projectResult = await testDeviceLogic(
 78 |         {
 79 |           projectPath: '/path/to/project.xcodeproj',
 80 |           scheme: 'MyScheme',
 81 |           deviceId: 'test-device-123',
 82 |         },
 83 |         mockExecutor,
 84 |         createMockFileSystemExecutor({
 85 |           mkdtemp: async () => '/tmp/xcodebuild-test-123',
 86 |           tmpdir: () => '/tmp',
 87 |           stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }),
 88 |           rm: async () => {},
 89 |         }),
 90 |       );
 91 |       expect(projectResult.isError).toBeFalsy();
 92 | 
 93 |       // Valid: workspace path only
 94 |       const workspaceResult = await testDeviceLogic(
 95 |         {
 96 |           workspacePath: '/path/to/workspace.xcworkspace',
 97 |           scheme: 'MyScheme',
 98 |           deviceId: 'test-device-123',
 99 |         },
100 |         mockExecutor,
101 |         createMockFileSystemExecutor({
102 |           mkdtemp: async () => '/tmp/xcodebuild-test-456',
103 |           tmpdir: () => '/tmp',
104 |           stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }),
105 |           rm: async () => {},
106 |         }),
107 |       );
108 |       expect(workspaceResult.isError).toBeFalsy();
109 |     });
110 |   });
111 | 
112 |   describe('Handler Requirements', () => {
113 |     it('should require scheme and device defaults', async () => {
114 |       const result = await testDevice.handler({});
115 | 
116 |       expect(result.isError).toBe(true);
117 |       expect(result.content[0].text).toContain('Missing required session defaults');
118 |       expect(result.content[0].text).toContain('Provide scheme and deviceId');
119 |     });
120 | 
121 |     it('should require project or workspace when defaults provide scheme and device', async () => {
122 |       sessionStore.setDefaults({ scheme: 'MyScheme', deviceId: 'test-device-123' });
123 | 
124 |       const result = await testDevice.handler({});
125 | 
126 |       expect(result.isError).toBe(true);
127 |       expect(result.content[0].text).toContain('Provide a project or workspace');
128 |     });
129 | 
130 |     it('should reject mutually exclusive project inputs when defaults satisfy requirements', async () => {
131 |       sessionStore.setDefaults({ scheme: 'MyScheme', deviceId: 'test-device-123' });
132 | 
133 |       const result = await testDevice.handler({
134 |         projectPath: '/path/to/project.xcodeproj',
135 |         workspacePath: '/path/to/workspace.xcworkspace',
136 |       });
137 | 
138 |       expect(result.isError).toBe(true);
139 |       expect(result.content[0].text).toContain('Parameter validation failed');
140 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
141 |     });
142 |   });
143 | 
144 |   describe('Handler Behavior (Complete Literal Returns)', () => {
145 |     beforeEach(() => {
146 |       // Clean setup for standard testing pattern
147 |     });
148 | 
149 |     it('should return successful test response with parsed results', async () => {
150 |       // Mock xcresulttool output
151 |       const mockExecutor = createMockExecutor({
152 |         success: true,
153 |         output: JSON.stringify({
154 |           title: 'MyScheme Tests',
155 |           result: 'SUCCESS',
156 |           totalTestCount: 5,
157 |           passedTests: 5,
158 |           failedTests: 0,
159 |           skippedTests: 0,
160 |           expectedFailures: 0,
161 |         }),
162 |       });
163 | 
164 |       const result = await testDeviceLogic(
165 |         {
166 |           projectPath: '/path/to/project.xcodeproj',
167 |           scheme: 'MyScheme',
168 |           deviceId: 'test-device-123',
169 |           configuration: 'Debug',
170 |           preferXcodebuild: false,
171 |           platform: 'iOS',
172 |         },
173 |         mockExecutor,
174 |         createMockFileSystemExecutor({
175 |           mkdtemp: async () => '/tmp/xcodebuild-test-123456',
176 |           tmpdir: () => '/tmp',
177 |           stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }),
178 |           rm: async () => {},
179 |         }),
180 |       );
181 | 
182 |       expect(result.content).toHaveLength(2);
183 |       expect(result.content[0].text).toContain('✅');
184 |       expect(result.content[1].text).toContain('Test Results Summary:');
185 |       expect(result.content[1].text).toContain('MyScheme Tests');
186 |     });
187 | 
188 |     it('should handle test failure scenarios', async () => {
189 |       // Mock xcresulttool output for failed tests
190 |       const mockExecutor = createMockExecutor({
191 |         success: true,
192 |         output: JSON.stringify({
193 |           title: 'MyScheme Tests',
194 |           result: 'FAILURE',
195 |           totalTestCount: 5,
196 |           passedTests: 3,
197 |           failedTests: 2,
198 |           skippedTests: 0,
199 |           expectedFailures: 0,
200 |           testFailures: [
201 |             {
202 |               testName: 'testExample',
203 |               targetName: 'MyTarget',
204 |               failureText: 'Expected true but was false',
205 |             },
206 |           ],
207 |         }),
208 |       });
209 | 
210 |       const result = await testDeviceLogic(
211 |         {
212 |           projectPath: '/path/to/project.xcodeproj',
213 |           scheme: 'MyScheme',
214 |           deviceId: 'test-device-123',
215 |           configuration: 'Debug',
216 |           preferXcodebuild: false,
217 |           platform: 'iOS',
218 |         },
219 |         mockExecutor,
220 |         createMockFileSystemExecutor({
221 |           mkdtemp: async () => '/tmp/xcodebuild-test-123456',
222 |           tmpdir: () => '/tmp',
223 |           stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }),
224 |           rm: async () => {},
225 |         }),
226 |       );
227 | 
228 |       expect(result.content).toHaveLength(2);
229 |       expect(result.content[1].text).toContain('Test Failures:');
230 |       expect(result.content[1].text).toContain('testExample');
231 |     });
232 | 
233 |     it('should handle xcresult parsing failures gracefully', async () => {
234 |       // Create a multi-call mock that handles different commands
235 |       let callCount = 0;
236 |       const mockExecutor = async (
237 |         _args: string[],
238 |         _description?: string,
239 |         _useShell?: boolean,
240 |         _opts?: { cwd?: string },
241 |         _detached?: boolean,
242 |       ) => {
243 |         callCount++;
244 | 
245 |         // First call is for xcodebuild test (successful)
246 |         if (callCount === 1) {
247 |           return createMockCommandResponse({ success: true, output: 'BUILD SUCCEEDED' });
248 |         }
249 | 
250 |         // Second call is for xcresulttool (fails)
251 |         return createMockCommandResponse({ success: false, error: 'xcresulttool failed' });
252 |       };
253 | 
254 |       const result = await testDeviceLogic(
255 |         {
256 |           projectPath: '/path/to/project.xcodeproj',
257 |           scheme: 'MyScheme',
258 |           deviceId: 'test-device-123',
259 |           configuration: 'Debug',
260 |           preferXcodebuild: false,
261 |           platform: 'iOS',
262 |         },
263 |         mockExecutor,
264 |         createMockFileSystemExecutor({
265 |           mkdtemp: async () => '/tmp/xcodebuild-test-123456',
266 |           tmpdir: () => '/tmp',
267 |           stat: async () => {
268 |             throw new Error('File not found');
269 |           },
270 |           rm: async () => {},
271 |         }),
272 |       );
273 | 
274 |       // When xcresult parsing fails, it falls back to original test result only
275 |       expect(result.content).toHaveLength(1);
276 |       expect(result.content[0].text).toContain('✅');
277 |     });
278 | 
279 |     it('should support different platforms', async () => {
280 |       // Mock xcresulttool output
281 |       const mockExecutor = createMockExecutor({
282 |         success: true,
283 |         output: JSON.stringify({
284 |           title: 'WatchApp Tests',
285 |           result: 'SUCCESS',
286 |           totalTestCount: 3,
287 |           passedTests: 3,
288 |           failedTests: 0,
289 |           skippedTests: 0,
290 |           expectedFailures: 0,
291 |         }),
292 |       });
293 | 
294 |       const result = await testDeviceLogic(
295 |         {
296 |           projectPath: '/path/to/project.xcodeproj',
297 |           scheme: 'WatchApp',
298 |           deviceId: 'watch-device-456',
299 |           configuration: 'Debug',
300 |           preferXcodebuild: false,
301 |           platform: 'watchOS',
302 |         },
303 |         mockExecutor,
304 |         createMockFileSystemExecutor({
305 |           mkdtemp: async () => '/tmp/xcodebuild-test-123456',
306 |           tmpdir: () => '/tmp',
307 |           stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }),
308 |           rm: async () => {},
309 |         }),
310 |       );
311 | 
312 |       expect(result.content).toHaveLength(2);
313 |       expect(result.content[1].text).toContain('WatchApp Tests');
314 |     });
315 | 
316 |     it('should handle optional parameters', async () => {
317 |       // Mock xcresulttool output
318 |       const mockExecutor = createMockExecutor({
319 |         success: true,
320 |         output: JSON.stringify({
321 |           title: 'Tests',
322 |           result: 'SUCCESS',
323 |           totalTestCount: 1,
324 |           passedTests: 1,
325 |           failedTests: 0,
326 |           skippedTests: 0,
327 |           expectedFailures: 0,
328 |         }),
329 |       });
330 | 
331 |       const result = await testDeviceLogic(
332 |         {
333 |           projectPath: '/path/to/project.xcodeproj',
334 |           scheme: 'MyScheme',
335 |           deviceId: 'test-device-123',
336 |           configuration: 'Release',
337 |           derivedDataPath: '/tmp/derived-data',
338 |           extraArgs: ['--verbose'],
339 |           preferXcodebuild: false,
340 |           platform: 'iOS',
341 |         },
342 |         mockExecutor,
343 |         createMockFileSystemExecutor({
344 |           mkdtemp: async () => '/tmp/xcodebuild-test-123456',
345 |           tmpdir: () => '/tmp',
346 |           stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }),
347 |           rm: async () => {},
348 |         }),
349 |       );
350 | 
351 |       expect(result.content).toHaveLength(2);
352 |       expect(result.content[0].text).toContain('✅');
353 |     });
354 | 
355 |     it('should handle workspace testing successfully', async () => {
356 |       // Mock xcresulttool output
357 |       const mockExecutor = createMockExecutor({
358 |         success: true,
359 |         output: JSON.stringify({
360 |           title: 'WorkspaceScheme Tests',
361 |           result: 'SUCCESS',
362 |           totalTestCount: 10,
363 |           passedTests: 10,
364 |           failedTests: 0,
365 |           skippedTests: 0,
366 |           expectedFailures: 0,
367 |         }),
368 |       });
369 | 
370 |       const result = await testDeviceLogic(
371 |         {
372 |           workspacePath: '/path/to/workspace.xcworkspace',
373 |           scheme: 'WorkspaceScheme',
374 |           deviceId: 'test-device-456',
375 |           configuration: 'Debug',
376 |           preferXcodebuild: false,
377 |           platform: 'iOS',
378 |         },
379 |         mockExecutor,
380 |         createMockFileSystemExecutor({
381 |           mkdtemp: async () => '/tmp/xcodebuild-test-workspace-123',
382 |           tmpdir: () => '/tmp',
383 |           stat: async () => ({ isDirectory: () => false, mtimeMs: 0 }),
384 |           rm: async () => {},
385 |         }),
386 |       );
387 | 
388 |       expect(result.content).toHaveLength(2);
389 |       expect(result.content[0].text).toContain('✅');
390 |       expect(result.content[1].text).toContain('Test Results Summary:');
391 |       expect(result.content[1].text).toContain('WorkspaceScheme Tests');
392 |     });
393 |   });
394 | });
395 | 
```

--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------

```bash
  1 | #!/bin/bash
  2 | set -e
  3 | 
  4 | # GitHub Release Creation Script
  5 | # This script handles only the GitHub release creation.
  6 | # Building and NPM publishing are handled by GitHub workflows.
  7 | #
  8 | # Usage: ./scripts/release.sh [VERSION|BUMP_TYPE] [OPTIONS]
  9 | # Run with --help for detailed usage information
 10 | FIRST_ARG=$1
 11 | DRY_RUN=false
 12 | VERSION=""
 13 | BUMP_TYPE=""
 14 | 
 15 | # Function to show help
 16 | show_help() {
 17 |   cat << 'EOF'
 18 | 📦 GitHub Release Creator
 19 | 
 20 | Creates releases with automatic semver bumping. Only handles GitHub release
 21 | creation - building and NPM publishing are handled by workflows.
 22 | 
 23 | USAGE:
 24 |     [VERSION|BUMP_TYPE] [OPTIONS]
 25 | 
 26 | ARGUMENTS:
 27 |     VERSION         Explicit version (e.g., 1.5.0, 2.0.0-beta.1)
 28 |     BUMP_TYPE       major | minor [default] | patch
 29 | 
 30 | OPTIONS:
 31 |     --dry-run       Preview without executing
 32 |     -h, --help      Show this help
 33 | 
 34 | EXAMPLES:
 35 |     (no args)       Interactive minor bump
 36 |     major           Interactive major bump
 37 |     1.5.0           Use specific version
 38 |     patch --dry-run Preview patch bump
 39 | 
 40 | EOF
 41 | 
 42 |   local highest_version=$(get_highest_version)
 43 |   if [[ -n "$highest_version" ]]; then
 44 |     echo "CURRENT: $highest_version"
 45 |     echo "NEXT: major=$(bump_version "$highest_version" "major") | minor=$(bump_version "$highest_version" "minor") | patch=$(bump_version "$highest_version" "patch")"
 46 |   else
 47 |     echo "No existing version tags found"
 48 |   fi
 49 |   echo ""
 50 | }
 51 | 
 52 | # Function to get the highest version from git tags
 53 | get_highest_version() {
 54 |   git tag | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?$' | sed 's/^v//' | sort -V | tail -1
 55 | }
 56 | 
 57 | # Function to parse version components
 58 | parse_version() {
 59 |   local version=$1
 60 |   echo "$version" | sed -E 's/^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$/\1 \2 \3 \4/'
 61 | }
 62 | 
 63 | # Function to bump version based on type
 64 | bump_version() {
 65 |   local current_version=$1
 66 |   local bump_type=$2
 67 | 
 68 |   local parsed=($(parse_version "$current_version"))
 69 |   local major=${parsed[0]}
 70 |   local minor=${parsed[1]}
 71 |   local patch=${parsed[2]}
 72 |   local prerelease=${parsed[3]:-""}
 73 | 
 74 |   # Remove prerelease for stable version bumps
 75 |   case $bump_type in
 76 |     major)
 77 |       echo "$((major + 1)).0.0"
 78 |       ;;
 79 |     minor)
 80 |       echo "${major}.$((minor + 1)).0"
 81 |       ;;
 82 |     patch)
 83 |       echo "${major}.${minor}.$((patch + 1))"
 84 |       ;;
 85 |     *)
 86 |       echo "❌ Unknown bump type: $bump_type" >&2
 87 |       exit 1
 88 |       ;;
 89 |   esac
 90 | }
 91 | 
 92 | # Function to validate version format
 93 | validate_version() {
 94 |   local version=$1
 95 |   if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?$ ]]; then
 96 |     echo "❌ Invalid version format: $version"
 97 |     echo "Version must be in format: x.y.z or x.y.z-tag.n (e.g., 1.4.0 or 1.4.0-beta.3)"
 98 |     return 1
 99 |   fi
100 |   return 0
101 | }
102 | 
103 | # Function to compare versions (returns 1 if first version is greater, 0 if equal, -1 if less)
104 | compare_versions() {
105 |   local version1=$1
106 |   local version2=$2
107 | 
108 |   local v1_base=${version1%%-*}
109 |   local v2_base=${version2%%-*}
110 |   local v1_pre=""
111 |   local v2_pre=""
112 | 
113 |   [[ "$version1" == *-* ]] && v1_pre=${version1#*-}
114 |   [[ "$version2" == *-* ]] && v2_pre=${version2#*-}
115 | 
116 |   # When base versions match, a stable release outranks any prerelease
117 |   if [[ "$v1_base" == "$v2_base" ]]; then
118 |     if [[ -z "$v1_pre" && -n "$v2_pre" ]]; then
119 |       echo 1
120 |       return
121 |     elif [[ -n "$v1_pre" && -z "$v2_pre" ]]; then
122 |       echo -1
123 |       return
124 |     elif [[ "$version1" == "$version2" ]]; then
125 |       echo 0
126 |       return
127 |     fi
128 |   fi
129 | 
130 |   # Fallback to version sort for differing bases or two prereleases
131 |   local sorted=$(printf "%s\n%s" "$version1" "$version2" | sort -V)
132 |   if [[ "$(echo "$sorted" | head -1)" == "$version1" ]]; then
133 |     echo -1
134 |   else
135 |     echo 1
136 |   fi
137 | }
138 | 
139 | # Function to ask for confirmation
140 | ask_confirmation() {
141 |   local suggested_version=$1
142 |   echo ""
143 |   echo "🚀 Suggested next version: $suggested_version"
144 |   read -p "Do you want to use this version? (y/N): " -n 1 -r
145 |   echo
146 |   if [[ $REPLY =~ ^[Yy]$ ]]; then
147 |     return 0
148 |   else
149 |     return 1
150 |   fi
151 | }
152 | 
153 | # Function to get version interactively
154 | get_version_interactively() {
155 |   echo ""
156 |   echo "Please enter the version manually:"
157 |   while true; do
158 |     read -p "Version: " manual_version
159 |     if validate_version "$manual_version"; then
160 |       local highest_version=$(get_highest_version)
161 |       if [[ -n "$highest_version" ]]; then
162 |         local comparison=$(compare_versions "$manual_version" "$highest_version")
163 |         if [[ $comparison -le 0 ]]; then
164 |           echo "❌ Version $manual_version is not newer than the highest existing version $highest_version"
165 |           continue
166 |         fi
167 |       fi
168 |       VERSION="$manual_version"
169 |       break
170 |     fi
171 |   done
172 | }
173 | 
174 | # Check for help flags first
175 | for arg in "$@"; do
176 |   if [[ "$arg" == "-h" ]] || [[ "$arg" == "--help" ]]; then
177 |     show_help
178 |     exit 0
179 |   fi
180 | done
181 | 
182 | # Check for arguments and set flags
183 | for arg in "$@"; do
184 |   if [[ "$arg" == "--dry-run" ]]; then
185 |     DRY_RUN=true
186 |   fi
187 | done
188 | 
189 | # Determine version or bump type (ignore --dry-run flag)
190 | if [[ -z "$FIRST_ARG" ]] || [[ "$FIRST_ARG" == "--dry-run" ]]; then
191 |   # No argument provided, default to minor bump
192 |   BUMP_TYPE="minor"
193 | elif [[ "$FIRST_ARG" == "major" ]] || [[ "$FIRST_ARG" == "minor" ]] || [[ "$FIRST_ARG" == "patch" ]]; then
194 |   # Bump type provided
195 |   BUMP_TYPE="$FIRST_ARG"
196 | else
197 |   # Version string provided
198 |   if validate_version "$FIRST_ARG"; then
199 |     VERSION="$FIRST_ARG"
200 |   else
201 |     exit 1
202 |   fi
203 | fi
204 | 
205 | # If bump type is set, calculate the suggested version
206 | if [[ -n "$BUMP_TYPE" ]]; then
207 |   HIGHEST_VERSION=$(get_highest_version)
208 |   if [[ -z "$HIGHEST_VERSION" ]]; then
209 |     echo "❌ No existing version tags found. Please provide a version manually."
210 |     get_version_interactively
211 |   else
212 |     SUGGESTED_VERSION=$(bump_version "$HIGHEST_VERSION" "$BUMP_TYPE")
213 | 
214 |     if ask_confirmation "$SUGGESTED_VERSION"; then
215 |       VERSION="$SUGGESTED_VERSION"
216 |     else
217 |       get_version_interactively
218 |     fi
219 |   fi
220 | fi
221 | 
222 | # Final validation and version comparison
223 | if [[ -z "$VERSION" ]]; then
224 |   echo "❌ No version determined"
225 |   exit 1
226 | fi
227 | 
228 | HIGHEST_VERSION=$(get_highest_version)
229 | if [[ -n "$HIGHEST_VERSION" ]]; then
230 |   COMPARISON=$(compare_versions "$VERSION" "$HIGHEST_VERSION")
231 |   if [[ $COMPARISON -le 0 ]]; then
232 |     echo "❌ Version $VERSION is not newer than the highest existing version $HIGHEST_VERSION"
233 |     exit 1
234 |   fi
235 | fi
236 | 
237 | # Detect current branch
238 | BRANCH=$(git rev-parse --abbrev-ref HEAD)
239 | 
240 | # Enforce branch policy - only allow releases from main
241 | if [[ "$BRANCH" != "main" ]]; then
242 |   echo "❌ Error: Releases must be created from the main branch."
243 |   echo "Current branch: $BRANCH"
244 |   echo "Please switch to main and try again."
245 |   exit 1
246 | fi
247 | 
248 | run() {
249 |   if $DRY_RUN; then
250 |     echo "[dry-run] $*"
251 |     return 0
252 |   fi
253 | 
254 |   "$@"
255 | }
256 | 
257 | # Portable in-place sed (BSD/macOS vs GNU/Linux)
258 | # - macOS/BSD sed requires: sed -i '' -E 's/.../.../' file
259 | # - GNU sed requires:       sed -i -E 's/.../.../' file
260 | sed_inplace() {
261 |   local expr="$1"
262 |   local file="$2"
263 | 
264 |   if sed --version >/dev/null 2>&1; then
265 |     # GNU sed
266 |     sed -i -E "$expr" "$file"
267 |   else
268 |     # BSD/macOS sed
269 |     sed -i '' -E "$expr" "$file"
270 |   fi
271 | }
272 | 
273 | # Ensure we're in the project root (parent of scripts directory)
274 | cd "$(dirname "$0")/.."
275 | 
276 | # Check if working directory is clean (only enforced for real runs)
277 | if ! $DRY_RUN; then
278 |   if ! git diff-index --quiet HEAD --; then
279 |     echo "❌ Error: Working directory is not clean."
280 |     echo "Please commit or stash your changes before creating a release."
281 |     exit 1
282 |   fi
283 | else
284 |   if ! git diff-index --quiet HEAD --; then
285 |     echo "⚠️  Dry-run: working directory is not clean (continuing)."
286 |   fi
287 | fi
288 | 
289 | # Check if package.json already has this version (from previous attempt)
290 | CURRENT_PACKAGE_VERSION=$(node -p "require('./package.json').version")
291 | if [[ "$CURRENT_PACKAGE_VERSION" == "$VERSION" ]]; then
292 |   echo "📦 Version $VERSION already set in package.json"
293 |   SKIP_VERSION_UPDATE=true
294 | else
295 |   SKIP_VERSION_UPDATE=false
296 | fi
297 | 
298 | if [[ "$SKIP_VERSION_UPDATE" == "false" ]]; then
299 |   # Version update
300 |   echo ""
301 |   echo "🔧 Setting version to $VERSION..."
302 |   run npm version "$VERSION" --no-git-tag-version
303 | 
304 |   # README update
305 |   echo ""
306 |   echo "📝 Updating version in README.md..."
307 |   # Update version references in code examples using extended regex for precise semver matching
308 |   README_AT_SEMVER_REGEX='@[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?(-[a-zA-Z0-9]+\.[0-9]+)*(-[a-zA-Z0-9]+)?'
309 |   run sed_inplace "s/${README_AT_SEMVER_REGEX}/@${VERSION}/g" README.md
310 | 
311 |   # Update URL-encoded version references in shield links
312 |   echo "📝 Updating version in README.md shield links..."
313 |   README_URLENCODED_NPM_AT_SEMVER_REGEX='npm%3Axcodebuildmcp%40[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?(-[a-zA-Z0-9]+\.[0-9]+)*(-[a-zA-Z0-9]+)?'
314 |   run sed_inplace "s/${README_URLENCODED_NPM_AT_SEMVER_REGEX}/npm%3Axcodebuildmcp%40${VERSION}/g" README.md
315 | 
316 |   # server.json update
317 |   echo ""
318 |   if [[ -f server.json ]]; then
319 |     echo "📝 Updating server.json version to $VERSION..."
320 |     run node -e "const fs=require('fs');const f='server.json';const j=JSON.parse(fs.readFileSync(f,'utf8'));j.version='${VERSION}';if(Array.isArray(j.packages)){j.packages=j.packages.map(p=>({...p,version:'${VERSION}'}));}fs.writeFileSync(f,JSON.stringify(j,null,2)+'\n');"
321 |   else
322 |     echo "⚠️  server.json not found; skipping update"
323 |   fi
324 | 
325 |   # Git operations
326 |   echo ""
327 |   echo "📦 Committing version changes..."
328 |   if [[ -f server.json ]]; then
329 |     run git add package.json package-lock.json README.md server.json
330 |   else
331 |     run git add package.json package-lock.json README.md
332 |   fi
333 |   run git commit -m "Release v$VERSION"
334 | else
335 |   echo "⏭️  Skipping version update (already done)"
336 |   # Ensure server.json still matches the desired version (in case of a partial previous run)
337 |   if [[ -f server.json ]]; then
338 |     CURRENT_SERVER_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('server.json','utf8')).version||'')")
339 |     if [[ "$CURRENT_SERVER_VERSION" != "$VERSION" ]]; then
340 |       echo "📝 Aligning server.json to $VERSION..."
341 |       run node -e "const fs=require('fs');const f='server.json';const j=JSON.parse(fs.readFileSync(f,'utf8'));j.version='${VERSION}';if(Array.isArray(j.packages)){j.packages=j.packages.map(p=>({...p,version:'${VERSION}'}));}fs.writeFileSync(f,JSON.stringify(j,null,2)+'\n');"
342 |       run git add server.json
343 |       run git commit -m "Align server.json for v$VERSION"
344 |     fi
345 |   fi
346 | fi
347 | 
348 | # Create or recreate tag at current HEAD
349 | echo "🏷️  Creating tag v$VERSION..."
350 | run git tag -f "v$VERSION"
351 | 
352 | echo ""
353 | echo "🚀 Pushing to origin..."
354 | run git push origin "$BRANCH" --tags
355 | 
356 | # In dry-run, stop here (don't monitor workflows, and don't claim a release happened).
357 | if $DRY_RUN; then
358 |   echo ""
359 |   echo "ℹ️  Dry-run: skipping GitHub Actions workflow monitoring."
360 |   exit 0
361 | fi
362 | 
363 | # Monitor the workflow and handle failures
364 | echo ""
365 | echo "⏳ Monitoring GitHub Actions workflow..."
366 | echo "This may take a few minutes..."
367 | 
368 | # Wait for workflow to start
369 | sleep 5
370 | 
371 | # Get the workflow run ID for this tag
372 | RUN_ID=$(gh run list --workflow=release.yml --limit=1 --json databaseId --jq '.[0].databaseId')
373 | 
374 | if [[ -n "$RUN_ID" ]]; then
375 |   echo "📊 Workflow run ID: $RUN_ID"
376 |   echo "🔍 Watching workflow progress..."
377 |   echo "(Press Ctrl+C to detach and monitor manually)"
378 |   echo ""
379 | 
380 |   # Watch the workflow with exit status
381 |   if gh run watch "$RUN_ID" --exit-status; then
382 |     echo ""
383 |     echo "✅ Release v$VERSION completed successfully!"
384 |     echo "📦 View on NPM: https://www.npmjs.com/package/xcodebuildmcp/v/$VERSION"
385 |     echo "🎉 View release: https://github.com/cameroncooke/XcodeBuildMCP/releases/tag/v$VERSION"
386 |     # MCP Registry verification link
387 |     echo "🔎 Verify MCP Registry: https://registry.modelcontextprotocol.io/v0/servers?search=com.xcodebuildmcp/XcodeBuildMCP&version=latest"
388 |   else
389 |     echo ""
390 |     echo "❌ CI workflow failed!"
391 |     echo ""
392 |     # Prefer job state: if the primary 'release' job succeeded, treat as success.
393 |     RELEASE_JOB_CONCLUSION=$(gh run view "$RUN_ID" --json jobs --jq '.jobs[] | select(.name=="release") | .conclusion')
394 |     if [ "$RELEASE_JOB_CONCLUSION" = "success" ]; then
395 |       echo "⚠️ Workflow reported failure, but primary 'release' job concluded SUCCESS."
396 |       echo "✅ Treating release as successful. Tag v$VERSION is kept."
397 |       echo "📦 Verify on NPM: https://www.npmjs.com/package/xcodebuildmcp/v/$VERSION"
398 |       exit 0
399 |     fi
400 |     echo "🧹 Cleaning up tags only (keeping version commit)..."
401 | 
402 |     # Delete remote tag
403 |     echo "  - Deleting remote tag v$VERSION..."
404 |     git push origin :refs/tags/v$VERSION 2>/dev/null || true
405 | 
406 |     # Delete local tag
407 |     echo "  - Deleting local tag v$VERSION..."
408 |     git tag -d v$VERSION
409 | 
410 |     echo ""
411 |     echo "✅ Tag cleanup complete!"
412 |     echo ""
413 |     echo "ℹ️  The version commit remains in your history."
414 |     echo "📝 To retry after fixing issues:"
415 |     echo "   1. Fix the CI issues"
416 |     echo "   2. Commit your fixes"
417 |     echo "   3. Run: ./scripts/release.sh $VERSION"
418 |     echo ""
419 |     echo "🔍 To see what failed: gh run view $RUN_ID --log-failed"
420 |     exit 1
421 |   fi
422 | else
423 |   echo "⚠️  Could not find workflow run. Please check manually:"
424 |   echo "https://github.com/cameroncooke/XcodeBuildMCP/actions"
425 | fi
426 | 
```
Page 9/16FirstPrevNextLast