#
tokens: 47501/50000 16/337 files (page 6/11)
lines: off (toggle) GitHub
raw markdown copy
This is page 6 of 11. Use http://codebase.md/cameroncooke/xcodebuildmcp?lines=false&page={x} to view the full context.

# Directory Structure

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

# Files

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

```typescript
/**
 * Tests for build_device plugin (unified)
 * Following CLAUDE.md testing standards with literal validation
 * Using dependency injection for deterministic testing
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts';
import buildDevice, { buildDeviceLogic } from '../build_device.ts';
import { sessionStore } from '../../../../utils/session-store.ts';

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

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

    it('should have correct description', () => {
      expect(buildDevice.description).toBe('Builds an app for a connected device.');
    });

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

    it('should expose only optional build-tuning fields in public schema', () => {
      const schema = z.object(buildDevice.schema).strict();
      expect(schema.safeParse({}).success).toBe(true);
      expect(
        schema.safeParse({ derivedDataPath: '/path/to/derived-data', extraArgs: [] }).success,
      ).toBe(true);
      expect(schema.safeParse({ projectPath: '/path/to/MyProject.xcodeproj' }).success).toBe(false);

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

  describe('XOR Validation', () => {
    it('should error when neither projectPath nor workspacePath provided', async () => {
      const result = await buildDevice.handler({
        scheme: 'MyScheme',
      });

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

    it('should error when both projectPath and workspacePath provided', async () => {
      const result = await buildDevice.handler({
        projectPath: '/path/to/MyProject.xcodeproj',
        workspacePath: '/path/to/MyProject.xcworkspace',
        scheme: 'MyScheme',
      });

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

  describe('Parameter Validation (via Handler)', () => {
    it('should return Zod validation error for missing scheme', async () => {
      const result = await buildDevice.handler({
        projectPath: '/path/to/MyProject.xcodeproj',
      });

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

    it('should return Zod validation error for invalid parameter types', async () => {
      const result = await buildDevice.handler({
        projectPath: 123, // Should be string
        scheme: 'MyScheme',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('projectPath');
      expect(result.content[0].text).toContain(
        'Tip: set session defaults via session-set-defaults',
      );
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should pass validation and execute successfully with valid project parameters', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Build succeeded',
      });

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

      expect(result.isError).toBeUndefined();
      expect(result.content).toHaveLength(2);
      expect(result.content[0].text).toContain('✅ iOS Device Build build succeeded');
    });

    it('should pass validation and execute successfully with valid workspace parameters', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Build succeeded',
      });

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

      expect(result.isError).toBeUndefined();
      expect(result.content).toHaveLength(2);
      expect(result.content[0].text).toContain('✅ iOS Device Build build succeeded');
    });

    it('should verify workspace command generation with mock executor', async () => {
      const commandCalls: Array<{
        args: string[];
        logPrefix: string;
        silent: boolean;
        timeout: number | undefined;
      }> = [];

      const stubExecutor = async (
        args: string[],
        logPrefix: string,
        silent: boolean,
        timeout?: number,
      ) => {
        commandCalls.push({ args, logPrefix, silent, timeout });
        return {
          success: true,
          output: 'Build succeeded',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      await buildDeviceLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
        },
        stubExecutor,
      );

      expect(commandCalls).toHaveLength(1);
      expect(commandCalls[0]).toEqual({
        args: [
          'xcodebuild',
          '-workspace',
          '/path/to/MyProject.xcworkspace',
          '-scheme',
          'MyScheme',
          '-configuration',
          'Debug',
          '-skipMacroValidation',
          '-destination',
          'generic/platform=iOS',
          'build',
        ],
        logPrefix: 'iOS Device Build',
        silent: true,
        timeout: undefined,
      });
    });

    it('should verify command generation with mock executor', async () => {
      const commandCalls: Array<{
        args: string[];
        logPrefix: string;
        silent: boolean;
        timeout: number | undefined;
      }> = [];

      const stubExecutor = async (
        args: string[],
        logPrefix: string,
        silent: boolean,
        timeout?: number,
      ) => {
        commandCalls.push({ args, logPrefix, silent, timeout });
        return {
          success: true,
          output: 'Build succeeded',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      await buildDeviceLogic(
        {
          projectPath: '/path/to/MyProject.xcodeproj',
          scheme: 'MyScheme',
        },
        stubExecutor,
      );

      expect(commandCalls).toHaveLength(1);
      expect(commandCalls[0]).toEqual({
        args: [
          'xcodebuild',
          '-project',
          '/path/to/MyProject.xcodeproj',
          '-scheme',
          'MyScheme',
          '-configuration',
          'Debug',
          '-skipMacroValidation',
          '-destination',
          'generic/platform=iOS',
          'build',
        ],
        logPrefix: 'iOS Device Build',
        silent: true,
        timeout: undefined,
      });
    });

    it('should return exact successful build response', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Build succeeded',
      });

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

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ iOS Device Build build succeeded for scheme MyScheme.',
          },
          {
            type: 'text',
            text: "Next Steps:\n1. Get app path: get_device_app_path({ scheme: 'MyScheme' })\n2. Get bundle ID: get_app_bundle_id({ appPath: 'PATH_FROM_STEP_1' })\n3. Launch: launch_app_device({ bundleId: 'BUNDLE_ID_FROM_STEP_2' })",
          },
        ],
      });
    });

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

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

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

    it('should include optional parameters in command', async () => {
      const commandCalls: Array<{
        args: string[];
        logPrefix: string;
        silent: boolean;
        timeout: number | undefined;
      }> = [];

      const stubExecutor = async (
        args: string[],
        logPrefix: string,
        silent: boolean,
        timeout?: number,
      ) => {
        commandCalls.push({ args, logPrefix, silent, timeout });
        return {
          success: true,
          output: 'Build succeeded',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      await buildDeviceLogic(
        {
          projectPath: '/path/to/MyProject.xcodeproj',
          scheme: 'MyScheme',
          configuration: 'Release',
          derivedDataPath: '/tmp/derived-data',
          extraArgs: ['--verbose'],
        },
        stubExecutor,
      );

      expect(commandCalls).toHaveLength(1);
      expect(commandCalls[0]).toEqual({
        args: [
          'xcodebuild',
          '-project',
          '/path/to/MyProject.xcodeproj',
          '-scheme',
          'MyScheme',
          '-configuration',
          'Release',
          '-skipMacroValidation',
          '-destination',
          'generic/platform=iOS',
          '-derivedDataPath',
          '/tmp/derived-data',
          '--verbose',
          'build',
        ],
        logPrefix: 'iOS Device Build',
        silent: true,
        timeout: undefined,
      });
    });
  });
});

```

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

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

import { describe, it, expect } from 'vitest';
import { z } from 'zod';
import plugin, { get_app_bundle_idLogic } from '../get_app_bundle_id.ts';
import {
  createMockFileSystemExecutor,
  createCommandMatchingMockExecutor,
} from '../../../../test-utils/mock-executors.ts';

describe('get_app_bundle_id plugin', () => {
  // Helper function to create mock executor for command matching
  const createMockExecutorForCommands = (results: Record<string, string | Error>) => {
    return createCommandMatchingMockExecutor(
      Object.fromEntries(
        Object.entries(results).map(([command, result]) => [
          command,
          result instanceof Error
            ? { success: false, error: result.message }
            : { success: true, output: result },
        ]),
      ),
    );
  };

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

    it('should have correct description', () => {
      expect(plugin.description).toBe(
        "Extracts the bundle identifier from an app bundle (.app) for any Apple platform (iOS, iPadOS, watchOS, tvOS, visionOS). IMPORTANT: You MUST provide the appPath parameter. Example: get_app_bundle_id({ appPath: '/path/to/your/app.app' })",
      );
    });

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

    it('should validate schema with valid inputs', () => {
      const schema = z.object(plugin.schema);
      expect(schema.safeParse({ appPath: '/path/to/MyApp.app' }).success).toBe(true);
      expect(schema.safeParse({ appPath: '/Users/dev/MyApp.app' }).success).toBe(true);
    });

    it('should validate schema with invalid inputs', () => {
      const schema = z.object(plugin.schema);
      expect(schema.safeParse({}).success).toBe(false);
      expect(schema.safeParse({ appPath: 123 }).success).toBe(false);
      expect(schema.safeParse({ appPath: null }).success).toBe(false);
      expect(schema.safeParse({ appPath: undefined }).success).toBe(false);
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should return error when appPath validation fails', async () => {
      // Test validation through the handler which uses Zod validation
      const result = await plugin.handler({});

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nappPath: Required',
          },
        ],
        isError: true,
      });
    });

    it('should return error when file exists validation fails', async () => {
      const mockExecutor = createMockExecutorForCommands({});
      const mockFileSystemExecutor = createMockFileSystemExecutor({
        existsSync: () => false,
      });

      const result = await get_app_bundle_idLogic(
        { appPath: '/path/to/MyApp.app' },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: "File not found: '/path/to/MyApp.app'. Please check the path and try again.",
          },
        ],
        isError: true,
      });
    });

    it('should return success with bundle ID using defaults read', async () => {
      const mockExecutor = createMockExecutorForCommands({
        'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': 'com.example.MyApp',
      });
      const mockFileSystemExecutor = createMockFileSystemExecutor({
        existsSync: () => true,
      });

      const result = await get_app_bundle_idLogic(
        { appPath: '/path/to/MyApp.app' },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ Bundle ID: com.example.MyApp',
          },
          {
            type: 'text',
            text: `Next Steps:
- Simulator: install_app_sim + launch_app_sim
- Device: install_app_device + launch_app_device`,
          },
        ],
        isError: false,
      });
    });

    it('should fallback to PlistBuddy when defaults read fails', async () => {
      const mockExecutor = createMockExecutorForCommands({
        'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
          'defaults read failed',
        ),
        '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
          'com.example.MyApp',
      });
      const mockFileSystemExecutor = createMockFileSystemExecutor({
        existsSync: () => true,
      });

      const result = await get_app_bundle_idLogic(
        { appPath: '/path/to/MyApp.app' },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ Bundle ID: com.example.MyApp',
          },
          {
            type: 'text',
            text: `Next Steps:
- Simulator: install_app_sim + launch_app_sim
- Device: install_app_device + launch_app_device`,
          },
        ],
        isError: false,
      });
    });

    it('should return error when both extraction methods fail', async () => {
      const mockExecutor = createMockExecutorForCommands({
        'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
          'defaults read failed',
        ),
        '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
          new Error('Command failed'),
      });
      const mockFileSystemExecutor = createMockFileSystemExecutor({
        existsSync: () => true,
      });

      const result = await get_app_bundle_idLogic(
        { appPath: '/path/to/MyApp.app' },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error extracting app bundle ID: Could not extract bundle ID from Info.plist: Command failed',
          },
          {
            type: 'text',
            text: 'Make sure the path points to a valid app bundle (.app directory).',
          },
        ],
        isError: true,
      });
    });

    it('should handle Error objects in catch blocks', async () => {
      const mockExecutor = createMockExecutorForCommands({
        'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
          'defaults read failed',
        ),
        '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
          new Error('Custom error message'),
      });
      const mockFileSystemExecutor = createMockFileSystemExecutor({
        existsSync: () => true,
      });

      const result = await get_app_bundle_idLogic(
        { appPath: '/path/to/MyApp.app' },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error extracting app bundle ID: Could not extract bundle ID from Info.plist: Custom error message',
          },
          {
            type: 'text',
            text: 'Make sure the path points to a valid app bundle (.app directory).',
          },
        ],
        isError: true,
      });
    });

    it('should handle string errors in catch blocks', async () => {
      const mockExecutor = createMockExecutorForCommands({
        'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
          'defaults read failed',
        ),
        '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
          new Error('String error'),
      });
      const mockFileSystemExecutor = createMockFileSystemExecutor({
        existsSync: () => true,
      });

      const result = await get_app_bundle_idLogic(
        { appPath: '/path/to/MyApp.app' },
        mockExecutor,
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error extracting app bundle ID: Could not extract bundle ID from Info.plist: String error',
          },
          {
            type: 'text',
            text: 'Make sure the path points to a valid app bundle (.app directory).',
          },
        ],
        isError: true,
      });
    });

    it('should handle schema validation error when appPath is null', async () => {
      // Test validation through the handler which uses Zod validation
      const result = await plugin.handler({ appPath: null });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nappPath: Expected string, received null',
          },
        ],
        isError: true,
      });
    });

    it('should handle schema validation with missing appPath', async () => {
      // Test validation through the handler which uses Zod validation
      const result = await plugin.handler({});

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nappPath: Required',
          },
        ],
        isError: true,
      });
    });

    it('should handle schema validation with undefined appPath', async () => {
      // Test validation through the handler which uses Zod validation
      const result = await plugin.handler({ appPath: undefined });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nappPath: Required',
          },
        ],
        isError: true,
      });
    });

    it('should handle schema validation with number type appPath', async () => {
      // Test validation through the handler which uses Zod validation
      const result = await plugin.handler({ appPath: 123 });

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nappPath: Expected string, received number',
          },
        ],
        isError: true,
      });
    });
  });
});

```

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

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import plugin, { showBuildSettingsLogic } from '../show_build_settings.ts';
import { sessionStore } from '../../../../utils/session-store.ts';

describe('show_build_settings plugin', () => {
  beforeEach(() => {
    sessionStore.clear();
  });
  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(plugin.name).toBe('show_build_settings');
    });

    it('should have correct description', () => {
      expect(plugin.description).toBe('Shows xcodebuild build settings.');
    });

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

    it('should expose an empty public schema', () => {
      const schema = z.object(plugin.schema).strict();
      expect(schema.safeParse({}).success).toBe(true);
      expect(schema.safeParse({ projectPath: '/path.xcodeproj' }).success).toBe(false);
      expect(schema.safeParse({ scheme: 'App' }).success).toBe(false);
      expect(Object.keys(plugin.schema)).toEqual([]);
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should execute with valid parameters', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Mock build settings output',
        error: undefined,
        process: { pid: 12345 },
      });

      const result = await showBuildSettingsLogic(
        { projectPath: '/valid/path.xcodeproj', scheme: 'MyScheme' },
        mockExecutor,
      );
      expect(result.isError).toBe(false);
      expect(result.content[0].text).toContain('✅ Build settings for scheme MyScheme:');
    });

    it('should test Zod validation through handler', async () => {
      // Test the actual tool handler which includes Zod validation
      const result = await plugin.handler({
        projectPath: null,
        scheme: 'MyScheme',
      });

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

    it('should return success with build settings', async () => {
      const calls: any[] = [];
      const mockExecutor = createMockExecutor({
        success: true,
        output: `Build settings from command line:
    ARCHS = arm64
    BUILD_DIR = /Users/dev/Build/Products
    CONFIGURATION = Debug
    DEVELOPMENT_TEAM = ABC123DEF4
    PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp
    PRODUCT_NAME = MyApp
    SUPPORTED_PLATFORMS = iphoneos iphonesimulator`,
        error: undefined,
        process: { pid: 12345 },
      });

      // Wrap mockExecutor to track calls
      const wrappedExecutor = (...args: any[]) => {
        calls.push(args);
        return mockExecutor(...args);
      };

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

      expect(calls).toHaveLength(1);
      expect(calls[0]).toEqual([
        [
          'xcodebuild',
          '-showBuildSettings',
          '-project',
          '/path/to/MyProject.xcodeproj',
          '-scheme',
          'MyScheme',
        ],
        'Show Build Settings',
        true,
      ]);

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ Build settings for scheme MyScheme:',
          },
          {
            type: 'text',
            text: `Build settings from command line:
    ARCHS = arm64
    BUILD_DIR = /Users/dev/Build/Products
    CONFIGURATION = Debug
    DEVELOPMENT_TEAM = ABC123DEF4
    PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp
    PRODUCT_NAME = MyApp
    SUPPORTED_PLATFORMS = iphoneos iphonesimulator`,
          },
        ],
        isError: false,
      });
    });

    it('should return error when command fails', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Scheme not found',
        process: { pid: 12345 },
      });

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

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Failed to show build settings: Scheme not found' }],
        isError: true,
      });
    });

    it('should handle Error objects in catch blocks', async () => {
      const mockExecutor = async () => {
        throw new Error('Command execution failed');
      };

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

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Error showing build settings: Command execution failed' }],
        isError: true,
      });
    });
  });

  describe('XOR Validation', () => {
    it('should error when neither projectPath nor workspacePath provided', async () => {
      const result = await plugin.handler({
        scheme: 'MyScheme',
      });

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

    it('should error when both projectPath and workspacePath provided', async () => {
      const result = await plugin.handler({
        projectPath: '/path/project.xcodeproj',
        workspacePath: '/path/workspace.xcworkspace',
        scheme: 'MyScheme',
      });

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

    it('should work with projectPath only', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Mock build settings output',
      });

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

      expect(result.isError).toBe(false);
      expect(result.content[0].text).toContain('✅ Build settings for scheme MyScheme:');
    });

    it('should work with workspacePath only', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Mock build settings output',
      });

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

      expect(result.isError).toBe(false);
      expect(result.content[0].text).toContain('✅ Build settings retrieved successfully');
    });
  });

  describe('Session requirement handling', () => {
    it('should require scheme when not provided', async () => {
      const result = await plugin.handler({
        projectPath: '/path/to/MyProject.xcodeproj',
      } as any);

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

    it('should surface project/workspace requirement even with scheme default', async () => {
      sessionStore.setDefaults({ scheme: 'MyScheme' });

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

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

  describe('showBuildSettingsLogic function', () => {
    it('should return success with build settings', async () => {
      const calls: any[] = [];
      const mockExecutor = createMockExecutor({
        success: true,
        output: `Build settings from command line:
    ARCHS = arm64
    BUILD_DIR = /Users/dev/Build/Products
    CONFIGURATION = Debug
    DEVELOPMENT_TEAM = ABC123DEF4
    PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp
    PRODUCT_NAME = MyApp
    SUPPORTED_PLATFORMS = iphoneos iphonesimulator`,
        error: undefined,
        process: { pid: 12345 },
      });

      // Wrap mockExecutor to track calls
      const wrappedExecutor = (...args: any[]) => {
        calls.push(args);
        return mockExecutor(...args);
      };

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

      expect(calls).toHaveLength(1);
      expect(calls[0]).toEqual([
        [
          'xcodebuild',
          '-showBuildSettings',
          '-project',
          '/path/to/MyProject.xcodeproj',
          '-scheme',
          'MyScheme',
        ],
        'Show Build Settings',
        true,
      ]);

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '✅ Build settings for scheme MyScheme:',
          },
          {
            type: 'text',
            text: `Build settings from command line:
    ARCHS = arm64
    BUILD_DIR = /Users/dev/Build/Products
    CONFIGURATION = Debug
    DEVELOPMENT_TEAM = ABC123DEF4
    PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp
    PRODUCT_NAME = MyApp
    SUPPORTED_PLATFORMS = iphoneos iphonesimulator`,
          },
        ],
        isError: false,
      });
    });

    it('should return error when command fails', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Scheme not found',
        process: { pid: 12345 },
      });

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

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Failed to show build settings: Scheme not found' }],
        isError: true,
      });
    });

    it('should handle Error objects in catch blocks', async () => {
      const mockExecutor = async () => {
        throw new Error('Command execution failed');
      };

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

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Error showing build settings: Command execution failed' }],
        isError: true,
      });
    });
  });
});

```

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

