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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/build-plugins/plugin-discovery.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { readdirSync, readFileSync, existsSync } from 'fs';
  2 | import { join } from 'path';
  3 | import path from 'path';
  4 | 
  5 | export function createPluginDiscoveryPlugin() {
  6 |   return {
  7 |     name: 'plugin-discovery',
  8 |     setup(build) {
  9 |       // Generate the workflow loaders file before build starts
 10 |       build.onStart(async () => {
 11 |         try {
 12 |           await generateWorkflowLoaders();
 13 |           await generateResourceLoaders();
 14 |         } catch (error) {
 15 |           console.error('Failed to generate loaders:', error);
 16 |           throw error;
 17 |         }
 18 |       });
 19 |     }
 20 |   };
 21 | }
 22 | 
 23 | async function generateWorkflowLoaders() {
 24 |   const pluginsDir = path.resolve(process.cwd(), 'src/mcp/tools');
 25 |   
 26 |   if (!existsSync(pluginsDir)) {
 27 |     throw new Error(`Plugins directory not found: ${pluginsDir}`);
 28 |   }
 29 | 
 30 |   // Scan for workflow directories
 31 |   const workflowDirs = readdirSync(pluginsDir, { withFileTypes: true })
 32 |     .filter(dirent => dirent.isDirectory())
 33 |     .map(dirent => dirent.name);
 34 | 
 35 |   const workflowLoaders = {};
 36 |   const workflowMetadata = {};
 37 | 
 38 |   for (const dirName of workflowDirs) {
 39 |     const dirPath = join(pluginsDir, dirName);
 40 |     const indexPath = join(dirPath, 'index.ts');
 41 |     
 42 |     // Check if workflow has index.ts file
 43 |     if (!existsSync(indexPath)) {
 44 |       console.warn(`Skipping ${dirName}: no index.ts file found`);
 45 |       continue;
 46 |     }
 47 | 
 48 |     // Try to extract workflow metadata from index.ts
 49 |     try {
 50 |       const indexContent = readFileSync(indexPath, 'utf8');
 51 |       const metadata = extractWorkflowMetadata(indexContent);
 52 |       
 53 |       if (metadata) {
 54 |         // Find all tool files in this workflow directory
 55 |         const toolFiles = readdirSync(dirPath, { withFileTypes: true })
 56 |           .filter(dirent => dirent.isFile())
 57 |           .map(dirent => dirent.name)
 58 |           .filter(name => 
 59 |             (name.endsWith('.ts') || name.endsWith('.js')) && 
 60 |             name !== 'index.ts' && 
 61 |             name !== 'index.js' &&
 62 |             !name.endsWith('.test.ts') &&
 63 |             !name.endsWith('.test.js') &&
 64 |             name !== 'active-processes.ts' // Special exclusion for swift-package
 65 |           );
 66 | 
 67 |         // Generate dynamic loader function that loads workflow and all its tools
 68 |         workflowLoaders[dirName] = generateWorkflowLoader(dirName, toolFiles);
 69 |         workflowMetadata[dirName] = metadata;
 70 |         
 71 |         console.log(`✅ Discovered workflow: ${dirName} - ${metadata.name} (${toolFiles.length} tools)`);
 72 |       } else {
 73 |         console.warn(`⚠️  Skipping ${dirName}: invalid workflow metadata`);
 74 |       }
 75 |     } catch (error) {
 76 |       console.warn(`⚠️  Error processing ${dirName}:`, error);
 77 |     }
 78 |   }
 79 | 
 80 |   // Generate the content for generated-plugins.ts
 81 |   const generatedContent = generatePluginsFileContent(workflowLoaders, workflowMetadata);
 82 |   
 83 |   // Write to the generated file
 84 |   const outputPath = path.resolve(process.cwd(), 'src/core/generated-plugins.ts');
 85 |   
 86 |   const fs = await import('fs');
 87 |   await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
 88 |   
 89 |   console.log(`🔧 Generated workflow loaders for ${Object.keys(workflowLoaders).length} workflows`);
 90 | }
 91 | 
 92 | function generateWorkflowLoader(workflowName, toolFiles) {
 93 |   const toolImports = toolFiles.map((file, index) => {
 94 |     const toolName = file.replace(/\.(ts|js)$/, '');
 95 |     return `const tool_${index} = await import('../mcp/tools/${workflowName}/${toolName}.js').then(m => m.default)`;
 96 |   }).join(';\n    ');
 97 | 
 98 |   const toolExports = toolFiles.map((file, index) => {
 99 |     const toolName = file.replace(/\.(ts|js)$/, '');
100 |     return `'${toolName}': tool_${index}`;
101 |   }).join(',\n      ');
102 | 
103 |   return `async () => {
104 |     const { workflow } = await import('../mcp/tools/${workflowName}/index.js');
105 |     ${toolImports ? toolImports + ';\n    ' : ''}
106 |     return {
107 |       workflow,
108 |       ${toolExports ? toolExports : ''}
109 |     };
110 |   }`;
111 | }
112 | 
113 | function extractWorkflowMetadata(content) {
114 |   try {
115 |     // Simple regex to extract workflow export object
116 |     const workflowMatch = content.match(/export\s+const\s+workflow\s*=\s*({[\s\S]*?});/);
117 |     
118 |     if (!workflowMatch) {
119 |       return null;
120 |     }
121 | 
122 |     const workflowObj = workflowMatch[1];
123 |     
124 |     // Extract name
125 |     const nameMatch = workflowObj.match(/name\s*:\s*['"`]([^'"`]+)['"`]/);
126 |     if (!nameMatch) return null;
127 |     
128 |     // Extract description
129 |     const descMatch = workflowObj.match(/description\s*:\s*['"`]([\s\S]*?)['"`]/);
130 |     if (!descMatch) return null;
131 | 
132 |     // Extract platforms (optional)
133 |     const platformsMatch = workflowObj.match(/platforms\s*:\s*\[([^\]]*)\]/);
134 |     let platforms;
135 |     if (platformsMatch) {
136 |       platforms = platformsMatch[1]
137 |         .split(',')
138 |         .map(p => p.trim().replace(/['"]/g, ''))
139 |         .filter(p => p.length > 0);
140 |     }
141 | 
142 |     // Extract targets (optional)
143 |     const targetsMatch = workflowObj.match(/targets\s*:\s*\[([^\]]*)\]/);
144 |     let targets;
145 |     if (targetsMatch) {
146 |       targets = targetsMatch[1]
147 |         .split(',')
148 |         .map(t => t.trim().replace(/['"]/g, ''))
149 |         .filter(t => t.length > 0);
150 |     }
151 | 
152 |     // Extract projectTypes (optional)
153 |     const projectTypesMatch = workflowObj.match(/projectTypes\s*:\s*\[([^\]]*)\]/);
154 |     let projectTypes;
155 |     if (projectTypesMatch) {
156 |       projectTypes = projectTypesMatch[1]
157 |         .split(',')
158 |         .map(pt => pt.trim().replace(/['"]/g, ''))
159 |         .filter(pt => pt.length > 0);
160 |     }
161 | 
162 |     // Extract capabilities (optional)
163 |     const capabilitiesMatch = workflowObj.match(/capabilities\s*:\s*\[([^\]]*)\]/);
164 |     let capabilities;
165 |     if (capabilitiesMatch) {
166 |       capabilities = capabilitiesMatch[1]
167 |         .split(',')
168 |         .map(c => c.trim().replace(/['"]/g, ''))
169 |         .filter(c => c.length > 0);
170 |     }
171 | 
172 |     const result = {
173 |       name: nameMatch[1],
174 |       description: descMatch[1]
175 |     };
176 | 
177 |     if (platforms) result.platforms = platforms;
178 |     if (targets) result.targets = targets;
179 |     if (projectTypes) result.projectTypes = projectTypes;
180 |     if (capabilities) result.capabilities = capabilities;
181 | 
182 |     return result;
183 |   } catch (error) {
184 |     console.warn('Failed to extract workflow metadata:', error);
185 |     return null;
186 |   }
187 | }
188 | 
189 | function generatePluginsFileContent(workflowLoaders, workflowMetadata) {
190 |   const loaderEntries = Object.entries(workflowLoaders)
191 |     .map(([key, loader]) => {
192 |       // Indent the loader function properly
193 |       const indentedLoader = loader
194 |         .split('\n')
195 |         .map((line, index) => index === 0 ? `  '${key}': ${line}` : `  ${line}`)
196 |         .join('\n');
197 |       return indentedLoader;
198 |     })
199 |     .join(',\n');
200 | 
201 |   const metadataEntries = Object.entries(workflowMetadata)
202 |     .map(([key, metadata]) => {
203 |       const metadataJson = JSON.stringify(metadata, null, 4)
204 |         .split('\n')
205 |         .map(line => `    ${line}`)
206 |         .join('\n');
207 |       return `  '${key}': ${metadataJson.trim()}`;
208 |     })
209 |     .join(',\n');
210 | 
211 |   return `// AUTO-GENERATED - DO NOT EDIT
212 | // This file is generated by the plugin discovery esbuild plugin
213 | 
214 | // Generated based on filesystem scan
215 | export const WORKFLOW_LOADERS = {
216 | ${loaderEntries}
217 | };
218 | 
219 | export type WorkflowName = keyof typeof WORKFLOW_LOADERS;
220 | 
221 | // Optional: Export workflow metadata for quick access
222 | export const WORKFLOW_METADATA = {
223 | ${metadataEntries}
224 | };
225 | `;
226 | }
227 | 
228 | async function generateResourceLoaders() {
229 |   const resourcesDir = path.resolve(process.cwd(), 'src/mcp/resources');
230 |   
231 |   if (!existsSync(resourcesDir)) {
232 |     console.log('Resources directory not found, skipping resource generation');
233 |     return;
234 |   }
235 | 
236 |   // Scan for resource files
237 |   const resourceFiles = readdirSync(resourcesDir, { withFileTypes: true })
238 |     .filter(dirent => dirent.isFile())
239 |     .map(dirent => dirent.name)
240 |     .filter(name => 
241 |       (name.endsWith('.ts') || name.endsWith('.js')) && 
242 |       !name.endsWith('.test.ts') &&
243 |       !name.endsWith('.test.js') &&
244 |       !name.startsWith('__') // Exclude test directories
245 |     );
246 | 
247 |   const resourceLoaders = {};
248 | 
249 |   for (const fileName of resourceFiles) {
250 |     const resourceName = fileName.replace(/\.(ts|js)$/, '');
251 |     
252 |     // Generate dynamic loader for this resource
253 |     resourceLoaders[resourceName] = `async () => {
254 |     const module = await import('../mcp/resources/${resourceName}.js');
255 |     return module.default;
256 |   }`;
257 |     
258 |     console.log(`✅ Discovered resource: ${resourceName}`);
259 |   }
260 | 
261 |   // Generate the content for generated-resources.ts
262 |   const generatedContent = generateResourcesFileContent(resourceLoaders);
263 |   
264 |   // Write to the generated file
265 |   const outputPath = path.resolve(process.cwd(), 'src/core/generated-resources.ts');
266 |   
267 |   const fs = await import('fs');
268 |   await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
269 |   
270 |   console.log(`🔧 Generated resource loaders for ${Object.keys(resourceLoaders).length} resources`);
271 | }
272 | 
273 | function generateResourcesFileContent(resourceLoaders) {
274 |   const loaderEntries = Object.entries(resourceLoaders)
275 |     .map(([key, loader]) => `  '${key}': ${loader}`)
276 |     .join(',\n');
277 | 
278 |   return `// AUTO-GENERATED - DO NOT EDIT
279 | // This file is generated by the plugin discovery esbuild plugin
280 | 
281 | export const RESOURCE_LOADERS = {
282 | ${loaderEntries}
283 | };
284 | 
285 | export type ResourceName = keyof typeof RESOURCE_LOADERS;
286 | `;
287 | }
```

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

```typescript
  1 | /**
  2 |  * Tests for start_sim_log_cap plugin
  3 |  */
  4 | import { describe, it, expect, beforeEach } from 'vitest';
  5 | import { z } from 'zod';
  6 | import plugin, { start_sim_log_capLogic } from '../start_sim_log_cap.ts';
  7 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
  8 | 
  9 | describe('start_sim_log_cap plugin', () => {
 10 |   // Reset any test state if needed
 11 | 
 12 |   describe('Export Field Validation (Literal)', () => {
 13 |     it('should export an object with required properties', () => {
 14 |       expect(plugin).toHaveProperty('name');
 15 |       expect(plugin).toHaveProperty('description');
 16 |       expect(plugin).toHaveProperty('schema');
 17 |       expect(plugin).toHaveProperty('handler');
 18 |     });
 19 | 
 20 |     it('should have correct tool name', () => {
 21 |       expect(plugin.name).toBe('start_sim_log_cap');
 22 |     });
 23 | 
 24 |     it('should have correct description', () => {
 25 |       expect(plugin.description).toBe(
 26 |         'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.',
 27 |       );
 28 |     });
 29 | 
 30 |     it('should have handler as a function', () => {
 31 |       expect(typeof plugin.handler).toBe('function');
 32 |     });
 33 | 
 34 |     it('should validate schema with valid parameters', () => {
 35 |       const schema = z.object(plugin.schema);
 36 |       expect(
 37 |         schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: 'com.example.app' }).success,
 38 |       ).toBe(true);
 39 |       expect(
 40 |         schema.safeParse({
 41 |           simulatorUuid: 'test-uuid',
 42 |           bundleId: 'com.example.app',
 43 |           captureConsole: true,
 44 |         }).success,
 45 |       ).toBe(true);
 46 |       expect(
 47 |         schema.safeParse({
 48 |           simulatorUuid: 'test-uuid',
 49 |           bundleId: 'com.example.app',
 50 |           captureConsole: false,
 51 |         }).success,
 52 |       ).toBe(true);
 53 |     });
 54 | 
 55 |     it('should reject invalid schema parameters', () => {
 56 |       const schema = z.object(plugin.schema);
 57 |       expect(schema.safeParse({ simulatorUuid: null, bundleId: 'com.example.app' }).success).toBe(
 58 |         false,
 59 |       );
 60 |       expect(
 61 |         schema.safeParse({ simulatorUuid: undefined, bundleId: 'com.example.app' }).success,
 62 |       ).toBe(false);
 63 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: null }).success).toBe(false);
 64 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: undefined }).success).toBe(
 65 |         false,
 66 |       );
 67 |       expect(
 68 |         schema.safeParse({
 69 |           simulatorUuid: 'test-uuid',
 70 |           bundleId: 'com.example.app',
 71 |           captureConsole: 'yes',
 72 |         }).success,
 73 |       ).toBe(false);
 74 |       expect(
 75 |         schema.safeParse({
 76 |           simulatorUuid: 'test-uuid',
 77 |           bundleId: 'com.example.app',
 78 |           captureConsole: 123,
 79 |         }).success,
 80 |       ).toBe(false);
 81 |     });
 82 |   });
 83 | 
 84 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 85 |     // Note: Parameter validation is now handled by createTypedTool wrapper
 86 |     // Invalid parameters will not reach the logic function, so we test valid scenarios
 87 | 
 88 |     it('should return error when log capture fails', async () => {
 89 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
 90 |       const logCaptureStub = (params: any, executor: any) => {
 91 |         return Promise.resolve({
 92 |           sessionId: '',
 93 |           logFilePath: '',
 94 |           processes: [],
 95 |           error: 'Permission denied',
 96 |         });
 97 |       };
 98 | 
 99 |       const result = await start_sim_log_capLogic(
100 |         {
101 |           simulatorUuid: 'test-uuid',
102 |           bundleId: 'com.example.app',
103 |         },
104 |         mockExecutor,
105 |         logCaptureStub,
106 |       );
107 | 
108 |       expect(result.isError).toBe(true);
109 |       expect(result.content[0].text).toBe('Error starting log capture: Permission denied');
110 |     });
111 | 
112 |     it('should return success with session ID when log capture starts successfully', async () => {
113 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
114 |       const logCaptureStub = (params: any, executor: any) => {
115 |         return Promise.resolve({
116 |           sessionId: 'test-uuid-123',
117 |           logFilePath: '/tmp/test.log',
118 |           processes: [],
119 |           error: undefined,
120 |         });
121 |       };
122 | 
123 |       const result = await start_sim_log_capLogic(
124 |         {
125 |           simulatorUuid: 'test-uuid',
126 |           bundleId: 'com.example.app',
127 |         },
128 |         mockExecutor,
129 |         logCaptureStub,
130 |       );
131 | 
132 |       expect(result.isError).toBeUndefined();
133 |       expect(result.content[0].text).toBe(
134 |         "Log capture started successfully. Session ID: test-uuid-123.\n\nNote: Only structured logs are being captured.\n\nNext Steps:\n1.  Interact with your simulator and app.\n2.  Use 'stop_sim_log_cap' with session ID 'test-uuid-123' to stop capture and retrieve logs.",
135 |       );
136 |     });
137 | 
138 |     it('should indicate console capture when captureConsole is true', async () => {
139 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
140 |       const logCaptureStub = (params: any, executor: any) => {
141 |         return Promise.resolve({
142 |           sessionId: 'test-uuid-123',
143 |           logFilePath: '/tmp/test.log',
144 |           processes: [],
145 |           error: undefined,
146 |         });
147 |       };
148 | 
149 |       const result = await start_sim_log_capLogic(
150 |         {
151 |           simulatorUuid: 'test-uuid',
152 |           bundleId: 'com.example.app',
153 |           captureConsole: true,
154 |         },
155 |         mockExecutor,
156 |         logCaptureStub,
157 |       );
158 | 
159 |       expect(result.content[0].text).toBe(
160 |         "Log capture started successfully. Session ID: test-uuid-123.\n\nNote: Your app was relaunched to capture console output.\n\nNext Steps:\n1.  Interact with your simulator and app.\n2.  Use 'stop_sim_log_cap' with session ID 'test-uuid-123' to stop capture and retrieve logs.",
161 |       );
162 |     });
163 | 
164 |     it('should create correct spawn commands for console capture', async () => {
165 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
166 |       const spawnCalls: Array<{
167 |         command: string;
168 |         args: string[];
169 |       }> = [];
170 | 
171 |       const logCaptureStub = (params: any, executor: any) => {
172 |         if (params.captureConsole) {
173 |           // Record the console capture spawn call
174 |           spawnCalls.push({
175 |             command: 'xcrun',
176 |             args: [
177 |               'simctl',
178 |               'launch',
179 |               '--console-pty',
180 |               '--terminate-running-process',
181 |               params.simulatorUuid,
182 |               params.bundleId,
183 |             ],
184 |           });
185 |         }
186 |         // Record the structured log capture spawn call
187 |         spawnCalls.push({
188 |           command: 'xcrun',
189 |           args: [
190 |             'simctl',
191 |             'spawn',
192 |             params.simulatorUuid,
193 |             'log',
194 |             'stream',
195 |             '--level=debug',
196 |             '--predicate',
197 |             `subsystem == "${params.bundleId}"`,
198 |           ],
199 |         });
200 | 
201 |         return Promise.resolve({
202 |           sessionId: 'test-uuid-123',
203 |           logFilePath: '/tmp/test.log',
204 |           processes: [],
205 |           error: undefined,
206 |         });
207 |       };
208 | 
209 |       await start_sim_log_capLogic(
210 |         {
211 |           simulatorUuid: 'test-uuid',
212 |           bundleId: 'com.example.app',
213 |           captureConsole: true,
214 |         },
215 |         mockExecutor,
216 |         logCaptureStub,
217 |       );
218 | 
219 |       // Should spawn both console capture and structured log capture
220 |       expect(spawnCalls).toHaveLength(2);
221 |       expect(spawnCalls[0]).toEqual({
222 |         command: 'xcrun',
223 |         args: [
224 |           'simctl',
225 |           'launch',
226 |           '--console-pty',
227 |           '--terminate-running-process',
228 |           'test-uuid',
229 |           'com.example.app',
230 |         ],
231 |       });
232 |       expect(spawnCalls[1]).toEqual({
233 |         command: 'xcrun',
234 |         args: [
235 |           'simctl',
236 |           'spawn',
237 |           'test-uuid',
238 |           'log',
239 |           'stream',
240 |           '--level=debug',
241 |           '--predicate',
242 |           'subsystem == "com.example.app"',
243 |         ],
244 |       });
245 |     });
246 | 
247 |     it('should create correct spawn commands for structured logs only', async () => {
248 |       const mockExecutor = createMockExecutor({ success: true, output: '' });
249 |       const spawnCalls: Array<{
250 |         command: string;
251 |         args: string[];
252 |       }> = [];
253 | 
254 |       const logCaptureStub = (params: any, executor: any) => {
255 |         // Record the structured log capture spawn call only
256 |         spawnCalls.push({
257 |           command: 'xcrun',
258 |           args: [
259 |             'simctl',
260 |             'spawn',
261 |             params.simulatorUuid,
262 |             'log',
263 |             'stream',
264 |             '--level=debug',
265 |             '--predicate',
266 |             `subsystem == "${params.bundleId}"`,
267 |           ],
268 |         });
269 | 
270 |         return Promise.resolve({
271 |           sessionId: 'test-uuid-123',
272 |           logFilePath: '/tmp/test.log',
273 |           processes: [],
274 |           error: undefined,
275 |         });
276 |       };
277 | 
278 |       await start_sim_log_capLogic(
279 |         {
280 |           simulatorUuid: 'test-uuid',
281 |           bundleId: 'com.example.app',
282 |           captureConsole: false,
283 |         },
284 |         mockExecutor,
285 |         logCaptureStub,
286 |       );
287 | 
288 |       // Should only spawn structured log capture
289 |       expect(spawnCalls).toHaveLength(1);
290 |       expect(spawnCalls[0]).toEqual({
291 |         command: 'xcrun',
292 |         args: [
293 |           'simctl',
294 |           'spawn',
295 |           'test-uuid',
296 |           'log',
297 |           'stream',
298 |           '--level=debug',
299 |           '--predicate',
300 |           'subsystem == "com.example.app"',
301 |         ],
302 |       });
303 |     });
304 |   });
305 | });
306 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for sim_statusbar plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect } from 'vitest';
  8 | import { z } from 'zod';
  9 | import { createMockExecutor, type CommandExecutor } from '../../../../test-utils/mock-executors.ts';
 10 | import simStatusbar, { sim_statusbarLogic } from '../sim_statusbar.ts';
 11 | 
 12 | describe('sim_statusbar tool', () => {
 13 |   describe('Export Field Validation (Literal)', () => {
 14 |     it('should have correct name', () => {
 15 |       expect(simStatusbar.name).toBe('sim_statusbar');
 16 |     });
 17 | 
 18 |     it('should have correct description', () => {
 19 |       expect(simStatusbar.description).toBe(
 20 |         '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).',
 21 |       );
 22 |     });
 23 | 
 24 |     it('should have handler function', () => {
 25 |       expect(typeof simStatusbar.handler).toBe('function');
 26 |     });
 27 | 
 28 |     it('should have correct schema with simulatorUuid string field and dataNetwork enum field', () => {
 29 |       const schema = z.object(simStatusbar.schema);
 30 | 
 31 |       // Valid inputs
 32 |       expect(
 33 |         schema.safeParse({ simulatorUuid: 'test-uuid-123', dataNetwork: 'wifi' }).success,
 34 |       ).toBe(true);
 35 |       expect(schema.safeParse({ simulatorUuid: 'ABC123-DEF456', dataNetwork: '3g' }).success).toBe(
 36 |         true,
 37 |       );
 38 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '4g' }).success).toBe(
 39 |         true,
 40 |       );
 41 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'lte' }).success).toBe(
 42 |         true,
 43 |       );
 44 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'lte-a' }).success).toBe(
 45 |         true,
 46 |       );
 47 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'lte+' }).success).toBe(
 48 |         true,
 49 |       );
 50 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g' }).success).toBe(
 51 |         true,
 52 |       );
 53 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g+' }).success).toBe(
 54 |         true,
 55 |       );
 56 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g-uwb' }).success).toBe(
 57 |         true,
 58 |       );
 59 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g-uc' }).success).toBe(
 60 |         true,
 61 |       );
 62 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'hide' }).success).toBe(
 63 |         true,
 64 |       );
 65 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'clear' }).success).toBe(
 66 |         true,
 67 |       );
 68 | 
 69 |       // Invalid inputs
 70 |       expect(schema.safeParse({ simulatorUuid: 123, dataNetwork: 'wifi' }).success).toBe(false);
 71 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'invalid' }).success).toBe(
 72 |         false,
 73 |       );
 74 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 123 }).success).toBe(
 75 |         false,
 76 |       );
 77 |       expect(schema.safeParse({ simulatorUuid: null, dataNetwork: 'wifi' }).success).toBe(false);
 78 |       expect(schema.safeParse({ simulatorUuid: 'test-uuid' }).success).toBe(false);
 79 |       expect(schema.safeParse({ dataNetwork: 'wifi' }).success).toBe(false);
 80 |       expect(schema.safeParse({}).success).toBe(false);
 81 |     });
 82 |   });
 83 | 
 84 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 85 |     it('should handle successful status bar data network setting', async () => {
 86 |       const mockExecutor = createMockExecutor({
 87 |         success: true,
 88 |         output: 'Status bar set successfully',
 89 |       });
 90 | 
 91 |       const result = await sim_statusbarLogic(
 92 |         {
 93 |           simulatorUuid: 'test-uuid-123',
 94 |           dataNetwork: 'wifi',
 95 |         },
 96 |         mockExecutor,
 97 |       );
 98 | 
 99 |       expect(result).toEqual({
100 |         content: [
101 |           {
102 |             type: 'text',
103 |             text: 'Successfully set simulator test-uuid-123 status bar data network to wifi',
104 |           },
105 |         ],
106 |       });
107 |     });
108 | 
109 |     it('should handle minimal valid parameters (Zod handles validation)', async () => {
110 |       // Note: With createTypedTool, Zod validation happens before the logic function is called
111 |       // So we test with a valid minimal parameter set since validation is handled upstream
112 |       const mockExecutor = createMockExecutor({
113 |         success: true,
114 |         output: 'Status bar set successfully',
115 |       });
116 | 
117 |       const result = await sim_statusbarLogic(
118 |         {
119 |           simulatorUuid: 'test-uuid-123',
120 |           dataNetwork: 'wifi',
121 |         },
122 |         mockExecutor,
123 |       );
124 | 
125 |       // The logic function should execute normally with valid parameters
126 |       // Zod validation errors are handled by createTypedTool wrapper
127 |       expect(result.isError).toBe(undefined);
128 |       expect(result.content[0].text).toContain('Successfully set simulator');
129 |     });
130 | 
131 |     it('should handle command failure', async () => {
132 |       const mockExecutor = createMockExecutor({
133 |         success: false,
134 |         error: 'Simulator not found',
135 |       });
136 | 
137 |       const result = await sim_statusbarLogic(
138 |         {
139 |           simulatorUuid: 'invalid-uuid',
140 |           dataNetwork: '3g',
141 |         },
142 |         mockExecutor,
143 |       );
144 | 
145 |       expect(result).toEqual({
146 |         content: [
147 |           {
148 |             type: 'text',
149 |             text: 'Failed to set status bar: Simulator not found',
150 |           },
151 |         ],
152 |         isError: true,
153 |       });
154 |     });
155 | 
156 |     it('should handle exception with Error object', async () => {
157 |       const mockExecutor: CommandExecutor = async () => {
158 |         throw new Error('Connection failed');
159 |       };
160 | 
161 |       const result = await sim_statusbarLogic(
162 |         {
163 |           simulatorUuid: 'test-uuid-123',
164 |           dataNetwork: '4g',
165 |         },
166 |         mockExecutor,
167 |       );
168 | 
169 |       expect(result).toEqual({
170 |         content: [
171 |           {
172 |             type: 'text',
173 |             text: 'Failed to set status bar: Connection failed',
174 |           },
175 |         ],
176 |         isError: true,
177 |       });
178 |     });
179 | 
180 |     it('should handle exception with string error', async () => {
181 |       const mockExecutor: CommandExecutor = async () => {
182 |         throw 'String error';
183 |       };
184 | 
185 |       const result = await sim_statusbarLogic(
186 |         {
187 |           simulatorUuid: 'test-uuid-123',
188 |           dataNetwork: 'lte',
189 |         },
190 |         mockExecutor,
191 |       );
192 | 
193 |       expect(result).toEqual({
194 |         content: [
195 |           {
196 |             type: 'text',
197 |             text: 'Failed to set status bar: String error',
198 |           },
199 |         ],
200 |         isError: true,
201 |       });
202 |     });
203 | 
204 |     it('should verify command generation with mock executor for override', async () => {
205 |       const calls: Array<{
206 |         command: string[];
207 |         operationDescription: string;
208 |         keepAlive: boolean;
209 |         timeout: number | undefined;
210 |       }> = [];
211 | 
212 |       const mockExecutor: CommandExecutor = async (
213 |         command,
214 |         operationDescription,
215 |         keepAlive,
216 |         timeout,
217 |       ) => {
218 |         calls.push({ command, operationDescription, keepAlive, timeout });
219 |         return {
220 |           success: true,
221 |           output: 'Status bar set successfully',
222 |           error: undefined,
223 |           process: { pid: 12345 },
224 |         };
225 |       };
226 | 
227 |       await sim_statusbarLogic(
228 |         {
229 |           simulatorUuid: 'test-uuid-123',
230 |           dataNetwork: 'wifi',
231 |         },
232 |         mockExecutor,
233 |       );
234 | 
235 |       expect(calls).toHaveLength(1);
236 |       expect(calls[0]).toEqual({
237 |         command: [
238 |           'xcrun',
239 |           'simctl',
240 |           'status_bar',
241 |           'test-uuid-123',
242 |           'override',
243 |           '--dataNetwork',
244 |           'wifi',
245 |         ],
246 |         operationDescription: 'Set Status Bar',
247 |         keepAlive: true,
248 |         timeout: undefined,
249 |       });
250 |     });
251 | 
252 |     it('should verify command generation for clear operation', async () => {
253 |       const calls: Array<{
254 |         command: string[];
255 |         operationDescription: string;
256 |         keepAlive: boolean;
257 |         timeout: number | undefined;
258 |       }> = [];
259 | 
260 |       const mockExecutor: CommandExecutor = async (
261 |         command,
262 |         operationDescription,
263 |         keepAlive,
264 |         timeout,
265 |       ) => {
266 |         calls.push({ command, operationDescription, keepAlive, timeout });
267 |         return {
268 |           success: true,
269 |           output: 'Status bar cleared successfully',
270 |           error: undefined,
271 |           process: { pid: 12345 },
272 |         };
273 |       };
274 | 
275 |       await sim_statusbarLogic(
276 |         {
277 |           simulatorUuid: 'test-uuid-123',
278 |           dataNetwork: 'clear',
279 |         },
280 |         mockExecutor,
281 |       );
282 | 
283 |       expect(calls).toHaveLength(1);
284 |       expect(calls[0]).toEqual({
285 |         command: ['xcrun', 'simctl', 'status_bar', 'test-uuid-123', 'clear'],
286 |         operationDescription: 'Set Status Bar',
287 |         keepAlive: true,
288 |         timeout: undefined,
289 |       });
290 |     });
291 | 
292 |     it('should handle successful clear operation', async () => {
293 |       const mockExecutor = createMockExecutor({
294 |         success: true,
295 |         output: 'Status bar cleared successfully',
296 |       });
297 | 
298 |       const result = await sim_statusbarLogic(
299 |         {
300 |           simulatorUuid: 'test-uuid-123',
301 |           dataNetwork: 'clear',
302 |         },
303 |         mockExecutor,
304 |       );
305 | 
306 |       expect(result).toEqual({
307 |         content: [
308 |           {
309 |             type: 'text',
310 |             text: 'Successfully cleared status bar overrides for simulator test-uuid-123',
311 |           },
312 |         ],
313 |       });
314 |     });
315 |   });
316 | });
317 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for list_schemes plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import { z } from 'zod';
  9 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
 10 | import plugin, { listSchemesLogic } from '../list_schemes.ts';
 11 | import { sessionStore } from '../../../../utils/session-store.ts';
 12 | 
 13 | describe('list_schemes plugin', () => {
 14 |   beforeEach(() => {
 15 |     sessionStore.clear();
 16 |   });
 17 | 
 18 |   describe('Export Field Validation (Literal)', () => {
 19 |     it('should have correct name', () => {
 20 |       expect(plugin.name).toBe('list_schemes');
 21 |     });
 22 | 
 23 |     it('should have correct description', () => {
 24 |       expect(plugin.description).toBe('Lists schemes for a project or workspace.');
 25 |     });
 26 | 
 27 |     it('should have handler function', () => {
 28 |       expect(typeof plugin.handler).toBe('function');
 29 |     });
 30 | 
 31 |     it('should expose an empty public schema', () => {
 32 |       const schema = z.object(plugin.schema).strict();
 33 |       expect(schema.safeParse({}).success).toBe(true);
 34 |       expect(schema.safeParse({ projectPath: '/path/to/MyProject.xcodeproj' }).success).toBe(false);
 35 |       expect(Object.keys(plugin.schema)).toEqual([]);
 36 |     });
 37 |   });
 38 | 
 39 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 40 |     it('should return success with schemes found', async () => {
 41 |       const mockExecutor = createMockExecutor({
 42 |         success: true,
 43 |         output: `Information about project "MyProject":
 44 |     Targets:
 45 |         MyProject
 46 |         MyProjectTests
 47 | 
 48 |     Build Configurations:
 49 |         Debug
 50 |         Release
 51 | 
 52 |     Schemes:
 53 |         MyProject
 54 |         MyProjectTests`,
 55 |       });
 56 | 
 57 |       const result = await listSchemesLogic(
 58 |         { projectPath: '/path/to/MyProject.xcodeproj' },
 59 |         mockExecutor,
 60 |       );
 61 | 
 62 |       expect(result).toEqual({
 63 |         content: [
 64 |           {
 65 |             type: 'text',
 66 |             text: '✅ Available schemes:',
 67 |           },
 68 |           {
 69 |             type: 'text',
 70 |             text: 'MyProject\nMyProjectTests',
 71 |           },
 72 |           {
 73 |             type: 'text',
 74 |             text: `Next Steps:
 75 | 1. Build the app: build_macos({ projectPath: "/path/to/MyProject.xcodeproj", scheme: "MyProject" })
 76 |    or for iOS: build_sim({ projectPath: "/path/to/MyProject.xcodeproj", scheme: "MyProject", simulatorName: "iPhone 16" })
 77 | 2. Show build settings: show_build_settings({ projectPath: "/path/to/MyProject.xcodeproj", scheme: "MyProject" })`,
 78 |           },
 79 |         ],
 80 |         isError: false,
 81 |       });
 82 |     });
 83 | 
 84 |     it('should return error when command fails', async () => {
 85 |       const mockExecutor = createMockExecutor({
 86 |         success: false,
 87 |         error: 'Project not found',
 88 |       });
 89 | 
 90 |       const result = await listSchemesLogic(
 91 |         { projectPath: '/path/to/MyProject.xcodeproj' },
 92 |         mockExecutor,
 93 |       );
 94 | 
 95 |       expect(result).toEqual({
 96 |         content: [{ type: 'text', text: 'Failed to list schemes: Project not found' }],
 97 |         isError: true,
 98 |       });
 99 |     });
100 | 
101 |     it('should return error when no schemes found in output', async () => {
102 |       const mockExecutor = createMockExecutor({
103 |         success: true,
104 |         output: 'Information about project "MyProject":\n    Targets:\n        MyProject',
105 |       });
106 | 
107 |       const result = await listSchemesLogic(
108 |         { projectPath: '/path/to/MyProject.xcodeproj' },
109 |         mockExecutor,
110 |       );
111 | 
112 |       expect(result).toEqual({
113 |         content: [{ type: 'text', text: 'No schemes found in the output' }],
114 |         isError: true,
115 |       });
116 |     });
117 | 
118 |     it('should return success with empty schemes list', async () => {
119 |       const mockExecutor = createMockExecutor({
120 |         success: true,
121 |         output: `Information about project "MinimalProject":
122 |     Targets:
123 |         MinimalProject
124 | 
125 |     Build Configurations:
126 |         Debug
127 |         Release
128 | 
129 |     Schemes:
130 | 
131 | `,
132 |       });
133 | 
134 |       const result = await listSchemesLogic(
135 |         { projectPath: '/path/to/MyProject.xcodeproj' },
136 |         mockExecutor,
137 |       );
138 | 
139 |       expect(result).toEqual({
140 |         content: [
141 |           {
142 |             type: 'text',
143 |             text: '✅ Available schemes:',
144 |           },
145 |           {
146 |             type: 'text',
147 |             text: '',
148 |           },
149 |           {
150 |             type: 'text',
151 |             text: '',
152 |           },
153 |         ],
154 |         isError: false,
155 |       });
156 |     });
157 | 
158 |     it('should handle Error objects in catch blocks', async () => {
159 |       const mockExecutor = async () => {
160 |         throw new Error('Command execution failed');
161 |       };
162 | 
163 |       const result = await listSchemesLogic(
164 |         { projectPath: '/path/to/MyProject.xcodeproj' },
165 |         mockExecutor,
166 |       );
167 | 
168 |       expect(result).toEqual({
169 |         content: [{ type: 'text', text: 'Error listing schemes: Command execution failed' }],
170 |         isError: true,
171 |       });
172 |     });
173 | 
174 |     it('should handle string error objects in catch blocks', async () => {
175 |       const mockExecutor = async () => {
176 |         throw 'String error';
177 |       };
178 | 
179 |       const result = await listSchemesLogic(
180 |         { projectPath: '/path/to/MyProject.xcodeproj' },
181 |         mockExecutor,
182 |       );
183 | 
184 |       expect(result).toEqual({
185 |         content: [{ type: 'text', text: 'Error listing schemes: String error' }],
186 |         isError: true,
187 |       });
188 |     });
189 | 
190 |     it('should verify command generation with mock executor', async () => {
191 |       const calls: any[] = [];
192 |       const mockExecutor = async (
193 |         command: string[],
194 |         action: string,
195 |         showOutput: boolean,
196 |         workingDir?: string,
197 |       ) => {
198 |         calls.push([command, action, showOutput, workingDir]);
199 |         return {
200 |           success: true,
201 |           output: `Information about project "MyProject":
202 |     Targets:
203 |         MyProject
204 | 
205 |     Build Configurations:
206 |         Debug
207 |         Release
208 | 
209 |     Schemes:
210 |         MyProject`,
211 |           error: undefined,
212 |           process: { pid: 12345 },
213 |         };
214 |       };
215 | 
216 |       await listSchemesLogic({ projectPath: '/path/to/MyProject.xcodeproj' }, mockExecutor);
217 | 
218 |       expect(calls).toEqual([
219 |         [
220 |           ['xcodebuild', '-list', '-project', '/path/to/MyProject.xcodeproj'],
221 |           'List Schemes',
222 |           true,
223 |           undefined,
224 |         ],
225 |       ]);
226 |     });
227 | 
228 |     it('should handle validation when testing with missing projectPath via plugin handler', async () => {
229 |       // Note: Direct logic function calls bypass Zod validation, so we test the actual plugin handler
230 |       // to verify Zod validation works properly. The createTypedTool wrapper handles validation.
231 |       const result = await plugin.handler({});
232 |       expect(result.isError).toBe(true);
233 |       expect(result.content[0].text).toContain('Missing required session defaults');
234 |       expect(result.content[0].text).toContain('Provide a project or workspace');
235 |     });
236 |   });
237 | 
238 |   describe('XOR Validation', () => {
239 |     it('should error when neither projectPath nor workspacePath provided', async () => {
240 |       const result = await plugin.handler({});
241 |       expect(result.isError).toBe(true);
242 |       expect(result.content[0].text).toContain('Missing required session defaults');
243 |       expect(result.content[0].text).toContain('Provide a project or workspace');
244 |     });
245 | 
246 |     it('should error when both projectPath and workspacePath provided', async () => {
247 |       const result = await plugin.handler({
248 |         projectPath: '/path/to/project.xcodeproj',
249 |         workspacePath: '/path/to/workspace.xcworkspace',
250 |       });
251 |       expect(result.isError).toBe(true);
252 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
253 |     });
254 | 
255 |     it('should handle empty strings as undefined', async () => {
256 |       const result = await plugin.handler({
257 |         projectPath: '',
258 |         workspacePath: '',
259 |       });
260 |       expect(result.isError).toBe(true);
261 |       expect(result.content[0].text).toContain('Missing required session defaults');
262 |       expect(result.content[0].text).toContain('Provide a project or workspace');
263 |     });
264 |   });
265 | 
266 |   describe('Workspace Support', () => {
267 |     it('should list schemes for workspace', async () => {
268 |       const mockExecutor = createMockExecutor({
269 |         success: true,
270 |         output: `Information about workspace "MyWorkspace":
271 |     Schemes:
272 |         MyApp
273 |         MyAppTests`,
274 |       });
275 | 
276 |       const result = await listSchemesLogic(
277 |         { workspacePath: '/path/to/MyProject.xcworkspace' },
278 |         mockExecutor,
279 |       );
280 | 
281 |       expect(result).toEqual({
282 |         content: [
283 |           {
284 |             type: 'text',
285 |             text: '✅ Available schemes:',
286 |           },
287 |           {
288 |             type: 'text',
289 |             text: 'MyApp\nMyAppTests',
290 |           },
291 |           {
292 |             type: 'text',
293 |             text: `Next Steps:
294 | 1. Build the app: build_macos({ workspacePath: "/path/to/MyProject.xcworkspace", scheme: "MyApp" })
295 |    or for iOS: build_sim({ workspacePath: "/path/to/MyProject.xcworkspace", scheme: "MyApp", simulatorName: "iPhone 16" })
296 | 2. Show build settings: show_build_settings({ workspacePath: "/path/to/MyProject.xcworkspace", scheme: "MyApp" })`,
297 |           },
298 |         ],
299 |         isError: false,
300 |       });
301 |     });
302 | 
303 |     it('should generate correct workspace command', async () => {
304 |       const calls: any[] = [];
305 |       const mockExecutor = async (
306 |         command: string[],
307 |         action: string,
308 |         showOutput: boolean,
309 |         workingDir?: string,
310 |       ) => {
311 |         calls.push([command, action, showOutput, workingDir]);
312 |         return {
313 |           success: true,
314 |           output: `Information about workspace "MyWorkspace":
315 |     Schemes:
316 |         MyApp`,
317 |           error: undefined,
318 |           process: { pid: 12345 },
319 |         };
320 |       };
321 | 
322 |       await listSchemesLogic({ workspacePath: '/path/to/MyProject.xcworkspace' }, mockExecutor);
323 | 
324 |       expect(calls).toEqual([
325 |         [
326 |           ['xcodebuild', '-list', '-workspace', '/path/to/MyProject.xcworkspace'],
327 |           'List Schemes',
328 |           true,
329 |           undefined,
330 |         ],
331 |       ]);
332 |     });
333 |   });
334 | });
335 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach } from 'vitest';
  2 | import { z } from 'zod';
  3 | import {
  4 |   createMockExecutor,
  5 |   createMockFileSystemExecutor,
  6 | } from '../../../../test-utils/mock-executors.ts';
  7 | 
  8 | // Import the plugin and logic function
  9 | import listSims, { list_simsLogic } from '../list_sims.ts';
 10 | 
 11 | describe('list_sims tool', () => {
 12 |   let callHistory: Array<{
 13 |     command: string[];
 14 |     logPrefix?: string;
 15 |     useShell?: boolean;
 16 |     env?: Record<string, string>;
 17 |   }>;
 18 | 
 19 |   callHistory = [];
 20 | 
 21 |   describe('Export Field Validation (Literal)', () => {
 22 |     it('should have correct name', () => {
 23 |       expect(listSims.name).toBe('list_sims');
 24 |     });
 25 | 
 26 |     it('should have correct description', () => {
 27 |       expect(listSims.description).toBe('Lists available iOS simulators with their UUIDs. ');
 28 |     });
 29 | 
 30 |     it('should have handler function', () => {
 31 |       expect(typeof listSims.handler).toBe('function');
 32 |     });
 33 | 
 34 |     it('should have correct schema with enabled boolean field', () => {
 35 |       const schema = z.object(listSims.schema);
 36 | 
 37 |       // Valid inputs
 38 |       expect(schema.safeParse({ enabled: true }).success).toBe(true);
 39 |       expect(schema.safeParse({ enabled: false }).success).toBe(true);
 40 |       expect(schema.safeParse({ enabled: undefined }).success).toBe(true);
 41 |       expect(schema.safeParse({}).success).toBe(true);
 42 | 
 43 |       // Invalid inputs
 44 |       expect(schema.safeParse({ enabled: 'yes' }).success).toBe(false);
 45 |       expect(schema.safeParse({ enabled: 1 }).success).toBe(false);
 46 |       expect(schema.safeParse({ enabled: null }).success).toBe(false);
 47 |     });
 48 |   });
 49 | 
 50 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 51 |     it('should handle successful simulator listing', async () => {
 52 |       const mockJsonOutput = JSON.stringify({
 53 |         devices: {
 54 |           'iOS 17.0': [
 55 |             {
 56 |               name: 'iPhone 15',
 57 |               udid: 'test-uuid-123',
 58 |               isAvailable: true,
 59 |               state: 'Shutdown',
 60 |             },
 61 |           ],
 62 |         },
 63 |       });
 64 | 
 65 |       const mockTextOutput = `== Devices ==
 66 | -- iOS 17.0 --
 67 |     iPhone 15 (test-uuid-123) (Shutdown)`;
 68 | 
 69 |       // Create a mock executor that returns different outputs based on command
 70 |       const mockExecutor = async (
 71 |         command: string[],
 72 |         logPrefix?: string,
 73 |         useShell?: boolean,
 74 |         env?: Record<string, string>,
 75 |       ) => {
 76 |         callHistory.push({ command, logPrefix, useShell, env });
 77 | 
 78 |         // Return JSON output for JSON command
 79 |         if (command.includes('--json')) {
 80 |           return {
 81 |             success: true,
 82 |             output: mockJsonOutput,
 83 |             error: undefined,
 84 |             process: { pid: 12345 },
 85 |           };
 86 |         }
 87 | 
 88 |         // Return text output for text command
 89 |         return {
 90 |           success: true,
 91 |           output: mockTextOutput,
 92 |           error: undefined,
 93 |           process: { pid: 12345 },
 94 |         };
 95 |       };
 96 | 
 97 |       const result = await list_simsLogic({ enabled: true }, mockExecutor);
 98 | 
 99 |       // Verify both commands were called
100 |       expect(callHistory).toHaveLength(2);
101 |       expect(callHistory[0]).toEqual({
102 |         command: ['xcrun', 'simctl', 'list', 'devices', '--json'],
103 |         logPrefix: 'List Simulators (JSON)',
104 |         useShell: true,
105 |         env: undefined,
106 |       });
107 |       expect(callHistory[1]).toEqual({
108 |         command: ['xcrun', 'simctl', 'list', 'devices'],
109 |         logPrefix: 'List Simulators (Text)',
110 |         useShell: true,
111 |         env: undefined,
112 |       });
113 | 
114 |       expect(result).toEqual({
115 |         content: [
116 |           {
117 |             type: 'text',
118 |             text: `Available iOS Simulators:
119 | 
120 | iOS 17.0:
121 | - iPhone 15 (test-uuid-123)
122 | 
123 | Next Steps:
124 | 1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' })
125 | 2. Open the simulator UI: open_sim({})
126 | 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' })
127 | 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`,
128 |           },
129 |         ],
130 |       });
131 |     });
132 | 
133 |     it('should handle successful listing with booted simulator', async () => {
134 |       const mockJsonOutput = JSON.stringify({
135 |         devices: {
136 |           'iOS 17.0': [
137 |             {
138 |               name: 'iPhone 15',
139 |               udid: 'test-uuid-123',
140 |               isAvailable: true,
141 |               state: 'Booted',
142 |             },
143 |           ],
144 |         },
145 |       });
146 | 
147 |       const mockTextOutput = `== Devices ==
148 | -- iOS 17.0 --
149 |     iPhone 15 (test-uuid-123) (Booted)`;
150 | 
151 |       const mockExecutor = async (command: string[]) => {
152 |         if (command.includes('--json')) {
153 |           return {
154 |             success: true,
155 |             output: mockJsonOutput,
156 |             error: undefined,
157 |             process: { pid: 12345 },
158 |           };
159 |         }
160 |         return {
161 |           success: true,
162 |           output: mockTextOutput,
163 |           error: undefined,
164 |           process: { pid: 12345 },
165 |         };
166 |       };
167 | 
168 |       const result = await list_simsLogic({ enabled: true }, mockExecutor);
169 | 
170 |       expect(result).toEqual({
171 |         content: [
172 |           {
173 |             type: 'text',
174 |             text: `Available iOS Simulators:
175 | 
176 | iOS 17.0:
177 | - iPhone 15 (test-uuid-123) [Booted]
178 | 
179 | Next Steps:
180 | 1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' })
181 | 2. Open the simulator UI: open_sim({})
182 | 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' })
183 | 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`,
184 |           },
185 |         ],
186 |       });
187 |     });
188 | 
189 |     it('should merge devices from text that are missing from JSON', async () => {
190 |       const mockJsonOutput = JSON.stringify({
191 |         devices: {
192 |           'iOS 18.6': [
193 |             {
194 |               name: 'iPhone 15',
195 |               udid: 'json-uuid-123',
196 |               isAvailable: true,
197 |               state: 'Shutdown',
198 |             },
199 |           ],
200 |         },
201 |       });
202 | 
203 |       const mockTextOutput = `== Devices ==
204 | -- iOS 18.6 --
205 |     iPhone 15 (json-uuid-123) (Shutdown)
206 | -- iOS 26.0 --
207 |     iPhone 17 Pro (text-uuid-456) (Shutdown)`;
208 | 
209 |       const mockExecutor = async (command: string[]) => {
210 |         if (command.includes('--json')) {
211 |           return {
212 |             success: true,
213 |             output: mockJsonOutput,
214 |             error: undefined,
215 |             process: { pid: 12345 },
216 |           };
217 |         }
218 |         return {
219 |           success: true,
220 |           output: mockTextOutput,
221 |           error: undefined,
222 |           process: { pid: 12345 },
223 |         };
224 |       };
225 | 
226 |       const result = await list_simsLogic({ enabled: true }, mockExecutor);
227 | 
228 |       // Should contain both iOS 18.6 from JSON and iOS 26.0 from text
229 |       expect(result).toEqual({
230 |         content: [
231 |           {
232 |             type: 'text',
233 |             text: `Available iOS Simulators:
234 | 
235 | iOS 18.6:
236 | - iPhone 15 (json-uuid-123)
237 | 
238 | iOS 26.0:
239 | - iPhone 17 Pro (text-uuid-456)
240 | 
241 | Next Steps:
242 | 1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' })
243 | 2. Open the simulator UI: open_sim({})
244 | 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' })
245 | 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`,
246 |           },
247 |         ],
248 |       });
249 |     });
250 | 
251 |     it('should handle command failure', async () => {
252 |       const mockExecutor = createMockExecutor({
253 |         success: false,
254 |         output: '',
255 |         error: 'Command failed',
256 |         process: { pid: 12345 },
257 |       });
258 | 
259 |       const result = await list_simsLogic({ enabled: true }, mockExecutor);
260 | 
261 |       expect(result).toEqual({
262 |         content: [
263 |           {
264 |             type: 'text',
265 |             text: 'Failed to list simulators: Command failed',
266 |           },
267 |         ],
268 |       });
269 |     });
270 | 
271 |     it('should handle JSON parse failure and fall back to text parsing', async () => {
272 |       const mockTextOutput = `== Devices ==
273 | -- iOS 17.0 --
274 |     iPhone 15 (test-uuid-456) (Shutdown)`;
275 | 
276 |       const mockExecutor = async (command: string[]) => {
277 |         // JSON command returns invalid JSON
278 |         if (command.includes('--json')) {
279 |           return {
280 |             success: true,
281 |             output: 'invalid json',
282 |             error: undefined,
283 |             process: { pid: 12345 },
284 |           };
285 |         }
286 | 
287 |         // Text command returns valid text output
288 |         return {
289 |           success: true,
290 |           output: mockTextOutput,
291 |           error: undefined,
292 |           process: { pid: 12345 },
293 |         };
294 |       };
295 | 
296 |       const result = await list_simsLogic({ enabled: true }, mockExecutor);
297 | 
298 |       // Should fall back to text parsing and extract devices
299 |       expect(result).toEqual({
300 |         content: [
301 |           {
302 |             type: 'text',
303 |             text: `Available iOS Simulators:
304 | 
305 | iOS 17.0:
306 | - iPhone 15 (test-uuid-456)
307 | 
308 | Next Steps:
309 | 1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' })
310 | 2. Open the simulator UI: open_sim({})
311 | 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' })
312 | 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`,
313 |           },
314 |         ],
315 |       });
316 |     });
317 | 
318 |     it('should handle exception with Error object', async () => {
319 |       const mockExecutor = createMockExecutor(new Error('Command execution failed'));
320 | 
321 |       const result = await list_simsLogic({ enabled: true }, mockExecutor);
322 | 
323 |       expect(result).toEqual({
324 |         content: [
325 |           {
326 |             type: 'text',
327 |             text: 'Failed to list simulators: Command execution failed',
328 |           },
329 |         ],
330 |       });
331 |     });
332 | 
333 |     it('should handle exception with string error', async () => {
334 |       const mockExecutor = createMockExecutor('String error');
335 | 
336 |       const result = await list_simsLogic({ enabled: true }, mockExecutor);
337 | 
338 |       expect(result).toEqual({
339 |         content: [
340 |           {
341 |             type: 'text',
342 |             text: 'Failed to list simulators: String error',
343 |           },
344 |         ],
345 |       });
346 |     });
347 |   });
348 | });
349 | 
```

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

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

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

```typescript
  1 | /**
  2 |  * Tests for stop_device_log_cap plugin
  3 |  */
  4 | import { describe, it, expect, beforeEach } from 'vitest';
  5 | import { EventEmitter } from 'events';
  6 | import { z } from 'zod';
  7 | import plugin, { stop_device_log_capLogic } from '../stop_device_log_cap.ts';
  8 | import { activeDeviceLogSessions, type DeviceLogSession } from '../start_device_log_cap.ts';
  9 | import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts';
 10 | 
 11 | // Note: Logger is allowed to execute normally (integration testing pattern)
 12 | 
 13 | describe('stop_device_log_cap plugin', () => {
 14 |   beforeEach(() => {
 15 |     // Clear actual active sessions before each test
 16 |     activeDeviceLogSessions.clear();
 17 |   });
 18 | 
 19 |   describe('Plugin Structure', () => {
 20 |     it('should export an object with required properties', () => {
 21 |       expect(plugin).toHaveProperty('name');
 22 |       expect(plugin).toHaveProperty('description');
 23 |       expect(plugin).toHaveProperty('schema');
 24 |       expect(plugin).toHaveProperty('handler');
 25 |     });
 26 | 
 27 |     it('should have correct tool name', () => {
 28 |       expect(plugin.name).toBe('stop_device_log_cap');
 29 |     });
 30 | 
 31 |     it('should have correct description', () => {
 32 |       expect(plugin.description).toBe(
 33 |         'Stops an active Apple device log capture session and returns the captured logs.',
 34 |       );
 35 |     });
 36 | 
 37 |     it('should have correct schema structure', () => {
 38 |       // Schema should be a plain object for MCP protocol compliance
 39 |       expect(typeof plugin.schema).toBe('object');
 40 |       expect(plugin.schema).toHaveProperty('logSessionId');
 41 | 
 42 |       // Validate that schema fields are Zod types that can be used for validation
 43 |       const schema = z.object(plugin.schema);
 44 |       expect(schema.safeParse({ logSessionId: 'test-session-id' }).success).toBe(true);
 45 |       expect(schema.safeParse({ logSessionId: 123 }).success).toBe(false);
 46 |     });
 47 | 
 48 |     it('should have handler as a function', () => {
 49 |       expect(typeof plugin.handler).toBe('function');
 50 |     });
 51 |   });
 52 | 
 53 |   describe('Handler Functionality', () => {
 54 |     // Helper function to create a test process
 55 |     function createTestProcess(
 56 |       options: {
 57 |         killed?: boolean;
 58 |         exitCode?: number | null;
 59 |       } = {},
 60 |     ) {
 61 |       const emitter = new EventEmitter();
 62 |       const processState = {
 63 |         killed: options.killed ?? false,
 64 |         exitCode: options.exitCode ?? (options.killed ? 0 : null),
 65 |         killCalls: [] as string[],
 66 |         kill(signal?: string) {
 67 |           if (this.killed) {
 68 |             return false;
 69 |           }
 70 |           this.killCalls.push(signal ?? 'SIGTERM');
 71 |           this.killed = true;
 72 |           this.exitCode = 0;
 73 |           emitter.emit('close', 0);
 74 |           return true;
 75 |         },
 76 |       };
 77 | 
 78 |       const testProcess = Object.assign(emitter, processState);
 79 |       return testProcess as typeof testProcess;
 80 |     }
 81 | 
 82 |     it('should handle stop log capture when session not found', async () => {
 83 |       const mockFileSystem = createMockFileSystemExecutor();
 84 | 
 85 |       const result = await stop_device_log_capLogic(
 86 |         {
 87 |           logSessionId: 'device-log-00008110-001A2C3D4E5F-com.example.MyApp',
 88 |         },
 89 |         mockFileSystem,
 90 |       );
 91 | 
 92 |       expect(result.content[0].text).toBe(
 93 |         'Failed to stop device log capture session device-log-00008110-001A2C3D4E5F-com.example.MyApp: Device log capture session not found: device-log-00008110-001A2C3D4E5F-com.example.MyApp',
 94 |       );
 95 |       expect(result.isError).toBe(true);
 96 |     });
 97 | 
 98 |     it('should handle successful log capture stop', async () => {
 99 |       const testSessionId = 'test-session-123';
100 |       const testLogFilePath = '/tmp/xcodemcp_device_log_test-session-123.log';
101 |       const testLogContent = 'Device log content here...';
102 | 
103 |       // Test active session
104 |       const testProcess = createTestProcess({
105 |         killed: false,
106 |         exitCode: null,
107 |       });
108 | 
109 |       activeDeviceLogSessions.set(testSessionId, {
110 |         process: testProcess as unknown as DeviceLogSession['process'],
111 |         logFilePath: testLogFilePath,
112 |         deviceUuid: '00008110-001A2C3D4E5F',
113 |         bundleId: 'com.example.MyApp',
114 |         hasEnded: false,
115 |       });
116 | 
117 |       // Configure test file system for successful operation
118 |       const mockFileSystem = createMockFileSystemExecutor({
119 |         existsSync: () => true,
120 |         readFile: async () => testLogContent,
121 |       });
122 | 
123 |       const result = await stop_device_log_capLogic(
124 |         {
125 |           logSessionId: testSessionId,
126 |         },
127 |         mockFileSystem,
128 |       );
129 | 
130 |       expect(result).toEqual({
131 |         content: [
132 |           {
133 |             type: 'text',
134 |             text: `✅ Device log capture session stopped successfully\n\nSession ID: ${testSessionId}\n\n--- Captured Logs ---\n${testLogContent}`,
135 |           },
136 |         ],
137 |       });
138 |       expect(result.isError).toBeUndefined();
139 |       expect(testProcess.killCalls).toEqual(['SIGTERM']);
140 |       expect(activeDeviceLogSessions.has(testSessionId)).toBe(false);
141 |     });
142 | 
143 |     it('should handle already killed process', async () => {
144 |       const testSessionId = 'test-session-456';
145 |       const testLogFilePath = '/tmp/xcodemcp_device_log_test-session-456.log';
146 |       const testLogContent = 'Device log content...';
147 | 
148 |       // Test active session with already killed process
149 |       const testProcess = createTestProcess({
150 |         killed: true,
151 |         exitCode: 0,
152 |       });
153 | 
154 |       activeDeviceLogSessions.set(testSessionId, {
155 |         process: testProcess as unknown as DeviceLogSession['process'],
156 |         logFilePath: testLogFilePath,
157 |         deviceUuid: '00008110-001A2C3D4E5F',
158 |         bundleId: 'com.example.MyApp',
159 |         hasEnded: false,
160 |       });
161 | 
162 |       // Configure test file system for successful operation
163 |       const mockFileSystem = createMockFileSystemExecutor({
164 |         existsSync: () => true,
165 |         readFile: async () => testLogContent,
166 |       });
167 | 
168 |       const result = await stop_device_log_capLogic(
169 |         {
170 |           logSessionId: testSessionId,
171 |         },
172 |         mockFileSystem,
173 |       );
174 | 
175 |       expect(result).toEqual({
176 |         content: [
177 |           {
178 |             type: 'text',
179 |             text: `✅ Device log capture session stopped successfully\n\nSession ID: ${testSessionId}\n\n--- Captured Logs ---\n${testLogContent}`,
180 |           },
181 |         ],
182 |       });
183 |       expect(testProcess.killCalls).toEqual([]); // Should not kill already killed process
184 |     });
185 | 
186 |     it('should handle file access failure', async () => {
187 |       const testSessionId = 'test-session-789';
188 |       const testLogFilePath = '/tmp/xcodemcp_device_log_test-session-789.log';
189 | 
190 |       // Test active session
191 |       const testProcess = createTestProcess({
192 |         killed: false,
193 |         exitCode: null,
194 |       });
195 | 
196 |       activeDeviceLogSessions.set(testSessionId, {
197 |         process: testProcess as unknown as DeviceLogSession['process'],
198 |         logFilePath: testLogFilePath,
199 |         deviceUuid: '00008110-001A2C3D4E5F',
200 |         bundleId: 'com.example.MyApp',
201 |         hasEnded: false,
202 |       });
203 | 
204 |       // Configure test file system for access failure (file doesn't exist)
205 |       const mockFileSystem = createMockFileSystemExecutor({
206 |         existsSync: () => false,
207 |       });
208 | 
209 |       const result = await stop_device_log_capLogic(
210 |         {
211 |           logSessionId: testSessionId,
212 |         },
213 |         mockFileSystem,
214 |       );
215 | 
216 |       expect(result).toEqual({
217 |         content: [
218 |           {
219 |             type: 'text',
220 |             text: `Failed to stop device log capture session ${testSessionId}: Log file not found: ${testLogFilePath}`,
221 |           },
222 |         ],
223 |         isError: true,
224 |       });
225 |       expect(activeDeviceLogSessions.has(testSessionId)).toBe(false); // Session still removed
226 |     });
227 | 
228 |     it('should handle file read failure', async () => {
229 |       const testSessionId = 'test-session-abc';
230 |       const testLogFilePath = '/tmp/xcodemcp_device_log_test-session-abc.log';
231 | 
232 |       // Test active session
233 |       const testProcess = createTestProcess({
234 |         killed: false,
235 |         exitCode: null,
236 |       });
237 | 
238 |       activeDeviceLogSessions.set(testSessionId, {
239 |         process: testProcess as unknown as DeviceLogSession['process'],
240 |         logFilePath: testLogFilePath,
241 |         deviceUuid: '00008110-001A2C3D4E5F',
242 |         bundleId: 'com.example.MyApp',
243 |         hasEnded: false,
244 |       });
245 | 
246 |       // Configure test file system for successful access but failed read
247 |       const mockFileSystem = createMockFileSystemExecutor({
248 |         existsSync: () => true,
249 |         readFile: async () => {
250 |           throw new Error('Read permission denied');
251 |         },
252 |       });
253 | 
254 |       const result = await stop_device_log_capLogic(
255 |         {
256 |           logSessionId: testSessionId,
257 |         },
258 |         mockFileSystem,
259 |       );
260 | 
261 |       expect(result).toEqual({
262 |         content: [
263 |           {
264 |             type: 'text',
265 |             text: `Failed to stop device log capture session ${testSessionId}: Read permission denied`,
266 |           },
267 |         ],
268 |         isError: true,
269 |       });
270 |     });
271 | 
272 |     it('should handle string error objects', async () => {
273 |       const testSessionId = 'test-session-def';
274 |       const testLogFilePath = '/tmp/xcodemcp_device_log_test-session-def.log';
275 | 
276 |       // Test active session
277 |       const testProcess = createTestProcess({
278 |         killed: false,
279 |         exitCode: null,
280 |       });
281 | 
282 |       activeDeviceLogSessions.set(testSessionId, {
283 |         process: testProcess as unknown as DeviceLogSession['process'],
284 |         logFilePath: testLogFilePath,
285 |         deviceUuid: '00008110-001A2C3D4E5F',
286 |         bundleId: 'com.example.MyApp',
287 |         hasEnded: false,
288 |       });
289 | 
290 |       // Configure test file system for access failure with string error
291 |       const mockFileSystem = createMockFileSystemExecutor({
292 |         existsSync: () => true,
293 |         readFile: async () => {
294 |           throw 'String error message';
295 |         },
296 |       });
297 | 
298 |       const result = await stop_device_log_capLogic(
299 |         {
300 |           logSessionId: testSessionId,
301 |         },
302 |         mockFileSystem,
303 |       );
304 | 
305 |       expect(result).toEqual({
306 |         content: [
307 |           {
308 |             type: 'text',
309 |             text: `Failed to stop device log capture session ${testSessionId}: String error message`,
310 |           },
311 |         ],
312 |         isError: true,
313 |       });
314 |     });
315 |   });
316 | });
317 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/logging/stop_device_log_cap.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Logging Plugin: Stop Device Log Capture
  3 |  *
  4 |  * Stops an active Apple device log capture session and returns the captured logs.
  5 |  */
  6 | 
  7 | import * as fs from 'fs';
  8 | import { z } from 'zod';
  9 | import { log } from '../../../utils/logging/index.ts';
 10 | import { activeDeviceLogSessions, type DeviceLogSession } from './start_device_log_cap.ts';
 11 | import { ToolResponse } from '../../../types/common.ts';
 12 | import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.ts';
 13 | import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
 14 | import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
 15 | 
 16 | // Define schema as ZodObject
 17 | const stopDeviceLogCapSchema = z.object({
 18 |   logSessionId: z.string().describe('The session ID returned by start_device_log_cap.'),
 19 | });
 20 | 
 21 | // Use z.infer for type safety
 22 | type StopDeviceLogCapParams = z.infer<typeof stopDeviceLogCapSchema>;
 23 | 
 24 | /**
 25 |  * Business logic for stopping device log capture session
 26 |  */
 27 | export async function stop_device_log_capLogic(
 28 |   params: StopDeviceLogCapParams,
 29 |   fileSystemExecutor: FileSystemExecutor,
 30 | ): Promise<ToolResponse> {
 31 |   const { logSessionId } = params;
 32 | 
 33 |   const session = activeDeviceLogSessions.get(logSessionId);
 34 |   if (!session) {
 35 |     log('warning', `Device log session not found: ${logSessionId}`);
 36 |     return {
 37 |       content: [
 38 |         {
 39 |           type: 'text',
 40 |           text: `Failed to stop device log capture session ${logSessionId}: Device log capture session not found: ${logSessionId}`,
 41 |         },
 42 |       ],
 43 |       isError: true,
 44 |     };
 45 |   }
 46 | 
 47 |   try {
 48 |     log('info', `Attempting to stop device log capture session: ${logSessionId}`);
 49 | 
 50 |     const shouldSignalStop =
 51 |       !(session.hasEnded ?? false) &&
 52 |       session.process.killed !== true &&
 53 |       session.process.exitCode == null;
 54 | 
 55 |     if (shouldSignalStop) {
 56 |       session.process.kill?.('SIGTERM');
 57 |     }
 58 | 
 59 |     await waitForSessionToFinish(session);
 60 | 
 61 |     if (session.logStream) {
 62 |       await ensureStreamClosed(session.logStream);
 63 |     }
 64 | 
 65 |     const logFilePath = session.logFilePath;
 66 |     activeDeviceLogSessions.delete(logSessionId);
 67 | 
 68 |     // Check file access
 69 |     if (!fileSystemExecutor.existsSync(logFilePath)) {
 70 |       throw new Error(`Log file not found: ${logFilePath}`);
 71 |     }
 72 | 
 73 |     const fileContent = await fileSystemExecutor.readFile(logFilePath, 'utf-8');
 74 |     log('info', `Successfully read device log content from ${logFilePath}`);
 75 | 
 76 |     log(
 77 |       'info',
 78 |       `Device log capture session ${logSessionId} stopped. Log file retained at: ${logFilePath}`,
 79 |     );
 80 | 
 81 |     return {
 82 |       content: [
 83 |         {
 84 |           type: 'text',
 85 |           text: `✅ Device log capture session stopped successfully\n\nSession ID: ${logSessionId}\n\n--- Captured Logs ---\n${fileContent}`,
 86 |         },
 87 |       ],
 88 |     };
 89 |   } catch (error) {
 90 |     const message = error instanceof Error ? error.message : String(error);
 91 |     log('error', `Failed to stop device log capture session ${logSessionId}: ${message}`);
 92 |     return {
 93 |       content: [
 94 |         {
 95 |           type: 'text',
 96 |           text: `Failed to stop device log capture session ${logSessionId}: ${message}`,
 97 |         },
 98 |       ],
 99 |       isError: true,
100 |     };
101 |   }
102 | }
103 | 
104 | type WriteStreamWithClosed = fs.WriteStream & { closed?: boolean };
105 | 
106 | async function ensureStreamClosed(stream: fs.WriteStream): Promise<void> {
107 |   const typedStream = stream as WriteStreamWithClosed;
108 |   if (typedStream.destroyed || typedStream.closed) {
109 |     return;
110 |   }
111 | 
112 |   await new Promise<void>((resolve) => {
113 |     const onClose = (): void => resolve();
114 |     typedStream.once('close', onClose);
115 |     typedStream.end();
116 |   }).catch(() => {
117 |     // Ignore cleanup errors – best-effort close
118 |   });
119 | }
120 | 
121 | async function waitForSessionToFinish(session: DeviceLogSession): Promise<void> {
122 |   if (session.hasEnded) {
123 |     return;
124 |   }
125 | 
126 |   if (session.process.exitCode != null) {
127 |     session.hasEnded = true;
128 |     return;
129 |   }
130 | 
131 |   if (typeof session.process.once === 'function') {
132 |     await new Promise<void>((resolve) => {
133 |       const onClose = (): void => {
134 |         clearTimeout(timeout);
135 |         session.hasEnded = true;
136 |         resolve();
137 |       };
138 | 
139 |       const timeout = setTimeout(() => {
140 |         session.process.removeListener?.('close', onClose);
141 |         session.hasEnded = true;
142 |         resolve();
143 |       }, 1000);
144 | 
145 |       session.process.once('close', onClose);
146 | 
147 |       if (session.hasEnded || session.process.exitCode != null) {
148 |         session.process.removeListener?.('close', onClose);
149 |         onClose();
150 |       }
151 |     });
152 |     return;
153 |   }
154 | 
155 |   // Fallback polling for minimal mock processes (primarily in tests)
156 |   for (let i = 0; i < 20; i += 1) {
157 |     if (session.hasEnded || session.process.exitCode != null) {
158 |       session.hasEnded = true;
159 |       break;
160 |     }
161 |     await new Promise((resolve) => setTimeout(resolve, 50));
162 |   }
163 | }
164 | 
165 | /**
166 |  * Type guard to check if an object has fs-like promises interface
167 |  */
168 | function hasPromisesInterface(obj: unknown): obj is { promises: typeof fs.promises } {
169 |   return typeof obj === 'object' && obj !== null && 'promises' in obj;
170 | }
171 | 
172 | /**
173 |  * Type guard to check if an object has existsSync method
174 |  */
175 | function hasExistsSyncMethod(obj: unknown): obj is { existsSync: typeof fs.existsSync } {
176 |   return typeof obj === 'object' && obj !== null && 'existsSync' in obj;
177 | }
178 | 
179 | /**
180 |  * Legacy support for backward compatibility
181 |  */
182 | export async function stopDeviceLogCapture(
183 |   logSessionId: string,
184 |   fileSystem?: unknown,
185 | ): Promise<{ logContent: string; error?: string }> {
186 |   // For backward compatibility, create a mock FileSystemExecutor from the fileSystem parameter
187 |   const fsToUse = fileSystem ?? fs;
188 |   const mockFileSystemExecutor: FileSystemExecutor = {
189 |     async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {
190 |       if (hasPromisesInterface(fsToUse)) {
191 |         await fsToUse.promises.mkdir(path, options);
192 |       } else {
193 |         await fs.promises.mkdir(path, options);
194 |       }
195 |     },
196 |     async readFile(path: string, encoding: BufferEncoding = 'utf8'): Promise<string> {
197 |       if (hasPromisesInterface(fsToUse)) {
198 |         const result = await fsToUse.promises.readFile(path, encoding);
199 |         return typeof result === 'string' ? result : (result as Buffer).toString();
200 |       } else {
201 |         const result = await fs.promises.readFile(path, encoding);
202 |         return typeof result === 'string' ? result : (result as Buffer).toString();
203 |       }
204 |     },
205 |     async writeFile(
206 |       path: string,
207 |       content: string,
208 |       encoding: BufferEncoding = 'utf8',
209 |     ): Promise<void> {
210 |       if (hasPromisesInterface(fsToUse)) {
211 |         await fsToUse.promises.writeFile(path, content, encoding);
212 |       } else {
213 |         await fs.promises.writeFile(path, content, encoding);
214 |       }
215 |     },
216 |     async cp(
217 |       source: string,
218 |       destination: string,
219 |       options?: { recursive?: boolean },
220 |     ): Promise<void> {
221 |       if (hasPromisesInterface(fsToUse)) {
222 |         await fsToUse.promises.cp(source, destination, options);
223 |       } else {
224 |         await fs.promises.cp(source, destination, options);
225 |       }
226 |     },
227 |     async readdir(path: string, options?: { withFileTypes?: boolean }): Promise<unknown[]> {
228 |       if (hasPromisesInterface(fsToUse)) {
229 |         if (options?.withFileTypes === true) {
230 |           const result = await fsToUse.promises.readdir(path, { withFileTypes: true });
231 |           return Array.isArray(result) ? result : [];
232 |         } else {
233 |           const result = await fsToUse.promises.readdir(path);
234 |           return Array.isArray(result) ? result : [];
235 |         }
236 |       } else {
237 |         if (options?.withFileTypes === true) {
238 |           const result = await fs.promises.readdir(path, { withFileTypes: true });
239 |           return Array.isArray(result) ? result : [];
240 |         } else {
241 |           const result = await fs.promises.readdir(path);
242 |           return Array.isArray(result) ? result : [];
243 |         }
244 |       }
245 |     },
246 |     async rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> {
247 |       if (hasPromisesInterface(fsToUse)) {
248 |         await fsToUse.promises.rm(path, options);
249 |       } else {
250 |         await fs.promises.rm(path, options);
251 |       }
252 |     },
253 |     existsSync(path: string): boolean {
254 |       if (hasExistsSyncMethod(fsToUse)) {
255 |         return fsToUse.existsSync(path);
256 |       } else {
257 |         return fs.existsSync(path);
258 |       }
259 |     },
260 |     async stat(path: string): Promise<{ isDirectory(): boolean }> {
261 |       if (hasPromisesInterface(fsToUse)) {
262 |         const result = await fsToUse.promises.stat(path);
263 |         return result as { isDirectory(): boolean };
264 |       } else {
265 |         const result = await fs.promises.stat(path);
266 |         return result as { isDirectory(): boolean };
267 |       }
268 |     },
269 |     async mkdtemp(prefix: string): Promise<string> {
270 |       if (hasPromisesInterface(fsToUse)) {
271 |         return await fsToUse.promises.mkdtemp(prefix);
272 |       } else {
273 |         return await fs.promises.mkdtemp(prefix);
274 |       }
275 |     },
276 |     tmpdir(): string {
277 |       return '/tmp';
278 |     },
279 |   };
280 | 
281 |   const result = await stop_device_log_capLogic({ logSessionId }, mockFileSystemExecutor);
282 | 
283 |   if (result.isError) {
284 |     const errorText = result.content[0]?.text;
285 |     const errorMessage =
286 |       typeof errorText === 'string'
287 |         ? errorText.replace(`Failed to stop device log capture session ${logSessionId}: `, '')
288 |         : 'Unknown error occurred';
289 | 
290 |     return {
291 |       logContent: '',
292 |       error: errorMessage,
293 |     };
294 |   }
295 | 
296 |   // Extract log content from successful response
297 |   const successText = result.content[0]?.text;
298 |   if (typeof successText !== 'string') {
299 |     return {
300 |       logContent: '',
301 |       error: 'Invalid response format: expected text content',
302 |     };
303 |   }
304 | 
305 |   const logContentMatch = successText.match(/--- Captured Logs ---\n([\s\S]*)$/);
306 |   const logContent = logContentMatch?.[1] ?? '';
307 | 
308 |   return { logContent };
309 | }
310 | 
311 | export default {
312 |   name: 'stop_device_log_cap',
313 |   description: 'Stops an active Apple device log capture session and returns the captured logs.',
314 |   schema: stopDeviceLogCapSchema.shape, // MCP SDK compatibility
315 |   handler: createTypedTool(
316 |     stopDeviceLogCapSchema,
317 |     (params: StopDeviceLogCapParams) => {
318 |       return stop_device_log_capLogic(params, getDefaultFileSystemExecutor());
319 |     },
320 |     getDefaultCommandExecutor,
321 |   ),
322 | };
323 | 
```

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

```typescript
  1 | /**
  2 |  * Mock Executors for Testing - Dependency Injection Architecture
  3 |  *
  4 |  * This module provides mock implementations of CommandExecutor and FileSystemExecutor
  5 |  * for testing purposes. These mocks are completely isolated from production dependencies
  6 |  * to avoid import chains that could trigger native module loading issues in test environments.
  7 |  *
  8 |  * IMPORTANT: These are EXACT copies of the mock functions originally in utils/command.js
  9 |  * to ensure zero behavioral changes during the file reorganization.
 10 |  *
 11 |  * Responsibilities:
 12 |  * - Providing mock command execution for tests
 13 |  * - Providing mock file system operations for tests
 14 |  * - Maintaining exact behavior compatibility with original implementations
 15 |  * - Avoiding any dependencies on production logging or instrumentation
 16 |  */
 17 | 
 18 | import { ChildProcess } from 'child_process';
 19 | import { CommandExecutor } from '../utils/CommandExecutor.ts';
 20 | import { FileSystemExecutor } from '../utils/FileSystemExecutor.ts';
 21 | 
 22 | /**
 23 |  * Create a mock executor for testing
 24 |  * @param result Mock command result or error to throw
 25 |  * @returns Mock executor function
 26 |  */
 27 | export function createMockExecutor(
 28 |   result:
 29 |     | {
 30 |         success?: boolean;
 31 |         output?: string;
 32 |         error?: string;
 33 |         process?: unknown;
 34 |         exitCode?: number;
 35 |         shouldThrow?: Error;
 36 |       }
 37 |     | Error
 38 |     | string,
 39 | ): CommandExecutor {
 40 |   // If result is Error or string, return executor that rejects
 41 |   if (result instanceof Error || typeof result === 'string') {
 42 |     return async () => {
 43 |       throw result;
 44 |     };
 45 |   }
 46 | 
 47 |   // If shouldThrow is specified, return executor that rejects with that error
 48 |   if (result.shouldThrow) {
 49 |     return async () => {
 50 |       throw result.shouldThrow;
 51 |     };
 52 |   }
 53 | 
 54 |   const mockProcess = {
 55 |     pid: 12345,
 56 |     stdout: null,
 57 |     stderr: null,
 58 |     stdin: null,
 59 |     stdio: [null, null, null],
 60 |     killed: false,
 61 |     connected: false,
 62 |     exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
 63 |     signalCode: null,
 64 |     spawnargs: [],
 65 |     spawnfile: 'sh',
 66 |   } as unknown as ChildProcess;
 67 | 
 68 |   return async () => ({
 69 |     success: result.success ?? true,
 70 |     output: result.output ?? '',
 71 |     error: result.error,
 72 |     process: (result.process ?? mockProcess) as ChildProcess,
 73 |     exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
 74 |   });
 75 | }
 76 | 
 77 | /**
 78 |  * Create a no-op executor that throws an error if called
 79 |  * Use this for tests where an executor is required but should never be called
 80 |  * @returns CommandExecutor that throws on invocation
 81 |  */
 82 | export function createNoopExecutor(): CommandExecutor {
 83 |   return async (command) => {
 84 |     throw new Error(
 85 |       `🚨 NOOP EXECUTOR CALLED! 🚨\n` +
 86 |         `Command: ${command.join(' ')}\n` +
 87 |         `This executor should never be called in this test context.\n` +
 88 |         `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
 89 |         `Either fix the test to avoid this code path, or use createMockExecutor() instead.`,
 90 |     );
 91 |   };
 92 | }
 93 | 
 94 | /**
 95 |  * Create a command-matching mock executor for testing multi-command scenarios
 96 |  * Perfect for tools that execute multiple commands (like screenshot: simctl + sips)
 97 |  *
 98 |  * @param commandMap - Map of command patterns to their mock responses
 99 |  * @returns CommandExecutor that matches commands and returns appropriate responses
100 |  *
101 |  * @example
102 |  * ```typescript
103 |  * const mockExecutor = createCommandMatchingMockExecutor({
104 |  *   'xcrun simctl': { output: 'Screenshot saved' },
105 |  *   'sips': { output: 'Image optimized' }
106 |  * });
107 |  * ```
108 |  */
109 | export function createCommandMatchingMockExecutor(
110 |   commandMap: Record<
111 |     string,
112 |     {
113 |       success?: boolean;
114 |       output?: string;
115 |       error?: string;
116 |       process?: unknown;
117 |       exitCode?: number;
118 |     }
119 |   >,
120 | ): CommandExecutor {
121 |   return async (command) => {
122 |     const commandStr = command.join(' ');
123 | 
124 |     // Find matching command pattern
125 |     const matchedKey = Object.keys(commandMap).find((key) => commandStr.includes(key));
126 | 
127 |     if (!matchedKey) {
128 |       throw new Error(
129 |         `🚨 UNEXPECTED COMMAND! 🚨\n` +
130 |           `Command: ${commandStr}\n` +
131 |           `Expected one of: ${Object.keys(commandMap).join(', ')}\n` +
132 |           `Available patterns: ${JSON.stringify(Object.keys(commandMap), null, 2)}`,
133 |       );
134 |     }
135 | 
136 |     const result = commandMap[matchedKey];
137 | 
138 |     const mockProcess = {
139 |       pid: 12345,
140 |       stdout: null,
141 |       stderr: null,
142 |       stdin: null,
143 |       stdio: [null, null, null],
144 |       killed: false,
145 |       connected: false,
146 |       exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
147 |       signalCode: null,
148 |       spawnargs: [],
149 |       spawnfile: 'sh',
150 |     } as unknown as ChildProcess;
151 | 
152 |     return {
153 |       success: result.success ?? true, // Success by default (as discussed)
154 |       output: result.output ?? '',
155 |       error: result.error,
156 |       process: (result.process ?? mockProcess) as ChildProcess,
157 |       exitCode: result.exitCode ?? (result.success === false ? 1 : 0),
158 |     };
159 |   };
160 | }
161 | 
162 | /**
163 |  * Create a mock file system executor for testing
164 |  */
165 | export function createMockFileSystemExecutor(
166 |   overrides?: Partial<FileSystemExecutor>,
167 | ): FileSystemExecutor {
168 |   return {
169 |     mkdir: async (): Promise<void> => {},
170 |     readFile: async (): Promise<string> => 'mock file content',
171 |     writeFile: async (): Promise<void> => {},
172 |     cp: async (): Promise<void> => {},
173 |     readdir: async (): Promise<unknown[]> => [],
174 |     rm: async (): Promise<void> => {},
175 |     existsSync: (): boolean => false,
176 |     stat: async (): Promise<{ isDirectory(): boolean }> => ({ isDirectory: (): boolean => true }),
177 |     mkdtemp: async (): Promise<string> => '/tmp/mock-temp-123456',
178 |     tmpdir: (): string => '/tmp',
179 |     ...overrides,
180 |   };
181 | }
182 | 
183 | /**
184 |  * Create a no-op file system executor that throws an error if called
185 |  * Use this for tests where an executor is required but should never be called
186 |  * @returns CommandExecutor that throws on invocation
187 |  */
188 | export function createNoopFileSystemExecutor(): FileSystemExecutor {
189 |   return {
190 |     mkdir: async (): Promise<void> => {
191 |       throw new Error(
192 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
193 |           `This executor should never be called in this test context.\n` +
194 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
195 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
196 |       );
197 |     },
198 |     readFile: async (): Promise<string> => {
199 |       throw new Error(
200 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
201 |           `This executor should never be called in this test context.\n` +
202 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
203 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
204 |       );
205 |     },
206 |     writeFile: async (): Promise<void> => {
207 |       throw new Error(
208 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
209 |           `This executor should never be called in this test context.\n` +
210 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
211 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
212 |       );
213 |     },
214 |     cp: async (): Promise<void> => {
215 |       throw new Error(
216 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
217 |           `This executor should never be called in this test context.\n` +
218 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
219 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
220 |       );
221 |     },
222 |     readdir: async (): Promise<unknown[]> => {
223 |       throw new Error(
224 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
225 |           `This executor should never be called in this test context.\n` +
226 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
227 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
228 |       );
229 |     },
230 |     rm: async (): Promise<void> => {
231 |       throw new Error(
232 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
233 |           `This executor should never be called in this test context.\n` +
234 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
235 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
236 |       );
237 |     },
238 |     existsSync: (): boolean => {
239 |       throw new Error(
240 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
241 |           `This executor should never be called in this test context.\n` +
242 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
243 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
244 |       );
245 |     },
246 |     stat: async (): Promise<{ isDirectory(): boolean }> => {
247 |       throw new Error(
248 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
249 |           `This executor should never be called in this test context.\n` +
250 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
251 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
252 |       );
253 |     },
254 |     mkdtemp: async (): Promise<string> => {
255 |       throw new Error(
256 |         `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` +
257 |           `This executor should never be called in this test context.\n` +
258 |           `If you see this error, it means the test is exercising a code path that wasn't expected.\n` +
259 |           `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`,
260 |       );
261 |     },
262 |     tmpdir: (): string => '/tmp',
263 |   };
264 | }
265 | 
266 | /**
267 |  * Create a mock environment detector for testing
268 |  * @param options Mock options for environment detection
269 |  * @returns Mock environment detector
270 |  */
271 | export function createMockEnvironmentDetector(
272 |   options: {
273 |     isRunningUnderClaudeCode?: boolean;
274 |   } = {},
275 | ): import('../utils/environment.js').EnvironmentDetector {
276 |   return {
277 |     isRunningUnderClaudeCode: () => options.isRunningUnderClaudeCode ?? false,
278 |   };
279 | }
280 | 
```

--------------------------------------------------------------------------------
/example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift:
--------------------------------------------------------------------------------

```swift
  1 | //
  2 | //  CalculatorAppTests.swift
  3 | //  CalculatorAppTests
  4 | //
  5 | //  Created by Cameron on 05/06/2025.
  6 | //
  7 | 
  8 | import XCTest
  9 | import SwiftUI
 10 | @testable import CalculatorApp
 11 | import CalculatorAppFeature
 12 | 
 13 | final class CalculatorAppTests: XCTestCase {
 14 | 
 15 |     override func setUpWithError() throws {
 16 |         continueAfterFailure = false
 17 |     }
 18 | 
 19 |     override func tearDownWithError() throws {
 20 |         // Clean up after each test
 21 |     }
 22 | }
 23 | 
 24 | // MARK: - App Lifecycle Tests
 25 | extension CalculatorAppTests {
 26 |     
 27 |     func testAppLaunch() throws {
 28 |         // Test that the app launches without crashing
 29 |         let app = CalculatorApp()
 30 |         XCTAssertNotNil(app, "App should initialize successfully")
 31 |     }
 32 |     
 33 |     func testContentViewInitialization() throws {
 34 |         // Test that ContentView initializes properly
 35 |         let contentView = ContentView()
 36 |         XCTAssertNotNil(contentView, "ContentView should initialize successfully")
 37 |     }
 38 | }
 39 | 
 40 | // MARK: - Calculator Service Integration Tests
 41 | extension CalculatorAppTests {
 42 |     
 43 |     func testCalculatorServiceCreation() throws {
 44 |         let service = CalculatorService()
 45 |         XCTAssertEqual(service.display, "0", "Calculator should start with display showing 0")
 46 |         XCTAssertEqual(service.expressionDisplay, "", "Calculator should start with empty expression")
 47 |     }
 48 |     
 49 |     func testCalculatorServiceFailure() throws {
 50 |         let service = CalculatorService()
 51 |         // This test is designed to fail to test error reporting
 52 |         XCTAssertEqual(service.display, "999", "This test should fail - display should be 0, not 999")
 53 |     }
 54 |     
 55 |     func testCalculatorServiceBasicOperation() throws {
 56 |         let service = CalculatorService()
 57 |         
 58 |         // Test basic addition
 59 |         service.inputNumber("5")
 60 |         service.setOperation(.add)
 61 |         service.inputNumber("3")
 62 |         service.calculate()
 63 |         
 64 |         XCTAssertEqual(service.display, "8", "5 + 3 should equal 8")
 65 |     }
 66 |     
 67 |     func testCalculatorServiceChainedOperations() throws {
 68 |         let service = CalculatorService()
 69 |         
 70 |         // Test chained operations: 10 + 5 * 2 = 30 (since calculator evaluates left to right)
 71 |         service.inputNumber("10")
 72 |         service.setOperation(.add)
 73 |         service.inputNumber("5")
 74 |         service.setOperation(.multiply)
 75 |         service.inputNumber("2")
 76 |         service.calculate()
 77 |         
 78 |         XCTAssertEqual(service.display, "30", "10 + 5 * 2 should equal 30 (left-to-right evaluation)")
 79 |     }
 80 |     
 81 |     func testCalculatorServiceClear() throws {
 82 |         let service = CalculatorService()
 83 |         
 84 |         // Set up some state
 85 |         service.inputNumber("123")
 86 |         service.setOperation(.add)
 87 |         service.inputNumber("456")
 88 |         
 89 |         // Clear should reset everything
 90 |         service.clear()
 91 |         
 92 |         XCTAssertEqual(service.display, "0", "Display should be 0 after clear")
 93 |         XCTAssertEqual(service.expressionDisplay, "", "Expression should be empty after clear")
 94 |     }
 95 | }
 96 | 
 97 | // MARK: - API Surface Tests
 98 | extension CalculatorAppTests {
 99 |     
100 |     func testCalculatorServicePublicInterface() throws {
101 |         let service = CalculatorService()
102 |         
103 |         // Test that all expected public methods are available
104 |         XCTAssertNoThrow(service.inputNumber("5"))
105 |         XCTAssertNoThrow(service.inputDecimal())
106 |         XCTAssertNoThrow(service.setOperation(.add))
107 |         XCTAssertNoThrow(service.calculate())
108 |         XCTAssertNoThrow(service.toggleSign())
109 |         XCTAssertNoThrow(service.percentage())
110 |         XCTAssertNoThrow(service.clear())
111 |     }
112 |     
113 |     func testCalculatorServicePublicProperties() throws {
114 |         let service = CalculatorService()
115 |         
116 |         // Test that all expected public properties are accessible
117 |         XCTAssertNotNil(service.display)
118 |         XCTAssertNotNil(service.expressionDisplay)
119 |         XCTAssertEqual(service.hasError, false)
120 |         
121 |         // Test testing support properties
122 |         XCTAssertEqual(service.currentValue, 0)
123 |         XCTAssertEqual(service.previousValue, 0)
124 |         XCTAssertNil(service.currentOperation)
125 |         XCTAssertEqual(service.willResetDisplay, false)
126 |     }
127 |     
128 |     func testCalculatorOperationsEnum() throws {
129 |         // Test that all operations are available
130 |         XCTAssertEqual(CalculatorService.Operation.add.rawValue, "+")
131 |         XCTAssertEqual(CalculatorService.Operation.subtract.rawValue, "-")
132 |         XCTAssertEqual(CalculatorService.Operation.multiply.rawValue, "×")
133 |         XCTAssertEqual(CalculatorService.Operation.divide.rawValue, "÷")
134 |         
135 |         // Test operation calculations
136 |         XCTAssertEqual(CalculatorService.Operation.add.calculate(5, 3), 8)
137 |         XCTAssertEqual(CalculatorService.Operation.subtract.calculate(5, 3), 2)
138 |         XCTAssertEqual(CalculatorService.Operation.multiply.calculate(5, 3), 15)
139 |         XCTAssertEqual(CalculatorService.Operation.divide.calculate(6, 3), 2)
140 |         XCTAssertEqual(CalculatorService.Operation.divide.calculate(5, 0), 0) // Division by zero
141 |     }
142 | }
143 | 
144 | // MARK: - Edge Case and Error Handling Tests
145 | extension CalculatorAppTests {
146 |     
147 |     func testDivisionByZero() throws {
148 |         let service = CalculatorService()
149 |         
150 |         service.inputNumber("10")
151 |         service.setOperation(.divide)
152 |         service.inputNumber("0")
153 |         service.calculate()
154 |         
155 |         XCTAssertEqual(service.display, "0", "Division by zero should return 0")
156 |     }
157 |     
158 |     func testLargeNumbers() throws {
159 |         let service = CalculatorService()
160 |         
161 |         // Test large number input
162 |         service.inputNumber("999999999")
163 |         XCTAssertEqual(service.display, "999999999", "Should handle large numbers")
164 |         
165 |         // Test large number calculation
166 |         service.setOperation(.multiply)
167 |         service.inputNumber("2")
168 |         service.calculate()
169 |         
170 |         // Should handle the result without crashing
171 |         XCTAssertNotEqual(service.display, "", "Should display some result for large calculations")
172 |     }
173 |     
174 |     func testRepeatedEquals() throws {
175 |         let service = CalculatorService()
176 |         
177 |         service.inputNumber("5")
178 |         service.setOperation(.add)
179 |         service.inputNumber("3")
180 |         service.calculate() // 5 + 3 = 8
181 |         
182 |         let firstResult = service.display
183 |         
184 |         service.calculate() // Should repeat last operation: 8 + 3 = 11
185 |         let secondResult = service.display
186 |         
187 |         XCTAssertEqual(firstResult, "8", "First calculation should be correct")
188 |         XCTAssertEqual(secondResult, "11", "Repeated equals should repeat last operation")
189 |     }
190 | }
191 | 
192 | // MARK: - Performance Tests
193 | extension CalculatorAppTests {
194 |     
195 |     func testCalculationPerformance() throws {
196 |         let service = CalculatorService()
197 |         
198 |         measure {
199 |             // Measure performance of 100 calculations
200 |             for i in 1...100 {
201 |                 service.clear()
202 |                 service.inputNumber("\(i)")
203 |                 service.setOperation(.multiply)
204 |                 service.inputNumber("2")
205 |                 service.calculate()
206 |             }
207 |         }
208 |     }
209 |     
210 |     func testLargeNumberInputPerformance() throws {
211 |         let service = CalculatorService()
212 |         
213 |         measure {
214 |             // Measure performance of inputting large numbers
215 |             service.clear()
216 |             for digit in "123456789012345" {
217 |                 service.inputNumber(String(digit))
218 |             }
219 |         }
220 |     }
221 | }
222 | 
223 | // MARK: - State Consistency Tests
224 | extension CalculatorAppTests {
225 |     
226 |     func testStateConsistencyAfterOperations() throws {
227 |         let service = CalculatorService()
228 |         
229 |         // Perform a series of operations and verify state remains consistent
230 |         service.inputNumber("10")
231 |         XCTAssertEqual(service.display, "10")
232 |         
233 |         service.setOperation(.add)
234 |         XCTAssertEqual(service.display, "10")
235 |         XCTAssertTrue(service.expressionDisplay.contains("10 +"))
236 |         
237 |         service.inputNumber("5")
238 |         XCTAssertEqual(service.display, "5")
239 |         
240 |         service.calculate()
241 |         XCTAssertEqual(service.display, "15")
242 |     }
243 |     
244 |     func testStateConsistencyWithDecimalNumbers() throws {
245 |         let service = CalculatorService()
246 |         
247 |         service.inputNumber("3")
248 |         service.inputDecimal()
249 |         service.inputNumber("14")
250 |         XCTAssertEqual(service.display, "3.14")
251 |         
252 |         service.setOperation(.multiply)
253 |         service.inputNumber("2")
254 |         service.calculate()
255 |         
256 |         XCTAssertEqual(service.display, "6.28")
257 |     }
258 |     
259 |     func testMultipleDecimalPointsHandling() throws {
260 |         let service = CalculatorService()
261 |         
262 |         service.inputNumber("1")
263 |         service.inputDecimal()
264 |         service.inputNumber("5")
265 |         service.inputDecimal() // This should be ignored
266 |         service.inputNumber("9")
267 |         
268 |         XCTAssertEqual(service.display, "1.59", "Multiple decimal points should be ignored")
269 |     }
270 | }
271 | 
272 | // MARK: - Component Integration Tests
273 | extension CalculatorAppTests {
274 |     
275 |     func testComplexCalculationWorkflow() throws {
276 |         let service = CalculatorService()
277 |         
278 |         // Test complex workflow through direct service calls
279 |         service.inputNumber("2")
280 |         service.inputNumber("5")
281 |         service.setOperation(.divide)
282 |         service.inputNumber("5")
283 |         service.calculate()
284 |         
285 |         XCTAssertEqual(service.display, "5", "Complex workflow should work correctly")
286 |         
287 |         // Test that we can continue with the result
288 |         service.setOperation(.multiply)
289 |         service.inputNumber("4")
290 |         service.calculate()
291 |         
292 |         XCTAssertEqual(service.display, "20", "Should be able to continue with previous result")
293 |     }
294 |     
295 |     func testPercentageCalculation() throws {
296 |         let service = CalculatorService()
297 |         
298 |         service.inputNumber("50")
299 |         service.percentage()
300 |         
301 |         XCTAssertEqual(service.display, "0.5", "50% should equal 0.5")
302 |     }
303 |     
304 |     func testSignToggle() throws {
305 |         let service = CalculatorService()
306 |         
307 |         service.inputNumber("42")
308 |         service.toggleSign()
309 |         XCTAssertEqual(service.display, "-42", "Should toggle to negative")
310 |         
311 |         service.toggleSign()
312 |         XCTAssertEqual(service.display, "42", "Should toggle back to positive")
313 |     }
314 | }
315 | 
```

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

```typescript
  1 | /**
  2 |  * Pure dependency injection test for launch_app_device plugin (device-shared)
  3 |  *
  4 |  * Tests plugin structure and app launching functionality including parameter validation,
  5 |  * command generation, file operations, and response formatting.
  6 |  *
  7 |  * Uses createMockExecutor for command execution and manual stubs for file operations.
  8 |  */
  9 | 
 10 | import { describe, it, expect, beforeEach } from 'vitest';
 11 | import { z } from 'zod';
 12 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
 13 | import launchAppDevice, { launch_app_deviceLogic } from '../launch_app_device.ts';
 14 | import { sessionStore } from '../../../../utils/session-store.ts';
 15 | 
 16 | describe('launch_app_device plugin (device-shared)', () => {
 17 |   beforeEach(() => {
 18 |     sessionStore.clear();
 19 |   });
 20 | 
 21 |   describe('Export Field Validation (Literal)', () => {
 22 |     it('should have correct name', () => {
 23 |       expect(launchAppDevice.name).toBe('launch_app_device');
 24 |     });
 25 | 
 26 |     it('should have correct description', () => {
 27 |       expect(launchAppDevice.description).toBe('Launches an app on a connected device.');
 28 |     });
 29 | 
 30 |     it('should have handler function', () => {
 31 |       expect(typeof launchAppDevice.handler).toBe('function');
 32 |     });
 33 | 
 34 |     it('should validate schema with valid inputs', () => {
 35 |       const schema = z.object(launchAppDevice.schema).strict();
 36 |       expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true);
 37 |       expect(schema.safeParse({}).success).toBe(false);
 38 |       expect(Object.keys(launchAppDevice.schema)).toEqual(['bundleId']);
 39 |     });
 40 | 
 41 |     it('should validate schema with invalid inputs', () => {
 42 |       const schema = z.object(launchAppDevice.schema).strict();
 43 |       expect(schema.safeParse({ bundleId: null }).success).toBe(false);
 44 |       expect(schema.safeParse({ bundleId: 123 }).success).toBe(false);
 45 |     });
 46 |   });
 47 | 
 48 |   describe('Handler Requirements', () => {
 49 |     it('should require deviceId when not provided', async () => {
 50 |       const result = await launchAppDevice.handler({ bundleId: 'com.example.app' });
 51 | 
 52 |       expect(result.isError).toBe(true);
 53 |       expect(result.content[0].text).toContain('deviceId is required');
 54 |     });
 55 |   });
 56 | 
 57 |   describe('Command Generation', () => {
 58 |     it('should generate correct devicectl command with required parameters', async () => {
 59 |       const calls: any[] = [];
 60 |       const mockExecutor = createMockExecutor({
 61 |         success: true,
 62 |         output: 'App launched successfully',
 63 |         process: { pid: 12345 },
 64 |       });
 65 | 
 66 |       const trackingExecutor = async (
 67 |         command: string[],
 68 |         logPrefix?: string,
 69 |         useShell?: boolean,
 70 |         env?: Record<string, string>,
 71 |       ) => {
 72 |         calls.push({ command, logPrefix, useShell, env });
 73 |         return mockExecutor(command, logPrefix, useShell, env);
 74 |       };
 75 | 
 76 |       await launch_app_deviceLogic(
 77 |         {
 78 |           deviceId: 'test-device-123',
 79 |           bundleId: 'com.example.app',
 80 |         },
 81 |         trackingExecutor,
 82 |       );
 83 | 
 84 |       expect(calls).toHaveLength(1);
 85 |       expect(calls[0].command).toEqual([
 86 |         'xcrun',
 87 |         'devicectl',
 88 |         'device',
 89 |         'process',
 90 |         'launch',
 91 |         '--device',
 92 |         'test-device-123',
 93 |         '--json-output',
 94 |         expect.stringMatching(/^\/.*\/launch-\d+\.json$/),
 95 |         '--terminate-existing',
 96 |         'com.example.app',
 97 |       ]);
 98 |       expect(calls[0].logPrefix).toBe('Launch app on device');
 99 |       expect(calls[0].useShell).toBe(true);
100 |       expect(calls[0].env).toBeUndefined();
101 |     });
102 | 
103 |     it('should generate command with different device and bundle parameters', async () => {
104 |       const calls: any[] = [];
105 |       const mockExecutor = createMockExecutor({
106 |         success: true,
107 |         output: 'Launch successful',
108 |         process: { pid: 54321 },
109 |       });
110 | 
111 |       const trackingExecutor = async (command: string[]) => {
112 |         calls.push({ command });
113 |         return mockExecutor(command);
114 |       };
115 | 
116 |       await launch_app_deviceLogic(
117 |         {
118 |           deviceId: '00008030-001E14BE2288802E',
119 |           bundleId: 'com.apple.mobilesafari',
120 |         },
121 |         trackingExecutor,
122 |       );
123 | 
124 |       expect(calls[0].command).toEqual([
125 |         'xcrun',
126 |         'devicectl',
127 |         'device',
128 |         'process',
129 |         'launch',
130 |         '--device',
131 |         '00008030-001E14BE2288802E',
132 |         '--json-output',
133 |         expect.stringMatching(/^\/.*\/launch-\d+\.json$/),
134 |         '--terminate-existing',
135 |         'com.apple.mobilesafari',
136 |       ]);
137 |     });
138 |   });
139 | 
140 |   describe('Success Path Tests', () => {
141 |     it('should return successful launch response without process ID', async () => {
142 |       const mockExecutor = createMockExecutor({
143 |         success: true,
144 |         output: 'App launched successfully',
145 |       });
146 | 
147 |       const result = await launch_app_deviceLogic(
148 |         {
149 |           deviceId: 'test-device-123',
150 |           bundleId: 'com.example.app',
151 |         },
152 |         mockExecutor,
153 |       );
154 | 
155 |       expect(result).toEqual({
156 |         content: [
157 |           {
158 |             type: 'text',
159 |             text: '✅ App launched successfully\n\nApp launched successfully',
160 |           },
161 |         ],
162 |       });
163 |     });
164 | 
165 |     it('should return successful launch response with detailed output', async () => {
166 |       const mockExecutor = createMockExecutor({
167 |         success: true,
168 |         output: 'Launch succeeded with detailed output',
169 |       });
170 | 
171 |       const result = await launch_app_deviceLogic(
172 |         {
173 |           deviceId: 'test-device-123',
174 |           bundleId: 'com.example.app',
175 |         },
176 |         mockExecutor,
177 |       );
178 | 
179 |       expect(result).toEqual({
180 |         content: [
181 |           {
182 |             type: 'text',
183 |             text: '✅ App launched successfully\n\nLaunch succeeded with detailed output',
184 |           },
185 |         ],
186 |       });
187 |     });
188 | 
189 |     it('should handle successful launch with process ID information', async () => {
190 |       // Mock fs operations for JSON parsing
191 |       const fs = await import('fs');
192 |       const originalReadFile = fs.promises.readFile;
193 |       const originalUnlink = fs.promises.unlink;
194 | 
195 |       const mockReadFile = (path: string) => {
196 |         if (path.includes('launch-')) {
197 |           return Promise.resolve(
198 |             JSON.stringify({
199 |               result: {
200 |                 process: {
201 |                   processIdentifier: 12345,
202 |                 },
203 |               },
204 |             }),
205 |           );
206 |         }
207 |         return originalReadFile(path);
208 |       };
209 | 
210 |       const mockUnlink = () => Promise.resolve();
211 | 
212 |       // Replace fs methods
213 |       fs.promises.readFile = mockReadFile;
214 |       fs.promises.unlink = mockUnlink;
215 | 
216 |       const mockExecutor = createMockExecutor({
217 |         success: true,
218 |         output: 'App launched successfully',
219 |       });
220 | 
221 |       const result = await launch_app_deviceLogic(
222 |         {
223 |           deviceId: 'test-device-123',
224 |           bundleId: 'com.example.app',
225 |         },
226 |         mockExecutor,
227 |       );
228 | 
229 |       // Restore fs methods
230 |       fs.promises.readFile = originalReadFile;
231 |       fs.promises.unlink = originalUnlink;
232 | 
233 |       expect(result).toEqual({
234 |         content: [
235 |           {
236 |             type: 'text',
237 |             text: '✅ App launched successfully\n\nApp launched successfully\n\nProcess ID: 12345\n\nNext Steps:\n1. Interact with your app on the device\n2. Stop the app: stop_app_device({ deviceId: "test-device-123", processId: 12345 })',
238 |           },
239 |         ],
240 |       });
241 |     });
242 | 
243 |     it('should handle successful launch with command output', async () => {
244 |       const mockExecutor = createMockExecutor({
245 |         success: true,
246 |         output: 'App "com.example.app" launched on device "test-device-123"',
247 |       });
248 | 
249 |       const result = await launch_app_deviceLogic(
250 |         {
251 |           deviceId: 'test-device-123',
252 |           bundleId: 'com.example.app',
253 |         },
254 |         mockExecutor,
255 |       );
256 | 
257 |       expect(result).toEqual({
258 |         content: [
259 |           {
260 |             type: 'text',
261 |             text: '✅ App launched successfully\n\nApp "com.example.app" launched on device "test-device-123"',
262 |           },
263 |         ],
264 |       });
265 |     });
266 |   });
267 | 
268 |   describe('Error Handling', () => {
269 |     it('should return launch failure response', async () => {
270 |       const mockExecutor = createMockExecutor({
271 |         success: false,
272 |         error: 'Launch failed: App not found',
273 |       });
274 | 
275 |       const result = await launch_app_deviceLogic(
276 |         {
277 |           deviceId: 'test-device-123',
278 |           bundleId: 'com.nonexistent.app',
279 |         },
280 |         mockExecutor,
281 |       );
282 | 
283 |       expect(result).toEqual({
284 |         content: [
285 |           {
286 |             type: 'text',
287 |             text: 'Failed to launch app: Launch failed: App not found',
288 |           },
289 |         ],
290 |         isError: true,
291 |       });
292 |     });
293 | 
294 |     it('should return command failure response with specific error', async () => {
295 |       const mockExecutor = createMockExecutor({
296 |         success: false,
297 |         error: 'Device not found: test-device-invalid',
298 |       });
299 | 
300 |       const result = await launch_app_deviceLogic(
301 |         {
302 |           deviceId: 'test-device-invalid',
303 |           bundleId: 'com.example.app',
304 |         },
305 |         mockExecutor,
306 |       );
307 | 
308 |       expect(result).toEqual({
309 |         content: [
310 |           {
311 |             type: 'text',
312 |             text: 'Failed to launch app: Device not found: test-device-invalid',
313 |           },
314 |         ],
315 |         isError: true,
316 |       });
317 |     });
318 | 
319 |     it('should handle executor exception with Error object', async () => {
320 |       const mockExecutor = createMockExecutor(new Error('Network error'));
321 | 
322 |       const result = await launch_app_deviceLogic(
323 |         {
324 |           deviceId: 'test-device-123',
325 |           bundleId: 'com.example.app',
326 |         },
327 |         mockExecutor,
328 |       );
329 | 
330 |       expect(result).toEqual({
331 |         content: [
332 |           {
333 |             type: 'text',
334 |             text: 'Failed to launch app on device: Network error',
335 |           },
336 |         ],
337 |         isError: true,
338 |       });
339 |     });
340 | 
341 |     it('should handle executor exception with string error', async () => {
342 |       const mockExecutor = createMockExecutor('String error');
343 | 
344 |       const result = await launch_app_deviceLogic(
345 |         {
346 |           deviceId: 'test-device-123',
347 |           bundleId: 'com.example.app',
348 |         },
349 |         mockExecutor,
350 |       );
351 | 
352 |       expect(result).toEqual({
353 |         content: [
354 |           {
355 |             type: 'text',
356 |             text: 'Failed to launch app on device: String error',
357 |           },
358 |         ],
359 |         isError: true,
360 |       });
361 |     });
362 |   });
363 | });
364 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for build_device plugin (unified)
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import { z } from 'zod';
  9 | import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts';
 10 | import buildDevice, { buildDeviceLogic } from '../build_device.ts';
 11 | import { sessionStore } from '../../../../utils/session-store.ts';
 12 | 
 13 | describe('build_device plugin', () => {
 14 |   beforeEach(() => {
 15 |     sessionStore.clear();
 16 |   });
 17 | 
 18 |   describe('Export Field Validation (Literal)', () => {
 19 |     it('should have correct name', () => {
 20 |       expect(buildDevice.name).toBe('build_device');
 21 |     });
 22 | 
 23 |     it('should have correct description', () => {
 24 |       expect(buildDevice.description).toBe('Builds an app for a connected device.');
 25 |     });
 26 | 
 27 |     it('should have handler function', () => {
 28 |       expect(typeof buildDevice.handler).toBe('function');
 29 |     });
 30 | 
 31 |     it('should expose only optional build-tuning fields in public schema', () => {
 32 |       const schema = z.object(buildDevice.schema).strict();
 33 |       expect(schema.safeParse({}).success).toBe(true);
 34 |       expect(
 35 |         schema.safeParse({ derivedDataPath: '/path/to/derived-data', extraArgs: [] }).success,
 36 |       ).toBe(true);
 37 |       expect(schema.safeParse({ projectPath: '/path/to/MyProject.xcodeproj' }).success).toBe(false);
 38 | 
 39 |       const schemaKeys = Object.keys(buildDevice.schema).sort();
 40 |       expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild']);
 41 |     });
 42 |   });
 43 | 
 44 |   describe('XOR Validation', () => {
 45 |     it('should error when neither projectPath nor workspacePath provided', async () => {
 46 |       const result = await buildDevice.handler({
 47 |         scheme: 'MyScheme',
 48 |       });
 49 | 
 50 |       expect(result.isError).toBe(true);
 51 |       expect(result.content[0].text).toContain('Missing required session defaults');
 52 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 53 |     });
 54 | 
 55 |     it('should error when both projectPath and workspacePath provided', async () => {
 56 |       const result = await buildDevice.handler({
 57 |         projectPath: '/path/to/MyProject.xcodeproj',
 58 |         workspacePath: '/path/to/MyProject.xcworkspace',
 59 |         scheme: 'MyScheme',
 60 |       });
 61 | 
 62 |       expect(result.isError).toBe(true);
 63 |       expect(result.content[0].text).toContain('Parameter validation failed');
 64 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 65 |     });
 66 |   });
 67 | 
 68 |   describe('Parameter Validation (via Handler)', () => {
 69 |     it('should return Zod validation error for missing scheme', async () => {
 70 |       const result = await buildDevice.handler({
 71 |         projectPath: '/path/to/MyProject.xcodeproj',
 72 |       });
 73 | 
 74 |       expect(result.isError).toBe(true);
 75 |       expect(result.content[0].text).toContain('Missing required session defaults');
 76 |       expect(result.content[0].text).toContain('scheme is required');
 77 |     });
 78 | 
 79 |     it('should return Zod validation error for invalid parameter types', async () => {
 80 |       const result = await buildDevice.handler({
 81 |         projectPath: 123, // Should be string
 82 |         scheme: 'MyScheme',
 83 |       });
 84 | 
 85 |       expect(result.isError).toBe(true);
 86 |       expect(result.content[0].text).toContain('Parameter validation failed');
 87 |       expect(result.content[0].text).toContain('projectPath');
 88 |       expect(result.content[0].text).toContain(
 89 |         'Tip: set session defaults via session-set-defaults',
 90 |       );
 91 |     });
 92 |   });
 93 | 
 94 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 95 |     it('should pass validation and execute successfully with valid project parameters', async () => {
 96 |       const mockExecutor = createMockExecutor({
 97 |         success: true,
 98 |         output: 'Build succeeded',
 99 |       });
100 | 
101 |       const result = await buildDeviceLogic(
102 |         {
103 |           projectPath: '/path/to/MyProject.xcodeproj',
104 |           scheme: 'MyScheme',
105 |         },
106 |         mockExecutor,
107 |       );
108 | 
109 |       expect(result.isError).toBeUndefined();
110 |       expect(result.content).toHaveLength(2);
111 |       expect(result.content[0].text).toContain('✅ iOS Device Build build succeeded');
112 |     });
113 | 
114 |     it('should pass validation and execute successfully with valid workspace parameters', async () => {
115 |       const mockExecutor = createMockExecutor({
116 |         success: true,
117 |         output: 'Build succeeded',
118 |       });
119 | 
120 |       const result = await buildDeviceLogic(
121 |         {
122 |           workspacePath: '/path/to/MyProject.xcworkspace',
123 |           scheme: 'MyScheme',
124 |         },
125 |         mockExecutor,
126 |       );
127 | 
128 |       expect(result.isError).toBeUndefined();
129 |       expect(result.content).toHaveLength(2);
130 |       expect(result.content[0].text).toContain('✅ iOS Device Build build succeeded');
131 |     });
132 | 
133 |     it('should verify workspace command generation with mock executor', async () => {
134 |       const commandCalls: Array<{
135 |         args: string[];
136 |         logPrefix: string;
137 |         silent: boolean;
138 |         timeout: number | undefined;
139 |       }> = [];
140 | 
141 |       const stubExecutor = async (
142 |         args: string[],
143 |         logPrefix: string,
144 |         silent: boolean,
145 |         timeout?: number,
146 |       ) => {
147 |         commandCalls.push({ args, logPrefix, silent, timeout });
148 |         return {
149 |           success: true,
150 |           output: 'Build succeeded',
151 |           error: undefined,
152 |           process: { pid: 12345 },
153 |         };
154 |       };
155 | 
156 |       await buildDeviceLogic(
157 |         {
158 |           workspacePath: '/path/to/MyProject.xcworkspace',
159 |           scheme: 'MyScheme',
160 |         },
161 |         stubExecutor,
162 |       );
163 | 
164 |       expect(commandCalls).toHaveLength(1);
165 |       expect(commandCalls[0]).toEqual({
166 |         args: [
167 |           'xcodebuild',
168 |           '-workspace',
169 |           '/path/to/MyProject.xcworkspace',
170 |           '-scheme',
171 |           'MyScheme',
172 |           '-configuration',
173 |           'Debug',
174 |           '-skipMacroValidation',
175 |           '-destination',
176 |           'generic/platform=iOS',
177 |           'build',
178 |         ],
179 |         logPrefix: 'iOS Device Build',
180 |         silent: true,
181 |         timeout: undefined,
182 |       });
183 |     });
184 | 
185 |     it('should verify command generation with mock executor', async () => {
186 |       const commandCalls: Array<{
187 |         args: string[];
188 |         logPrefix: string;
189 |         silent: boolean;
190 |         timeout: number | undefined;
191 |       }> = [];
192 | 
193 |       const stubExecutor = async (
194 |         args: string[],
195 |         logPrefix: string,
196 |         silent: boolean,
197 |         timeout?: number,
198 |       ) => {
199 |         commandCalls.push({ args, logPrefix, silent, timeout });
200 |         return {
201 |           success: true,
202 |           output: 'Build succeeded',
203 |           error: undefined,
204 |           process: { pid: 12345 },
205 |         };
206 |       };
207 | 
208 |       await buildDeviceLogic(
209 |         {
210 |           projectPath: '/path/to/MyProject.xcodeproj',
211 |           scheme: 'MyScheme',
212 |         },
213 |         stubExecutor,
214 |       );
215 | 
216 |       expect(commandCalls).toHaveLength(1);
217 |       expect(commandCalls[0]).toEqual({
218 |         args: [
219 |           'xcodebuild',
220 |           '-project',
221 |           '/path/to/MyProject.xcodeproj',
222 |           '-scheme',
223 |           'MyScheme',
224 |           '-configuration',
225 |           'Debug',
226 |           '-skipMacroValidation',
227 |           '-destination',
228 |           'generic/platform=iOS',
229 |           'build',
230 |         ],
231 |         logPrefix: 'iOS Device Build',
232 |         silent: true,
233 |         timeout: undefined,
234 |       });
235 |     });
236 | 
237 |     it('should return exact successful build response', async () => {
238 |       const mockExecutor = createMockExecutor({
239 |         success: true,
240 |         output: 'Build succeeded',
241 |       });
242 | 
243 |       const result = await buildDeviceLogic(
244 |         {
245 |           projectPath: '/path/to/MyProject.xcodeproj',
246 |           scheme: 'MyScheme',
247 |         },
248 |         mockExecutor,
249 |       );
250 | 
251 |       expect(result).toEqual({
252 |         content: [
253 |           {
254 |             type: 'text',
255 |             text: '✅ iOS Device Build build succeeded for scheme MyScheme.',
256 |           },
257 |           {
258 |             type: 'text',
259 |             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' })",
260 |           },
261 |         ],
262 |       });
263 |     });
264 | 
265 |     it('should return exact build failure response', async () => {
266 |       const mockExecutor = createMockExecutor({
267 |         success: false,
268 |         error: 'Compilation error',
269 |       });
270 | 
271 |       const result = await buildDeviceLogic(
272 |         {
273 |           projectPath: '/path/to/MyProject.xcodeproj',
274 |           scheme: 'MyScheme',
275 |         },
276 |         mockExecutor,
277 |       );
278 | 
279 |       expect(result).toEqual({
280 |         content: [
281 |           {
282 |             type: 'text',
283 |             text: '❌ [stderr] Compilation error',
284 |           },
285 |           {
286 |             type: 'text',
287 |             text: '❌ iOS Device Build build failed for scheme MyScheme.',
288 |           },
289 |         ],
290 |         isError: true,
291 |       });
292 |     });
293 | 
294 |     it('should include optional parameters in command', async () => {
295 |       const commandCalls: Array<{
296 |         args: string[];
297 |         logPrefix: string;
298 |         silent: boolean;
299 |         timeout: number | undefined;
300 |       }> = [];
301 | 
302 |       const stubExecutor = async (
303 |         args: string[],
304 |         logPrefix: string,
305 |         silent: boolean,
306 |         timeout?: number,
307 |       ) => {
308 |         commandCalls.push({ args, logPrefix, silent, timeout });
309 |         return {
310 |           success: true,
311 |           output: 'Build succeeded',
312 |           error: undefined,
313 |           process: { pid: 12345 },
314 |         };
315 |       };
316 | 
317 |       await buildDeviceLogic(
318 |         {
319 |           projectPath: '/path/to/MyProject.xcodeproj',
320 |           scheme: 'MyScheme',
321 |           configuration: 'Release',
322 |           derivedDataPath: '/tmp/derived-data',
323 |           extraArgs: ['--verbose'],
324 |         },
325 |         stubExecutor,
326 |       );
327 | 
328 |       expect(commandCalls).toHaveLength(1);
329 |       expect(commandCalls[0]).toEqual({
330 |         args: [
331 |           'xcodebuild',
332 |           '-project',
333 |           '/path/to/MyProject.xcodeproj',
334 |           '-scheme',
335 |           'MyScheme',
336 |           '-configuration',
337 |           'Release',
338 |           '-skipMacroValidation',
339 |           '-destination',
340 |           'generic/platform=iOS',
341 |           '-derivedDataPath',
342 |           '/tmp/derived-data',
343 |           '--verbose',
344 |           'build',
345 |         ],
346 |         logPrefix: 'iOS Device Build',
347 |         silent: true,
348 |         timeout: undefined,
349 |       });
350 |     });
351 |   });
352 | });
353 | 
```

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

```typescript
  1 | /**
  2 |  * Test for get_app_bundle_id plugin - Dependency Injection Architecture
  3 |  *
  4 |  * Tests the plugin structure and exported components for get_app_bundle_id tool.
  5 |  * Uses pure dependency injection with createMockFileSystemExecutor.
  6 |  * NO VITEST MOCKING ALLOWED - Only createMockFileSystemExecutor
  7 |  *
  8 |  * Plugin location: plugins/project-discovery/get_app_bundle_id.ts
  9 |  */
 10 | 
 11 | import { describe, it, expect } from 'vitest';
 12 | import { z } from 'zod';
 13 | import plugin, { get_app_bundle_idLogic } from '../get_app_bundle_id.ts';
 14 | import {
 15 |   createMockFileSystemExecutor,
 16 |   createCommandMatchingMockExecutor,
 17 | } from '../../../../test-utils/mock-executors.ts';
 18 | 
 19 | describe('get_app_bundle_id plugin', () => {
 20 |   // Helper function to create mock executor for command matching
 21 |   const createMockExecutorForCommands = (results: Record<string, string | Error>) => {
 22 |     return createCommandMatchingMockExecutor(
 23 |       Object.fromEntries(
 24 |         Object.entries(results).map(([command, result]) => [
 25 |           command,
 26 |           result instanceof Error
 27 |             ? { success: false, error: result.message }
 28 |             : { success: true, output: result },
 29 |         ]),
 30 |       ),
 31 |     );
 32 |   };
 33 | 
 34 |   describe('Export Field Validation (Literal)', () => {
 35 |     it('should have correct name', () => {
 36 |       expect(plugin.name).toBe('get_app_bundle_id');
 37 |     });
 38 | 
 39 |     it('should have correct description', () => {
 40 |       expect(plugin.description).toBe(
 41 |         "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' })",
 42 |       );
 43 |     });
 44 | 
 45 |     it('should have handler function', () => {
 46 |       expect(typeof plugin.handler).toBe('function');
 47 |     });
 48 | 
 49 |     it('should validate schema with valid inputs', () => {
 50 |       const schema = z.object(plugin.schema);
 51 |       expect(schema.safeParse({ appPath: '/path/to/MyApp.app' }).success).toBe(true);
 52 |       expect(schema.safeParse({ appPath: '/Users/dev/MyApp.app' }).success).toBe(true);
 53 |     });
 54 | 
 55 |     it('should validate schema with invalid inputs', () => {
 56 |       const schema = z.object(plugin.schema);
 57 |       expect(schema.safeParse({}).success).toBe(false);
 58 |       expect(schema.safeParse({ appPath: 123 }).success).toBe(false);
 59 |       expect(schema.safeParse({ appPath: null }).success).toBe(false);
 60 |       expect(schema.safeParse({ appPath: undefined }).success).toBe(false);
 61 |     });
 62 |   });
 63 | 
 64 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 65 |     it('should return error when appPath validation fails', async () => {
 66 |       // Test validation through the handler which uses Zod validation
 67 |       const result = await plugin.handler({});
 68 | 
 69 |       expect(result).toEqual({
 70 |         content: [
 71 |           {
 72 |             type: 'text',
 73 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nappPath: Required',
 74 |           },
 75 |         ],
 76 |         isError: true,
 77 |       });
 78 |     });
 79 | 
 80 |     it('should return error when file exists validation fails', async () => {
 81 |       const mockExecutor = createMockExecutorForCommands({});
 82 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
 83 |         existsSync: () => false,
 84 |       });
 85 | 
 86 |       const result = await get_app_bundle_idLogic(
 87 |         { appPath: '/path/to/MyApp.app' },
 88 |         mockExecutor,
 89 |         mockFileSystemExecutor,
 90 |       );
 91 | 
 92 |       expect(result).toEqual({
 93 |         content: [
 94 |           {
 95 |             type: 'text',
 96 |             text: "File not found: '/path/to/MyApp.app'. Please check the path and try again.",
 97 |           },
 98 |         ],
 99 |         isError: true,
100 |       });
101 |     });
102 | 
103 |     it('should return success with bundle ID using defaults read', async () => {
104 |       const mockExecutor = createMockExecutorForCommands({
105 |         'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': 'com.example.MyApp',
106 |       });
107 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
108 |         existsSync: () => true,
109 |       });
110 | 
111 |       const result = await get_app_bundle_idLogic(
112 |         { appPath: '/path/to/MyApp.app' },
113 |         mockExecutor,
114 |         mockFileSystemExecutor,
115 |       );
116 | 
117 |       expect(result).toEqual({
118 |         content: [
119 |           {
120 |             type: 'text',
121 |             text: '✅ Bundle ID: com.example.MyApp',
122 |           },
123 |           {
124 |             type: 'text',
125 |             text: `Next Steps:
126 | - Simulator: install_app_sim + launch_app_sim
127 | - Device: install_app_device + launch_app_device`,
128 |           },
129 |         ],
130 |         isError: false,
131 |       });
132 |     });
133 | 
134 |     it('should fallback to PlistBuddy when defaults read fails', async () => {
135 |       const mockExecutor = createMockExecutorForCommands({
136 |         'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
137 |           'defaults read failed',
138 |         ),
139 |         '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
140 |           'com.example.MyApp',
141 |       });
142 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
143 |         existsSync: () => true,
144 |       });
145 | 
146 |       const result = await get_app_bundle_idLogic(
147 |         { appPath: '/path/to/MyApp.app' },
148 |         mockExecutor,
149 |         mockFileSystemExecutor,
150 |       );
151 | 
152 |       expect(result).toEqual({
153 |         content: [
154 |           {
155 |             type: 'text',
156 |             text: '✅ Bundle ID: com.example.MyApp',
157 |           },
158 |           {
159 |             type: 'text',
160 |             text: `Next Steps:
161 | - Simulator: install_app_sim + launch_app_sim
162 | - Device: install_app_device + launch_app_device`,
163 |           },
164 |         ],
165 |         isError: false,
166 |       });
167 |     });
168 | 
169 |     it('should return error when both extraction methods fail', async () => {
170 |       const mockExecutor = createMockExecutorForCommands({
171 |         'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
172 |           'defaults read failed',
173 |         ),
174 |         '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
175 |           new Error('Command failed'),
176 |       });
177 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
178 |         existsSync: () => true,
179 |       });
180 | 
181 |       const result = await get_app_bundle_idLogic(
182 |         { appPath: '/path/to/MyApp.app' },
183 |         mockExecutor,
184 |         mockFileSystemExecutor,
185 |       );
186 | 
187 |       expect(result).toEqual({
188 |         content: [
189 |           {
190 |             type: 'text',
191 |             text: 'Error extracting app bundle ID: Could not extract bundle ID from Info.plist: Command failed',
192 |           },
193 |           {
194 |             type: 'text',
195 |             text: 'Make sure the path points to a valid app bundle (.app directory).',
196 |           },
197 |         ],
198 |         isError: true,
199 |       });
200 |     });
201 | 
202 |     it('should handle Error objects in catch blocks', async () => {
203 |       const mockExecutor = createMockExecutorForCommands({
204 |         'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
205 |           'defaults read failed',
206 |         ),
207 |         '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
208 |           new Error('Custom error message'),
209 |       });
210 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
211 |         existsSync: () => true,
212 |       });
213 | 
214 |       const result = await get_app_bundle_idLogic(
215 |         { appPath: '/path/to/MyApp.app' },
216 |         mockExecutor,
217 |         mockFileSystemExecutor,
218 |       );
219 | 
220 |       expect(result).toEqual({
221 |         content: [
222 |           {
223 |             type: 'text',
224 |             text: 'Error extracting app bundle ID: Could not extract bundle ID from Info.plist: Custom error message',
225 |           },
226 |           {
227 |             type: 'text',
228 |             text: 'Make sure the path points to a valid app bundle (.app directory).',
229 |           },
230 |         ],
231 |         isError: true,
232 |       });
233 |     });
234 | 
235 |     it('should handle string errors in catch blocks', async () => {
236 |       const mockExecutor = createMockExecutorForCommands({
237 |         'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
238 |           'defaults read failed',
239 |         ),
240 |         '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
241 |           new Error('String error'),
242 |       });
243 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
244 |         existsSync: () => true,
245 |       });
246 | 
247 |       const result = await get_app_bundle_idLogic(
248 |         { appPath: '/path/to/MyApp.app' },
249 |         mockExecutor,
250 |         mockFileSystemExecutor,
251 |       );
252 | 
253 |       expect(result).toEqual({
254 |         content: [
255 |           {
256 |             type: 'text',
257 |             text: 'Error extracting app bundle ID: Could not extract bundle ID from Info.plist: String error',
258 |           },
259 |           {
260 |             type: 'text',
261 |             text: 'Make sure the path points to a valid app bundle (.app directory).',
262 |           },
263 |         ],
264 |         isError: true,
265 |       });
266 |     });
267 | 
268 |     it('should handle schema validation error when appPath is null', async () => {
269 |       // Test validation through the handler which uses Zod validation
270 |       const result = await plugin.handler({ appPath: null });
271 | 
272 |       expect(result).toEqual({
273 |         content: [
274 |           {
275 |             type: 'text',
276 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nappPath: Expected string, received null',
277 |           },
278 |         ],
279 |         isError: true,
280 |       });
281 |     });
282 | 
283 |     it('should handle schema validation with missing appPath', async () => {
284 |       // Test validation through the handler which uses Zod validation
285 |       const result = await plugin.handler({});
286 | 
287 |       expect(result).toEqual({
288 |         content: [
289 |           {
290 |             type: 'text',
291 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nappPath: Required',
292 |           },
293 |         ],
294 |         isError: true,
295 |       });
296 |     });
297 | 
298 |     it('should handle schema validation with undefined appPath', async () => {
299 |       // Test validation through the handler which uses Zod validation
300 |       const result = await plugin.handler({ appPath: undefined });
301 | 
302 |       expect(result).toEqual({
303 |         content: [
304 |           {
305 |             type: 'text',
306 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nappPath: Required',
307 |           },
308 |         ],
309 |         isError: true,
310 |       });
311 |     });
312 | 
313 |     it('should handle schema validation with number type appPath', async () => {
314 |       // Test validation through the handler which uses Zod validation
315 |       const result = await plugin.handler({ appPath: 123 });
316 | 
317 |       expect(result).toEqual({
318 |         content: [
319 |           {
320 |             type: 'text',
321 |             text: 'Error: Parameter validation failed\nDetails: Invalid parameters:\nappPath: Expected string, received number',
322 |           },
323 |         ],
324 |         isError: true,
325 |       });
326 |     });
327 |   });
328 | });
329 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach } from 'vitest';
  2 | import { z } from 'zod';
  3 | import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
  4 | import plugin, { showBuildSettingsLogic } from '../show_build_settings.ts';
  5 | import { sessionStore } from '../../../../utils/session-store.ts';
  6 | 
  7 | describe('show_build_settings plugin', () => {
  8 |   beforeEach(() => {
  9 |     sessionStore.clear();
 10 |   });
 11 |   describe('Export Field Validation (Literal)', () => {
 12 |     it('should have correct name', () => {
 13 |       expect(plugin.name).toBe('show_build_settings');
 14 |     });
 15 | 
 16 |     it('should have correct description', () => {
 17 |       expect(plugin.description).toBe('Shows xcodebuild build settings.');
 18 |     });
 19 | 
 20 |     it('should have handler function', () => {
 21 |       expect(typeof plugin.handler).toBe('function');
 22 |     });
 23 | 
 24 |     it('should expose an empty public schema', () => {
 25 |       const schema = z.object(plugin.schema).strict();
 26 |       expect(schema.safeParse({}).success).toBe(true);
 27 |       expect(schema.safeParse({ projectPath: '/path.xcodeproj' }).success).toBe(false);
 28 |       expect(schema.safeParse({ scheme: 'App' }).success).toBe(false);
 29 |       expect(Object.keys(plugin.schema)).toEqual([]);
 30 |     });
 31 |   });
 32 | 
 33 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 34 |     it('should execute with valid parameters', async () => {
 35 |       const mockExecutor = createMockExecutor({
 36 |         success: true,
 37 |         output: 'Mock build settings output',
 38 |         error: undefined,
 39 |         process: { pid: 12345 },
 40 |       });
 41 | 
 42 |       const result = await showBuildSettingsLogic(
 43 |         { projectPath: '/valid/path.xcodeproj', scheme: 'MyScheme' },
 44 |         mockExecutor,
 45 |       );
 46 |       expect(result.isError).toBe(false);
 47 |       expect(result.content[0].text).toContain('✅ Build settings for scheme MyScheme:');
 48 |     });
 49 | 
 50 |     it('should test Zod validation through handler', async () => {
 51 |       // Test the actual tool handler which includes Zod validation
 52 |       const result = await plugin.handler({
 53 |         projectPath: null,
 54 |         scheme: 'MyScheme',
 55 |       });
 56 | 
 57 |       expect(result.isError).toBe(true);
 58 |       expect(result.content[0].text).toContain('Missing required session defaults');
 59 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 60 |     });
 61 | 
 62 |     it('should return success with build settings', async () => {
 63 |       const calls: any[] = [];
 64 |       const mockExecutor = createMockExecutor({
 65 |         success: true,
 66 |         output: `Build settings from command line:
 67 |     ARCHS = arm64
 68 |     BUILD_DIR = /Users/dev/Build/Products
 69 |     CONFIGURATION = Debug
 70 |     DEVELOPMENT_TEAM = ABC123DEF4
 71 |     PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp
 72 |     PRODUCT_NAME = MyApp
 73 |     SUPPORTED_PLATFORMS = iphoneos iphonesimulator`,
 74 |         error: undefined,
 75 |         process: { pid: 12345 },
 76 |       });
 77 | 
 78 |       // Wrap mockExecutor to track calls
 79 |       const wrappedExecutor = (...args: any[]) => {
 80 |         calls.push(args);
 81 |         return mockExecutor(...args);
 82 |       };
 83 | 
 84 |       const result = await showBuildSettingsLogic(
 85 |         {
 86 |           projectPath: '/path/to/MyProject.xcodeproj',
 87 |           scheme: 'MyScheme',
 88 |         },
 89 |         wrappedExecutor,
 90 |       );
 91 | 
 92 |       expect(calls).toHaveLength(1);
 93 |       expect(calls[0]).toEqual([
 94 |         [
 95 |           'xcodebuild',
 96 |           '-showBuildSettings',
 97 |           '-project',
 98 |           '/path/to/MyProject.xcodeproj',
 99 |           '-scheme',
100 |           'MyScheme',
101 |         ],
102 |         'Show Build Settings',
103 |         true,
104 |       ]);
105 | 
106 |       expect(result).toEqual({
107 |         content: [
108 |           {
109 |             type: 'text',
110 |             text: '✅ Build settings for scheme MyScheme:',
111 |           },
112 |           {
113 |             type: 'text',
114 |             text: `Build settings from command line:
115 |     ARCHS = arm64
116 |     BUILD_DIR = /Users/dev/Build/Products
117 |     CONFIGURATION = Debug
118 |     DEVELOPMENT_TEAM = ABC123DEF4
119 |     PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp
120 |     PRODUCT_NAME = MyApp
121 |     SUPPORTED_PLATFORMS = iphoneos iphonesimulator`,
122 |           },
123 |         ],
124 |         isError: false,
125 |       });
126 |     });
127 | 
128 |     it('should return error when command fails', async () => {
129 |       const mockExecutor = createMockExecutor({
130 |         success: false,
131 |         output: '',
132 |         error: 'Scheme not found',
133 |         process: { pid: 12345 },
134 |       });
135 | 
136 |       const result = await showBuildSettingsLogic(
137 |         {
138 |           projectPath: '/path/to/MyProject.xcodeproj',
139 |           scheme: 'InvalidScheme',
140 |         },
141 |         mockExecutor,
142 |       );
143 | 
144 |       expect(result).toEqual({
145 |         content: [{ type: 'text', text: 'Failed to show build settings: Scheme not found' }],
146 |         isError: true,
147 |       });
148 |     });
149 | 
150 |     it('should handle Error objects in catch blocks', async () => {
151 |       const mockExecutor = async () => {
152 |         throw new Error('Command execution failed');
153 |       };
154 | 
155 |       const result = await showBuildSettingsLogic(
156 |         {
157 |           projectPath: '/path/to/MyProject.xcodeproj',
158 |           scheme: 'MyScheme',
159 |         },
160 |         mockExecutor,
161 |       );
162 | 
163 |       expect(result).toEqual({
164 |         content: [{ type: 'text', text: 'Error showing build settings: Command execution failed' }],
165 |         isError: true,
166 |       });
167 |     });
168 |   });
169 | 
170 |   describe('XOR Validation', () => {
171 |     it('should error when neither projectPath nor workspacePath provided', async () => {
172 |       const result = await plugin.handler({
173 |         scheme: 'MyScheme',
174 |       });
175 | 
176 |       expect(result.isError).toBe(true);
177 |       expect(result.content[0].text).toContain('Missing required session defaults');
178 |       expect(result.content[0].text).toContain('Provide a project or workspace');
179 |     });
180 | 
181 |     it('should error when both projectPath and workspacePath provided', async () => {
182 |       const result = await plugin.handler({
183 |         projectPath: '/path/project.xcodeproj',
184 |         workspacePath: '/path/workspace.xcworkspace',
185 |         scheme: 'MyScheme',
186 |       });
187 | 
188 |       expect(result.isError).toBe(true);
189 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
190 |     });
191 | 
192 |     it('should work with projectPath only', async () => {
193 |       const mockExecutor = createMockExecutor({
194 |         success: true,
195 |         output: 'Mock build settings output',
196 |       });
197 | 
198 |       const result = await showBuildSettingsLogic(
199 |         { projectPath: '/valid/path.xcodeproj', scheme: 'MyScheme' },
200 |         mockExecutor,
201 |       );
202 | 
203 |       expect(result.isError).toBe(false);
204 |       expect(result.content[0].text).toContain('✅ Build settings for scheme MyScheme:');
205 |     });
206 | 
207 |     it('should work with workspacePath only', async () => {
208 |       const mockExecutor = createMockExecutor({
209 |         success: true,
210 |         output: 'Mock build settings output',
211 |       });
212 | 
213 |       const result = await showBuildSettingsLogic(
214 |         { workspacePath: '/valid/path.xcworkspace', scheme: 'MyScheme' },
215 |         mockExecutor,
216 |       );
217 | 
218 |       expect(result.isError).toBe(false);
219 |       expect(result.content[0].text).toContain('✅ Build settings retrieved successfully');
220 |     });
221 |   });
222 | 
223 |   describe('Session requirement handling', () => {
224 |     it('should require scheme when not provided', async () => {
225 |       const result = await plugin.handler({
226 |         projectPath: '/path/to/MyProject.xcodeproj',
227 |       } as any);
228 | 
229 |       expect(result.isError).toBe(true);
230 |       expect(result.content[0].text).toContain('Missing required session defaults');
231 |       expect(result.content[0].text).toContain('scheme is required');
232 |     });
233 | 
234 |     it('should surface project/workspace requirement even with scheme default', async () => {
235 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
236 | 
237 |       const result = await plugin.handler({});
238 | 
239 |       expect(result.isError).toBe(true);
240 |       expect(result.content[0].text).toContain('Missing required session defaults');
241 |       expect(result.content[0].text).toContain('Provide a project or workspace');
242 |     });
243 |   });
244 | 
245 |   describe('showBuildSettingsLogic function', () => {
246 |     it('should return success with build settings', async () => {
247 |       const calls: any[] = [];
248 |       const mockExecutor = createMockExecutor({
249 |         success: true,
250 |         output: `Build settings from command line:
251 |     ARCHS = arm64
252 |     BUILD_DIR = /Users/dev/Build/Products
253 |     CONFIGURATION = Debug
254 |     DEVELOPMENT_TEAM = ABC123DEF4
255 |     PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp
256 |     PRODUCT_NAME = MyApp
257 |     SUPPORTED_PLATFORMS = iphoneos iphonesimulator`,
258 |         error: undefined,
259 |         process: { pid: 12345 },
260 |       });
261 | 
262 |       // Wrap mockExecutor to track calls
263 |       const wrappedExecutor = (...args: any[]) => {
264 |         calls.push(args);
265 |         return mockExecutor(...args);
266 |       };
267 | 
268 |       const result = await showBuildSettingsLogic(
269 |         {
270 |           projectPath: '/path/to/MyProject.xcodeproj',
271 |           scheme: 'MyScheme',
272 |         },
273 |         wrappedExecutor,
274 |       );
275 | 
276 |       expect(calls).toHaveLength(1);
277 |       expect(calls[0]).toEqual([
278 |         [
279 |           'xcodebuild',
280 |           '-showBuildSettings',
281 |           '-project',
282 |           '/path/to/MyProject.xcodeproj',
283 |           '-scheme',
284 |           'MyScheme',
285 |         ],
286 |         'Show Build Settings',
287 |         true,
288 |       ]);
289 | 
290 |       expect(result).toEqual({
291 |         content: [
292 |           {
293 |             type: 'text',
294 |             text: '✅ Build settings for scheme MyScheme:',
295 |           },
296 |           {
297 |             type: 'text',
298 |             text: `Build settings from command line:
299 |     ARCHS = arm64
300 |     BUILD_DIR = /Users/dev/Build/Products
301 |     CONFIGURATION = Debug
302 |     DEVELOPMENT_TEAM = ABC123DEF4
303 |     PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp
304 |     PRODUCT_NAME = MyApp
305 |     SUPPORTED_PLATFORMS = iphoneos iphonesimulator`,
306 |           },
307 |         ],
308 |         isError: false,
309 |       });
310 |     });
311 | 
312 |     it('should return error when command fails', async () => {
313 |       const mockExecutor = createMockExecutor({
314 |         success: false,
315 |         output: '',
316 |         error: 'Scheme not found',
317 |         process: { pid: 12345 },
318 |       });
319 | 
320 |       const result = await showBuildSettingsLogic(
321 |         {
322 |           projectPath: '/path/to/MyProject.xcodeproj',
323 |           scheme: 'InvalidScheme',
324 |         },
325 |         mockExecutor,
326 |       );
327 | 
328 |       expect(result).toEqual({
329 |         content: [{ type: 'text', text: 'Failed to show build settings: Scheme not found' }],
330 |         isError: true,
331 |       });
332 |     });
333 | 
334 |     it('should handle Error objects in catch blocks', async () => {
335 |       const mockExecutor = async () => {
336 |         throw new Error('Command execution failed');
337 |       };
338 | 
339 |       const result = await showBuildSettingsLogic(
340 |         {
341 |           projectPath: '/path/to/MyProject.xcodeproj',
342 |           scheme: 'MyScheme',
343 |         },
344 |         mockExecutor,
345 |       );
346 | 
347 |       expect(result).toEqual({
348 |         content: [{ type: 'text', text: 'Error showing build settings: Command execution failed' }],
349 |         isError: true,
350 |       });
351 |     });
352 |   });
353 | });
354 | 
```
Page 7/14FirstPrevNextLast