```typescript
/**
 * Device Shared Plugin: Test Device (Unified)
 *
 * Runs tests for an Apple project or workspace on a physical device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro)
 * using xcodebuild test and parses xcresult output. Accepts mutually exclusive `projectPath` or `workspacePath`.
 */

import { z } from 'zod';
import { join } from 'path';
import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
import { createTextResponse } from '../../../utils/responses/index.ts';
import { normalizeTestRunnerEnv } from '../../../utils/environment.ts';
import type {
  CommandExecutor,
  FileSystemExecutor,
  CommandExecOptions,
} from '../../../utils/execution/index.ts';
import {
  getDefaultCommandExecutor,
  getDefaultFileSystemExecutor,
} from '../../../utils/execution/index.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath
const baseSchemaObject = z.object({
  projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
  workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
  scheme: z.string().describe('The scheme to test'),
  deviceId: z.string().describe('UDID of the device (obtained from list_devices)'),
  configuration: z.string().optional().describe('Build configuration (Debug, Release)'),
  derivedDataPath: z.string().optional().describe('Path to derived data directory'),
  extraArgs: z.array(z.string()).optional().describe('Additional arguments to pass to xcodebuild'),
  preferXcodebuild: z.boolean().optional().describe('Prefer xcodebuild over faster alternatives'),
  platform: z
    .enum(['iOS', 'watchOS', 'tvOS', 'visionOS'])
    .optional()
    .describe('Target platform (defaults to iOS)'),
  testRunnerEnv: z
    .record(z.string(), z.string())
    .optional()
    .describe(
      'Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)',
    ),
});

const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);

const testDeviceSchema = baseSchema
  .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
    message: 'Either projectPath or workspacePath is required.',
  })
  .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
    message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
  });

export type TestDeviceParams = z.infer<typeof testDeviceSchema>;

/**
 * Type definition for test summary structure from xcresulttool
 * (JavaScript implementation - no actual interface, this is just documentation)
 */

/**
 * Parse xcresult bundle using xcrun xcresulttool
 */
async function parseXcresultBundle(
  resultBundlePath: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
): Promise<string> {
  try {
    // Use injected executor for testing
    const result = await executor(
      ['xcrun', 'xcresulttool', 'get', 'test-results', 'summary', '--path', resultBundlePath],
      'Parse xcresult bundle',
    );
    if (!result.success) {
      throw new Error(result.error ?? 'Failed to execute xcresulttool');
    }
    if (!result.output || result.output.trim().length === 0) {
      throw new Error('xcresulttool returned no output');
    }

    // Parse JSON response and format as human-readable
    const summaryData = JSON.parse(result.output) as Record<string, unknown>;
    return formatTestSummary(summaryData);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error parsing xcresult bundle: ${errorMessage}`);
    throw error;
  }
}

/**
 * Format test summary JSON into human-readable text
 */
function formatTestSummary(summary: Record<string, unknown>): string {
  const lines = [];

  lines.push(`Test Summary: ${summary.title ?? 'Unknown'}`);
  lines.push(`Overall Result: ${summary.result ?? 'Unknown'}`);
  lines.push('');

  lines.push('Test Counts:');
  lines.push(`  Total: ${summary.totalTestCount ?? 0}`);
  lines.push(`  Passed: ${summary.passedTests ?? 0}`);
  lines.push(`  Failed: ${summary.failedTests ?? 0}`);
  lines.push(`  Skipped: ${summary.skippedTests ?? 0}`);
  lines.push(`  Expected Failures: ${summary.expectedFailures ?? 0}`);
  lines.push('');

  if (summary.environmentDescription) {
    lines.push(`Environment: ${summary.environmentDescription}`);
    lines.push('');
  }

  if (
    summary.devicesAndConfigurations &&
    Array.isArray(summary.devicesAndConfigurations) &&
    summary.devicesAndConfigurations.length > 0
  ) {
    const deviceConfig = summary.devicesAndConfigurations[0] as Record<string, unknown>;
    const device = deviceConfig.device as Record<string, unknown> | undefined;
    if (device) {
      lines.push(
        `Device: ${device.deviceName ?? 'Unknown'} (${device.platform ?? 'Unknown'} ${device.osVersion ?? 'Unknown'})`,
      );
      lines.push('');
    }
  }

  if (
    summary.testFailures &&
    Array.isArray(summary.testFailures) &&
    summary.testFailures.length > 0
  ) {
    lines.push('Test Failures:');
    summary.testFailures.forEach((failureItem, index) => {
      const failure = failureItem as Record<string, unknown>;
      lines.push(
        `  ${index + 1}. ${failure.testName ?? 'Unknown Test'} (${failure.targetName ?? 'Unknown Target'})`,
      );
      if (failure.failureText) {
        lines.push(`     ${failure.failureText}`);
      }
    });
    lines.push('');
  }

  if (summary.topInsights && Array.isArray(summary.topInsights) && summary.topInsights.length > 0) {
    lines.push('Insights:');
    summary.topInsights.forEach((insightItem, index) => {
      const insight = insightItem as Record<string, unknown>;
      lines.push(
        `  ${index + 1}. [${insight.impact ?? 'Unknown'}] ${insight.text ?? 'No description'}`,
      );
    });
  }

  return lines.join('\n');
}

/**
 * Business logic for running tests with platform-specific handling.
 * Exported for direct testing and reuse.
 */
export async function testDeviceLogic(
  params: TestDeviceParams,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<ToolResponse> {
  log(
    'info',
    `Starting test run for scheme ${params.scheme} on platform ${params.platform ?? 'iOS'} (internal)`,
  );

  let tempDir: string | undefined;
  const cleanup = async (): Promise<void> => {
    if (!tempDir) return;
    try {
      await fileSystemExecutor.rm(tempDir, { recursive: true, force: true });
    } catch (cleanupError) {
      log('warn', `Failed to clean up temporary directory: ${cleanupError}`);
    }
  };

  try {
    // Create temporary directory for xcresult bundle
    tempDir = await fileSystemExecutor.mkdtemp(
      join(fileSystemExecutor.tmpdir(), 'xcodebuild-test-'),
    );
    const resultBundlePath = join(tempDir, 'TestResults.xcresult');

    // Add resultBundlePath to extraArgs
    const extraArgs = [...(params.extraArgs ?? []), `-resultBundlePath`, resultBundlePath];

    // Prepare execution options with TEST_RUNNER_ environment variables
    const execOpts: CommandExecOptions | undefined = params.testRunnerEnv
      ? { env: normalizeTestRunnerEnv(params.testRunnerEnv) }
      : undefined;

    // Run the test command
    const testResult = await executeXcodeBuildCommand(
      {
        projectPath: params.projectPath,
        workspacePath: params.workspacePath,
        scheme: params.scheme,
        configuration: params.configuration ?? 'Debug',
        derivedDataPath: params.derivedDataPath,
        extraArgs,
      },
      {
        platform: (params.platform as XcodePlatform) || XcodePlatform.iOS,
        simulatorName: undefined,
        simulatorId: undefined,
        deviceId: params.deviceId,
        useLatestOS: false,
        logPrefix: 'Test Run',
      },
      params.preferXcodebuild,
      'test',
      executor,
      execOpts,
    );

    // Parse xcresult bundle if it exists, regardless of whether tests passed or failed
    // Test failures are expected and should not prevent xcresult parsing
    try {
      log('info', `Attempting to parse xcresult bundle at: ${resultBundlePath}`);

      // Check if the file exists
      try {
        await fileSystemExecutor.stat(resultBundlePath);
        log('info', `xcresult bundle exists at: ${resultBundlePath}`);
      } catch {
        log('warn', `xcresult bundle does not exist at: ${resultBundlePath}`);
        throw new Error(`xcresult bundle not found at ${resultBundlePath}`);
      }

      const testSummary = await parseXcresultBundle(resultBundlePath, executor);
      log('info', 'Successfully parsed xcresult bundle');

      // Clean up temporary directory
      await cleanup();

      // Return combined result - preserve isError from testResult (test failures should be marked as errors)
      return {
        content: [
          ...(testResult.content || []),
          {
            type: 'text',
            text: '\nTest Results Summary:\n' + testSummary,
          },
        ],
        isError: testResult.isError,
      };
    } catch (parseError) {
      // If parsing fails, return original test result
      log('warn', `Failed to parse xcresult bundle: ${parseError}`);

      await cleanup();

      return testResult;
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error during test run: ${errorMessage}`);
    return createTextResponse(`Error during test run: ${errorMessage}`, true);
  } finally {
    await cleanup();
  }
}

export default {
  name: 'test_device',
  description: 'Runs tests on a physical Apple device.',
  schema: baseSchemaObject.omit({
    projectPath: true,
    workspacePath: true,
    scheme: true,
    deviceId: true,
    configuration: true,
  } as const).shape,
  handler: createSessionAwareTool<TestDeviceParams>({
    internalSchema: testDeviceSchema as unknown as z.ZodType<TestDeviceParams>,
    logicFunction: (params: TestDeviceParams, executor: CommandExecutor) =>
      testDeviceLogic(
        {
          ...params,
          platform: params.platform ?? 'iOS',
        },
        executor,
        getDefaultFileSystemExecutor(),
      ),
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme', 'deviceId'], message: 'Provide scheme and deviceId' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
    ],
    exclusivePairs: [['projectPath', 'workspacePath']],
  }),
};

```

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

```typescript
/**
 * stop_sim_log_cap Plugin Tests - Test coverage for stop_sim_log_cap plugin
 *
 * This test file provides complete coverage for the stop_sim_log_cap plugin:
 * - Plugin structure validation
 * - Handler functionality (stop log capture session and retrieve captured logs)
 * - Error handling for validation and log capture failures
 *
 * Tests follow the canonical testing patterns from CLAUDE.md with deterministic
 * response validation and comprehensive parameter testing.
 * Converted to pure dependency injection without vitest mocking.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import stopSimLogCap, { stop_sim_log_capLogic } from '../stop_sim_log_cap.ts';
import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts';
import { activeLogSessions } from '../../../../utils/log_capture.ts';

describe('stop_sim_log_cap plugin', () => {
  let mockFileSystem: any;

  beforeEach(() => {
    mockFileSystem = createMockFileSystemExecutor();
    // Clear any active sessions before each test
    activeLogSessions.clear();
  });

  // Helper function to create a test log session
  async function createTestLogSession(sessionId: string, logContent: string = '') {
    const mockProcess = {
      pid: 12345,
      killed: false,
      exitCode: null,
      kill: () => {},
    };

    const logFilePath = `/tmp/xcodemcp_sim_log_test_${sessionId}.log`;

    // Create actual file for the test
    const fs = await import('fs/promises');
    await fs.writeFile(logFilePath, logContent, 'utf-8');

    activeLogSessions.set(sessionId, {
      processes: [mockProcess as any],
      logFilePath: logFilePath,
      simulatorUuid: 'test-simulator-uuid',
      bundleId: 'com.example.TestApp',
    });
  }

  describe('Export Field Validation (Literal)', () => {
    it('should have correct plugin structure', () => {
      expect(stopSimLogCap).toHaveProperty('name');
      expect(stopSimLogCap).toHaveProperty('description');
      expect(stopSimLogCap).toHaveProperty('schema');
      expect(stopSimLogCap).toHaveProperty('handler');

      expect(stopSimLogCap.name).toBe('stop_sim_log_cap');
      expect(stopSimLogCap.description).toBe(
        'Stops an active simulator log capture session and returns the captured logs.',
      );
      expect(typeof stopSimLogCap.handler).toBe('function');
      expect(typeof stopSimLogCap.schema).toBe('object');
    });

    it('should have correct schema structure', () => {
      // Schema should be a plain object for MCP protocol compliance
      expect(typeof stopSimLogCap.schema).toBe('object');
      expect(stopSimLogCap.schema).toHaveProperty('logSessionId');

      // Validate that schema fields are Zod types that can be used for validation
      const schema = z.object(stopSimLogCap.schema);
      expect(schema.safeParse({ logSessionId: 'test-session-id' }).success).toBe(true);
      expect(schema.safeParse({ logSessionId: 123 }).success).toBe(false);
    });

    it('should validate schema with valid parameters', () => {
      expect(stopSimLogCap.schema.logSessionId.safeParse('test-session-id').success).toBe(true);
    });

    it('should reject invalid schema parameters', () => {
      expect(stopSimLogCap.schema.logSessionId.safeParse(null).success).toBe(false);
      expect(stopSimLogCap.schema.logSessionId.safeParse(undefined).success).toBe(false);
      expect(stopSimLogCap.schema.logSessionId.safeParse(123).success).toBe(false);
      expect(stopSimLogCap.schema.logSessionId.safeParse(true).success).toBe(false);
    });
  });

  describe('Input Validation', () => {
    it('should handle null logSessionId (validation handled by framework)', async () => {
      // With typed tool factory, invalid params won't reach the logic function
      // This test now validates that the logic function works with valid empty strings
      await createTestLogSession('', 'Log content for empty session');

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: '',
        },
        mockFileSystem,
      );

      expect(result.isError).toBeUndefined();
      expect(result.content[0].text).toBe(
        'Log capture session  stopped successfully. Log content follows:\n\nLog content for empty session',
      );
    });

    it('should handle undefined logSessionId (validation handled by framework)', async () => {
      // With typed tool factory, invalid params won't reach the logic function
      // This test now validates that the logic function works with valid empty strings
      await createTestLogSession('', 'Log content for empty session');

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: '',
        },
        mockFileSystem,
      );

      expect(result.isError).toBeUndefined();
      expect(result.content[0].text).toBe(
        'Log capture session  stopped successfully. Log content follows:\n\nLog content for empty session',
      );
    });

    it('should handle empty string logSessionId', async () => {
      await createTestLogSession('', 'Log content for empty session');

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: '',
        },
        mockFileSystem,
      );

      expect(result.isError).toBeUndefined();
      expect(result.content[0].text).toBe(
        'Log capture session  stopped successfully. Log content follows:\n\nLog content for empty session',
      );
    });
  });

  describe('Function Call Generation', () => {
    it('should call stopLogCapture with correct parameters', async () => {
      await createTestLogSession('test-session-id', 'Mock log content from file');

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: 'test-session-id',
        },
        mockFileSystem,
      );

      expect(result.isError).toBeUndefined();
      expect(result.content[0].text).toBe(
        'Log capture session test-session-id stopped successfully. Log content follows:\n\nMock log content from file',
      );
    });

    it('should call stopLogCapture with different session ID', async () => {
      await createTestLogSession('different-session-id', 'Different log content');

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: 'different-session-id',
        },
        mockFileSystem,
      );

      expect(result.isError).toBeUndefined();
      expect(result.content[0].text).toBe(
        'Log capture session different-session-id stopped successfully. Log content follows:\n\nDifferent log content',
      );
    });
  });

  describe('Response Processing', () => {
    it('should handle successful log capture stop', async () => {
      await createTestLogSession('test-session-id', 'Mock log content from file');

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: 'test-session-id',
        },
        mockFileSystem,
      );

      expect(result.isError).toBeUndefined();
      expect(result.content[0].text).toBe(
        'Log capture session test-session-id stopped successfully. Log content follows:\n\nMock log content from file',
      );
    });

    it('should handle empty log content', async () => {
      await createTestLogSession('test-session-id', '');

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: 'test-session-id',
        },
        mockFileSystem,
      );

      expect(result.isError).toBeUndefined();
      expect(result.content[0].text).toBe(
        'Log capture session test-session-id stopped successfully. Log content follows:\n\n',
      );
    });

    it('should handle multiline log content', async () => {
      await createTestLogSession('test-session-id', 'Line 1\nLine 2\nLine 3');

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: 'test-session-id',
        },
        mockFileSystem,
      );

      expect(result.isError).toBeUndefined();
      expect(result.content[0].text).toBe(
        'Log capture session test-session-id stopped successfully. Log content follows:\n\nLine 1\nLine 2\nLine 3',
      );
    });

    it('should handle log capture stop errors for non-existent session', async () => {
      const result = await stop_sim_log_capLogic(
        {
          logSessionId: 'non-existent-session',
        },
        mockFileSystem,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toBe(
        'Error stopping log capture session non-existent-session: Log capture session not found: non-existent-session',
      );
    });

    it('should handle file read errors', async () => {
      // Create session but make file reading fail in the log_capture utility
      const mockProcess = {
        pid: 12345,
        killed: false,
        exitCode: null,
        kill: () => {},
      };

      activeLogSessions.set('test-session-id', {
        processes: [mockProcess as any],
        logFilePath: `/tmp/test_file_not_found.log`,
        simulatorUuid: 'test-simulator-uuid',
        bundleId: 'com.example.TestApp',
      });

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: 'test-session-id',
        },
        mockFileSystem,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain(
        'Error stopping log capture session test-session-id:',
      );
    });

    it('should handle permission errors', async () => {
      // Create session but make file reading fail in the log_capture utility
      const mockProcess = {
        pid: 12345,
        killed: false,
        exitCode: null,
        kill: () => {},
      };

      activeLogSessions.set('test-session-id', {
        processes: [mockProcess as any],
        logFilePath: `/tmp/test_permission_denied.log`,
        simulatorUuid: 'test-simulator-uuid',
        bundleId: 'com.example.TestApp',
      });

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: 'test-session-id',
        },
        mockFileSystem,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain(
        'Error stopping log capture session test-session-id:',
      );
    });

    it('should handle various error types', async () => {
      // Create session but make file reading fail in the log_capture utility
      const mockProcess = {
        pid: 12345,
        killed: false,
        exitCode: null,
        kill: () => {},
      };

      activeLogSessions.set('test-session-id', {
        processes: [mockProcess as any],
        logFilePath: `/tmp/test_generic_error.log`,
        simulatorUuid: 'test-simulator-uuid',
        bundleId: 'com.example.TestApp',
      });

      const result = await stop_sim_log_capLogic(
        {
          logSessionId: 'test-session-id',
        },
        mockFileSystem,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain(
        'Error stopping log capture session test-session-id:',
      );
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for set_sim_location plugin
 * Following CLAUDE.md testing standards with literal validation
 * Using pure dependency injection for deterministic testing
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts';
import setSimLocation, { set_sim_locationLogic } from '../set_sim_location.ts';

describe('set_sim_location tool', () => {
  // No mocks to clear since we use pure dependency injection

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

    it('should have correct description', () => {
      expect(setSimLocation.description).toBe('Sets a custom GPS location for the simulator.');
    });

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

    it('should have correct schema with simulatorUuid string field and latitude/longitude number fields', () => {
      const schema = z.object(setSimLocation.schema);

      // Valid inputs
      expect(
        schema.safeParse({
          simulatorUuid: 'test-uuid-123',
          latitude: 37.7749,
          longitude: -122.4194,
        }).success,
      ).toBe(true);
      expect(
        schema.safeParse({ simulatorUuid: 'ABC123-DEF456', latitude: 0, longitude: 0 }).success,
      ).toBe(true);
      expect(
        schema.safeParse({ simulatorUuid: 'test-uuid', latitude: 90, longitude: 180 }).success,
      ).toBe(true);
      expect(
        schema.safeParse({ simulatorUuid: 'test-uuid', latitude: -90, longitude: -180 }).success,
      ).toBe(true);
      expect(
        schema.safeParse({ simulatorUuid: 'test-uuid', latitude: 45.5, longitude: -73.6 }).success,
      ).toBe(true);

      // Invalid inputs
      expect(
        schema.safeParse({ simulatorUuid: 123, latitude: 37.7749, longitude: -122.4194 }).success,
      ).toBe(false);
      expect(
        schema.safeParse({ simulatorUuid: 'test-uuid', latitude: 'invalid', longitude: -122.4194 })
          .success,
      ).toBe(false);
      expect(
        schema.safeParse({ simulatorUuid: 'test-uuid', latitude: 37.7749, longitude: 'invalid' })
          .success,
      ).toBe(false);
      expect(
        schema.safeParse({ simulatorUuid: null, latitude: 37.7749, longitude: -122.4194 }).success,
      ).toBe(false);
      expect(schema.safeParse({ simulatorUuid: 'test-uuid', longitude: -122.4194 }).success).toBe(
        false,
      );
      expect(schema.safeParse({ simulatorUuid: 'test-uuid', latitude: 37.7749 }).success).toBe(
        false,
      );
      expect(schema.safeParse({ latitude: 37.7749, longitude: -122.4194 }).success).toBe(false);
      expect(schema.safeParse({}).success).toBe(false);
    });
  });

  describe('Command Generation', () => {
    it('should generate correct simctl command', async () => {
      let capturedCommand: string[] = [];

      const mockExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'Location set successfully',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid-123',
          latitude: 37.7749,
          longitude: -122.4194,
        },
        mockExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcrun',
        'simctl',
        'location',
        'test-uuid-123',
        'set',
        '37.7749,-122.4194',
      ]);
    });

    it('should generate command with different coordinates', async () => {
      let capturedCommand: string[] = [];

      const mockExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'Location set successfully',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      await set_sim_locationLogic(
        {
          simulatorUuid: 'different-uuid',
          latitude: 45.5,
          longitude: -73.6,
        },
        mockExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcrun',
        'simctl',
        'location',
        'different-uuid',
        'set',
        '45.5,-73.6',
      ]);
    });

    it('should generate command with negative coordinates', async () => {
      let capturedCommand: string[] = [];

      const mockExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'Location set successfully',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid',
          latitude: -90,
          longitude: -180,
        },
        mockExecutor,
      );

      expect(capturedCommand).toEqual([
        'xcrun',
        'simctl',
        'location',
        'test-uuid',
        'set',
        '-90,-180',
      ]);
    });
  });

  describe('Response Processing', () => {
    it('should handle successful location setting', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Location set successfully',
        error: undefined,
      });

      const result = await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid-123',
          latitude: 37.7749,
          longitude: -122.4194,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Successfully set simulator test-uuid-123 location to 37.7749,-122.4194',
          },
        ],
      });
    });

    it('should handle latitude validation failure', async () => {
      const result = await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid-123',
          latitude: 95,
          longitude: -122.4194,
        },
        createNoopExecutor(),
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Latitude must be between -90 and 90 degrees',
          },
        ],
      });
    });

    it('should handle longitude validation failure', async () => {
      const result = await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid-123',
          latitude: 37.7749,
          longitude: -185,
        },
        createNoopExecutor(),
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Longitude must be between -180 and 180 degrees',
          },
        ],
      });
    });

    it('should handle command failure', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Simulator not found',
      });

      const result = await set_sim_locationLogic(
        {
          simulatorUuid: 'invalid-uuid',
          latitude: 37.7749,
          longitude: -122.4194,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to set simulator location: Simulator not found',
          },
        ],
      });
    });

    it('should handle exception with Error object', async () => {
      const mockExecutor = createMockExecutor(new Error('Connection failed'));

      const result = await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid-123',
          latitude: 37.7749,
          longitude: -122.4194,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to set simulator location: Connection failed',
          },
        ],
      });
    });

    it('should handle exception with string error', async () => {
      const mockExecutor = createMockExecutor('String error');

      const result = await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid-123',
          latitude: 37.7749,
          longitude: -122.4194,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to set simulator location: String error',
          },
        ],
      });
    });

    it('should handle boundary values for coordinates', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Location set successfully',
        error: undefined,
      });

      const result = await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid-123',
          latitude: 90,
          longitude: 180,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Successfully set simulator test-uuid-123 location to 90,180',
          },
        ],
      });
    });

    it('should handle boundary values for negative coordinates', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Location set successfully',
        error: undefined,
      });

      const result = await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid-123',
          latitude: -90,
          longitude: -180,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Successfully set simulator test-uuid-123 location to -90,-180',
          },
        ],
      });
    });

    it('should handle zero coordinates', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Location set successfully',
        error: undefined,
      });

      const result = await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid-123',
          latitude: 0,
          longitude: 0,
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Successfully set simulator test-uuid-123 location to 0,0',
          },
        ],
      });
    });

    it('should verify correct executor arguments', async () => {
      let capturedArgs: any[] = [];

      const mockExecutor = async (...args: any[]) => {
        capturedArgs = args;
        return {
          success: true,
          output: 'Location set successfully',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      await set_sim_locationLogic(
        {
          simulatorUuid: 'test-uuid-123',
          latitude: 37.7749,
          longitude: -122.4194,
        },
        mockExecutor,
      );

      expect(capturedArgs).toEqual([
        ['xcrun', 'simctl', 'location', 'test-uuid-123', 'set', '37.7749,-122.4194'],
        'Set Simulator Location',
        true,
        {},
      ]);
    });
  });
});

```

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

```typescript
/**
 * Tests for list_devices plugin (device-shared)
 * This tests the re-exported plugin from device-workspace
 * Following CLAUDE.md testing standards with literal validation
 *
 * Note: This is a re-export test. Comprehensive handler tests are in device-workspace/list_devices.test.ts
 */

import { describe, it, expect } from 'vitest';
import {
  createMockExecutor,
  createMockFileSystemExecutor,
} from '../../../../test-utils/mock-executors.ts';

// Import the logic function and re-export
import listDevices, { list_devicesLogic } from '../list_devices.ts';

describe('list_devices plugin (device-shared)', () => {
  describe('Export Field Validation (Literal)', () => {
    it('should export list_devicesLogic function', () => {
      expect(typeof list_devicesLogic).toBe('function');
    });

    it('should have correct name', () => {
      expect(listDevices.name).toBe('list_devices');
    });

    it('should have correct description', () => {
      expect(listDevices.description).toBe(
        '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.',
      );
    });

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

    it('should have empty schema', () => {
      expect(listDevices.schema).toEqual({});
    });
  });

  describe('Command Generation Tests', () => {
    it('should generate correct devicectl command', async () => {
      const devicectlJson = {
        result: {
          devices: [
            {
              identifier: 'test-device-123',
              visibilityClass: 'Default',
              connectionProperties: {
                pairingState: 'paired',
                tunnelState: 'connected',
                transportType: 'USB',
              },
              deviceProperties: {
                name: 'Test iPhone',
                platformIdentifier: 'com.apple.platform.iphoneos',
                osVersionNumber: '17.0',
              },
              hardwareProperties: {
                productType: 'iPhone15,2',
              },
            },
          ],
        },
      };

      // Track command calls
      const commandCalls: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: Record<string, string>;
      }> = [];

      // Create mock executor
      const mockExecutor = createMockExecutor({
        success: true,
        output: '',
      });

      // Wrap to track calls
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        commandCalls.push({ command, logPrefix, useShell, env });
        return mockExecutor(command, logPrefix, useShell, env);
      };

      // Create mock path dependencies
      const mockPathDeps = {
        tmpdir: () => '/tmp',
        join: (...paths: string[]) => paths.join('/'),
      };

      // Create mock filesystem with specific behavior
      const mockFsDeps = createMockFileSystemExecutor({
        readFile: async () => JSON.stringify(devicectlJson),
        unlink: async () => {},
      });

      await list_devicesLogic({}, trackingExecutor, mockPathDeps, mockFsDeps);

      expect(commandCalls).toHaveLength(1);
      expect(commandCalls[0].command).toEqual([
        'xcrun',
        'devicectl',
        'list',
        'devices',
        '--json-output',
        '/tmp/devicectl-123.json',
      ]);
      expect(commandCalls[0].logPrefix).toBe('List Devices (devicectl with JSON)');
      expect(commandCalls[0].useShell).toBe(true);
      expect(commandCalls[0].env).toBeUndefined();
    });

    it('should generate correct xctrace fallback command', async () => {
      // Track command calls
      const commandCalls: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        env?: Record<string, string>;
      }> = [];

      // Create tracking executor with call count behavior
      let callCount = 0;
      const trackingExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callCount++;
        commandCalls.push({ command, logPrefix, useShell, env });

        if (callCount === 1) {
          // First call fails (devicectl)
          return {
            success: false,
            output: '',
            error: 'devicectl failed',
            process: { pid: 12345 },
          };
        } else {
          // Second call succeeds (xctrace)
          return {
            success: true,
            output: 'iPhone 15 (12345678-1234-1234-1234-123456789012)',
            error: undefined,
            process: { pid: 12345 },
          };
        }
      };

      // Create mock path dependencies
      const mockPathDeps = {
        tmpdir: () => '/tmp',
        join: (...paths: string[]) => paths.join('/'),
      };

      // Create mock filesystem that throws for readFile
      const mockFsDeps = createMockFileSystemExecutor({
        readFile: async () => {
          throw new Error('File not found');
        },
        unlink: async () => {},
      });

      await list_devicesLogic({}, trackingExecutor, mockPathDeps, mockFsDeps);

      expect(commandCalls).toHaveLength(2);
      expect(commandCalls[1].command).toEqual(['xcrun', 'xctrace', 'list', 'devices']);
      expect(commandCalls[1].logPrefix).toBe('List Devices (xctrace)');
      expect(commandCalls[1].useShell).toBe(true);
      expect(commandCalls[1].env).toBeUndefined();
    });
  });

  describe('Success Path Tests', () => {
    it('should return successful devicectl response with parsed devices', async () => {
      const devicectlJson = {
        result: {
          devices: [
            {
              identifier: 'test-device-123',
              visibilityClass: 'Default',
              connectionProperties: {
                pairingState: 'paired',
                tunnelState: 'connected',
                transportType: 'USB',
              },
              deviceProperties: {
                name: 'Test iPhone',
                platformIdentifier: 'com.apple.platform.iphoneos',
                osVersionNumber: '17.0',
              },
              hardwareProperties: {
                productType: 'iPhone15,2',
              },
            },
          ],
        },
      };

      const mockExecutor = createMockExecutor({
        success: true,
        output: '',
      });

      // Create mock path dependencies
      const mockPathDeps = {
        tmpdir: () => '/tmp',
        join: (...paths: string[]) => paths.join('/'),
      };

      // Create mock filesystem with specific behavior
      const mockFsDeps = createMockFileSystemExecutor({
        readFile: async () => JSON.stringify(devicectlJson),
        unlink: async () => {},
      });

      const result = await list_devicesLogic({}, mockExecutor, mockPathDeps, mockFsDeps);

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            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.\n",
          },
        ],
      });
    });

    it('should return successful xctrace fallback response', async () => {
      // Create executor with call count behavior
      let callCount = 0;
      const mockExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callCount++;
        if (callCount === 1) {
          // First call fails (devicectl)
          return {
            success: false,
            output: '',
            error: 'devicectl failed',
            process: { pid: 12345 },
          };
        } else {
          // Second call succeeds (xctrace)
          return {
            success: true,
            output: 'iPhone 15 (12345678-1234-1234-1234-123456789012)',
            error: undefined,
            process: { pid: 12345 },
          };
        }
      };

      // Create mock path dependencies
      const mockPathDeps = {
        tmpdir: () => '/tmp',
        join: (...paths: string[]) => paths.join('/'),
      };

      // Create mock filesystem that throws for readFile
      const mockFsDeps = createMockFileSystemExecutor({
        readFile: async () => {
          throw new Error('File not found');
        },
        unlink: async () => {},
      });

      const result = await list_devicesLogic({}, mockExecutor, mockPathDeps, mockFsDeps);

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            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.',
          },
        ],
      });
    });

    it('should return successful no devices found response', async () => {
      const devicectlJson = {
        result: {
          devices: [],
        },
      };

      // Create executor with call count behavior
      let callCount = 0;
      const mockExecutor = async (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: Record<string, string>,
      ) => {
        callCount++;
        if (callCount === 1) {
          // First call succeeds (devicectl)
          return {
            success: true,
            output: '',
            error: undefined,
            process: { pid: 12345 },
          };
        } else {
          // Second call succeeds (xctrace) with empty output
          return {
            success: true,
            output: '',
            error: undefined,
            process: { pid: 12345 },
          };
        }
      };

      // Create mock path dependencies
      const mockPathDeps = {
        tmpdir: () => '/tmp',
        join: (...paths: string[]) => paths.join('/'),
      };

      // Create mock filesystem with empty devices response
      const mockFsDeps = createMockFileSystemExecutor({
        readFile: async () => JSON.stringify(devicectlJson),
        unlink: async () => {},
      });

      const result = await list_devicesLogic({}, mockExecutor, mockPathDeps, mockFsDeps);

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            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.',
          },
        ],
      });
    });
  });

  // Note: Handler functionality is thoroughly tested in device-workspace/list_devices.test.ts
  // This test file only verifies the re-export works correctly
});

```

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

```markdown
---
name: xcodebuild-mcp-qa-tester
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>
tools: Task, Bash, Glob, Grep, LS, ExitPlanMode, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, ListMcpResourcesTool, ReadMcpResourceTool
color: purple
---

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.

## Your Core Responsibilities

1. **Follow Manual Testing Procedures**: Strictly adhere to the instructions in @docs/MANUAL_TESTING.md for systematic test execution
2. **Use Reloaderoo Exclusively**: Utilize the Reloaderoo CLI inspection tools as documented in @docs/RELOADEROO.md for all testing activities
3. **Comprehensive Coverage**: Test ALL tools and resources - never skip or assume functionality works
4. **Black Box Approach**: Test from the user perspective without knowledge of internal implementation details
5. **Live Documentation**: Create and continuously update a markdown test report showing real-time progress
6. **MANDATORY COMPLETION**: Continue testing until EVERY SINGLE tool and resource has been tested - DO NOT STOP until 100% completion is achieved

## MANDATORY Test Report Creation and Updates

### Step 1: Create Initial Test Report (IMMEDIATELY)
**BEFORE TESTING BEGINS**, you MUST:

1. **Create Test Report File**: Generate a markdown file in the workspace root named `TESTING_REPORT_<YYYY-MM-DD>_<HH-MM>.md`
2. **Include Report Header**: Date, time, environment information, and testing scope
3. **Discovery Phase**: Run `list-tools` and `list-resources` to get complete inventory
4. **Create Checkbox Lists**: Add unchecked markdown checkboxes for every single tool and resource discovered

### Test Report Initial Structure
```markdown
# XcodeBuildMCP Testing Report
**Date:** YYYY-MM-DD HH:MM:SS  
**Environment:** [System details]  
**Testing Scope:** Comprehensive black box testing of all tools and resources

## Test Summary
- **Total Tools:** [X]
- **Total Resources:** [Y]
- **Tests Completed:** 0/[X+Y]
- **Tests Passed:** 0
- **Tests Failed:** 0

## Tools Testing Checklist
- [ ] Tool: tool_name_1 - Test with valid parameters
- [ ] Tool: tool_name_2 - Test with valid parameters
[... all tools discovered ...]

## Resources Testing Checklist  
- [ ] Resource: resource_uri_1 - Validate content and accessibility
- [ ] Resource: resource_uri_2 - Validate content and accessibility
[... all resources discovered ...]

## Detailed Test Results
[Updated as tests are completed]

## Failed Tests
[Updated if any failures occur]
```

### Step 2: Continuous Updates (AFTER EACH TEST)
**IMMEDIATELY after completing each test**, you MUST update the test report with:

1. **Check the box**: Change `- [ ]` to `- [x]` for the completed test
2. **Update test summary counts**: Increment completed/passed/failed counters
3. **Add detailed result**: Append to "Detailed Test Results" section with:
   - Test command used
   - Verification method
   - Validation summary
   - Pass/fail status

### Live Update Example
After testing `list_sims` tool, update the report:
```markdown
- [x] Tool: list_sims - Test with valid parameters ✅ PASSED

## Detailed Test Results

### Tool: list_sims ✅ PASSED
**Command:** `npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/index.js`
**Verification:** Command returned JSON array with 6 simulator objects
**Validation Summary:** Successfully discovered 6 available simulators with UUIDs, names, and boot status
**Timestamp:** 2025-01-29 14:30:15
```

## Testing Methodology

### Pre-Testing Setup
- Always start by building the project: `npm run build`
- Verify Reloaderoo is available: `npx reloaderoo@latest --help`
- Check server connectivity: `npx reloaderoo@latest inspect ping -- node build/index.js`
- Get server information: `npx reloaderoo@latest inspect server-info -- node build/index.js`

### Systematic Testing Workflow
1. **Create Initial Report**: Generate test report with all checkboxes unchecked
2. **Individual Testing**: Test each tool/resource systematically
3. **Live Updates**: Update report immediately after each test completion
4. **Continuous Tracking**: Report serves as real-time progress tracker
5. **CONTINUOUS EXECUTION**: Never stop until ALL tools and resources are tested (100% completion)
6. **Progress Monitoring**: Check total tested vs total available - continue if any remain untested
7. **Final Review**: Ensure all checkboxes are marked and results documented

### CRITICAL: NO EARLY TERMINATION
- **NEVER STOP** testing until every single tool and resource has been tested
- If you have tested X out of Y items, IMMEDIATELY continue testing the remaining Y-X items
- The only acceptable completion state is 100% coverage (all checkboxes checked)
- Do not summarize or conclude until literally every tool and resource has been individually tested
- Use the test report checkbox count as your progress indicator - if any boxes remain unchecked, CONTINUE TESTING

### Tool Testing Process
For each tool:
1. Execute test with `npx reloaderoo@latest inspect call-tool <tool_name> --params '<json>' -- node build/index.js`
2. Verify response format and content
3. **IMMEDIATELY** update test report with result
4. Check the box and add detailed verification summary
5. Move to next tool

### Resource Testing Process
For each resource:
1. Execute test with `npx reloaderoo@latest inspect read-resource "<uri>" -- node build/index.js`
2. Verify resource accessibility and content format
3. **IMMEDIATELY** update test report with result
4. Check the box and add detailed verification summary
5. Move to next resource

## Quality Standards

### Thoroughness Over Speed
- **NEVER rush testing** - take time to be comprehensive
- Test every single tool and resource without exception
- Update the test report after every single test - no batching
- The markdown report is the single source of truth for progress

### Test Documentation Requirements
- Record the exact command used for each test
- Document expected vs actual results
- Note any warnings, errors, or unexpected behavior
- Include full JSON responses for failed tests
- Categorize issues by severity (critical, major, minor)
- **MANDATORY**: Update test report immediately after each test completion

### Validation Criteria
- All tools must respond without errors for valid inputs
- Error messages must be clear and actionable for invalid inputs
- JSON responses must be properly formatted
- Resource URIs must be accessible and return valid data
- Tool descriptions must accurately reflect functionality

## Testing Environment Considerations

### Prerequisites Validation
- Verify Xcode is installed and accessible
- Check for required simulators and devices
- Validate development environment setup
- Ensure all dependencies are available

### Platform-Specific Testing
- Test iOS simulator tools with actual simulators
- Validate device tools (when devices are available)
- Test macOS-specific functionality
- Verify Swift Package Manager integration

## Test Report Management

### File Naming Convention
- Format: `TESTING_REPORT_<YYYY-MM-DD>_<HH-MM>.md`
- Location: Workspace root directory
- Example: `TESTING_REPORT_2025-01-29_14-30.md`

### Update Requirements
- **Real-time updates**: Update after every single test completion
- **No batching**: Never wait to update multiple tests at once
- **Checkbox tracking**: Visual progress through checked/unchecked boxes
- **Detailed results**: Each test gets a dedicated result section
- **Summary statistics**: Keep running totals updated

### Verification Summary Requirements
Every test result MUST answer: "How did you know this test passed?"

Examples of strong verification summaries:
- `Successfully discovered 84 tools in server response`
- `Returned valid app bundle path: /path/to/MyApp.app`
- `Listed 6 simulators with expected UUID format and boot status`
- `Resource returned JSON array with 4 device objects containing UDID and name fields`
- `Tool correctly rejected invalid parameters with clear error message`

## Error Investigation Protocol

1. **Reproduce Consistently**: Ensure errors can be reproduced reliably
2. **Isolate Variables**: Test with minimal parameters to isolate issues
3. **Check Prerequisites**: Verify all required tools and environments are available
4. **Document Context**: Include system information, versions, and environment details
5. **Update Report**: Document failures immediately in the test report

## Critical Success Criteria

- ✅ Test report created BEFORE any testing begins with all checkboxes unchecked
- ✅ Every single tool has its own checkbox and detailed result section
- ✅ Every single resource has its own checkbox and detailed result section
- ✅ Report updated IMMEDIATELY after each individual test completion
- ✅ No tool or resource is skipped or grouped together
- ✅ Each verification summary clearly explains how success was determined
- ✅ Real-time progress tracking through checkbox completion
- ✅ Test report serves as the single source of truth for all testing progress
- ✅ **100% COMPLETION MANDATORY**: All checkboxes must be checked before considering testing complete

## ABSOLUTE COMPLETION REQUIREMENT

**YOU MUST NOT STOP TESTING UNTIL:**
- Every single tool discovered by `list-tools` has been individually tested
- Every single resource discovered by `list-resources` has been individually tested  
- All checkboxes in your test report are marked as complete
- The test summary shows X/X completion (100%)

**IF TESTING IS NOT 100% COMPLETE:**
- Immediately identify which tools/resources remain untested
- Continue systematic testing of the remaining items
- Update the test report after each additional test
- Do not provide final summaries or conclusions until literally everything is tested

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

```

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

```typescript
/**
 * Pure dependency injection test for discover_projs plugin
 *
 * Tests the plugin structure and project discovery functionality
 * including parameter validation, file system operations, and response formatting.
 *
 * Uses createMockFileSystemExecutor for file system operations.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import plugin, { discover_projsLogic } from '../discover_projs.ts';
import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts';

describe('discover_projs plugin', () => {
  let mockFileSystemExecutor: any;

  // Create mock file system executor
  mockFileSystemExecutor = createMockFileSystemExecutor({
    stat: async () => ({ isDirectory: () => true }),
    readdir: async () => [],
  });

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

    it('should have correct description', () => {
      expect(plugin.description).toBe(
        'Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.',
      );
    });

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

    it('should validate schema with valid inputs', () => {
      const schema = z.object(plugin.schema);
      expect(schema.safeParse({ workspaceRoot: '/path/to/workspace' }).success).toBe(true);
      expect(
        schema.safeParse({ workspaceRoot: '/path/to/workspace', scanPath: 'subdir' }).success,
      ).toBe(true);
      expect(schema.safeParse({ workspaceRoot: '/path/to/workspace', maxDepth: 3 }).success).toBe(
        true,
      );
      expect(
        schema.safeParse({
          workspaceRoot: '/path/to/workspace',
          scanPath: 'subdir',
          maxDepth: 5,
        }).success,
      ).toBe(true);
    });

    it('should validate schema with invalid inputs', () => {
      const schema = z.object(plugin.schema);
      expect(schema.safeParse({}).success).toBe(false);
      expect(schema.safeParse({ workspaceRoot: 123 }).success).toBe(false);
      expect(schema.safeParse({ workspaceRoot: '/path', scanPath: 123 }).success).toBe(false);
      expect(schema.safeParse({ workspaceRoot: '/path', maxDepth: 'invalid' }).success).toBe(false);
      expect(schema.safeParse({ workspaceRoot: '/path', maxDepth: -1 }).success).toBe(false);
      expect(schema.safeParse({ workspaceRoot: '/path', maxDepth: 1.5 }).success).toBe(false);
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should handle workspaceRoot parameter correctly when provided', async () => {
      mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true });
      mockFileSystemExecutor.readdir = async () => [];

      const result = await discover_projsLogic(
        { workspaceRoot: '/workspace' },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
        isError: false,
      });
    });

    it('should return error when scan path does not exist', async () => {
      mockFileSystemExecutor.stat = async () => {
        throw new Error('ENOENT: no such file or directory');
      };

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '.',
          maxDepth: 5,
        },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to access scan path: /workspace. Error: ENOENT: no such file or directory',
          },
        ],
        isError: true,
      });
    });

    it('should return error when scan path is not a directory', async () => {
      mockFileSystemExecutor.stat = async () => ({ isDirectory: () => false });

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '.',
          maxDepth: 5,
        },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Scan path is not a directory: /workspace' }],
        isError: true,
      });
    });

    it('should return success with no projects found', async () => {
      mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true });
      mockFileSystemExecutor.readdir = async () => [];

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '.',
          maxDepth: 5,
        },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
        isError: false,
      });
    });

    it('should return success with projects found', async () => {
      mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true });
      mockFileSystemExecutor.readdir = async () => [
        { name: 'MyApp.xcodeproj', isDirectory: () => true, isSymbolicLink: () => false },
        { name: 'MyWorkspace.xcworkspace', isDirectory: () => true, isSymbolicLink: () => false },
      ];

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '.',
          maxDepth: 5,
        },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          { type: 'text', text: 'Discovery finished. Found 1 projects and 1 workspaces.' },
          { type: 'text', text: 'Projects found:\n - /workspace/MyApp.xcodeproj' },
          { type: 'text', text: 'Workspaces found:\n - /workspace/MyWorkspace.xcworkspace' },
        ],
        isError: false,
      });
    });

    it('should handle fs error with code', async () => {
      const error = new Error('Permission denied');
      (error as any).code = 'EACCES';
      mockFileSystemExecutor.stat = async () => {
        throw error;
      };

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '.',
          maxDepth: 5,
        },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Failed to access scan path: /workspace. Error: Permission denied',
          },
        ],
        isError: true,
      });
    });

    it('should handle string error', async () => {
      mockFileSystemExecutor.stat = async () => {
        throw 'String error';
      };

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '.',
          maxDepth: 5,
        },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          { type: 'text', text: 'Failed to access scan path: /workspace. Error: String error' },
        ],
        isError: true,
      });
    });

    it('should handle workspaceRoot parameter correctly', async () => {
      mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true });
      mockFileSystemExecutor.readdir = async () => [];

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
        },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
        isError: false,
      });
    });

    it('should handle scan path outside workspace root', async () => {
      // Mock path normalization to simulate path outside workspace root
      mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true });
      mockFileSystemExecutor.readdir = async () => [];

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '../outside',
          maxDepth: 5,
        },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
        isError: false,
      });
    });

    it('should handle error with object containing message and code properties', async () => {
      const errorObject = {
        message: 'Access denied',
        code: 'EACCES',
      };
      mockFileSystemExecutor.stat = async () => {
        throw errorObject;
      };

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '.',
          maxDepth: 5,
        },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [
          { type: 'text', text: 'Failed to access scan path: /workspace. Error: Access denied' },
        ],
        isError: true,
      });
    });

    it('should handle max depth reached during recursive scan', async () => {
      let readdirCallCount = 0;

      mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true });
      mockFileSystemExecutor.readdir = async () => {
        readdirCallCount++;
        if (readdirCallCount <= 3) {
          return [
            {
              name: `subdir${readdirCallCount}`,
              isDirectory: () => true,
              isSymbolicLink: () => false,
            },
          ];
        }
        return [];
      };

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '.',
          maxDepth: 3,
        },
        mockFileSystemExecutor,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
        isError: false,
      });
    });

    it('should handle skipped directory types during scan', async () => {
      mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true });
      mockFileSystemExecutor.readdir = async () => [
        { name: 'build', isDirectory: () => true, isSymbolicLink: () => false },
        { name: 'DerivedData', isDirectory: () => true, isSymbolicLink: () => false },
        { name: 'symlink', isDirectory: () => true, isSymbolicLink: () => true },
        { name: 'regular.txt', isDirectory: () => false, isSymbolicLink: () => false },
      ];

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '.',
          maxDepth: 5,
        },
        mockFileSystemExecutor,
      );

      // Test that skipped directories and files are correctly filtered out
      expect(result).toEqual({
        content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
        isError: false,
      });
    });

    it('should handle error during recursive directory reading', async () => {
      mockFileSystemExecutor.stat = async () => ({ isDirectory: () => true });
      mockFileSystemExecutor.readdir = async () => {
        const readError = new Error('Permission denied');
        (readError as any).code = 'EACCES';
        throw readError;
      };

      const result = await discover_projsLogic(
        {
          workspaceRoot: '/workspace',
          scanPath: '.',
          maxDepth: 5,
        },
        mockFileSystemExecutor,
      );

      // The function should handle the error gracefully and continue
      expect(result).toEqual({
        content: [{ type: 'text', text: 'Discovery finished. Found 0 projects and 0 workspaces.' }],
        isError: false,
      });
    });
  });
});

```

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

```bash
#!/bin/bash
set -e

# GitHub Release Creation Script
# This script handles only the GitHub release creation.
# Building and NPM publishing are handled by GitHub workflows.
#
# Usage: ./scripts/release.sh [VERSION|BUMP_TYPE] [OPTIONS]
# Run with --help for detailed usage information
FIRST_ARG=$1
DRY_RUN=false
VERSION=""
BUMP_TYPE=""

# Function to show help
show_help() {
  cat << 'EOF'
📦 GitHub Release Creator

Creates releases with automatic semver bumping. Only handles GitHub release
creation - building and NPM publishing are handled by workflows.

USAGE:
    [VERSION|BUMP_TYPE] [OPTIONS]

ARGUMENTS:
    VERSION         Explicit version (e.g., 1.5.0, 2.0.0-beta.1)
    BUMP_TYPE       major | minor [default] | patch

OPTIONS:
    --dry-run       Preview without executing
    -h, --help      Show this help

EXAMPLES:
    (no args)       Interactive minor bump
    major           Interactive major bump
    1.5.0           Use specific version
    patch --dry-run Preview patch bump

EOF

  local highest_version=$(get_highest_version)
  if [[ -n "$highest_version" ]]; then
    echo "CURRENT: $highest_version"
    echo "NEXT: major=$(bump_version "$highest_version" "major") | minor=$(bump_version "$highest_version" "minor") | patch=$(bump_version "$highest_version" "patch")"
  else
    echo "No existing version tags found"
  fi
  echo ""
}

# Function to get the highest version from git tags
get_highest_version() {
  git tag | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?$' | sed 's/^v//' | sort -V | tail -1
}

# Function to parse version components
parse_version() {
  local version=$1
  echo "$version" | sed -E 's/^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$/\1 \2 \3 \4/'
}

# Function to bump version based on type
bump_version() {
  local current_version=$1
  local bump_type=$2

  local parsed=($(parse_version "$current_version"))
  local major=${parsed[0]}
  local minor=${parsed[1]}
  local patch=${parsed[2]}
  local prerelease=${parsed[3]:-""}

  # Remove prerelease for stable version bumps
  case $bump_type in
    major)
      echo "$((major + 1)).0.0"
      ;;
    minor)
      echo "${major}.$((minor + 1)).0"
      ;;
    patch)
      echo "${major}.${minor}.$((patch + 1))"
      ;;
    *)
      echo "❌ Unknown bump type: $bump_type" >&2
      exit 1
      ;;
  esac
}

# Function to validate version format
validate_version() {
  local version=$1
  if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?$ ]]; then
    echo "❌ Invalid version format: $version"
    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)"
    return 1
  fi
  return 0
}

# Function to compare versions (returns 1 if first version is greater, 0 if equal, -1 if less)
compare_versions() {
  local version1=$1
  local version2=$2

  # Remove prerelease parts for comparison
  local v1_stable=$(echo "$version1" | sed -E 's/(-.*)?$//')
  local v2_stable=$(echo "$version2" | sed -E 's/(-.*)?$//')

  if [[ "$v1_stable" == "$v2_stable" ]]; then
    echo 0
    return
  fi

  # Use sort -V to compare versions
  local sorted=$(printf "%s\n%s" "$v1_stable" "$v2_stable" | sort -V)
  if [[ "$(echo "$sorted" | head -1)" == "$v1_stable" ]]; then
    echo -1
  else
    echo 1
  fi
}

# Function to ask for confirmation
ask_confirmation() {
  local suggested_version=$1
  echo ""
  echo "🚀 Suggested next version: $suggested_version"
  read -p "Do you want to use this version? (y/N): " -n 1 -r
  echo
  if [[ $REPLY =~ ^[Yy]$ ]]; then
    return 0
  else
    return 1
  fi
}

# Function to get version interactively
get_version_interactively() {
  echo ""
  echo "Please enter the version manually:"
  while true; do
    read -p "Version: " manual_version
    if validate_version "$manual_version"; then
      local highest_version=$(get_highest_version)
      if [[ -n "$highest_version" ]]; then
        local comparison=$(compare_versions "$manual_version" "$highest_version")
        if [[ $comparison -le 0 ]]; then
          echo "❌ Version $manual_version is not newer than the highest existing version $highest_version"
          continue
        fi
      fi
      VERSION="$manual_version"
      break
    fi
  done
}

# Check for help flags first
for arg in "$@"; do
  if [[ "$arg" == "-h" ]] || [[ "$arg" == "--help" ]]; then
    show_help
    exit 0
  fi
done

# Check for arguments and set flags
for arg in "$@"; do
  if [[ "$arg" == "--dry-run" ]]; then
    DRY_RUN=true
  fi
done

# Determine version or bump type (ignore --dry-run flag)
if [[ -z "$FIRST_ARG" ]] || [[ "$FIRST_ARG" == "--dry-run" ]]; then
  # No argument provided, default to minor bump
  BUMP_TYPE="minor"
elif [[ "$FIRST_ARG" == "major" ]] || [[ "$FIRST_ARG" == "minor" ]] || [[ "$FIRST_ARG" == "patch" ]]; then
  # Bump type provided
  BUMP_TYPE="$FIRST_ARG"
else
  # Version string provided
  if validate_version "$FIRST_ARG"; then
    VERSION="$FIRST_ARG"
  else
    exit 1
  fi
fi

# If bump type is set, calculate the suggested version
if [[ -n "$BUMP_TYPE" ]]; then
  HIGHEST_VERSION=$(get_highest_version)
  if [[ -z "$HIGHEST_VERSION" ]]; then
    echo "❌ No existing version tags found. Please provide a version manually."
    get_version_interactively
  else
    SUGGESTED_VERSION=$(bump_version "$HIGHEST_VERSION" "$BUMP_TYPE")

    if ask_confirmation "$SUGGESTED_VERSION"; then
      VERSION="$SUGGESTED_VERSION"
    else
      get_version_interactively
    fi
  fi
fi

# Final validation and version comparison
if [[ -z "$VERSION" ]]; then
  echo "❌ No version determined"
  exit 1
fi

HIGHEST_VERSION=$(get_highest_version)
if [[ -n "$HIGHEST_VERSION" ]]; then
  COMPARISON=$(compare_versions "$VERSION" "$HIGHEST_VERSION")
  if [[ $COMPARISON -le 0 ]]; then
    echo "❌ Version $VERSION is not newer than the highest existing version $HIGHEST_VERSION"
    exit 1
  fi
fi

# Detect current branch
BRANCH=$(git rev-parse --abbrev-ref HEAD)

# Enforce branch policy - only allow releases from main
if [[ "$BRANCH" != "main" ]]; then
  echo "❌ Error: Releases must be created from the main branch."
  echo "Current branch: $BRANCH"
  echo "Please switch to main and try again."
  exit 1
fi

run() {
  if $DRY_RUN; then
    echo "[dry-run] $*"
  else
    eval "$@"
  fi
}

# Ensure we're in the project root (parent of scripts directory)
cd "$(dirname "$0")/.."

# Check if working directory is clean
if ! git diff-index --quiet HEAD --; then
  echo "❌ Error: Working directory is not clean."
  echo "Please commit or stash your changes before creating a release."
  exit 1
fi

# Check if package.json already has this version (from previous attempt)
CURRENT_PACKAGE_VERSION=$(node -p "require('./package.json').version")
if [[ "$CURRENT_PACKAGE_VERSION" == "$VERSION" ]]; then
  echo "📦 Version $VERSION already set in package.json"
  SKIP_VERSION_UPDATE=true
else
  SKIP_VERSION_UPDATE=false
fi

if [[ "$SKIP_VERSION_UPDATE" == "false" ]]; then
  # Version update
  echo ""
  echo "🔧 Setting version to $VERSION..."
  run "npm version \"$VERSION\" --no-git-tag-version"

  # README update
  echo ""
  echo "📝 Updating version in README.md..."
  # Update version references in code examples using extended regex for precise semver matching
  run "sed -i '' -E 's/@[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+\.[0-9]+)?(-[a-zA-Z0-9]+\.[0-9]+)*(-[a-zA-Z0-9]+)?/@'"$VERSION"'/g' README.md"

  # Update URL-encoded version references in shield links
  echo "📝 Updating version in README.md shield links..."
  run "sed -i '' -E 's/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]+)?/npm%3Axcodebuildmcp%40'"$VERSION"'/g' README.md"

  # server.json update
  echo ""
  if [[ -f server.json ]]; then
    echo "📝 Updating server.json version to $VERSION..."
    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');\""
  else
    echo "⚠️  server.json not found; skipping update"
  fi

  # Git operations
  echo ""
  echo "📦 Committing version changes..."
  if [[ -f server.json ]]; then
    run "git add package.json README.md server.json"
  else
    run "git add package.json README.md"
  fi
  run "git commit -m \"Release v$VERSION\""
else
  echo "⏭️  Skipping version update (already done)"
  # Ensure server.json still matches the desired version (in case of a partial previous run)
  if [[ -f server.json ]]; then
    CURRENT_SERVER_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('server.json','utf8')).version||'')")
    if [[ "$CURRENT_SERVER_VERSION" != "$VERSION" ]]; then
      echo "📝 Aligning server.json to $VERSION..."
      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');\""
      run "git add server.json"
      run "git commit -m \"Align server.json for v$VERSION\""
    fi
  fi
fi

# Create or recreate tag at current HEAD
echo "🏷️  Creating tag v$VERSION..."
run "git tag -f \"v$VERSION\""

echo ""
echo "🚀 Pushing to origin..."
run "git push origin $BRANCH --tags"

# Monitor the workflow and handle failures
echo ""
echo "⏳ Monitoring GitHub Actions workflow..."
echo "This may take a few minutes..."

# Wait for workflow to start
sleep 5

# Get the workflow run ID for this tag
RUN_ID=$(gh run list --workflow=release.yml --limit=1 --json databaseId --jq '.[0].databaseId')

if [[ -n "$RUN_ID" ]]; then
  echo "📊 Workflow run ID: $RUN_ID"
  echo "🔍 Watching workflow progress..."
  echo "(Press Ctrl+C to detach and monitor manually)"
  echo ""

  # Watch the workflow with exit status
  if gh run watch "$RUN_ID" --exit-status; then
    echo ""
    echo "✅ Release v$VERSION completed successfully!"
    echo "📦 View on NPM: https://www.npmjs.com/package/xcodebuildmcp/v/$VERSION"
    echo "🎉 View release: https://github.com/cameroncooke/XcodeBuildMCP/releases/tag/v$VERSION"
    # MCP Registry verification link
    echo "🔎 Verify MCP Registry: https://registry.modelcontextprotocol.io/v0/servers?search=com.xcodebuildmcp/XcodeBuildMCP&version=latest
  else
    echo ""
    echo "❌ CI workflow failed!"
    echo ""
    # Prefer job state: if the primary 'release' job succeeded, treat as success.
    RELEASE_JOB_CONCLUSION=$(gh run view "$RUN_ID" --json jobs --jq '.jobs[] | select(.name=="release") | .conclusion')
    if [ "$RELEASE_JOB_CONCLUSION" = "success" ]; then
      echo "⚠️ Workflow reported failure, but primary 'release' job concluded SUCCESS."
      echo "✅ Treating release as successful. Tag v$VERSION is kept."
      echo "📦 Verify on NPM: https://www.npmjs.com/package/xcodebuildmcp/v/$VERSION"
      exit 0
    fi
    echo "🧹 Cleaning up tags only (keeping version commit)..."

    # Delete remote tag
    echo "  - Deleting remote tag v$VERSION..."
    git push origin :refs/tags/v$VERSION 2>/dev/null || true

    # Delete local tag
    echo "  - Deleting local tag v$VERSION..."
    git tag -d v$VERSION

    echo ""
    echo "✅ Tag cleanup complete!"
    echo ""
    echo "ℹ️  The version commit remains in your history."
    echo "📝 To retry after fixing issues:"
    echo "   1. Fix the CI issues"
    echo "   2. Commit your fixes"
    echo "   3. Run: ./scripts/release.sh $VERSION"
    echo ""
    echo "🔍 To see what failed: gh run view $RUN_ID --log-failed"
    exit 1
  fi
else
  echo "⚠️  Could not find workflow run. Please check manually:"
  echo "https://github.com/cameroncooke/XcodeBuildMCP/actions"
fi

```

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

```typescript
/**
 * Simulator Get App Path Plugin: Get Simulator App Path (Unified)
 *
 * Gets the app bundle path for a simulator by UUID or name using either a project or workspace file.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 * Accepts mutually exclusive `simulatorId` or `simulatorName`.
 */

import { z } from 'zod';
import { log } from '../../../utils/logging/index.ts';
import { createTextResponse } from '../../../utils/responses/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import { ToolResponse } from '../../../types/common.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

const XcodePlatform = {
  macOS: 'macOS',
  iOS: 'iOS',
  iOSSimulator: 'iOS Simulator',
  watchOS: 'watchOS',
  watchOSSimulator: 'watchOS Simulator',
  tvOS: 'tvOS',
  tvOSSimulator: 'tvOS Simulator',
  visionOS: 'visionOS',
  visionOSSimulator: 'visionOS Simulator',
};

function constructDestinationString(
  platform: string,
  simulatorName: string,
  simulatorId: string,
  useLatest: boolean = true,
  arch?: string,
): string {
  const isSimulatorPlatform = [
    XcodePlatform.iOSSimulator,
    XcodePlatform.watchOSSimulator,
    XcodePlatform.tvOSSimulator,
    XcodePlatform.visionOSSimulator,
  ].includes(platform);

  // If ID is provided for a simulator, it takes precedence and uniquely identifies it.
  if (isSimulatorPlatform && simulatorId) {
    return `platform=${platform},id=${simulatorId}`;
  }

  // If name is provided for a simulator
  if (isSimulatorPlatform && simulatorName) {
    return `platform=${platform},name=${simulatorName}${useLatest ? ',OS=latest' : ''}`;
  }

  // If it's a simulator platform but neither ID nor name is provided (should be prevented by callers now)
  if (isSimulatorPlatform && !simulatorId && !simulatorName) {
    log(
      'warning',
      `Constructing generic destination for ${platform} without name or ID. This might not be specific enough.`,
    );
    throw new Error(`Simulator name or ID is required for specific ${platform} operations`);
  }

  // Handle non-simulator platforms
  switch (platform) {
    case XcodePlatform.macOS:
      return arch ? `platform=macOS,arch=${arch}` : 'platform=macOS';
    case XcodePlatform.iOS:
      return 'generic/platform=iOS';
    case XcodePlatform.watchOS:
      return 'generic/platform=watchOS';
    case XcodePlatform.tvOS:
      return 'generic/platform=tvOS';
    case XcodePlatform.visionOS:
      return 'generic/platform=visionOS';
  }
  // Fallback just in case (shouldn't be reached with enum)
  log('error', `Reached unexpected point in constructDestinationString for platform: ${platform}`);
  return `platform=${platform}`;
}

// Define base schema
const baseGetSimulatorAppPathSchema = z.object({
  projectPath: z
    .string()
    .optional()
    .describe('Path to .xcodeproj file. Provide EITHER this OR workspacePath, not both'),
  workspacePath: z
    .string()
    .optional()
    .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'),
  scheme: z.string().describe('The scheme to use (Required)'),
  platform: z
    .enum(['iOS Simulator', 'watchOS Simulator', 'tvOS Simulator', 'visionOS Simulator'])
    .describe('Target simulator platform (Required)'),
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator (from list_sims). Provide EITHER this OR simulatorName, not both',
    ),
  simulatorName: z
    .string()
    .optional()
    .describe(
      "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
    ),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  useLatestOS: z
    .boolean()
    .optional()
    .describe('Whether to use the latest OS version for the named simulator'),
  arch: z.string().optional().describe('Optional architecture'),
});

// Add XOR validation with preprocessing
const getSimulatorAppPathSchema = z.preprocess(
  nullifyEmptyStrings,
  baseGetSimulatorAppPathSchema
    .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
      message: 'Either projectPath or workspacePath is required.',
    })
    .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
      message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
    })
    .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
      message: 'Either simulatorId or simulatorName is required.',
    })
    .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
      message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
    }),
);

// Use z.infer for type safety
type GetSimulatorAppPathParams = z.infer<typeof getSimulatorAppPathSchema>;

/**
 * Exported business logic function for getting app path
 */
export async function get_sim_app_pathLogic(
  params: GetSimulatorAppPathParams,
  executor: CommandExecutor,
): Promise<ToolResponse> {
  // Set defaults - Zod validation already ensures required params are present
  const projectPath = params.projectPath;
  const workspacePath = params.workspacePath;
  const scheme = params.scheme;
  const platform = params.platform;
  const simulatorId = params.simulatorId;
  const simulatorName = params.simulatorName;
  const configuration = params.configuration ?? 'Debug';
  const useLatestOS = params.useLatestOS ?? true;
  const arch = params.arch;

  // Log warning if useLatestOS is provided with simulatorId
  if (simulatorId && params.useLatestOS !== undefined) {
    log(
      'warning',
      `useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)`,
    );
  }

  log('info', `Getting app path for scheme ${scheme} on platform ${platform}`);

  try {
    // Create the command array for xcodebuild with -showBuildSettings option
    const command = ['xcodebuild', '-showBuildSettings'];

    // Add the workspace or project (XOR validation ensures exactly one is provided)
    if (workspacePath) {
      command.push('-workspace', workspacePath);
    } else if (projectPath) {
      command.push('-project', projectPath);
    }

    // Add the scheme and configuration
    command.push('-scheme', scheme);
    command.push('-configuration', configuration);

    // Handle destination based on platform
    const isSimulatorPlatform = [
      XcodePlatform.iOSSimulator,
      XcodePlatform.watchOSSimulator,
      XcodePlatform.tvOSSimulator,
      XcodePlatform.visionOSSimulator,
    ].includes(platform);

    let destinationString = '';

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

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

    // Execute the command directly
    const result = await executor(command, 'Get App Path', true, undefined);

    if (!result.success) {
      return createTextResponse(`Failed to get app path: ${result.error}`, true);
    }

    if (!result.output) {
      return createTextResponse('Failed to extract build settings output from the result.', true);
    }

    const buildSettingsOutput = result.output;
    const builtProductsDirMatch = buildSettingsOutput.match(/^\s*BUILT_PRODUCTS_DIR\s*=\s*(.+)$/m);
    const fullProductNameMatch = buildSettingsOutput.match(/^\s*FULL_PRODUCT_NAME\s*=\s*(.+)$/m);

    if (!builtProductsDirMatch || !fullProductNameMatch) {
      return createTextResponse(
        'Failed to extract app path from build settings. Make sure the app has been built first.',
        true,
      );
    }

    const builtProductsDir = builtProductsDirMatch[1].trim();
    const fullProductName = fullProductNameMatch[1].trim();
    const appPath = `${builtProductsDir}/${fullProductName}`;

    let nextStepsText = '';
    if (platform === XcodePlatform.macOS) {
      nextStepsText = `Next Steps:
1. Get bundle ID: get_mac_bundle_id({ appPath: "${appPath}" })
2. Launch the app: launch_mac_app({ appPath: "${appPath}" })`;
    } else if (isSimulatorPlatform) {
      nextStepsText = `Next Steps:
1. Get bundle ID: get_app_bundle_id({ appPath: "${appPath}" })
2. Boot simulator: boot_sim({ simulatorUuid: "SIMULATOR_UUID" })
3. Install app: install_app_sim({ simulatorUuid: "SIMULATOR_UUID", appPath: "${appPath}" })
4. Launch app: launch_app_sim({ simulatorUuid: "SIMULATOR_UUID", bundleId: "BUNDLE_ID" })`;
    } else if (
      [
        XcodePlatform.iOS,
        XcodePlatform.watchOS,
        XcodePlatform.tvOS,
        XcodePlatform.visionOS,
      ].includes(platform)
    ) {
      nextStepsText = `Next Steps:
1. Get bundle ID: get_app_bundle_id({ appPath: "${appPath}" })
2. Install app on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "${appPath}" })
3. Launch app on device: launch_app_device({ deviceId: "DEVICE_UDID", bundleId: "BUNDLE_ID" })`;
    } else {
      // For other platforms
      nextStepsText = `Next Steps:
1. The app has been built for ${platform}
2. Use platform-specific deployment tools to install and run the app`;
    }

    return {
      content: [
        {
          type: 'text',
          text: `✅ App path retrieved successfully: ${appPath}`,
        },
        {
          type: 'text',
          text: nextStepsText,
        },
      ],
      isError: false,
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error retrieving app path: ${errorMessage}`);
    return createTextResponse(`Error retrieving app path: ${errorMessage}`, true);
  }
}

const publicSchemaObject = baseGetSimulatorAppPathSchema.omit({
  projectPath: true,
  workspacePath: true,
  scheme: true,
  simulatorId: true,
  simulatorName: true,
  configuration: true,
  useLatestOS: true,
  arch: true,
} as const);

export default {
  name: 'get_sim_app_path',
  description: 'Retrieves the built app path for an iOS simulator.',
  schema: publicSchemaObject.shape,
  handler: createSessionAwareTool<GetSimulatorAppPathParams>({
    internalSchema: getSimulatorAppPathSchema as unknown as z.ZodType<GetSimulatorAppPathParams>,
    logicFunction: get_sim_app_pathLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [
      ['projectPath', 'workspacePath'],
      ['simulatorId', 'simulatorName'],
    ],
  }),
};

```

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

```typescript
/**
 * macOS Shared Plugin: Test macOS (Unified)
 *
 * Runs tests for a macOS project or workspace using xcodebuild test and parses xcresult output.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 */

import { z } from 'zod';
import { join } from 'path';
import { ToolResponse, XcodePlatform } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
import { createTextResponse } from '../../../utils/responses/index.ts';
import { normalizeTestRunnerEnv } from '../../../utils/environment.ts';
import type {
  CommandExecutor,
  FileSystemExecutor,
  CommandExecOptions,
} from '../../../utils/execution/index.ts';
import {
  getDefaultCommandExecutor,
  getDefaultFileSystemExecutor,
} from '../../../utils/execution/index.ts';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath
const baseSchemaObject = z.object({
  projectPath: z.string().optional().describe('Path to the .xcodeproj file'),
  workspacePath: z.string().optional().describe('Path to the .xcworkspace file'),
  scheme: z.string().describe('The scheme to use'),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  derivedDataPath: z
    .string()
    .optional()
    .describe('Path where build products and other derived data will go'),
  extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
  preferXcodebuild: z
    .boolean()
    .optional()
    .describe('If true, prefers xcodebuild over the experimental incremental build system'),
  testRunnerEnv: z
    .record(z.string(), z.string())
    .optional()
    .describe(
      'Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)',
    ),
});

const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);

const publicSchemaObject = baseSchemaObject.omit({
  projectPath: true,
  workspacePath: true,
  scheme: true,
  configuration: true,
} as const);

const testMacosSchema = baseSchema
  .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
    message: 'Either projectPath or workspacePath is required.',
  })
  .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
    message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
  });

export type TestMacosParams = z.infer<typeof testMacosSchema>;

/**
 * Type definition for test summary structure from xcresulttool
 * @typedef {Object} TestSummary
 * @property {string} [title]
 * @property {string} [result]
 * @property {number} [totalTestCount]
 * @property {number} [passedTests]
 * @property {number} [failedTests]
 * @property {number} [skippedTests]
 * @property {number} [expectedFailures]
 * @property {string} [environmentDescription]
 * @property {Array<Object>} [devicesAndConfigurations]
 * @property {Array<Object>} [testFailures]
 * @property {Array<Object>} [topInsights]
 */

/**
 * Parse xcresult bundle using xcrun xcresulttool
 */
async function parseXcresultBundle(
  resultBundlePath: string,
  executor: CommandExecutor = getDefaultCommandExecutor(),
): Promise<string> {
  try {
    const result = await executor(
      ['xcrun', 'xcresulttool', 'get', 'test-results', 'summary', '--path', resultBundlePath],
      'Parse xcresult bundle',
      true,
    );

    if (!result.success) {
      throw new Error(result.error ?? 'Failed to parse xcresult bundle');
    }

    // Parse JSON response and format as human-readable
    let summary: unknown;
    try {
      summary = JSON.parse(result.output || '{}');
    } catch (parseError) {
      throw new Error(`Failed to parse JSON output: ${parseError}`);
    }

    if (typeof summary !== 'object' || summary === null) {
      throw new Error('Invalid JSON output: expected object');
    }

    return formatTestSummary(summary as Record<string, unknown>);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error parsing xcresult bundle: ${errorMessage}`);
    throw error;
  }
}

/**
 * Format test summary JSON into human-readable text
 */
function formatTestSummary(summary: Record<string, unknown>): string {
  const lines = [];

  lines.push(`Test Summary: ${summary.title ?? 'Unknown'}`);
  lines.push(`Overall Result: ${summary.result ?? 'Unknown'}`);
  lines.push('');

  lines.push('Test Counts:');
  lines.push(`  Total: ${summary.totalTestCount ?? 0}`);
  lines.push(`  Passed: ${summary.passedTests ?? 0}`);
  lines.push(`  Failed: ${summary.failedTests ?? 0}`);
  lines.push(`  Skipped: ${summary.skippedTests ?? 0}`);
  lines.push(`  Expected Failures: ${summary.expectedFailures ?? 0}`);
  lines.push('');

  if (summary.environmentDescription) {
    lines.push(`Environment: ${summary.environmentDescription}`);
    lines.push('');
  }

  if (
    summary.devicesAndConfigurations &&
    Array.isArray(summary.devicesAndConfigurations) &&
    summary.devicesAndConfigurations.length > 0
  ) {
    const firstDeviceConfig: unknown = summary.devicesAndConfigurations[0];
    if (
      typeof firstDeviceConfig === 'object' &&
      firstDeviceConfig !== null &&
      'device' in firstDeviceConfig
    ) {
      const device: unknown = (firstDeviceConfig as Record<string, unknown>).device;
      if (typeof device === 'object' && device !== null) {
        const deviceRecord = device as Record<string, unknown>;
        const deviceName =
          'deviceName' in deviceRecord && typeof deviceRecord.deviceName === 'string'
            ? deviceRecord.deviceName
            : 'Unknown';
        const platform =
          'platform' in deviceRecord && typeof deviceRecord.platform === 'string'
            ? deviceRecord.platform
            : 'Unknown';
        const osVersion =
          'osVersion' in deviceRecord && typeof deviceRecord.osVersion === 'string'
            ? deviceRecord.osVersion
            : 'Unknown';

        lines.push(`Device: ${deviceName} (${platform} ${osVersion})`);
        lines.push('');
      }
    }
  }

  if (
    summary.testFailures &&
    Array.isArray(summary.testFailures) &&
    summary.testFailures.length > 0
  ) {
    lines.push('Test Failures:');
    summary.testFailures.forEach((failure: unknown, index: number) => {
      if (typeof failure === 'object' && failure !== null) {
        const failureRecord = failure as Record<string, unknown>;
        const testName =
          'testName' in failureRecord && typeof failureRecord.testName === 'string'
            ? failureRecord.testName
            : 'Unknown Test';
        const targetName =
          'targetName' in failureRecord && typeof failureRecord.targetName === 'string'
            ? failureRecord.targetName
            : 'Unknown Target';

        lines.push(`  ${index + 1}. ${testName} (${targetName})`);

        if ('failureText' in failureRecord && typeof failureRecord.failureText === 'string') {
          lines.push(`     ${failureRecord.failureText}`);
        }
      }
    });
    lines.push('');
  }

  if (summary.topInsights && Array.isArray(summary.topInsights) && summary.topInsights.length > 0) {
    lines.push('Insights:');
    summary.topInsights.forEach((insight: unknown, index: number) => {
      if (typeof insight === 'object' && insight !== null) {
        const insightRecord = insight as Record<string, unknown>;
        const impact =
          'impact' in insightRecord && typeof insightRecord.impact === 'string'
            ? insightRecord.impact
            : 'Unknown';
        const text =
          'text' in insightRecord && typeof insightRecord.text === 'string'
            ? insightRecord.text
            : 'No description';

        lines.push(`  ${index + 1}. [${impact}] ${text}`);
      }
    });
  }

  return lines.join('\n');
}

/**
 * Business logic for testing a macOS project or workspace.
 * Exported for direct testing and reuse.
 */
export async function testMacosLogic(
  params: TestMacosParams,
  executor: CommandExecutor = getDefaultCommandExecutor(),
  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<ToolResponse> {
  log('info', `Starting test run for scheme ${params.scheme} on platform macOS (internal)`);

  try {
    // Create temporary directory for xcresult bundle
    const tempDir = await fileSystemExecutor.mkdtemp(
      join(fileSystemExecutor.tmpdir(), 'xcodebuild-test-'),
    );
    const resultBundlePath = join(tempDir, 'TestResults.xcresult');

    // Add resultBundlePath to extraArgs
    const extraArgs = [...(params.extraArgs ?? []), `-resultBundlePath`, resultBundlePath];

    // Prepare execution options with TEST_RUNNER_ environment variables
    const execOpts: CommandExecOptions | undefined = params.testRunnerEnv
      ? { env: normalizeTestRunnerEnv(params.testRunnerEnv) }
      : undefined;

    // Run the test command
    const testResult = await executeXcodeBuildCommand(
      {
        projectPath: params.projectPath,
        workspacePath: params.workspacePath,
        scheme: params.scheme,
        configuration: params.configuration ?? 'Debug',
        derivedDataPath: params.derivedDataPath,
        extraArgs,
      },
      {
        platform: XcodePlatform.macOS,
        logPrefix: 'Test Run',
      },
      params.preferXcodebuild ?? false,
      'test',
      executor,
      execOpts,
    );

    // Parse xcresult bundle if it exists, regardless of whether tests passed or failed
    // Test failures are expected and should not prevent xcresult parsing
    try {
      log('info', `Attempting to parse xcresult bundle at: ${resultBundlePath}`);

      // Check if the file exists
      try {
        await fileSystemExecutor.stat(resultBundlePath);
        log('info', `xcresult bundle exists at: ${resultBundlePath}`);
      } catch {
        log('warn', `xcresult bundle does not exist at: ${resultBundlePath}`);
        throw new Error(`xcresult bundle not found at ${resultBundlePath}`);
      }

      const testSummary = await parseXcresultBundle(resultBundlePath, executor);
      log('info', 'Successfully parsed xcresult bundle');

      // Clean up temporary directory
      await fileSystemExecutor.rm(tempDir, { recursive: true, force: true });

      // Return combined result - preserve isError from testResult (test failures should be marked as errors)
      return {
        content: [
          ...(testResult.content ?? []),
          {
            type: 'text',
            text: '\nTest Results Summary:\n' + testSummary,
          },
        ],
        isError: testResult.isError,
      };
    } catch (parseError) {
      // If parsing fails, return original test result
      log('warn', `Failed to parse xcresult bundle: ${parseError}`);

      // Clean up temporary directory even if parsing fails
      try {
        await fileSystemExecutor.rm(tempDir, { recursive: true, force: true });
      } catch (cleanupError) {
        log('warn', `Failed to clean up temporary directory: ${cleanupError}`);
      }

      return testResult;
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error during test run: ${errorMessage}`);
    return createTextResponse(`Error during test run: ${errorMessage}`, true);
  }
}

export default {
  name: 'test_macos',
  description: 'Runs tests for a macOS target.',
  schema: publicSchemaObject.shape,
  handler: createSessionAwareTool<TestMacosParams>({
    internalSchema: testMacosSchema as unknown as z.ZodType<TestMacosParams>,
    logicFunction: (params, executor) =>
      testMacosLogic(params, executor, getDefaultFileSystemExecutor()),
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
    ],
    exclusivePairs: [['projectPath', 'workspacePath']],
  }),
};

```

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

```typescript
/**
 * Tests for test_device plugin
 * Following CLAUDE.md testing standards with literal validation
 * Using pure dependency injection for deterministic testing
 * NO VITEST MOCKING ALLOWED - Only createMockExecutor and manual stubs
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import {
  createMockExecutor,
  createMockFileSystemExecutor,
} from '../../../../test-utils/mock-executors.ts';
import testDevice, { testDeviceLogic } from '../test_device.ts';
import { sessionStore } from '../../../../utils/session-store.ts';

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

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

    it('should have correct description', () => {
      expect(testDevice.description).toBe('Runs tests on a physical Apple device.');
    });

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

    it('should expose only session-free fields in public schema', () => {
      const schema = z.object(testDevice.schema).strict();
      expect(
        schema.safeParse({
          derivedDataPath: '/path/to/derived-data',
          extraArgs: ['--arg1'],
          preferXcodebuild: true,
          platform: 'iOS',
          testRunnerEnv: { FOO: 'bar' },
        }).success,
      ).toBe(true);
      expect(schema.safeParse({}).success).toBe(true);
      expect(schema.safeParse({ projectPath: '/path/to/project.xcodeproj' }).success).toBe(false);

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

    it('should validate XOR between projectPath and workspacePath', async () => {
      // This would be validated at the schema level via createTypedTool
      // We test the schema validation through successful logic calls instead
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          title: 'Test Schema',
          result: 'SUCCESS',
          totalTestCount: 1,
          passedTests: 1,
          failedTests: 0,
          skippedTests: 0,
          expectedFailures: 0,
        }),
      });

      // Valid: project path only
      const projectResult = await testDeviceLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          deviceId: 'test-device-123',
        },
        mockExecutor,
        createMockFileSystemExecutor({
          mkdtemp: async () => '/tmp/xcodebuild-test-123',
          tmpdir: () => '/tmp',
          stat: async () => ({ isFile: () => true }),
          rm: async () => {},
        }),
      );
      expect(projectResult.isError).toBeFalsy();

      // Valid: workspace path only
      const workspaceResult = await testDeviceLogic(
        {
          workspacePath: '/path/to/workspace.xcworkspace',
          scheme: 'MyScheme',
          deviceId: 'test-device-123',
        },
        mockExecutor,
        createMockFileSystemExecutor({
          mkdtemp: async () => '/tmp/xcodebuild-test-456',
          tmpdir: () => '/tmp',
          stat: async () => ({ isFile: () => true }),
          rm: async () => {},
        }),
      );
      expect(workspaceResult.isError).toBeFalsy();
    });
  });

  describe('Handler Requirements', () => {
    it('should require scheme and device defaults', async () => {
      const result = await testDevice.handler({});

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Missing required session defaults');
      expect(result.content[0].text).toContain('Provide scheme and deviceId');
    });

    it('should require project or workspace when defaults provide scheme and device', async () => {
      sessionStore.setDefaults({ scheme: 'MyScheme', deviceId: 'test-device-123' });

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

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

    it('should reject mutually exclusive project inputs when defaults satisfy requirements', async () => {
      sessionStore.setDefaults({ scheme: 'MyScheme', deviceId: 'test-device-123' });

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

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

  describe('Handler Behavior (Complete Literal Returns)', () => {
    beforeEach(() => {
      // Clean setup for standard testing pattern
    });

    it('should return successful test response with parsed results', async () => {
      // Mock xcresulttool output
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          title: 'MyScheme Tests',
          result: 'SUCCESS',
          totalTestCount: 5,
          passedTests: 5,
          failedTests: 0,
          skippedTests: 0,
          expectedFailures: 0,
        }),
      });

      const result = await testDeviceLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          deviceId: 'test-device-123',
          configuration: 'Debug',
          preferXcodebuild: false,
          platform: 'iOS',
        },
        mockExecutor,
        createMockFileSystemExecutor({
          mkdtemp: async () => '/tmp/xcodebuild-test-123456',
          tmpdir: () => '/tmp',
          stat: async () => ({ isFile: () => true }),
          rm: async () => {},
        }),
      );

      expect(result.content).toHaveLength(2);
      expect(result.content[0].text).toContain('✅');
      expect(result.content[1].text).toContain('Test Results Summary:');
      expect(result.content[1].text).toContain('MyScheme Tests');
    });

    it('should handle test failure scenarios', async () => {
      // Mock xcresulttool output for failed tests
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          title: 'MyScheme Tests',
          result: 'FAILURE',
          totalTestCount: 5,
          passedTests: 3,
          failedTests: 2,
          skippedTests: 0,
          expectedFailures: 0,
          testFailures: [
            {
              testName: 'testExample',
              targetName: 'MyTarget',
              failureText: 'Expected true but was false',
            },
          ],
        }),
      });

      const result = await testDeviceLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          deviceId: 'test-device-123',
          configuration: 'Debug',
          preferXcodebuild: false,
          platform: 'iOS',
        },
        mockExecutor,
        createMockFileSystemExecutor({
          mkdtemp: async () => '/tmp/xcodebuild-test-123456',
          tmpdir: () => '/tmp',
          stat: async () => ({ isFile: () => true }),
          rm: async () => {},
        }),
      );

      expect(result.content).toHaveLength(2);
      expect(result.content[1].text).toContain('Test Failures:');
      expect(result.content[1].text).toContain('testExample');
    });

    it('should handle xcresult parsing failures gracefully', async () => {
      // Create a multi-call mock that handles different commands
      let callCount = 0;
      const mockExecutor = async (args: string[], description: string) => {
        callCount++;

        // First call is for xcodebuild test (successful)
        if (callCount === 1) {
          return { success: true, output: 'BUILD SUCCEEDED' };
        }

        // Second call is for xcresulttool (fails)
        return { success: false, error: 'xcresulttool failed' };
      };

      const result = await testDeviceLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          deviceId: 'test-device-123',
          configuration: 'Debug',
          preferXcodebuild: false,
          platform: 'iOS',
        },
        mockExecutor,
        createMockFileSystemExecutor({
          mkdtemp: async () => '/tmp/xcodebuild-test-123456',
          tmpdir: () => '/tmp',
          stat: async () => {
            throw new Error('File not found');
          },
          rm: async () => {},
        }),
      );

      // When xcresult parsing fails, it falls back to original test result only
      expect(result.content).toHaveLength(1);
      expect(result.content[0].text).toContain('✅');
    });

    it('should support different platforms', async () => {
      // Mock xcresulttool output
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          title: 'WatchApp Tests',
          result: 'SUCCESS',
          totalTestCount: 3,
          passedTests: 3,
          failedTests: 0,
          skippedTests: 0,
          expectedFailures: 0,
        }),
      });

      const result = await testDeviceLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'WatchApp',
          deviceId: 'watch-device-456',
          configuration: 'Debug',
          preferXcodebuild: false,
          platform: 'watchOS',
        },
        mockExecutor,
        createMockFileSystemExecutor({
          mkdtemp: async () => '/tmp/xcodebuild-test-123456',
          tmpdir: () => '/tmp',
          stat: async () => ({ isFile: () => true }),
          rm: async () => {},
        }),
      );

      expect(result.content).toHaveLength(2);
      expect(result.content[1].text).toContain('WatchApp Tests');
    });

    it('should handle optional parameters', async () => {
      // Mock xcresulttool output
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          title: 'Tests',
          result: 'SUCCESS',
          totalTestCount: 1,
          passedTests: 1,
          failedTests: 0,
          skippedTests: 0,
          expectedFailures: 0,
        }),
      });

      const result = await testDeviceLogic(
        {
          projectPath: '/path/to/project.xcodeproj',
          scheme: 'MyScheme',
          deviceId: 'test-device-123',
          configuration: 'Release',
          derivedDataPath: '/tmp/derived-data',
          extraArgs: ['--verbose'],
          preferXcodebuild: false,
          platform: 'iOS',
        },
        mockExecutor,
        createMockFileSystemExecutor({
          mkdtemp: async () => '/tmp/xcodebuild-test-123456',
          tmpdir: () => '/tmp',
          stat: async () => ({ isFile: () => true }),
          rm: async () => {},
        }),
      );

      expect(result.content).toHaveLength(2);
      expect(result.content[0].text).toContain('✅');
    });

    it('should handle workspace testing successfully', async () => {
      // Mock xcresulttool output
      const mockExecutor = createMockExecutor({
        success: true,
        output: JSON.stringify({
          title: 'WorkspaceScheme Tests',
          result: 'SUCCESS',
          totalTestCount: 10,
          passedTests: 10,
          failedTests: 0,
          skippedTests: 0,
          expectedFailures: 0,
        }),
      });

      const result = await testDeviceLogic(
        {
          workspacePath: '/path/to/workspace.xcworkspace',
          scheme: 'WorkspaceScheme',
          deviceId: 'test-device-456',
          configuration: 'Debug',
          preferXcodebuild: false,
          platform: 'iOS',
        },
        mockExecutor,
        createMockFileSystemExecutor({
          mkdtemp: async () => '/tmp/xcodebuild-test-workspace-123',
          tmpdir: () => '/tmp',
          stat: async () => ({ isFile: () => true }),
          rm: async () => {},
        }),
      );

      expect(result.content).toHaveLength(2);
      expect(result.content[0].text).toContain('✅');
      expect(result.content[1].text).toContain('Test Results Summary:');
      expect(result.content[1].text).toContain('WorkspaceScheme Tests');
    });
  });
});

```

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

```typescript
/**
 * Tests for swift_package_run plugin
 * Following CLAUDE.md testing standards with literal validation
 * Integration tests using dependency injection for deterministic testing
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts';
import swiftPackageRun, { swift_package_runLogic } from '../swift_package_run.ts';

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

    it('should have correct description', () => {
      expect(swiftPackageRun.description).toBe(
        'Runs an executable target from a Swift Package with swift run',
      );
    });

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

    it('should validate schema correctly', () => {
      // Test packagePath (required string)
      expect(swiftPackageRun.schema.packagePath.safeParse('valid/path').success).toBe(true);
      expect(swiftPackageRun.schema.packagePath.safeParse(null).success).toBe(false);

      // Test executableName (optional string)
      expect(swiftPackageRun.schema.executableName.safeParse('MyExecutable').success).toBe(true);
      expect(swiftPackageRun.schema.executableName.safeParse(undefined).success).toBe(true);
      expect(swiftPackageRun.schema.executableName.safeParse(123).success).toBe(false);

      // Test arguments (optional array of strings)
      expect(swiftPackageRun.schema.arguments.safeParse(['arg1', 'arg2']).success).toBe(true);
      expect(swiftPackageRun.schema.arguments.safeParse(undefined).success).toBe(true);
      expect(swiftPackageRun.schema.arguments.safeParse(['arg1', 123]).success).toBe(false);

      // Test configuration (optional enum)
      expect(swiftPackageRun.schema.configuration.safeParse('debug').success).toBe(true);
      expect(swiftPackageRun.schema.configuration.safeParse('release').success).toBe(true);
      expect(swiftPackageRun.schema.configuration.safeParse(undefined).success).toBe(true);
      expect(swiftPackageRun.schema.configuration.safeParse('invalid').success).toBe(false);

      // Test timeout (optional number)
      expect(swiftPackageRun.schema.timeout.safeParse(30).success).toBe(true);
      expect(swiftPackageRun.schema.timeout.safeParse(undefined).success).toBe(true);
      expect(swiftPackageRun.schema.timeout.safeParse('30').success).toBe(false);

      // Test background (optional boolean)
      expect(swiftPackageRun.schema.background.safeParse(true).success).toBe(true);
      expect(swiftPackageRun.schema.background.safeParse(false).success).toBe(true);
      expect(swiftPackageRun.schema.background.safeParse(undefined).success).toBe(true);
      expect(swiftPackageRun.schema.background.safeParse('true').success).toBe(false);

      // Test parseAsLibrary (optional boolean)
      expect(swiftPackageRun.schema.parseAsLibrary.safeParse(true).success).toBe(true);
      expect(swiftPackageRun.schema.parseAsLibrary.safeParse(false).success).toBe(true);
      expect(swiftPackageRun.schema.parseAsLibrary.safeParse(undefined).success).toBe(true);
      expect(swiftPackageRun.schema.parseAsLibrary.safeParse('true').success).toBe(false);
    });
  });

  let executorCalls: any[] = [];

  beforeEach(() => {
    executorCalls = [];
  });

  describe('Command Generation Testing', () => {
    it('should build correct command for basic run (foreground mode)', async () => {
      const mockExecutor = (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: any,
      ) => {
        executorCalls.push({ command, logPrefix, useShell, env });
        return Promise.resolve({
          success: true,
          output: 'Process completed',
          error: undefined,
          process: { pid: 12345 },
        });
      };

      await swift_package_runLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(executorCalls[0]).toEqual({
        command: ['swift', 'run', '--package-path', '/test/package'],
        logPrefix: 'Swift Package Run',
        useShell: true,
        env: undefined,
      });
    });

    it('should build correct command with release configuration', async () => {
      const mockExecutor = (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: any,
      ) => {
        executorCalls.push({ command, logPrefix, useShell, env });
        return Promise.resolve({
          success: true,
          output: 'Process completed',
          error: undefined,
          process: { pid: 12345 },
        });
      };

      await swift_package_runLogic(
        {
          packagePath: '/test/package',
          configuration: 'release',
        },
        mockExecutor,
      );

      expect(executorCalls[0]).toEqual({
        command: ['swift', 'run', '--package-path', '/test/package', '-c', 'release'],
        logPrefix: 'Swift Package Run',
        useShell: true,
        env: undefined,
      });
    });

    it('should build correct command with executable name', async () => {
      const mockExecutor = (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: any,
      ) => {
        executorCalls.push({ command, logPrefix, useShell, env });
        return Promise.resolve({
          success: true,
          output: 'Process completed',
          error: undefined,
          process: { pid: 12345 },
        });
      };

      await swift_package_runLogic(
        {
          packagePath: '/test/package',
          executableName: 'MyApp',
        },
        mockExecutor,
      );

      expect(executorCalls[0]).toEqual({
        command: ['swift', 'run', '--package-path', '/test/package', 'MyApp'],
        logPrefix: 'Swift Package Run',
        useShell: true,
        env: undefined,
      });
    });

    it('should build correct command with arguments', async () => {
      const mockExecutor = (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: any,
      ) => {
        executorCalls.push({ command, logPrefix, useShell, env });
        return Promise.resolve({
          success: true,
          output: 'Process completed',
          error: undefined,
          process: { pid: 12345 },
        });
      };

      await swift_package_runLogic(
        {
          packagePath: '/test/package',
          arguments: ['arg1', 'arg2'],
        },
        mockExecutor,
      );

      expect(executorCalls[0]).toEqual({
        command: ['swift', 'run', '--package-path', '/test/package', '--', 'arg1', 'arg2'],
        logPrefix: 'Swift Package Run',
        useShell: true,
        env: undefined,
      });
    });

    it('should build correct command with parseAsLibrary flag', async () => {
      const mockExecutor = (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: any,
      ) => {
        executorCalls.push({ command, logPrefix, useShell, env });
        return Promise.resolve({
          success: true,
          output: 'Process completed',
          error: undefined,
          process: { pid: 12345 },
        });
      };

      await swift_package_runLogic(
        {
          packagePath: '/test/package',
          parseAsLibrary: true,
        },
        mockExecutor,
      );

      expect(executorCalls[0]).toEqual({
        command: [
          'swift',
          'run',
          '--package-path',
          '/test/package',
          '-Xswiftc',
          '-parse-as-library',
        ],
        logPrefix: 'Swift Package Run',
        useShell: true,
        env: undefined,
      });
    });

    it('should build correct command with all parameters', async () => {
      const mockExecutor = (
        command: string[],
        logPrefix?: string,
        useShell?: boolean,
        env?: any,
      ) => {
        executorCalls.push({ command, logPrefix, useShell, env });
        return Promise.resolve({
          success: true,
          output: 'Process completed',
          error: undefined,
          process: { pid: 12345 },
        });
      };

      await swift_package_runLogic(
        {
          packagePath: '/test/package',
          executableName: 'MyApp',
          configuration: 'release',
          arguments: ['arg1'],
          parseAsLibrary: true,
        },
        mockExecutor,
      );

      expect(executorCalls[0]).toEqual({
        command: [
          'swift',
          'run',
          '--package-path',
          '/test/package',
          '-c',
          'release',
          '-Xswiftc',
          '-parse-as-library',
          'MyApp',
          '--',
          'arg1',
        ],
        logPrefix: 'Swift Package Run',
        useShell: true,
        env: undefined,
      });
    });

    it('should not call executor for background mode', async () => {
      // For background mode, no executor should be called since it uses direct spawn
      const mockExecutor = createNoopExecutor();

      const result = await swift_package_runLogic(
        {
          packagePath: '/test/package',
          background: true,
        },
        mockExecutor,
      );

      // Should return success without calling executor
      expect(result.content[0].text).toContain('🚀 Started executable in background');
    });
  });

  describe('Response Logic Testing', () => {
    it('should return validation error for missing packagePath', async () => {
      // Since the tool now uses createTypedTool, Zod validation happens at the handler level
      // Test the handler directly to see Zod validation
      const result = await swiftPackageRun.handler({});

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\npackagePath: Required',
          },
        ],
        isError: true,
      });
    });

    it('should return success response for background mode', async () => {
      const mockExecutor = createNoopExecutor();
      const result = await swift_package_runLogic(
        {
          packagePath: '/test/package',
          background: true,
        },
        mockExecutor,
      );

      expect(result.content[0].text).toContain('🚀 Started executable in background');
      expect(result.content[0].text).toContain('💡 Process is running independently');
    });

    it('should return success response for successful execution', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Hello, World!',
      });

      const result = await swift_package_runLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          { type: 'text', text: '✅ Swift executable completed successfully.' },
          { type: 'text', text: '💡 Process finished cleanly. Check output for results.' },
          { type: 'text', text: 'Hello, World!' },
        ],
      });
    });

    it('should return error response for failed execution', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Compilation failed',
      });

      const result = await swift_package_runLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          { type: 'text', text: '❌ Swift executable failed.' },
          { type: 'text', text: '(no output)' },
          { type: 'text', text: 'Errors:\nCompilation failed' },
        ],
      });
    });

    it('should handle executor error', async () => {
      const mockExecutor = createMockExecutor(new Error('Command not found'));

      const result = await swift_package_runLogic(
        {
          packagePath: '/test/package',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Error: Failed to execute swift run\nDetails: Command not found',
          },
        ],
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/docs/TOOLS.md:
--------------------------------------------------------------------------------

```markdown
# XcodeBuildMCP Tools Reference

XcodeBuildMCP provides 61 tools organized into 12 workflow groups for comprehensive Apple development workflows.

## Workflow Groups

### Dynamic Tool Discovery (`discovery`)
**Purpose**: Intelligent discovery and recommendation of appropriate development workflows based on project structure and requirements (1 tools)

- `discover_tools` - Analyzes a natural language task description and enables the most relevant development workflow. Prioritizes project/workspace workflows (simulator/device/macOS) and also supports task-based workflows (simulator-management, logging) and Swift packages.
### iOS Device Development (`device`)
**Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting physical devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Build, test, deploy, and debug apps on real hardware. (7 tools)

- `build_device` - Builds an app from a project or workspace for a physical Apple device. Provide exactly one of projectPath or workspacePath. Example: build_device({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })
- `get_device_app_path` - Gets the app bundle path for a physical device application (iOS, watchOS, tvOS, visionOS) using either a project or workspace. Provide exactly one of projectPath or workspacePath. Example: get_device_app_path({ projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme' })
- `install_app_device` - Installs an app on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Requires deviceId and appPath.
- `launch_app_device` - Launches an app on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Requires deviceId and bundleId.
- `list_devices` - 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.
- `stop_app_device` - Stops an app running on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Requires deviceId and processId.
- `test_device` - Runs tests for an Apple project or workspace on a physical device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) using xcodebuild test and parses xcresult output. Provide exactly one of projectPath or workspacePath.
### iOS Simulator Development (`simulator`)
**Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. Build, test, deploy, and interact with iOS apps on simulators. (12 tools)

- `boot_sim` - Boots an iOS simulator. After booting, use open_sim() to make the simulator visible.
- `build_run_sim` - Builds and runs an app from a project or workspace on a specific simulator by UUID or name. Provide exactly one of projectPath or workspacePath, and exactly one of simulatorId or simulatorName.
- `build_sim` - Builds an app from a project or workspace for a specific simulator by UUID or name. Provide exactly one of projectPath or workspacePath, and exactly one of simulatorId or simulatorName.
- `get_sim_app_path` - Gets the app bundle path for a simulator by UUID or name using either a project or workspace file.
- `install_app_sim` - Installs an app in an iOS simulator.
- `launch_app_logs_sim` - Launches an app in an iOS simulator and captures its logs.
- `launch_app_sim` - Launches an app in an iOS simulator by UUID or name. If simulator window isn't visible, use open_sim() first. or launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
- `list_sims` - Lists available iOS simulators with their UUIDs.
- `open_sim` - Opens the iOS Simulator app.
- `record_sim_video` - Starts or stops video capture for an iOS simulator using AXe. Provide exactly one of start=true or stop=true. On stop, outputFile is required. fps defaults to 30.
- `stop_app_sim` - Stops an app running in an iOS simulator by UUID or name. or stop_app_sim({ simulatorName: "iPhone 16", bundleId: "com.example.MyApp" })
- `test_sim` - Runs tests on a simulator by UUID or name using xcodebuild test and parses xcresult output. Works with both Xcode projects (.xcodeproj) and workspaces (.xcworkspace).
### Log Capture & Management (`logging`)
**Purpose**: Log capture and management tools for iOS simulators and physical devices. Start, stop, and analyze application and system logs during development and testing. (4 tools)

- `start_device_log_cap` - Starts capturing logs from a specified Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) by launching the app with console output. Returns a session ID.
- `start_sim_log_cap` - Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.
- `stop_device_log_cap` - Stops an active Apple device log capture session and returns the captured logs.
- `stop_sim_log_cap` - Stops an active simulator log capture session and returns the captured logs.
### macOS Development (`macos`)
**Purpose**: Complete macOS development workflow for both .xcodeproj and .xcworkspace files. Build, test, deploy, and manage macOS applications. (6 tools)

- `build_macos` - Builds a macOS app using xcodebuild from a project or workspace. Provide exactly one of projectPath or workspacePath. Example: build_macos({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })
- `build_run_macos` - Builds and runs a macOS app from a project or workspace in one step. Provide exactly one of projectPath or workspacePath. Example: build_run_macos({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })
- `get_mac_app_path` - Gets the app bundle path for a macOS application using either a project or workspace. Provide exactly one of projectPath or workspacePath. Example: get_mac_app_path({ projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme' })
- `launch_mac_app` - Launches a macOS application. Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app.
- `stop_mac_app` - Stops a running macOS application. Can stop by app name or process ID.
- `test_macos` - Runs tests for a macOS project or workspace using xcodebuild test and parses xcresult output. Provide exactly one of projectPath or workspacePath.
### Project Discovery (`project-discovery`)
**Purpose**: Discover and examine Xcode projects, workspaces, and Swift packages. Analyze project structure, schemes, build settings, and bundle information. (5 tools)

- `discover_projs` - Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.
- `get_app_bundle_id` - Extracts the bundle identifier from an app bundle (.app) for any Apple platform (iOS, iPadOS, watchOS, tvOS, visionOS).
- `get_mac_bundle_id` - Extracts the bundle identifier from a macOS app bundle (.app). Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id.
- `list_schemes` - Lists available schemes for either a project or a workspace. Provide exactly one of projectPath or workspacePath. Example: list_schemes({ projectPath: '/path/to/MyProject.xcodeproj' })
- `show_build_settings` - Shows build settings from either a project or workspace using xcodebuild. Provide exactly one of projectPath or workspacePath, plus scheme. Example: show_build_settings({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })
### Project Scaffolding (`project-scaffolding`)
**Purpose**: Tools for creating new iOS and macOS projects from templates. Bootstrap new applications with best practices, standard configurations, and modern project structures. (2 tools)

- `scaffold_ios_project` - Scaffold a new iOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper iOS configuration.
- `scaffold_macos_project` - Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration.
### Project Utilities (`utilities`)
**Purpose**: Essential project maintenance utilities for cleaning and managing existing projects. Provides clean operations for both .xcodeproj and .xcworkspace files. (1 tools)

- `clean` - Cleans build products for either a project or a workspace using xcodebuild. Provide exactly one of projectPath or workspacePath. Platform defaults to iOS if not specified. Example: clean({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', platform: 'iOS' })
### Simulator Management (`simulator-management`)
**Purpose**: Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators, erasing simulator content and settings, and setting simulator environment options like location, network, statusbar and appearance. (5 tools)

- `erase_sims` - Erases simulator content and settings. Provide exactly one of: simulatorUdid or all=true. Optional: shutdownFirst to shut down before erasing.
- `reset_sim_location` - Resets the simulator's location to default.
- `set_sim_appearance` - Sets the appearance mode (dark/light) of an iOS simulator.
- `set_sim_location` - Sets a custom GPS location for the simulator.
- `sim_statusbar` - Sets the data network indicator in the iOS simulator status bar. Use "clear" to reset all overrides, or specify a network type (hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc).
### Swift Package Manager (`swift-package`)
**Purpose**: Swift Package Manager operations for building, testing, running, and managing Swift packages and dependencies. Complete SPM workflow support. (6 tools)

- `swift_package_build` - Builds a Swift Package with swift build
- `swift_package_clean` - Cleans Swift Package build artifacts and derived data
- `swift_package_list` - Lists currently running Swift Package processes
- `swift_package_run` - Runs an executable target from a Swift Package with swift run
- `swift_package_stop` - Stops a running Swift Package executable started with swift_package_run
- `swift_package_test` - Runs tests for a Swift Package with swift test
### System Doctor (`doctor`)
**Purpose**: Debug tools and system doctor for troubleshooting XcodeBuildMCP server, development environment, and tool availability. (1 tools)

- `doctor` - Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.
### UI Testing & Automation (`ui-testing`)
**Purpose**: UI automation and accessibility testing tools for iOS simulators. Perform gestures, interactions, screenshots, and UI analysis for automated testing workflows. (11 tools)

- `button` - Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri
- `describe_ui` - Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation.
- `gesture` - Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge
- `key_press` - Press a single key by keycode on the simulator. Common keycodes: 40=Return, 42=Backspace, 43=Tab, 44=Space, 58-67=F1-F10.
- `key_sequence` - Press key sequence using HID keycodes on iOS simulator with configurable delay
- `long_press` - Long press at specific coordinates for given duration (ms). Use describe_ui for precise coordinates (don't guess from screenshots).
- `screenshot` - Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).
- `swipe` - Swipe from one point to another. Use describe_ui for precise coordinates (don't guess from screenshots). Supports configurable timing.
- `tap` - Tap at specific coordinates. Use describe_ui to get precise element coordinates (don't guess from screenshots). Supports optional timing delays.
- `touch` - Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots).
- `type_text` - Type text (supports US keyboard characters). Use describe_ui to find text field, tap to focus, then type.

## Summary Statistics

- **Total Tools**: 61 canonical tools + 22 re-exports = 83 total
- **Workflow Groups**: 12

---

*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2025-09-22*

```

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

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

import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import {
  createMockExecutor,
  createMockFileSystemExecutor,
  createNoopExecutor,
} from '../../../../test-utils/mock-executors.ts';
import gesturePlugin, { gestureLogic } from '../gesture.ts';

describe('Gesture Plugin', () => {
  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(gesturePlugin.name).toBe('gesture');
    });

    it('should have correct description', () => {
      expect(gesturePlugin.description).toBe(
        'Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge',
      );
    });

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

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

      // Valid case
      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
        }).success,
      ).toBe(true);

      // Invalid simulatorUuid
      expect(
        schema.safeParse({
          simulatorUuid: 'invalid-uuid',
          preset: 'scroll-up',
        }).success,
      ).toBe(false);

      // Invalid preset
      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'invalid-preset',
        }).success,
      ).toBe(false);

      // Valid optional parameters
      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
          screenWidth: 375,
          screenHeight: 667,
          duration: 1.5,
          delta: 100,
          preDelay: 0.5,
          postDelay: 0.2,
        }).success,
      ).toBe(true);

      // Invalid optional parameters
      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
          screenWidth: 0,
        }).success,
      ).toBe(false);

      expect(
        schema.safeParse({
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
          duration: -1,
        }).success,
      ).toBe(false);
    });
  });

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

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
      };

      await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'gesture',
        'scroll-up',
        '--udid',
        '12345678-1234-1234-1234-123456789012',
      ]);
    });

    it('should generate correct axe command for gesture with screen dimensions', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'gesture completed',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
      };

      await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'swipe-from-left-edge',
          screenWidth: 375,
          screenHeight: 667,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'gesture',
        'swipe-from-left-edge',
        '--screen-width',
        '375',
        '--screen-height',
        '667',
        '--udid',
        '12345678-1234-1234-1234-123456789012',
      ]);
    });

    it('should generate correct axe command for gesture with all parameters', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'gesture completed',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
      };

      await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-down',
          screenWidth: 414,
          screenHeight: 896,
          duration: 2.0,
          delta: 150,
          preDelay: 0.5,
          postDelay: 0.3,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'gesture',
        'scroll-down',
        '--screen-width',
        '414',
        '--screen-height',
        '896',
        '--duration',
        '2',
        '--delta',
        '150',
        '--pre-delay',
        '0.5',
        '--post-delay',
        '0.3',
        '--udid',
        '12345678-1234-1234-1234-123456789012',
      ]);
    });

    it('should generate correct axe command with different gesture presets', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'gesture completed',
          error: undefined,
          process: { pid: 12345 },
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
      };

      await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'swipe-from-bottom-edge',
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'gesture',
        'swipe-from-bottom-edge',
        '--udid',
        '12345678-1234-1234-1234-123456789012',
      ]);
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    // Note: Parameter validation is now handled by Zod schema validation in createTypedTool,
    // so invalid parameters never reach gestureLogic. The schema validation tests above
    // cover parameter validation scenarios.

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

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
      };

      const result = await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: "Gesture 'scroll-up' executed successfully." }],
        isError: false,
      });
    });

    it('should return success for gesture execution with all optional parameters', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'gesture completed',
        error: undefined,
        process: { pid: 12345 },
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
      };

      const result = await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'swipe-from-left-edge',
          screenWidth: 375,
          screenHeight: 667,
          duration: 1.0,
          delta: 50,
          preDelay: 0.1,
          postDelay: 0.2,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [{ type: 'text', text: "Gesture 'swipe-from-left-edge' executed successfully." }],
        isError: false,
      });
    });

    it('should handle DependencyError when axe is not available', async () => {
      const mockAxeHelpers = {
        getAxePath: () => null,
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text',
              text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
            },
          ],
          isError: true,
        }),
      };

      const result = await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
        },
        createNoopExecutor(),
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.',
          },
        ],
        isError: true,
      });
    });

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

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
      };

      const result = await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: "Error: Failed to execute gesture 'scroll-up': axe command 'gesture' failed.\nDetails: axe command failed",
          },
        ],
        isError: true,
      });
    });

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

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
      };

      const result = await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
        },
        mockExecutor,
        mockAxeHelpers,
      );

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

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

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
      };

      const result = await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
        },
        mockExecutor,
        mockAxeHelpers,
      );

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

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

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
      };

      const result = await gestureLogic(
        {
          simulatorUuid: '12345678-1234-1234-1234-123456789012',
          preset: 'scroll-up',
        },
        mockExecutor,
        mockAxeHelpers,
      );

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

```
Page 6/11FirstPrevNextLast