This is page 5 of 11. Use http://codebase.md/cameroncooke/xcodebuildmcp?page={x} to view the full context. # Directory Structure ``` ├── .axe-version ├── .claude │ └── agents │ └── xcodebuild-mcp-qa-tester.md ├── .cursor │ ├── BUGBOT.md │ └── environment.json ├── .cursorrules ├── .github │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows │ ├── ci.yml │ ├── claude-code-review.yml │ ├── claude-dispatch.yml │ ├── claude.yml │ ├── droid-code-review.yml │ ├── README.md │ ├── release.yml │ └── sentry.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── mcp.json │ ├── settings.json │ └── tasks.json ├── AGENTS.md ├── banner.png ├── build-plugins │ ├── plugin-discovery.js │ ├── plugin-discovery.ts │ └── tsconfig.json ├── CHANGELOG.md ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── docs │ ├── ARCHITECTURE.md │ ├── CODE_QUALITY.md │ ├── CONTRIBUTING.md │ ├── ESLINT_TYPE_SAFETY.md │ ├── MANUAL_TESTING.md │ ├── NODEJS_2025.md │ ├── PLUGIN_DEVELOPMENT.md │ ├── RELEASE_PROCESS.md │ ├── RELOADEROO_FOR_XCODEBUILDMCP.md │ ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md │ ├── RELOADEROO.md │ ├── session_management_plan.md │ ├── session-aware-migration-todo.md │ ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md │ ├── TESTING.md │ └── TOOLS.md ├── eslint.config.js ├── example_projects │ ├── .vscode │ │ └── launch.json │ ├── iOS │ │ ├── .cursor │ │ │ └── rules │ │ │ └── errors.mdc │ │ ├── .vscode │ │ │ └── settings.json │ │ ├── Makefile │ │ ├── MCPTest │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── ContentView.swift │ │ │ ├── MCPTestApp.swift │ │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ ├── MCPTest.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── MCPTest.xcscheme │ │ └── MCPTestUITests │ │ └── MCPTestUITests.swift │ ├── iOS_Calculator │ │ ├── CalculatorApp │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── CalculatorApp.swift │ │ │ └── CalculatorApp.xctestplan │ │ ├── CalculatorApp.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── CalculatorApp.xcscheme │ │ ├── CalculatorApp.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ ├── CalculatorAppPackage │ │ │ ├── .gitignore │ │ │ ├── Package.swift │ │ │ ├── Sources │ │ │ │ └── CalculatorAppFeature │ │ │ │ ├── BackgroundEffect.swift │ │ │ │ ├── CalculatorButton.swift │ │ │ │ ├── CalculatorDisplay.swift │ │ │ │ ├── CalculatorInputHandler.swift │ │ │ │ ├── CalculatorService.swift │ │ │ │ └── ContentView.swift │ │ │ └── Tests │ │ │ └── CalculatorAppFeatureTests │ │ │ └── CalculatorServiceTests.swift │ │ ├── CalculatorAppTests │ │ │ └── CalculatorAppTests.swift │ │ └── Config │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── Shared.xcconfig │ │ └── Tests.xcconfig │ ├── macOS │ │ ├── MCPTest │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── ContentView.swift │ │ │ ├── MCPTest.entitlements │ │ │ ├── MCPTestApp.swift │ │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ └── MCPTest.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ └── xcschemes │ │ └── MCPTest.xcscheme │ └── spm │ ├── .gitignore │ ├── Package.resolved │ ├── Package.swift │ ├── Sources │ │ ├── long-server │ │ │ └── main.swift │ │ ├── quick-task │ │ │ └── main.swift │ │ ├── spm │ │ │ └── main.swift │ │ └── TestLib │ │ └── TaskManager.swift │ └── Tests │ └── TestLibTests │ └── SimpleTests.swift ├── LICENSE ├── mcp-install-dark.png ├── package-lock.json ├── package.json ├── README.md ├── scripts │ ├── analysis │ │ └── tools-analysis.ts │ ├── bundle-axe.sh │ ├── check-code-patterns.js │ ├── release.sh │ ├── tools-cli.ts │ └── update-tools-docs.ts ├── server.json ├── smithery.yaml ├── src │ ├── core │ │ ├── __tests__ │ │ │ └── resources.test.ts │ │ ├── dynamic-tools.ts │ │ ├── plugin-registry.ts │ │ ├── plugin-types.ts │ │ └── resources.ts │ ├── doctor-cli.ts │ ├── index.ts │ ├── mcp │ │ ├── resources │ │ │ ├── __tests__ │ │ │ │ ├── devices.test.ts │ │ │ │ ├── doctor.test.ts │ │ │ │ └── simulators.test.ts │ │ │ ├── devices.ts │ │ │ ├── doctor.ts │ │ │ └── simulators.ts │ │ └── tools │ │ ├── device │ │ │ ├── __tests__ │ │ │ │ ├── build_device.test.ts │ │ │ │ ├── get_device_app_path.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── install_app_device.test.ts │ │ │ │ ├── launch_app_device.test.ts │ │ │ │ ├── list_devices.test.ts │ │ │ │ ├── re-exports.test.ts │ │ │ │ ├── stop_app_device.test.ts │ │ │ │ └── test_device.test.ts │ │ │ ├── build_device.ts │ │ │ ├── clean.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_app_bundle_id.ts │ │ │ ├── get_device_app_path.ts │ │ │ ├── index.ts │ │ │ ├── install_app_device.ts │ │ │ ├── launch_app_device.ts │ │ │ ├── list_devices.ts │ │ │ ├── list_schemes.ts │ │ │ ├── show_build_settings.ts │ │ │ ├── start_device_log_cap.ts │ │ │ ├── stop_app_device.ts │ │ │ ├── stop_device_log_cap.ts │ │ │ └── test_device.ts │ │ ├── discovery │ │ │ ├── __tests__ │ │ │ │ └── discover_tools.test.ts │ │ │ ├── discover_tools.ts │ │ │ └── index.ts │ │ ├── doctor │ │ │ ├── __tests__ │ │ │ │ ├── doctor.test.ts │ │ │ │ └── index.test.ts │ │ │ ├── doctor.ts │ │ │ ├── index.ts │ │ │ └── lib │ │ │ └── doctor.deps.ts │ │ ├── logging │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ ├── start_device_log_cap.test.ts │ │ │ │ ├── start_sim_log_cap.test.ts │ │ │ │ ├── stop_device_log_cap.test.ts │ │ │ │ └── stop_sim_log_cap.test.ts │ │ │ ├── index.ts │ │ │ ├── start_device_log_cap.ts │ │ │ ├── start_sim_log_cap.ts │ │ │ ├── stop_device_log_cap.ts │ │ │ └── stop_sim_log_cap.ts │ │ ├── macos │ │ │ ├── __tests__ │ │ │ │ ├── build_macos.test.ts │ │ │ │ ├── build_run_macos.test.ts │ │ │ │ ├── get_mac_app_path.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── launch_mac_app.test.ts │ │ │ │ ├── re-exports.test.ts │ │ │ │ ├── stop_mac_app.test.ts │ │ │ │ └── test_macos.test.ts │ │ │ ├── build_macos.ts │ │ │ ├── build_run_macos.ts │ │ │ ├── clean.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_mac_app_path.ts │ │ │ ├── get_mac_bundle_id.ts │ │ │ ├── index.ts │ │ │ ├── launch_mac_app.ts │ │ │ ├── list_schemes.ts │ │ │ ├── show_build_settings.ts │ │ │ ├── stop_mac_app.ts │ │ │ └── test_macos.ts │ │ ├── project-discovery │ │ │ ├── __tests__ │ │ │ │ ├── discover_projs.test.ts │ │ │ │ ├── get_app_bundle_id.test.ts │ │ │ │ ├── get_mac_bundle_id.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── list_schemes.test.ts │ │ │ │ └── show_build_settings.test.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_app_bundle_id.ts │ │ │ ├── get_mac_bundle_id.ts │ │ │ ├── index.ts │ │ │ ├── list_schemes.ts │ │ │ └── show_build_settings.ts │ │ ├── project-scaffolding │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ ├── scaffold_ios_project.test.ts │ │ │ │ └── scaffold_macos_project.test.ts │ │ │ ├── index.ts │ │ │ ├── scaffold_ios_project.ts │ │ │ └── scaffold_macos_project.ts │ │ ├── session-management │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ ├── session_clear_defaults.test.ts │ │ │ │ ├── session_set_defaults.test.ts │ │ │ │ └── session_show_defaults.test.ts │ │ │ ├── index.ts │ │ │ ├── session_clear_defaults.ts │ │ │ ├── session_set_defaults.ts │ │ │ └── session_show_defaults.ts │ │ ├── simulator │ │ │ ├── __tests__ │ │ │ │ ├── boot_sim.test.ts │ │ │ │ ├── build_run_sim.test.ts │ │ │ │ ├── build_sim.test.ts │ │ │ │ ├── get_sim_app_path.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── install_app_sim.test.ts │ │ │ │ ├── launch_app_logs_sim.test.ts │ │ │ │ ├── launch_app_sim.test.ts │ │ │ │ ├── list_sims.test.ts │ │ │ │ ├── open_sim.test.ts │ │ │ │ ├── record_sim_video.test.ts │ │ │ │ ├── screenshot.test.ts │ │ │ │ ├── stop_app_sim.test.ts │ │ │ │ └── test_sim.test.ts │ │ │ ├── boot_sim.ts │ │ │ ├── build_run_sim.ts │ │ │ ├── build_sim.ts │ │ │ ├── clean.ts │ │ │ ├── describe_ui.ts │ │ │ ├── discover_projs.ts │ │ │ ├── get_app_bundle_id.ts │ │ │ ├── get_sim_app_path.ts │ │ │ ├── index.ts │ │ │ ├── install_app_sim.ts │ │ │ ├── launch_app_logs_sim.ts │ │ │ ├── launch_app_sim.ts │ │ │ ├── list_schemes.ts │ │ │ ├── list_sims.ts │ │ │ ├── open_sim.ts │ │ │ ├── record_sim_video.ts │ │ │ ├── screenshot.ts │ │ │ ├── show_build_settings.ts │ │ │ ├── stop_app_sim.ts │ │ │ └── test_sim.ts │ │ ├── simulator-management │ │ │ ├── __tests__ │ │ │ │ ├── erase_sims.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── reset_sim_location.test.ts │ │ │ │ ├── set_sim_appearance.test.ts │ │ │ │ ├── set_sim_location.test.ts │ │ │ │ └── sim_statusbar.test.ts │ │ │ ├── boot_sim.ts │ │ │ ├── erase_sims.ts │ │ │ ├── index.ts │ │ │ ├── list_sims.ts │ │ │ ├── open_sim.ts │ │ │ ├── reset_sim_location.ts │ │ │ ├── set_sim_appearance.ts │ │ │ ├── set_sim_location.ts │ │ │ └── sim_statusbar.ts │ │ ├── swift-package │ │ │ ├── __tests__ │ │ │ │ ├── active-processes.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── swift_package_build.test.ts │ │ │ │ ├── swift_package_clean.test.ts │ │ │ │ ├── swift_package_list.test.ts │ │ │ │ ├── swift_package_run.test.ts │ │ │ │ ├── swift_package_stop.test.ts │ │ │ │ └── swift_package_test.test.ts │ │ │ ├── active-processes.ts │ │ │ ├── index.ts │ │ │ ├── swift_package_build.ts │ │ │ ├── swift_package_clean.ts │ │ │ ├── swift_package_list.ts │ │ │ ├── swift_package_run.ts │ │ │ ├── swift_package_stop.ts │ │ │ └── swift_package_test.ts │ │ ├── ui-testing │ │ │ ├── __tests__ │ │ │ │ ├── button.test.ts │ │ │ │ ├── describe_ui.test.ts │ │ │ │ ├── gesture.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── key_press.test.ts │ │ │ │ ├── key_sequence.test.ts │ │ │ │ ├── long_press.test.ts │ │ │ │ ├── screenshot.test.ts │ │ │ │ ├── swipe.test.ts │ │ │ │ ├── tap.test.ts │ │ │ │ ├── touch.test.ts │ │ │ │ └── type_text.test.ts │ │ │ ├── button.ts │ │ │ ├── describe_ui.ts │ │ │ ├── gesture.ts │ │ │ ├── index.ts │ │ │ ├── key_press.ts │ │ │ ├── key_sequence.ts │ │ │ ├── long_press.ts │ │ │ ├── screenshot.ts │ │ │ ├── swipe.ts │ │ │ ├── tap.ts │ │ │ ├── touch.ts │ │ │ └── type_text.ts │ │ └── utilities │ │ ├── __tests__ │ │ │ ├── clean.test.ts │ │ │ └── index.test.ts │ │ ├── clean.ts │ │ └── index.ts │ ├── server │ │ └── server.ts │ ├── test-utils │ │ └── mock-executors.ts │ ├── types │ │ └── common.ts │ └── utils │ ├── __tests__ │ │ ├── build-utils.test.ts │ │ ├── environment.test.ts │ │ ├── session-aware-tool-factory.test.ts │ │ ├── session-store.test.ts │ │ ├── simulator-utils.test.ts │ │ ├── test-runner-env-integration.test.ts │ │ └── typed-tool-factory.test.ts │ ├── axe │ │ └── index.ts │ ├── axe-helpers.ts │ ├── build │ │ └── index.ts │ ├── build-utils.ts │ ├── capabilities.ts │ ├── command.ts │ ├── CommandExecutor.ts │ ├── environment.ts │ ├── errors.ts │ ├── execution │ │ └── index.ts │ ├── FileSystemExecutor.ts │ ├── log_capture.ts │ ├── log-capture │ │ └── index.ts │ ├── logger.ts │ ├── logging │ │ └── index.ts │ ├── plugin-registry │ │ └── index.ts │ ├── responses │ │ └── index.ts │ ├── schema-helpers.ts │ ├── sentry.ts │ ├── session-store.ts │ ├── simulator-utils.ts │ ├── template │ │ └── index.ts │ ├── template-manager.ts │ ├── test │ │ └── index.ts │ ├── test-common.ts │ ├── tool-registry.ts │ ├── typed-tool-factory.ts │ ├── validation │ │ └── index.ts │ ├── validation.ts │ ├── version │ │ └── index.ts │ ├── video_capture.ts │ ├── video-capture │ │ └── index.ts │ ├── xcode.ts │ ├── xcodemake │ │ └── index.ts │ └── xcodemake.ts ├── tsconfig.json ├── tsconfig.test.json ├── tsup.config.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for swift_package_test plugin * Following CLAUDE.md testing standards with literal validation * Using dependency injection for deterministic testing */ import { describe, it, expect } from 'vitest'; import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, } from '../../../../test-utils/mock-executors.ts'; import swiftPackageTest, { swift_package_testLogic } from '../swift_package_test.ts'; describe('swift_package_test plugin', () => { describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(swiftPackageTest.name).toBe('swift_package_test'); }); it('should have correct description', () => { expect(swiftPackageTest.description).toBe('Runs tests for a Swift Package with swift test'); }); it('should have handler function', () => { expect(typeof swiftPackageTest.handler).toBe('function'); }); it('should validate schema correctly', () => { // Test required fields expect(swiftPackageTest.schema.packagePath.safeParse('/test/package').success).toBe(true); expect(swiftPackageTest.schema.packagePath.safeParse('').success).toBe(true); // Test optional fields expect(swiftPackageTest.schema.testProduct.safeParse('MyTests').success).toBe(true); expect(swiftPackageTest.schema.testProduct.safeParse(undefined).success).toBe(true); expect(swiftPackageTest.schema.filter.safeParse('Test.*').success).toBe(true); expect(swiftPackageTest.schema.filter.safeParse(undefined).success).toBe(true); expect(swiftPackageTest.schema.configuration.safeParse('debug').success).toBe(true); expect(swiftPackageTest.schema.configuration.safeParse('release').success).toBe(true); expect(swiftPackageTest.schema.configuration.safeParse(undefined).success).toBe(true); expect(swiftPackageTest.schema.parallel.safeParse(true).success).toBe(true); expect(swiftPackageTest.schema.parallel.safeParse(undefined).success).toBe(true); expect(swiftPackageTest.schema.showCodecov.safeParse(true).success).toBe(true); expect(swiftPackageTest.schema.showCodecov.safeParse(undefined).success).toBe(true); expect(swiftPackageTest.schema.parseAsLibrary.safeParse(true).success).toBe(true); expect(swiftPackageTest.schema.parseAsLibrary.safeParse(undefined).success).toBe(true); // Test invalid inputs expect(swiftPackageTest.schema.packagePath.safeParse(null).success).toBe(false); expect(swiftPackageTest.schema.configuration.safeParse('invalid').success).toBe(false); expect(swiftPackageTest.schema.parallel.safeParse('yes').success).toBe(false); expect(swiftPackageTest.schema.showCodecov.safeParse('yes').success).toBe(false); expect(swiftPackageTest.schema.parseAsLibrary.safeParse('yes').success).toBe(false); }); }); describe('Command Generation Testing', () => { it('should build correct command for basic test', async () => { const calls: any[] = []; const mockExecutor = async ( args: string[], name: string, hideOutput: boolean, workingDir: string | undefined, ) => { calls.push({ args, name, hideOutput, workingDir }); return { success: true, output: 'Test Passed', error: undefined, process: { pid: 12345 }, }; }; await swift_package_testLogic( { packagePath: '/test/package', }, mockExecutor, ); expect(calls).toHaveLength(1); expect(calls[0]).toEqual({ args: ['swift', 'test', '--package-path', '/test/package'], name: 'Swift Package Test', hideOutput: true, workingDir: undefined, }); }); it('should build correct command with all parameters', async () => { const calls: any[] = []; const mockExecutor = async ( args: string[], name: string, hideOutput: boolean, workingDir: string | undefined, ) => { calls.push({ args, name, hideOutput, workingDir }); return { success: true, output: 'Tests completed', error: undefined, process: { pid: 12345 }, }; }; await swift_package_testLogic( { packagePath: '/test/package', testProduct: 'MyTests', filter: 'Test.*', configuration: 'release', parallel: false, showCodecov: true, parseAsLibrary: true, }, mockExecutor, ); expect(calls).toHaveLength(1); expect(calls[0]).toEqual({ args: [ 'swift', 'test', '--package-path', '/test/package', '-c', 'release', '--test-product', 'MyTests', '--filter', 'Test.*', '--no-parallel', '--show-code-coverage', '-Xswiftc', '-parse-as-library', ], name: 'Swift Package Test', hideOutput: true, workingDir: undefined, }); }); }); describe('Response Logic Testing', () => { it('should handle empty packagePath parameter', async () => { // When packagePath is empty, the function should still process it // but the command execution may fail, which is handled by the executor const mockExecutor = createMockExecutor({ success: true, output: 'Tests completed with empty path', }); const result = await swift_package_testLogic({ packagePath: '' }, mockExecutor); expect(result.isError).toBe(false); expect(result.content[0].text).toBe('✅ Swift package tests completed.'); }); it('should return successful test response', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'All tests passed.', }); const result = await swift_package_testLogic( { packagePath: '/test/package', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ Swift package tests completed.' }, { type: 'text', text: '💡 Next: Execute your app with swift_package_run if tests passed', }, { type: 'text', text: 'All tests passed.' }, ], isError: false, }); }); it('should return error response for test failure', async () => { const mockExecutor = createMockExecutor({ success: false, error: '2 tests failed', }); const result = await swift_package_testLogic( { packagePath: '/test/package', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Swift package tests failed\nDetails: 2 tests failed', }, ], isError: true, }); }); it('should handle spawn error', async () => { const mockExecutor = async () => { throw new Error('spawn ENOENT'); }; const result = await swift_package_testLogic( { packagePath: '/test/package', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Failed to execute swift test\nDetails: spawn ENOENT', }, ], isError: true, }); }); it('should handle successful test with parameters', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'Tests completed.', }); const result = await swift_package_testLogic( { packagePath: '/test/package', testProduct: 'MyTests', filter: 'Test.*', configuration: 'release', parallel: false, showCodecov: true, parseAsLibrary: true, }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ Swift package tests completed.' }, { type: 'text', text: '💡 Next: Execute your app with swift_package_run if tests passed', }, { type: 'text', text: 'Tests completed.' }, ], isError: false, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/__tests__/install_app_sim.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import installAppSim, { install_app_simLogic } from '../install_app_sim.ts'; describe('install_app_sim tool', () => { beforeEach(() => { sessionStore.clear(); }); describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(installAppSim.name).toBe('install_app_sim'); }); it('should have concise description', () => { expect(installAppSim.description).toBe('Installs an app in an iOS simulator.'); }); it('should expose public schema with only appPath', () => { const schema = z.object(installAppSim.schema); expect(schema.safeParse({ appPath: '/path/to/app.app' }).success).toBe(true); expect(schema.safeParse({ appPath: 42 }).success).toBe(false); expect(schema.safeParse({}).success).toBe(false); expect(Object.keys(installAppSim.schema)).toEqual(['appPath']); }); }); describe('Handler Requirements', () => { it('should require simulatorId when not provided', async () => { const result = await installAppSim.handler({ appPath: '/path/to/app.app' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Missing required session defaults'); expect(result.content[0].text).toContain('simulatorId is required'); expect(result.content[0].text).toContain('session-set-defaults'); }); it('should validate appPath when simulatorId default exists', async () => { sessionStore.setDefaults({ simulatorId: 'SIM-UUID' }); const result = await installAppSim.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('appPath: Required'); expect(result.content[0].text).toContain( 'Tip: set session defaults via session-set-defaults', ); }); }); describe('Command Generation', () => { it('should generate correct simctl install command', async () => { const executorCalls: unknown[] = []; const mockExecutor = (...args: unknown[]) => { executorCalls.push(args); return Promise.resolve({ success: true, output: 'App installed', error: undefined, process: { pid: 12345 }, }); }; const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); await install_app_simLogic( { simulatorId: 'test-uuid-123', appPath: '/path/to/app.app', }, mockExecutor, mockFileSystem, ); expect(executorCalls).toEqual([ [ ['xcrun', 'simctl', 'install', 'test-uuid-123', '/path/to/app.app'], 'Install App in Simulator', true, undefined, ], [ ['defaults', 'read', '/path/to/app.app/Info', 'CFBundleIdentifier'], 'Extract Bundle ID', false, undefined, ], ]); }); it('should generate command with different simulator identifier', async () => { const executorCalls: unknown[] = []; const mockExecutor = (...args: unknown[]) => { executorCalls.push(args); return Promise.resolve({ success: true, output: 'App installed', error: undefined, process: { pid: 12345 }, }); }; const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); await install_app_simLogic( { simulatorId: 'different-uuid-456', appPath: '/different/path/MyApp.app', }, mockExecutor, mockFileSystem, ); expect(executorCalls).toEqual([ [ ['xcrun', 'simctl', 'install', 'different-uuid-456', '/different/path/MyApp.app'], 'Install App in Simulator', true, undefined, ], [ ['defaults', 'read', '/different/path/MyApp.app/Info', 'CFBundleIdentifier'], 'Extract Bundle ID', false, undefined, ], ]); }); }); describe('Logic Behavior (Literal Returns)', () => { it('should handle file does not exist', async () => { const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => false, }); const result = await install_app_simLogic( { simulatorId: 'test-uuid-123', appPath: '/path/to/app.app', }, createNoopExecutor(), mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: "File not found: '/path/to/app.app'. Please check the path and try again.", }, ], isError: true, }); }); it('should handle successful install', async () => { let callCount = 0; const mockExecutor = () => { callCount++; if (callCount === 1) { return Promise.resolve({ success: true, output: 'App installed', error: undefined, process: { pid: 12345 }, }); } return Promise.resolve({ success: true, output: 'com.example.myapp', error: undefined, process: { pid: 12345 }, }); }; const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await install_app_simLogic( { simulatorId: 'test-uuid-123', appPath: '/path/to/app.app', }, mockExecutor, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: 'App installed successfully in simulator test-uuid-123', }, { type: 'text', text: `Next Steps: 1. Open the Simulator app: open_sim({}) 2. Launch the app: launch_app_sim({ simulatorId: "test-uuid-123", bundleId: "com.example.myapp" })`, }, ], }); }); it('should handle command failure', async () => { const mockExecutor = () => Promise.resolve({ success: false, output: '', error: 'Install failed', process: { pid: 12345 }, }); const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await install_app_simLogic( { simulatorId: 'test-uuid-123', appPath: '/path/to/app.app', }, mockExecutor, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Install app in simulator operation failed: Install failed', }, ], }); }); it('should handle exception with Error object', async () => { const mockExecutor = () => Promise.reject(new Error('Command execution failed')); const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await install_app_simLogic( { simulatorId: 'test-uuid-123', appPath: '/path/to/app.app', }, mockExecutor, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Install app in simulator operation failed: Command execution failed', }, ], }); }); it('should handle exception with string error', async () => { const mockExecutor = () => Promise.reject('String error'); const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await install_app_simLogic( { simulatorId: 'test-uuid-123', appPath: '/path/to/app.app', }, mockExecutor, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Install app in simulator operation failed: String error', }, ], }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/swift_package_run.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import path from 'node:path'; import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts'; import { log } from '../../../utils/logging/index.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { ToolResponse, createTextContent } from '../../../types/common.ts'; import { addProcess } from './active-processes.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const swiftPackageRunSchema = z.object({ packagePath: z.string().describe('Path to the Swift package root (Required)'), executableName: z .string() .optional() .describe('Name of executable to run (defaults to package name)'), arguments: z.array(z.string()).optional().describe('Arguments to pass to the executable'), configuration: z .enum(['debug', 'release']) .optional() .describe("Build configuration: 'debug' (default) or 'release'"), timeout: z.number().optional().describe('Timeout in seconds (default: 30, max: 300)'), background: z .boolean() .optional() .describe('Run in background and return immediately (default: false)'), parseAsLibrary: z .boolean() .optional() .describe('Add -parse-as-library flag for @main support (default: false)'), }); // Use z.infer for type safety type SwiftPackageRunParams = z.infer<typeof swiftPackageRunSchema>; export async function swift_package_runLogic( params: SwiftPackageRunParams, executor: CommandExecutor, ): Promise<ToolResponse> { const resolvedPath = path.resolve(params.packagePath); const timeout = Math.min(params.timeout ?? 30, 300) * 1000; // Convert to ms, max 5 minutes // Detect test environment to prevent real spawn calls during testing const isTestEnvironment = process.env.VITEST === 'true' || process.env.NODE_ENV === 'test'; const swiftArgs = ['run', '--package-path', resolvedPath]; if (params.configuration && params.configuration.toLowerCase() === 'release') { swiftArgs.push('-c', 'release'); } else if (params.configuration && params.configuration.toLowerCase() !== 'debug') { return createTextResponse("Invalid configuration. Use 'debug' or 'release'.", true); } if (params.parseAsLibrary) { swiftArgs.push('-Xswiftc', '-parse-as-library'); } if (params.executableName) { swiftArgs.push(params.executableName); } // Add double dash before executable arguments if (params.arguments && params.arguments.length > 0) { swiftArgs.push('--'); swiftArgs.push(...params.arguments); } log('info', `Running swift ${swiftArgs.join(' ')}`); try { if (params.background) { // Background mode: Use CommandExecutor but don't wait for completion if (isTestEnvironment) { // In test environment, return mock response without real process const mockPid = 12345; return { content: [ createTextContent( `🚀 Started executable in background (PID: ${mockPid})\n` + `💡 Process is running independently. Use swift_package_stop with PID ${mockPid} to terminate when needed.`, ), ], }; } else { // Production: use CommandExecutor to start the process const command = ['swift', ...swiftArgs]; // Filter out undefined values from process.env const cleanEnv = Object.fromEntries( Object.entries(process.env).filter(([, value]) => value !== undefined), ) as Record<string, string>; const result = await executor( command, 'Swift Package Run (Background)', true, cleanEnv, true, ); // Store the process in active processes system if available if (result.process?.pid) { addProcess(result.process.pid, { process: { kill: (signal?: string) => { // Adapt string signal to NodeJS.Signals if (result.process) { result.process.kill(signal as NodeJS.Signals); } }, on: (event: string, callback: () => void) => { if (result.process) { result.process.on(event, callback); } }, pid: result.process.pid, }, startedAt: new Date(), }); return { content: [ createTextContent( `🚀 Started executable in background (PID: ${result.process.pid})\n` + `💡 Process is running independently. Use swift_package_stop with PID ${result.process.pid} to terminate when needed.`, ), ], }; } else { return { content: [ createTextContent( `🚀 Started executable in background\n` + `💡 Process is running independently. PID not available for this execution.`, ), ], }; } } } else { // Foreground mode: use CommandExecutor but handle long-running processes const command = ['swift', ...swiftArgs]; // Create a promise that will either complete with the command result or timeout const commandPromise = executor(command, 'Swift Package Run', true, undefined); const timeoutPromise = new Promise<{ success: boolean; output: string; error: string; timedOut: boolean; }>((resolve) => { setTimeout(() => { resolve({ success: false, output: '', error: `Process timed out after ${timeout / 1000} seconds`, timedOut: true, }); }, timeout); }); // Race between command completion and timeout const result = await Promise.race([commandPromise, timeoutPromise]); if ('timedOut' in result && result.timedOut) { // For timeout case, the process may still be running - provide timeout response if (isTestEnvironment) { // In test environment, return mock response const mockPid = 12345; return { content: [ createTextContent( `⏱️ Process timed out after ${timeout / 1000} seconds but may continue running.`, ), createTextContent(`PID: ${mockPid} (mock)`), createTextContent( `💡 Process may still be running. Use swift_package_stop with PID ${mockPid} to terminate when needed.`, ), createTextContent(result.output || '(no output so far)'), ], }; } else { // Production: timeout occurred, but we don't start a new process return { content: [ createTextContent(`⏱️ Process timed out after ${timeout / 1000} seconds.`), createTextContent( `💡 Process execution exceeded the timeout limit. Consider using background mode for long-running executables.`, ), createTextContent(result.output || '(no output so far)'), ], }; } } if (result.success) { return { content: [ createTextContent('✅ Swift executable completed successfully.'), createTextContent('💡 Process finished cleanly. Check output for results.'), createTextContent(result.output || '(no output)'), ], }; } else { const content = [ createTextContent('❌ Swift executable failed.'), createTextContent(result.output || '(no output)'), ]; if (result.error) { content.push(createTextContent(`Errors:\n${result.error}`)); } return { content }; } } } catch (error) { const message = error instanceof Error ? error.message : String(error); log('error', `Swift run failed: ${message}`); return createErrorResponse('Failed to execute swift run', message); } } export default { name: 'swift_package_run', description: 'Runs an executable target from a Swift Package with swift run', schema: swiftPackageRunSchema.shape, // MCP SDK compatibility handler: createTypedTool( swiftPackageRunSchema, swift_package_runLogic, getDefaultCommandExecutor, ), }; ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/__tests__/install_app_device.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for install_app_device plugin (device-shared) * Following CLAUDE.md testing standards with literal validation * Using dependency injection for deterministic testing */ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; import installAppDevice, { install_app_deviceLogic } from '../install_app_device.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; describe('install_app_device plugin', () => { beforeEach(() => { sessionStore.clear(); }); describe('Handler Requirements', () => { it('should require deviceId when session defaults are missing', async () => { const result = await installAppDevice.handler({ appPath: '/path/to/test.app', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('deviceId is required'); }); }); describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(installAppDevice.name).toBe('install_app_device'); }); it('should have correct description', () => { expect(installAppDevice.description).toBe('Installs an app on a connected device.'); }); it('should have handler function', () => { expect(typeof installAppDevice.handler).toBe('function'); }); it('should require appPath in public schema', () => { const schema = z.object(installAppDevice.schema).strict(); expect(schema.safeParse({ appPath: '/path/to/test.app' }).success).toBe(true); expect(schema.safeParse({}).success).toBe(false); expect(schema.safeParse({ deviceId: 'test-device-123' }).success).toBe(false); expect(Object.keys(installAppDevice.schema)).toEqual(['appPath']); }); }); describe('Command Generation', () => { it('should generate correct devicectl command with basic parameters', async () => { let capturedCommand: unknown[] = []; let capturedDescription: string = ''; let capturedUseShell: boolean = false; let capturedEnv: unknown = undefined; const mockExecutor = createMockExecutor({ success: true, output: 'App installation successful', process: { pid: 12345 }, }); const trackingExecutor = async ( command: unknown[], description: string, useShell: boolean, env: unknown, ) => { capturedCommand = command; capturedDescription = description; capturedUseShell = useShell; capturedEnv = env; return mockExecutor(command, description, useShell, env); }; await install_app_deviceLogic( { deviceId: 'test-device-123', appPath: '/path/to/test.app', }, trackingExecutor, ); expect(capturedCommand).toEqual([ 'xcrun', 'devicectl', 'device', 'install', 'app', '--device', 'test-device-123', '/path/to/test.app', ]); expect(capturedDescription).toBe('Install app on device'); expect(capturedUseShell).toBe(true); expect(capturedEnv).toBe(undefined); }); it('should generate correct command with different device ID', async () => { let capturedCommand: unknown[] = []; const mockExecutor = createMockExecutor({ success: true, output: 'App installation successful', process: { pid: 12345 }, }); const trackingExecutor = async (command: unknown[]) => { capturedCommand = command; return mockExecutor(command); }; await install_app_deviceLogic( { deviceId: 'different-device-uuid', appPath: '/apps/MyApp.app', }, trackingExecutor, ); expect(capturedCommand).toEqual([ 'xcrun', 'devicectl', 'device', 'install', 'app', '--device', 'different-device-uuid', '/apps/MyApp.app', ]); }); it('should generate correct command with paths containing spaces', async () => { let capturedCommand: unknown[] = []; const mockExecutor = createMockExecutor({ success: true, output: 'App installation successful', process: { pid: 12345 }, }); const trackingExecutor = async (command: unknown[]) => { capturedCommand = command; return mockExecutor(command); }; await install_app_deviceLogic( { deviceId: 'test-device-123', appPath: '/path/to/My App.app', }, trackingExecutor, ); expect(capturedCommand).toEqual([ 'xcrun', 'devicectl', 'device', 'install', 'app', '--device', 'test-device-123', '/path/to/My App.app', ]); }); }); describe('Success Path Tests', () => { it('should return successful installation response', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'App installation successful', }); const result = await install_app_deviceLogic( { deviceId: 'test-device-123', appPath: '/path/to/test.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ App installed successfully on device test-device-123\n\nApp installation successful', }, ], }); }); it('should return successful installation with detailed output', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'Installing app...\nApp bundle: /path/to/test.app\nInstallation completed successfully', }); const result = await install_app_deviceLogic( { deviceId: 'device-456', appPath: '/apps/TestApp.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ App installed successfully on device device-456\n\nInstalling app...\nApp bundle: /path/to/test.app\nInstallation completed successfully', }, ], }); }); it('should return successful installation with empty output', async () => { const mockExecutor = createMockExecutor({ success: true, output: '', }); const result = await install_app_deviceLogic( { deviceId: 'empty-output-device', appPath: '/path/to/app.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ App installed successfully on device empty-output-device\n\n', }, ], }); }); }); describe('Error Handling', () => { it('should return installation failure response', async () => { const mockExecutor = createMockExecutor({ success: false, error: 'Installation failed: App not found', }); const result = await install_app_deviceLogic( { deviceId: 'test-device-123', appPath: '/path/to/nonexistent.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to install app: Installation failed: App not found', }, ], isError: true, }); }); it('should return exception handling response', async () => { const mockExecutor = createMockExecutor(new Error('Network error')); const result = await install_app_deviceLogic( { deviceId: 'test-device-123', appPath: '/path/to/test.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to install app on device: Network error', }, ], isError: true, }); }); it('should return string error handling response', async () => { const mockExecutor = createMockExecutor('String error'); const result = await install_app_deviceLogic( { deviceId: 'test-device-123', appPath: '/path/to/test.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to install app on device: String error', }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for describe_ui tool plugin */ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; import describeUIPlugin, { describe_uiLogic } from '../describe_ui.ts'; describe('Describe UI Plugin', () => { let mockCalls: any[] = []; mockCalls = []; describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(describeUIPlugin.name).toBe('describe_ui'); }); it('should have correct description', () => { expect(describeUIPlugin.description).toBe( 'Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation.', ); }); it('should have handler function', () => { expect(typeof describeUIPlugin.handler).toBe('function'); }); it('should validate schema fields with safeParse', () => { const schema = z.object(describeUIPlugin.schema); // Valid case expect( schema.safeParse({ simulatorUuid: '12345678-1234-1234-1234-123456789012', }).success, ).toBe(true); // Invalid simulatorUuid expect( schema.safeParse({ simulatorUuid: 'invalid-uuid', }).success, ).toBe(false); // Missing simulatorUuid expect(schema.safeParse({}).success).toBe(false); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { it('should handle missing simulatorUuid via schema validation', async () => { // Test the actual handler (not just the logic function) // This demonstrates that Zod validation catches missing parameters const result = await describeUIPlugin.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('simulatorUuid: Required'); }); it('should handle invalid simulatorUuid format via schema validation', async () => { // Test the actual handler with invalid UUID format const result = await describeUIPlugin.handler({ simulatorUuid: 'invalid-uuid-format', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('Invalid Simulator UUID format'); }); it('should return success for valid describe_ui execution', async () => { const uiHierarchy = '{"elements": [{"type": "Button", "frame": {"x": 100, "y": 200, "width": 50, "height": 30}}]}'; const mockExecutor = createMockExecutor({ success: true, output: uiHierarchy, error: undefined, process: { pid: 12345 }, }); // Create mock axe helpers const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), }; // Wrap executor to track calls const executorCalls: any[] = []; const trackingExecutor = async (...args: any[]) => { executorCalls.push(args); return mockExecutor(...args); }; const result = await describe_uiLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', }, trackingExecutor, mockAxeHelpers, ); expect(executorCalls[0]).toEqual([ ['/usr/local/bin/axe', 'describe-ui', '--udid', '12345678-1234-1234-1234-123456789012'], '[AXe]: describe-ui', false, {}, ]); expect(result).toEqual({ content: [ { type: 'text', text: 'Accessibility hierarchy retrieved successfully:\n```json\n{"elements": [{"type": "Button", "frame": {"x": 100, "y": 200, "width": 50, "height": 30}}]}\n```', }, { type: 'text', text: `Next Steps: - Use frame coordinates for tap/swipe (center: x+width/2, y+height/2) - Re-run describe_ui after layout changes - Screenshots are for visual verification only`, }, ], }); }); it('should handle DependencyError when axe is not available', async () => { // Create mock axe helpers that return null for axe path const mockAxeHelpers = { getAxePath: () => null, getBundledAxeEnvironment: () => ({}), createAxeNotAvailableResponse: () => ({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }), }; const result = await describe_uiLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', }, createNoopExecutor(), mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Bundled axe tool not found. UI automation features are not available.\n\nThis is likely an installation issue with the npm package.\nPlease reinstall xcodebuildmcp or report this issue.', }, ], isError: true, }); }); it('should handle AxeError from failed command execution', async () => { const mockExecutor = createMockExecutor({ success: false, output: '', error: 'axe command failed', process: { pid: 12345 }, }); // Create mock axe helpers const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), }; const result = await describe_uiLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: "Error: Failed to get accessibility hierarchy: axe command 'describe-ui' failed.\nDetails: axe command failed", }, ], isError: true, }); }); it('should handle SystemError from command execution', async () => { const mockExecutor = createMockExecutor(new Error('ENOENT: no such file or directory')); // Create mock axe helpers const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), }; const result = await describe_uiLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory', ), }, ], isError: true, }); }); it('should handle unexpected Error objects', async () => { const mockExecutor = createMockExecutor(new Error('Unexpected error')); // Create mock axe helpers const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), }; const result = await describe_uiLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: expect.stringContaining( 'Error: System error executing axe: Failed to execute axe command: Unexpected error', ), }, ], isError: true, }); }); it('should handle unexpected string errors', async () => { const mockExecutor = createMockExecutor('String error'); // Create mock axe helpers const mockAxeHelpers = { getAxePath: () => '/usr/local/bin/axe', getBundledAxeEnvironment: () => ({}), }; const result = await describe_uiLogic( { simulatorUuid: '12345678-1234-1234-1234-123456789012', }, mockExecutor, mockAxeHelpers, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: System error executing axe: Failed to execute axe command: String error', }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for swift_package_build plugin * Following CLAUDE.md testing standards with literal validation * Using dependency injection for deterministic testing */ import { describe, it, expect, beforeEach } from 'vitest'; import { createMockExecutor, createMockFileSystemExecutor, createNoopExecutor, } from '../../../../test-utils/mock-executors.ts'; import swiftPackageBuild, { swift_package_buildLogic } from '../swift_package_build.ts'; describe('swift_package_build plugin', () => { describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(swiftPackageBuild.name).toBe('swift_package_build'); }); it('should have correct description', () => { expect(swiftPackageBuild.description).toBe('Builds a Swift Package with swift build'); }); it('should have handler function', () => { expect(typeof swiftPackageBuild.handler).toBe('function'); }); it('should validate schema correctly', () => { // Test required fields expect(swiftPackageBuild.schema.packagePath.safeParse('/test/package').success).toBe(true); expect(swiftPackageBuild.schema.packagePath.safeParse('').success).toBe(true); // Test optional fields expect(swiftPackageBuild.schema.targetName.safeParse('MyTarget').success).toBe(true); expect(swiftPackageBuild.schema.targetName.safeParse(undefined).success).toBe(true); expect(swiftPackageBuild.schema.configuration.safeParse('debug').success).toBe(true); expect(swiftPackageBuild.schema.configuration.safeParse('release').success).toBe(true); expect(swiftPackageBuild.schema.configuration.safeParse(undefined).success).toBe(true); expect(swiftPackageBuild.schema.architectures.safeParse(['arm64']).success).toBe(true); expect(swiftPackageBuild.schema.architectures.safeParse(undefined).success).toBe(true); expect(swiftPackageBuild.schema.parseAsLibrary.safeParse(true).success).toBe(true); expect(swiftPackageBuild.schema.parseAsLibrary.safeParse(undefined).success).toBe(true); // Test invalid inputs expect(swiftPackageBuild.schema.packagePath.safeParse(null).success).toBe(false); expect(swiftPackageBuild.schema.configuration.safeParse('invalid').success).toBe(false); expect(swiftPackageBuild.schema.architectures.safeParse('not-array').success).toBe(false); expect(swiftPackageBuild.schema.parseAsLibrary.safeParse('yes').success).toBe(false); }); }); let executorCalls: any[] = []; beforeEach(() => { executorCalls = []; }); describe('Command Generation Testing', () => { it('should build correct command for basic build', async () => { const executor = async (args: any, description: any, useShell: any, cwd: any) => { executorCalls.push({ args, description, useShell, cwd }); return { success: true, output: 'Build succeeded', error: undefined, process: { pid: 12345 }, }; }; await swift_package_buildLogic( { packagePath: '/test/package', }, executor, ); expect(executorCalls).toEqual([ { args: ['swift', 'build', '--package-path', '/test/package'], description: 'Swift Package Build', useShell: true, cwd: undefined, }, ]); }); it('should build correct command with release configuration', async () => { const executor = async (args: any, description: any, useShell: any, cwd: any) => { executorCalls.push({ args, description, useShell, cwd }); return { success: true, output: 'Build succeeded', error: undefined, process: { pid: 12345 }, }; }; await swift_package_buildLogic( { packagePath: '/test/package', configuration: 'release', }, executor, ); expect(executorCalls).toEqual([ { args: ['swift', 'build', '--package-path', '/test/package', '-c', 'release'], description: 'Swift Package Build', useShell: true, cwd: undefined, }, ]); }); it('should build correct command with all parameters', async () => { const executor = async (args: any, description: any, useShell: any, cwd: any) => { executorCalls.push({ args, description, useShell, cwd }); return { success: true, output: 'Build succeeded', error: undefined, process: { pid: 12345 }, }; }; await swift_package_buildLogic( { packagePath: '/test/package', targetName: 'MyTarget', configuration: 'release', architectures: ['arm64', 'x86_64'], parseAsLibrary: true, }, executor, ); expect(executorCalls).toEqual([ { args: [ 'swift', 'build', '--package-path', '/test/package', '-c', 'release', '--target', 'MyTarget', '--arch', 'arm64', '--arch', 'x86_64', '-Xswiftc', '-parse-as-library', ], description: 'Swift Package Build', useShell: true, cwd: undefined, }, ]); }); }); describe('Response Logic Testing', () => { it('should handle missing packagePath parameter (Zod handles validation)', async () => { // Note: With createTypedTool, Zod validation happens before the logic function is called // So we test with a valid but minimal parameter set since validation is handled upstream const executor = createMockExecutor({ success: true, output: 'Build succeeded', }); const result = await swift_package_buildLogic({ packagePath: '/test/package' }, executor); // The logic function should execute normally with valid parameters // Zod validation errors are handled by createTypedTool wrapper expect(result.isError).toBe(false); }); it('should return successful build response', async () => { const executor = createMockExecutor({ success: true, output: 'Build complete.', }); const result = await swift_package_buildLogic( { packagePath: '/test/package', }, executor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ Swift package build succeeded.' }, { type: 'text', text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run', }, { type: 'text', text: 'Build complete.' }, ], isError: false, }); }); it('should return error response for build failure', async () => { const executor = createMockExecutor({ success: false, error: 'Compilation failed: error in main.swift', }); const result = await swift_package_buildLogic( { packagePath: '/test/package', }, executor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Swift package build failed\nDetails: Compilation failed: error in main.swift', }, ], isError: true, }); }); it('should handle spawn error', async () => { const executor = async () => { throw new Error('spawn ENOENT'); }; const result = await swift_package_buildLogic( { packagePath: '/test/package', }, executor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Error: Failed to execute swift build\nDetails: spawn ENOENT', }, ], isError: true, }); }); it('should handle successful build with parameters', async () => { const executor = createMockExecutor({ success: true, output: 'Build complete.', }); const result = await swift_package_buildLogic( { packagePath: '/test/package', targetName: 'MyTarget', configuration: 'release', architectures: ['arm64', 'x86_64'], parseAsLibrary: true, }, executor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ Swift package build succeeded.' }, { type: 'text', text: '💡 Next: Run tests with swift_package_test or execute with swift_package_run', }, { type: 'text', text: 'Build complete.' }, ], isError: false, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/doctor/__tests__/doctor.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for doctor plugin * Following CLAUDE.md testing standards with literal validation * Using dependency injection for deterministic testing */ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import doctor, { runDoctor, type DoctorDependencies } from '../doctor.ts'; function createDeps(overrides?: Partial<DoctorDependencies>): DoctorDependencies { const base: DoctorDependencies = { binaryChecker: { async checkBinaryAvailability(binary: string) { // default: all available with generic version return { available: true, version: `${binary} version 1.0.0` }; }, }, xcode: { async getXcodeInfo() { return { version: 'Xcode 15.0 - Build version 15A240d', path: '/Applications/Xcode.app/Contents/Developer', selectedXcode: '/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild', xcrunVersion: 'xcrun version 65', }; }, }, env: { getEnvironmentVariables() { const x: Record<string, string | undefined> = { XCODEBUILDMCP_DEBUG: 'true', INCREMENTAL_BUILDS_ENABLED: '1', PATH: '/usr/local/bin:/usr/bin:/bin', DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer', HOME: '/Users/testuser', USER: 'testuser', TMPDIR: '/tmp', NODE_ENV: 'test', SENTRY_DISABLED: 'false', }; return x; }, getSystemInfo() { return { platform: 'darwin', release: '25.0.0', arch: 'arm64', cpus: '10 x Apple M3', memory: '32 GB', hostname: 'localhost', username: 'testuser', homedir: '/Users/testuser', tmpdir: '/tmp', }; }, getNodeInfo() { return { version: 'v22.0.0', execPath: '/usr/local/bin/node', pid: '123', ppid: '1', platform: 'darwin', arch: 'arm64', cwd: '/', argv: 'node build/index.js', }; }, }, plugins: { async getPluginSystemInfo() { return { totalPlugins: 1, pluginDirectories: 1, pluginsByDirectory: { doctor: ['doctor'] }, systemMode: 'plugin-based', }; }, }, features: { areAxeToolsAvailable: () => true, isXcodemakeEnabled: () => true, isXcodemakeAvailable: async () => true, doesMakefileExist: () => true, }, runtime: { async getRuntimeToolInfo() { return { mode: 'static' as const, enabledWorkflows: ['doctor', 'discovery'], enabledTools: ['doctor', 'discover_tools'], totalRegistered: 2, }; }, }, }; return { ...base, ...overrides, binaryChecker: { ...base.binaryChecker, ...(overrides?.binaryChecker ?? {}), }, xcode: { ...base.xcode, ...(overrides?.xcode ?? {}), }, env: { ...base.env, ...(overrides?.env ?? {}), }, plugins: { ...base.plugins, ...(overrides?.plugins ?? {}), }, features: { ...base.features, ...(overrides?.features ?? {}), }, }; } describe('doctor tool', () => { // Reset any state if needed describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(doctor.name).toBe('doctor'); }); it('should have correct description', () => { expect(doctor.description).toBe( 'Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.', ); }); it('should have handler function', () => { expect(typeof doctor.handler).toBe('function'); }); it('should have correct schema with enabled boolean field', () => { const schema = z.object(doctor.schema); // Valid inputs expect(schema.safeParse({ enabled: true }).success).toBe(true); expect(schema.safeParse({ enabled: false }).success).toBe(true); expect(schema.safeParse({}).success).toBe(true); // enabled is optional // Invalid inputs expect(schema.safeParse({ enabled: 'true' }).success).toBe(false); expect(schema.safeParse({ enabled: 1 }).success).toBe(false); expect(schema.safeParse({ enabled: null }).success).toBe(false); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { it('should handle successful doctor execution', async () => { const deps = createDeps(); const result = await runDoctor({ enabled: true }, deps); expect(result.content).toEqual([ { type: 'text', text: result.content[0].text, }, ]); expect(typeof result.content[0].text).toBe('string'); }); it('should handle plugin loading failure', async () => { const deps = createDeps({ plugins: { async getPluginSystemInfo() { return { error: 'Plugin loading failed', systemMode: 'error' }; }, }, }); const result = await runDoctor({ enabled: true }, deps); expect(result.content).toEqual([ { type: 'text', text: result.content[0].text, }, ]); expect(typeof result.content[0].text).toBe('string'); }); it('should handle xcode command failure', async () => { const deps = createDeps({ xcode: { async getXcodeInfo() { return { error: 'Xcode not found' }; }, }, }); const result = await runDoctor({ enabled: true }, deps); expect(result.content).toEqual([ { type: 'text', text: result.content[0].text, }, ]); expect(typeof result.content[0].text).toBe('string'); }); it('should handle xcodemake check failure', async () => { const deps = createDeps({ features: { areAxeToolsAvailable: () => true, isXcodemakeEnabled: () => true, isXcodemakeAvailable: async () => false, doesMakefileExist: () => true, }, binaryChecker: { async checkBinaryAvailability(binary: string) { if (binary === 'xcodemake') return { available: false }; return { available: true, version: `${binary} version 1.0.0` }; }, }, }); const result = await runDoctor({ enabled: true }, deps); expect(result.content).toEqual([ { type: 'text', text: result.content[0].text, }, ]); expect(typeof result.content[0].text).toBe('string'); }); it('should handle axe tools not available', async () => { const deps = createDeps({ features: { areAxeToolsAvailable: () => false, isXcodemakeEnabled: () => false, isXcodemakeAvailable: async () => false, doesMakefileExist: () => false, }, binaryChecker: { async checkBinaryAvailability(binary: string) { if (binary === 'axe') return { available: false }; if (binary === 'xcodemake') return { available: false }; if (binary === 'mise') return { available: true, version: 'mise 1.0.0' }; return { available: true }; }, }, env: { getEnvironmentVariables() { const x: Record<string, string | undefined> = { XCODEBUILDMCP_DEBUG: 'true', INCREMENTAL_BUILDS_ENABLED: '0', PATH: '/usr/local/bin:/usr/bin:/bin', DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer', HOME: '/Users/testuser', USER: 'testuser', TMPDIR: '/tmp', NODE_ENV: 'test', SENTRY_DISABLED: 'true', }; return x; }, getSystemInfo: () => ({ platform: 'darwin', release: '25.0.0', arch: 'arm64', cpus: '10 x Apple M3', memory: '32 GB', hostname: 'localhost', username: 'testuser', homedir: '/Users/testuser', tmpdir: '/tmp', }), getNodeInfo: () => ({ version: 'v22.0.0', execPath: '/usr/local/bin/node', pid: '123', ppid: '1', platform: 'darwin', arch: 'arm64', cwd: '/', argv: 'node build/index.js', }), }, }); const result = await runDoctor({ enabled: true }, deps); expect(result.content).toEqual([ { type: 'text', text: result.content[0].text, }, ]); expect(typeof result.content[0].text).toBe('string'); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/macos/__tests__/launch_mac_app.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Pure dependency injection test for launch_mac_app plugin * * Tests plugin structure and macOS app launching functionality including parameter validation, * command generation, file validation, and response formatting. * * Uses manual call tracking and createMockFileSystemExecutor for file operations. */ import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts'; import launchMacApp, { launch_mac_appLogic } from '../launch_mac_app.ts'; describe('launch_mac_app plugin', () => { describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(launchMacApp.name).toBe('launch_mac_app'); }); it('should have correct description', () => { expect(launchMacApp.description).toBe( "Launches a macOS application. IMPORTANT: You MUST provide the appPath parameter. Example: launch_mac_app({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app.", ); }); it('should have handler function', () => { expect(typeof launchMacApp.handler).toBe('function'); }); it('should validate schema with valid inputs', () => { const schema = z.object(launchMacApp.schema); expect( schema.safeParse({ appPath: '/path/to/MyApp.app', }).success, ).toBe(true); expect( schema.safeParse({ appPath: '/Applications/Calculator.app', args: ['--debug'], }).success, ).toBe(true); expect( schema.safeParse({ appPath: '/path/to/MyApp.app', args: ['--debug', '--verbose'], }).success, ).toBe(true); }); it('should validate schema with invalid inputs', () => { const schema = z.object(launchMacApp.schema); expect(schema.safeParse({}).success).toBe(false); expect(schema.safeParse({ appPath: null }).success).toBe(false); expect(schema.safeParse({ appPath: 123 }).success).toBe(false); expect(schema.safeParse({ appPath: '/path/to/MyApp.app', args: 'not-array' }).success).toBe( false, ); }); }); describe('Input Validation', () => { it('should handle non-existent app path', async () => { const mockExecutor = async () => Promise.resolve({ stdout: '', stderr: '' }); const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => false, }); const result = await launch_mac_appLogic( { appPath: '/path/to/NonExistent.app', }, mockExecutor, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: "File not found: '/path/to/NonExistent.app'. Please check the path and try again.", }, ], isError: true, }); }); }); describe('Command Generation', () => { it('should generate correct command with minimal parameters', async () => { const calls: any[] = []; const mockExecutor = async (command: string[]) => { calls.push({ command }); return { stdout: '', stderr: '' }; }; const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); await launch_mac_appLogic( { appPath: '/path/to/MyApp.app', }, mockExecutor, mockFileSystem, ); expect(calls).toHaveLength(1); expect(calls[0].command).toEqual(['open', '/path/to/MyApp.app']); }); it('should generate correct command with args parameter', async () => { const calls: any[] = []; const mockExecutor = async (command: string[]) => { calls.push({ command }); return { stdout: '', stderr: '' }; }; const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); await launch_mac_appLogic( { appPath: '/path/to/MyApp.app', args: ['--debug', '--verbose'], }, mockExecutor, mockFileSystem, ); expect(calls).toHaveLength(1); expect(calls[0].command).toEqual([ 'open', '/path/to/MyApp.app', '--args', '--debug', '--verbose', ]); }); it('should generate correct command with empty args array', async () => { const calls: any[] = []; const mockExecutor = async (command: string[]) => { calls.push({ command }); return { stdout: '', stderr: '' }; }; const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); await launch_mac_appLogic( { appPath: '/path/to/MyApp.app', args: [], }, mockExecutor, mockFileSystem, ); expect(calls).toHaveLength(1); expect(calls[0].command).toEqual(['open', '/path/to/MyApp.app']); }); it('should handle paths with spaces correctly', async () => { const calls: any[] = []; const mockExecutor = async (command: string[]) => { calls.push({ command }); return { stdout: '', stderr: '' }; }; const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); await launch_mac_appLogic( { appPath: '/Applications/My App.app', }, mockExecutor, mockFileSystem, ); expect(calls).toHaveLength(1); expect(calls[0].command).toEqual(['open', '/Applications/My App.app']); }); }); describe('Response Processing', () => { it('should return successful launch response', async () => { const mockExecutor = async () => Promise.resolve({ stdout: '', stderr: '' }); const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await launch_mac_appLogic( { appPath: '/path/to/MyApp.app', }, mockExecutor, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ macOS app launched successfully: /path/to/MyApp.app', }, ], }); }); it('should return successful launch response with args', async () => { const mockExecutor = async () => Promise.resolve({ stdout: '', stderr: '' }); const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await launch_mac_appLogic( { appPath: '/path/to/MyApp.app', args: ['--debug', '--verbose'], }, mockExecutor, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ macOS app launched successfully: /path/to/MyApp.app', }, ], }); }); it('should handle launch failure with Error object', async () => { const mockExecutor = async () => { throw new Error('App not found'); }; const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await launch_mac_appLogic( { appPath: '/path/to/MyApp.app', }, mockExecutor, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: '❌ Launch macOS app operation failed: App not found', }, ], isError: true, }); }); it('should handle launch failure with string error', async () => { const mockExecutor = async () => { throw 'Permission denied'; }; const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await launch_mac_appLogic( { appPath: '/path/to/MyApp.app', }, mockExecutor, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: '❌ Launch macOS app operation failed: Permission denied', }, ], isError: true, }); }); it('should handle launch failure with unknown error type', async () => { const mockExecutor = async () => { throw 123; }; const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await launch_mac_appLogic( { appPath: '/path/to/MyApp.app', }, mockExecutor, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: '❌ Launch macOS app operation failed: 123', }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/project-discovery/__tests__/get_mac_bundle_id.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import plugin, { get_mac_bundle_idLogic } from '../get_mac_bundle_id.ts'; import { createMockFileSystemExecutor, createCommandMatchingMockExecutor, } from '../../../../test-utils/mock-executors.ts'; describe('get_mac_bundle_id plugin', () => { // Helper function to create mock executor for command matching const createMockExecutorForCommands = (results: Record<string, string | Error>) => { return createCommandMatchingMockExecutor( Object.fromEntries( Object.entries(results).map(([command, result]) => [ command, result instanceof Error ? { success: false, error: result.message } : { success: true, output: result }, ]), ), ); }; describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(plugin.name).toBe('get_mac_bundle_id'); }); it('should have correct description', () => { expect(plugin.description).toBe( "Extracts the bundle identifier from a macOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_mac_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id.", ); }); it('should have handler function', () => { expect(typeof plugin.handler).toBe('function'); }); it('should validate schema with valid inputs', () => { const schema = z.object(plugin.schema); expect(schema.safeParse({ appPath: '/Applications/TextEdit.app' }).success).toBe(true); expect(schema.safeParse({ appPath: '/Users/dev/MyApp.app' }).success).toBe(true); }); it('should validate schema with invalid inputs', () => { const schema = z.object(plugin.schema); expect(schema.safeParse({}).success).toBe(false); expect(schema.safeParse({ appPath: 123 }).success).toBe(false); expect(schema.safeParse({ appPath: null }).success).toBe(false); expect(schema.safeParse({ appPath: undefined }).success).toBe(false); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { // Note: appPath validation is now handled by Zod schema validation in createTypedTool // This test would not reach the logic function as Zod validation occurs before it it('should return error when file exists validation fails', async () => { const mockExecutor = createMockExecutorForCommands({}); const mockFileSystemExecutor = createMockFileSystemExecutor({ existsSync: () => false, }); const result = await get_mac_bundle_idLogic( { appPath: '/Applications/MyApp.app' }, mockExecutor, mockFileSystemExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: "File not found: '/Applications/MyApp.app'. Please check the path and try again.", }, ], isError: true, }); }); it('should return success with bundle ID using defaults read', async () => { const mockExecutor = createMockExecutorForCommands({ 'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': 'com.example.MyMacApp', }); const mockFileSystemExecutor = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await get_mac_bundle_idLogic( { appPath: '/Applications/MyApp.app' }, mockExecutor, mockFileSystemExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ Bundle ID: com.example.MyMacApp', }, { type: 'text', text: `Next Steps: - Launch: launch_mac_app({ appPath: "/Applications/MyApp.app" }) - Build again: build_macos({ scheme: "SCHEME_NAME" })`, }, ], isError: false, }); }); it('should fallback to PlistBuddy when defaults read fails', async () => { const mockExecutor = createMockExecutorForCommands({ 'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error( 'defaults read failed', ), '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"': 'com.example.MyMacApp', }); const mockFileSystemExecutor = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await get_mac_bundle_idLogic( { appPath: '/Applications/MyApp.app' }, mockExecutor, mockFileSystemExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ Bundle ID: com.example.MyMacApp', }, { type: 'text', text: `Next Steps: - Launch: launch_mac_app({ appPath: "/Applications/MyApp.app" }) - Build again: build_macos({ scheme: "SCHEME_NAME" })`, }, ], isError: false, }); }); it('should return error when both extraction methods fail', async () => { const mockExecutor = createMockExecutorForCommands({ 'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error( 'Command failed', ), '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"': new Error('Command failed'), }); const mockFileSystemExecutor = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await get_mac_bundle_idLogic( { appPath: '/Applications/MyApp.app' }, mockExecutor, mockFileSystemExecutor, ); expect(result.isError).toBe(true); expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Error extracting macOS bundle ID'); expect(result.content[0].text).toContain('Could not extract bundle ID from Info.plist'); expect(result.content[0].text).toContain('Command failed'); expect(result.content[1].type).toBe('text'); expect(result.content[1].text).toBe( 'Make sure the path points to a valid macOS app bundle (.app directory).', ); }); it('should handle Error objects in catch blocks', async () => { const mockExecutor = createMockExecutorForCommands({ 'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error( 'Custom error message', ), '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"': new Error('Custom error message'), }); const mockFileSystemExecutor = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await get_mac_bundle_idLogic( { appPath: '/Applications/MyApp.app' }, mockExecutor, mockFileSystemExecutor, ); expect(result.isError).toBe(true); expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Error extracting macOS bundle ID'); expect(result.content[0].text).toContain('Could not extract bundle ID from Info.plist'); expect(result.content[0].text).toContain('Custom error message'); expect(result.content[1].type).toBe('text'); expect(result.content[1].text).toBe( 'Make sure the path points to a valid macOS app bundle (.app directory).', ); }); it('should handle string errors in catch blocks', async () => { const mockExecutor = createMockExecutorForCommands({ 'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error( 'String error', ), '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"': new Error('String error'), }); const mockFileSystemExecutor = createMockFileSystemExecutor({ existsSync: () => true, }); const result = await get_mac_bundle_idLogic( { appPath: '/Applications/MyApp.app' }, mockExecutor, mockFileSystemExecutor, ); expect(result.isError).toBe(true); expect(result.content).toHaveLength(2); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Error extracting macOS bundle ID'); expect(result.content[0].text).toContain('Could not extract bundle ID from Info.plist'); expect(result.content[0].text).toContain('String error'); expect(result.content[1].type).toBe('text'); expect(result.content[1].text).toBe( 'Make sure the path points to a valid macOS app bundle (.app directory).', ); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/project-discovery/discover_projs.ts: -------------------------------------------------------------------------------- ```typescript /** * Project Discovery Plugin: Discover Projects * * Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) * and workspace (.xcworkspace) files. */ import { z } from 'zod'; import * as path from 'node:path'; import { log } from '../../../utils/logging/index.ts'; import { ToolResponse, createTextContent } from '../../../types/common.ts'; import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.ts'; import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Constants const DEFAULT_MAX_DEPTH = 5; const SKIPPED_DIRS = new Set(['build', 'DerivedData', 'Pods', '.git', 'node_modules']); // Type definition for Dirent-like objects returned by readdir with withFileTypes: true interface DirentLike { name: string; isDirectory(): boolean; isSymbolicLink(): boolean; } /** * Recursively scans directories to find Xcode projects and workspaces. */ async function _findProjectsRecursive( currentDirAbs: string, workspaceRootAbs: string, currentDepth: number, maxDepth: number, results: { projects: string[]; workspaces: string[] }, fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(), ): Promise<void> { // Explicit depth check (now simplified as maxDepth is always non-negative) if (currentDepth >= maxDepth) { log('debug', `Max depth ${maxDepth} reached at ${currentDirAbs}, stopping recursion.`); return; } log('debug', `Scanning directory: ${currentDirAbs} at depth ${currentDepth}`); const normalizedWorkspaceRoot = path.normalize(workspaceRootAbs); try { // Use the injected fileSystemExecutor const entries = await fileSystemExecutor.readdir(currentDirAbs, { withFileTypes: true }); for (const rawEntry of entries) { // Cast the unknown entry to DirentLike interface for type safety const entry = rawEntry as DirentLike; const absoluteEntryPath = path.join(currentDirAbs, entry.name); const relativePath = path.relative(workspaceRootAbs, absoluteEntryPath); // --- Skip conditions --- if (entry.isSymbolicLink()) { log('debug', `Skipping symbolic link: ${relativePath}`); continue; } // Skip common build/dependency directories by name if (entry.isDirectory() && SKIPPED_DIRS.has(entry.name)) { log('debug', `Skipping standard directory: ${relativePath}`); continue; } // Ensure entry is within the workspace root (security/sanity check) if (!path.normalize(absoluteEntryPath).startsWith(normalizedWorkspaceRoot)) { log( 'warn', `Skipping entry outside workspace root: ${absoluteEntryPath} (Workspace: ${workspaceRootAbs})`, ); continue; } // --- Process entries --- if (entry.isDirectory()) { let isXcodeBundle = false; if (entry.name.endsWith('.xcodeproj')) { results.projects.push(absoluteEntryPath); // Use absolute path log('debug', `Found project: ${absoluteEntryPath}`); isXcodeBundle = true; } else if (entry.name.endsWith('.xcworkspace')) { results.workspaces.push(absoluteEntryPath); // Use absolute path log('debug', `Found workspace: ${absoluteEntryPath}`); isXcodeBundle = true; } // Recurse into regular directories, but not into found project/workspace bundles if (!isXcodeBundle) { await _findProjectsRecursive( absoluteEntryPath, workspaceRootAbs, currentDepth + 1, maxDepth, results, fileSystemExecutor, ); } } } } catch (error) { let code; let message = 'Unknown error'; if (error instanceof Error) { message = error.message; if ('code' in error) { code = error.code; } } else if (typeof error === 'object' && error !== null) { if ('message' in error && typeof error.message === 'string') { message = error.message; } if ('code' in error && typeof error.code === 'string') { code = error.code; } } else { message = String(error); } if (code === 'EPERM' || code === 'EACCES') { log('debug', `Permission denied scanning directory: ${currentDirAbs}`); } else { log( 'warning', `Error scanning directory ${currentDirAbs}: ${message} (Code: ${code ?? 'N/A'})`, ); } } } // Define schema as ZodObject const discoverProjsSchema = z.object({ workspaceRoot: z.string().describe('The absolute path of the workspace root to scan within.'), scanPath: z .string() .optional() .describe('Optional: Path relative to workspace root to scan. Defaults to workspace root.'), maxDepth: z .number() .int() .nonnegative() .optional() .describe(`Optional: Maximum directory depth to scan. Defaults to ${DEFAULT_MAX_DEPTH}.`), }); // Use z.infer for type safety type DiscoverProjsParams = z.infer<typeof discoverProjsSchema>; /** * Business logic for discovering projects. * Exported for testing purposes. */ export async function discover_projsLogic( params: DiscoverProjsParams, fileSystemExecutor: FileSystemExecutor, ): Promise<ToolResponse> { // Apply defaults const scanPath = params.scanPath ?? '.'; const maxDepth = params.maxDepth ?? DEFAULT_MAX_DEPTH; const workspaceRoot = params.workspaceRoot; const relativeScanPath = scanPath; // Calculate and validate the absolute scan path const requestedScanPath = path.resolve(workspaceRoot, relativeScanPath ?? '.'); let absoluteScanPath = requestedScanPath; const normalizedWorkspaceRoot = path.normalize(workspaceRoot); if (!path.normalize(absoluteScanPath).startsWith(normalizedWorkspaceRoot)) { log( 'warn', `Requested scan path '${relativeScanPath}' resolved outside workspace root '${workspaceRoot}'. Defaulting scan to workspace root.`, ); absoluteScanPath = normalizedWorkspaceRoot; } const results = { projects: [], workspaces: [] }; log( 'info', `Starting project discovery request: path=${absoluteScanPath}, maxDepth=${maxDepth}, workspace=${workspaceRoot}`, ); try { // Ensure the scan path exists and is a directory const stats = await fileSystemExecutor.stat(absoluteScanPath); if (!stats.isDirectory()) { const errorMsg = `Scan path is not a directory: ${absoluteScanPath}`; log('error', errorMsg); // Return ToolResponse error format return { content: [createTextContent(errorMsg)], isError: true, }; } } catch (error) { let code; let message = 'Unknown error accessing scan path'; // Type guards - refined if (error instanceof Error) { message = error.message; // Check for code property specific to Node.js fs errors if ('code' in error) { code = error.code; } } else if (typeof error === 'object' && error !== null) { if ('message' in error && typeof error.message === 'string') { message = error.message; } if ('code' in error && typeof error.code === 'string') { code = error.code; } } else { message = String(error); } const errorMsg = `Failed to access scan path: ${absoluteScanPath}. Error: ${message}`; log('error', `${errorMsg} - Code: ${code ?? 'N/A'}`); return { content: [createTextContent(errorMsg)], isError: true, }; } // Start the recursive scan from the validated absolute path await _findProjectsRecursive( absoluteScanPath, workspaceRoot, 0, maxDepth, results, fileSystemExecutor, ); log( 'info', `Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`, ); const responseContent = [ createTextContent( `Discovery finished. Found ${results.projects.length} projects and ${results.workspaces.length} workspaces.`, ), ]; // Sort results for consistent output results.projects.sort(); results.workspaces.sort(); if (results.projects.length > 0) { responseContent.push( createTextContent(`Projects found:\n - ${results.projects.join('\n - ')}`), ); } if (results.workspaces.length > 0) { responseContent.push( createTextContent(`Workspaces found:\n - ${results.workspaces.join('\n - ')}`), ); } return { content: responseContent, isError: false, }; } export default { name: 'discover_projs', description: 'Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.', schema: discoverProjsSchema.shape, // MCP SDK compatibility handler: createTypedTool( discoverProjsSchema, (params: DiscoverProjsParams) => { return discover_projsLogic(params, getDefaultFileSystemExecutor()); }, getDefaultCommandExecutor, ), }; ``` -------------------------------------------------------------------------------- /build-plugins/plugin-discovery.js: -------------------------------------------------------------------------------- ```javascript import { readdirSync, readFileSync, existsSync } from 'fs'; import { join } from 'path'; import path from 'path'; export function createPluginDiscoveryPlugin() { return { name: 'plugin-discovery', setup(build) { // Generate the workflow loaders file before build starts build.onStart(async () => { try { await generateWorkflowLoaders(); await generateResourceLoaders(); } catch (error) { console.error('Failed to generate loaders:', error); throw error; } }); } }; } async function generateWorkflowLoaders() { const pluginsDir = path.resolve(process.cwd(), 'src/mcp/tools'); if (!existsSync(pluginsDir)) { throw new Error(`Plugins directory not found: ${pluginsDir}`); } // Scan for workflow directories const workflowDirs = readdirSync(pluginsDir, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name); const workflowLoaders = {}; const workflowMetadata = {}; for (const dirName of workflowDirs) { const dirPath = join(pluginsDir, dirName); const indexPath = join(dirPath, 'index.ts'); // Check if workflow has index.ts file if (!existsSync(indexPath)) { console.warn(`Skipping ${dirName}: no index.ts file found`); continue; } // Try to extract workflow metadata from index.ts try { const indexContent = readFileSync(indexPath, 'utf8'); const metadata = extractWorkflowMetadata(indexContent); if (metadata) { // Find all tool files in this workflow directory const toolFiles = readdirSync(dirPath, { withFileTypes: true }) .filter(dirent => dirent.isFile()) .map(dirent => dirent.name) .filter(name => (name.endsWith('.ts') || name.endsWith('.js')) && name !== 'index.ts' && name !== 'index.js' && !name.endsWith('.test.ts') && !name.endsWith('.test.js') && name !== 'active-processes.ts' // Special exclusion for swift-package ); // Generate dynamic loader function that loads workflow and all its tools workflowLoaders[dirName] = generateWorkflowLoader(dirName, toolFiles); workflowMetadata[dirName] = metadata; console.log(`✅ Discovered workflow: ${dirName} - ${metadata.name} (${toolFiles.length} tools)`); } else { console.warn(`⚠️ Skipping ${dirName}: invalid workflow metadata`); } } catch (error) { console.warn(`⚠️ Error processing ${dirName}:`, error); } } // Generate the content for generated-plugins.ts const generatedContent = generatePluginsFileContent(workflowLoaders, workflowMetadata); // Write to the generated file const outputPath = path.resolve(process.cwd(), 'src/core/generated-plugins.ts'); const fs = await import('fs'); await fs.promises.writeFile(outputPath, generatedContent, 'utf8'); console.log(`🔧 Generated workflow loaders for ${Object.keys(workflowLoaders).length} workflows`); } function generateWorkflowLoader(workflowName, toolFiles) { const toolImports = toolFiles.map((file, index) => { const toolName = file.replace(/\.(ts|js)$/, ''); return `const tool_${index} = await import('../mcp/tools/${workflowName}/${toolName}.js').then(m => m.default)`; }).join(';\n '); const toolExports = toolFiles.map((file, index) => { const toolName = file.replace(/\.(ts|js)$/, ''); return `'${toolName}': tool_${index}`; }).join(',\n '); return `async () => { const { workflow } = await import('../mcp/tools/${workflowName}/index.js'); ${toolImports ? toolImports + ';\n ' : ''} return { workflow, ${toolExports ? toolExports : ''} }; }`; } function extractWorkflowMetadata(content) { try { // Simple regex to extract workflow export object const workflowMatch = content.match(/export\s+const\s+workflow\s*=\s*({[\s\S]*?});/); if (!workflowMatch) { return null; } const workflowObj = workflowMatch[1]; // Extract name const nameMatch = workflowObj.match(/name\s*:\s*['"`]([^'"`]+)['"`]/); if (!nameMatch) return null; // Extract description const descMatch = workflowObj.match(/description\s*:\s*['"`]([\s\S]*?)['"`]/); if (!descMatch) return null; // Extract platforms (optional) const platformsMatch = workflowObj.match(/platforms\s*:\s*\[([^\]]*)\]/); let platforms; if (platformsMatch) { platforms = platformsMatch[1] .split(',') .map(p => p.trim().replace(/['"]/g, '')) .filter(p => p.length > 0); } // Extract targets (optional) const targetsMatch = workflowObj.match(/targets\s*:\s*\[([^\]]*)\]/); let targets; if (targetsMatch) { targets = targetsMatch[1] .split(',') .map(t => t.trim().replace(/['"]/g, '')) .filter(t => t.length > 0); } // Extract projectTypes (optional) const projectTypesMatch = workflowObj.match(/projectTypes\s*:\s*\[([^\]]*)\]/); let projectTypes; if (projectTypesMatch) { projectTypes = projectTypesMatch[1] .split(',') .map(pt => pt.trim().replace(/['"]/g, '')) .filter(pt => pt.length > 0); } // Extract capabilities (optional) const capabilitiesMatch = workflowObj.match(/capabilities\s*:\s*\[([^\]]*)\]/); let capabilities; if (capabilitiesMatch) { capabilities = capabilitiesMatch[1] .split(',') .map(c => c.trim().replace(/['"]/g, '')) .filter(c => c.length > 0); } const result = { name: nameMatch[1], description: descMatch[1] }; if (platforms) result.platforms = platforms; if (targets) result.targets = targets; if (projectTypes) result.projectTypes = projectTypes; if (capabilities) result.capabilities = capabilities; return result; } catch (error) { console.warn('Failed to extract workflow metadata:', error); return null; } } function generatePluginsFileContent(workflowLoaders, workflowMetadata) { const loaderEntries = Object.entries(workflowLoaders) .map(([key, loader]) => { // Indent the loader function properly const indentedLoader = loader .split('\n') .map((line, index) => index === 0 ? ` '${key}': ${line}` : ` ${line}`) .join('\n'); return indentedLoader; }) .join(',\n'); const metadataEntries = Object.entries(workflowMetadata) .map(([key, metadata]) => { const metadataJson = JSON.stringify(metadata, null, 4) .split('\n') .map(line => ` ${line}`) .join('\n'); return ` '${key}': ${metadataJson.trim()}`; }) .join(',\n'); return `// AUTO-GENERATED - DO NOT EDIT // This file is generated by the plugin discovery esbuild plugin // Generated based on filesystem scan export const WORKFLOW_LOADERS = { ${loaderEntries} }; export type WorkflowName = keyof typeof WORKFLOW_LOADERS; // Optional: Export workflow metadata for quick access export const WORKFLOW_METADATA = { ${metadataEntries} }; `; } async function generateResourceLoaders() { const resourcesDir = path.resolve(process.cwd(), 'src/mcp/resources'); if (!existsSync(resourcesDir)) { console.log('Resources directory not found, skipping resource generation'); return; } // Scan for resource files const resourceFiles = readdirSync(resourcesDir, { withFileTypes: true }) .filter(dirent => dirent.isFile()) .map(dirent => dirent.name) .filter(name => (name.endsWith('.ts') || name.endsWith('.js')) && !name.endsWith('.test.ts') && !name.endsWith('.test.js') && !name.startsWith('__') // Exclude test directories ); const resourceLoaders = {}; for (const fileName of resourceFiles) { const resourceName = fileName.replace(/\.(ts|js)$/, ''); // Generate dynamic loader for this resource resourceLoaders[resourceName] = `async () => { const module = await import('../mcp/resources/${resourceName}.js'); return module.default; }`; console.log(`✅ Discovered resource: ${resourceName}`); } // Generate the content for generated-resources.ts const generatedContent = generateResourcesFileContent(resourceLoaders); // Write to the generated file const outputPath = path.resolve(process.cwd(), 'src/core/generated-resources.ts'); const fs = await import('fs'); await fs.promises.writeFile(outputPath, generatedContent, 'utf8'); console.log(`🔧 Generated resource loaders for ${Object.keys(resourceLoaders).length} resources`); } function generateResourcesFileContent(resourceLoaders) { const loaderEntries = Object.entries(resourceLoaders) .map(([key, loader]) => ` '${key}': ${loader}`) .join(',\n'); return `// AUTO-GENERATED - DO NOT EDIT // This file is generated by the plugin discovery esbuild plugin export const RESOURCE_LOADERS = { ${loaderEntries} }; export type ResourceName = keyof typeof RESOURCE_LOADERS; `; } ``` -------------------------------------------------------------------------------- /src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for start_sim_log_cap plugin */ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import plugin, { start_sim_log_capLogic } from '../start_sim_log_cap.ts'; import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; describe('start_sim_log_cap plugin', () => { // Reset any test state if needed describe('Export Field Validation (Literal)', () => { it('should export an object with required properties', () => { expect(plugin).toHaveProperty('name'); expect(plugin).toHaveProperty('description'); expect(plugin).toHaveProperty('schema'); expect(plugin).toHaveProperty('handler'); }); it('should have correct tool name', () => { expect(plugin.name).toBe('start_sim_log_cap'); }); it('should have correct description', () => { expect(plugin.description).toBe( 'Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs.', ); }); it('should have handler as a function', () => { expect(typeof plugin.handler).toBe('function'); }); it('should validate schema with valid parameters', () => { const schema = z.object(plugin.schema); expect( schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: 'com.example.app' }).success, ).toBe(true); expect( schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: 'com.example.app', captureConsole: true, }).success, ).toBe(true); expect( schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: 'com.example.app', captureConsole: false, }).success, ).toBe(true); }); it('should reject invalid schema parameters', () => { const schema = z.object(plugin.schema); expect(schema.safeParse({ simulatorUuid: null, bundleId: 'com.example.app' }).success).toBe( false, ); expect( schema.safeParse({ simulatorUuid: undefined, bundleId: 'com.example.app' }).success, ).toBe(false); expect(schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: null }).success).toBe(false); expect(schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: undefined }).success).toBe( false, ); expect( schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: 'com.example.app', captureConsole: 'yes', }).success, ).toBe(false); expect( schema.safeParse({ simulatorUuid: 'test-uuid', bundleId: 'com.example.app', captureConsole: 123, }).success, ).toBe(false); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { // Note: Parameter validation is now handled by createTypedTool wrapper // Invalid parameters will not reach the logic function, so we test valid scenarios it('should return error when log capture fails', async () => { const mockExecutor = createMockExecutor({ success: true, output: '' }); const logCaptureStub = (params: any, executor: any) => { return Promise.resolve({ sessionId: '', logFilePath: '', processes: [], error: 'Permission denied', }); }; const result = await start_sim_log_capLogic( { simulatorUuid: 'test-uuid', bundleId: 'com.example.app', }, mockExecutor, logCaptureStub, ); expect(result.isError).toBe(true); expect(result.content[0].text).toBe('Error starting log capture: Permission denied'); }); it('should return success with session ID when log capture starts successfully', async () => { const mockExecutor = createMockExecutor({ success: true, output: '' }); const logCaptureStub = (params: any, executor: any) => { return Promise.resolve({ sessionId: 'test-uuid-123', logFilePath: '/tmp/test.log', processes: [], error: undefined, }); }; const result = await start_sim_log_capLogic( { simulatorUuid: 'test-uuid', bundleId: 'com.example.app', }, mockExecutor, logCaptureStub, ); expect(result.isError).toBeUndefined(); expect(result.content[0].text).toBe( "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.", ); }); it('should indicate console capture when captureConsole is true', async () => { const mockExecutor = createMockExecutor({ success: true, output: '' }); const logCaptureStub = (params: any, executor: any) => { return Promise.resolve({ sessionId: 'test-uuid-123', logFilePath: '/tmp/test.log', processes: [], error: undefined, }); }; const result = await start_sim_log_capLogic( { simulatorUuid: 'test-uuid', bundleId: 'com.example.app', captureConsole: true, }, mockExecutor, logCaptureStub, ); expect(result.content[0].text).toBe( "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.", ); }); it('should create correct spawn commands for console capture', async () => { const mockExecutor = createMockExecutor({ success: true, output: '' }); const spawnCalls: Array<{ command: string; args: string[]; }> = []; const logCaptureStub = (params: any, executor: any) => { if (params.captureConsole) { // Record the console capture spawn call spawnCalls.push({ command: 'xcrun', args: [ 'simctl', 'launch', '--console-pty', '--terminate-running-process', params.simulatorUuid, params.bundleId, ], }); } // Record the structured log capture spawn call spawnCalls.push({ command: 'xcrun', args: [ 'simctl', 'spawn', params.simulatorUuid, 'log', 'stream', '--level=debug', '--predicate', `subsystem == "${params.bundleId}"`, ], }); return Promise.resolve({ sessionId: 'test-uuid-123', logFilePath: '/tmp/test.log', processes: [], error: undefined, }); }; await start_sim_log_capLogic( { simulatorUuid: 'test-uuid', bundleId: 'com.example.app', captureConsole: true, }, mockExecutor, logCaptureStub, ); // Should spawn both console capture and structured log capture expect(spawnCalls).toHaveLength(2); expect(spawnCalls[0]).toEqual({ command: 'xcrun', args: [ 'simctl', 'launch', '--console-pty', '--terminate-running-process', 'test-uuid', 'com.example.app', ], }); expect(spawnCalls[1]).toEqual({ command: 'xcrun', args: [ 'simctl', 'spawn', 'test-uuid', 'log', 'stream', '--level=debug', '--predicate', 'subsystem == "com.example.app"', ], }); }); it('should create correct spawn commands for structured logs only', async () => { const mockExecutor = createMockExecutor({ success: true, output: '' }); const spawnCalls: Array<{ command: string; args: string[]; }> = []; const logCaptureStub = (params: any, executor: any) => { // Record the structured log capture spawn call only spawnCalls.push({ command: 'xcrun', args: [ 'simctl', 'spawn', params.simulatorUuid, 'log', 'stream', '--level=debug', '--predicate', `subsystem == "${params.bundleId}"`, ], }); return Promise.resolve({ sessionId: 'test-uuid-123', logFilePath: '/tmp/test.log', processes: [], error: undefined, }); }; await start_sim_log_capLogic( { simulatorUuid: 'test-uuid', bundleId: 'com.example.app', captureConsole: false, }, mockExecutor, logCaptureStub, ); // Should only spawn structured log capture expect(spawnCalls).toHaveLength(1); expect(spawnCalls[0]).toEqual({ command: 'xcrun', args: [ 'simctl', 'spawn', 'test-uuid', 'log', 'stream', '--level=debug', '--predicate', 'subsystem == "com.example.app"', ], }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for sim_statusbar plugin * Following CLAUDE.md testing standards with literal validation * Using dependency injection for deterministic testing */ import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, type CommandExecutor } from '../../../../test-utils/mock-executors.ts'; import simStatusbar, { sim_statusbarLogic } from '../sim_statusbar.ts'; describe('sim_statusbar tool', () => { describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(simStatusbar.name).toBe('sim_statusbar'); }); it('should have correct description', () => { expect(simStatusbar.description).toBe( '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).', ); }); it('should have handler function', () => { expect(typeof simStatusbar.handler).toBe('function'); }); it('should have correct schema with simulatorUuid string field and dataNetwork enum field', () => { const schema = z.object(simStatusbar.schema); // Valid inputs expect( schema.safeParse({ simulatorUuid: 'test-uuid-123', dataNetwork: 'wifi' }).success, ).toBe(true); expect(schema.safeParse({ simulatorUuid: 'ABC123-DEF456', dataNetwork: '3g' }).success).toBe( true, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '4g' }).success).toBe( true, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'lte' }).success).toBe( true, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'lte-a' }).success).toBe( true, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'lte+' }).success).toBe( true, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g' }).success).toBe( true, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g+' }).success).toBe( true, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g-uwb' }).success).toBe( true, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: '5g-uc' }).success).toBe( true, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'hide' }).success).toBe( true, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'clear' }).success).toBe( true, ); // Invalid inputs expect(schema.safeParse({ simulatorUuid: 123, dataNetwork: 'wifi' }).success).toBe(false); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 'invalid' }).success).toBe( false, ); expect(schema.safeParse({ simulatorUuid: 'test-uuid', dataNetwork: 123 }).success).toBe( false, ); expect(schema.safeParse({ simulatorUuid: null, dataNetwork: 'wifi' }).success).toBe(false); expect(schema.safeParse({ simulatorUuid: 'test-uuid' }).success).toBe(false); expect(schema.safeParse({ dataNetwork: 'wifi' }).success).toBe(false); expect(schema.safeParse({}).success).toBe(false); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { it('should handle successful status bar data network setting', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'Status bar set successfully', }); const result = await sim_statusbarLogic( { simulatorUuid: 'test-uuid-123', dataNetwork: 'wifi', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Successfully set simulator test-uuid-123 status bar data network to wifi', }, ], }); }); it('should handle minimal valid parameters (Zod handles validation)', async () => { // Note: With createTypedTool, Zod validation happens before the logic function is called // So we test with a valid minimal parameter set since validation is handled upstream const mockExecutor = createMockExecutor({ success: true, output: 'Status bar set successfully', }); const result = await sim_statusbarLogic( { simulatorUuid: 'test-uuid-123', dataNetwork: 'wifi', }, mockExecutor, ); // The logic function should execute normally with valid parameters // Zod validation errors are handled by createTypedTool wrapper expect(result.isError).toBe(undefined); expect(result.content[0].text).toContain('Successfully set simulator'); }); it('should handle command failure', async () => { const mockExecutor = createMockExecutor({ success: false, error: 'Simulator not found', }); const result = await sim_statusbarLogic( { simulatorUuid: 'invalid-uuid', dataNetwork: '3g', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to set status bar: Simulator not found', }, ], isError: true, }); }); it('should handle exception with Error object', async () => { const mockExecutor: CommandExecutor = async () => { throw new Error('Connection failed'); }; const result = await sim_statusbarLogic( { simulatorUuid: 'test-uuid-123', dataNetwork: '4g', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to set status bar: Connection failed', }, ], isError: true, }); }); it('should handle exception with string error', async () => { const mockExecutor: CommandExecutor = async () => { throw 'String error'; }; const result = await sim_statusbarLogic( { simulatorUuid: 'test-uuid-123', dataNetwork: 'lte', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to set status bar: String error', }, ], isError: true, }); }); it('should verify command generation with mock executor for override', async () => { const calls: Array<{ command: string[]; operationDescription: string; keepAlive: boolean; timeout: number | undefined; }> = []; const mockExecutor: CommandExecutor = async ( command, operationDescription, keepAlive, timeout, ) => { calls.push({ command, operationDescription, keepAlive, timeout }); return { success: true, output: 'Status bar set successfully', error: undefined, process: { pid: 12345 }, }; }; await sim_statusbarLogic( { simulatorUuid: 'test-uuid-123', dataNetwork: 'wifi', }, mockExecutor, ); expect(calls).toHaveLength(1); expect(calls[0]).toEqual({ command: [ 'xcrun', 'simctl', 'status_bar', 'test-uuid-123', 'override', '--dataNetwork', 'wifi', ], operationDescription: 'Set Status Bar', keepAlive: true, timeout: undefined, }); }); it('should verify command generation for clear operation', async () => { const calls: Array<{ command: string[]; operationDescription: string; keepAlive: boolean; timeout: number | undefined; }> = []; const mockExecutor: CommandExecutor = async ( command, operationDescription, keepAlive, timeout, ) => { calls.push({ command, operationDescription, keepAlive, timeout }); return { success: true, output: 'Status bar cleared successfully', error: undefined, process: { pid: 12345 }, }; }; await sim_statusbarLogic( { simulatorUuid: 'test-uuid-123', dataNetwork: 'clear', }, mockExecutor, ); expect(calls).toHaveLength(1); expect(calls[0]).toEqual({ command: ['xcrun', 'simctl', 'status_bar', 'test-uuid-123', 'clear'], operationDescription: 'Set Status Bar', keepAlive: true, timeout: undefined, }); }); it('should handle successful clear operation', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'Status bar cleared successfully', }); const result = await sim_statusbarLogic( { simulatorUuid: 'test-uuid-123', dataNetwork: 'clear', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Successfully cleared status bar overrides for simulator test-uuid-123', }, ], }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for list_schemes plugin * Following CLAUDE.md testing standards with literal validation * Using dependency injection for deterministic testing */ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; import plugin, { listSchemesLogic } from '../list_schemes.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; describe('list_schemes plugin', () => { beforeEach(() => { sessionStore.clear(); }); describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(plugin.name).toBe('list_schemes'); }); it('should have correct description', () => { expect(plugin.description).toBe('Lists schemes for a project or workspace.'); }); it('should have handler function', () => { expect(typeof plugin.handler).toBe('function'); }); it('should expose an empty public schema', () => { const schema = z.object(plugin.schema).strict(); expect(schema.safeParse({}).success).toBe(true); expect(schema.safeParse({ projectPath: '/path/to/MyProject.xcodeproj' }).success).toBe(false); expect(Object.keys(plugin.schema)).toEqual([]); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { it('should return success with schemes found', async () => { const mockExecutor = createMockExecutor({ success: true, output: `Information about project "MyProject": Targets: MyProject MyProjectTests Build Configurations: Debug Release Schemes: MyProject MyProjectTests`, }); const result = await listSchemesLogic( { projectPath: '/path/to/MyProject.xcodeproj' }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ Available schemes:', }, { type: 'text', text: 'MyProject\nMyProjectTests', }, { type: 'text', text: `Next Steps: 1. Build the app: build_macos({ projectPath: "/path/to/MyProject.xcodeproj", scheme: "MyProject" }) or for iOS: build_sim({ projectPath: "/path/to/MyProject.xcodeproj", scheme: "MyProject", simulatorName: "iPhone 16" }) 2. Show build settings: show_build_settings({ projectPath: "/path/to/MyProject.xcodeproj", scheme: "MyProject" })`, }, ], isError: false, }); }); it('should return error when command fails', async () => { const mockExecutor = createMockExecutor({ success: false, error: 'Project not found', }); const result = await listSchemesLogic( { projectPath: '/path/to/MyProject.xcodeproj' }, mockExecutor, ); expect(result).toEqual({ content: [{ type: 'text', text: 'Failed to list schemes: Project not found' }], isError: true, }); }); it('should return error when no schemes found in output', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'Information about project "MyProject":\n Targets:\n MyProject', }); const result = await listSchemesLogic( { projectPath: '/path/to/MyProject.xcodeproj' }, mockExecutor, ); expect(result).toEqual({ content: [{ type: 'text', text: 'No schemes found in the output' }], isError: true, }); }); it('should return success with empty schemes list', async () => { const mockExecutor = createMockExecutor({ success: true, output: `Information about project "MinimalProject": Targets: MinimalProject Build Configurations: Debug Release Schemes: `, }); const result = await listSchemesLogic( { projectPath: '/path/to/MyProject.xcodeproj' }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ Available schemes:', }, { type: 'text', text: '', }, { type: 'text', text: '', }, ], isError: false, }); }); it('should handle Error objects in catch blocks', async () => { const mockExecutor = async () => { throw new Error('Command execution failed'); }; const result = await listSchemesLogic( { projectPath: '/path/to/MyProject.xcodeproj' }, mockExecutor, ); expect(result).toEqual({ content: [{ type: 'text', text: 'Error listing schemes: Command execution failed' }], isError: true, }); }); it('should handle string error objects in catch blocks', async () => { const mockExecutor = async () => { throw 'String error'; }; const result = await listSchemesLogic( { projectPath: '/path/to/MyProject.xcodeproj' }, mockExecutor, ); expect(result).toEqual({ content: [{ type: 'text', text: 'Error listing schemes: String error' }], isError: true, }); }); it('should verify command generation with mock executor', async () => { const calls: any[] = []; const mockExecutor = async ( command: string[], action: string, showOutput: boolean, workingDir?: string, ) => { calls.push([command, action, showOutput, workingDir]); return { success: true, output: `Information about project "MyProject": Targets: MyProject Build Configurations: Debug Release Schemes: MyProject`, error: undefined, process: { pid: 12345 }, }; }; await listSchemesLogic({ projectPath: '/path/to/MyProject.xcodeproj' }, mockExecutor); expect(calls).toEqual([ [ ['xcodebuild', '-list', '-project', '/path/to/MyProject.xcodeproj'], 'List Schemes', true, undefined, ], ]); }); it('should handle validation when testing with missing projectPath via plugin handler', async () => { // Note: Direct logic function calls bypass Zod validation, so we test the actual plugin handler // to verify Zod validation works properly. The createTypedTool wrapper handles validation. const result = await plugin.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Missing required session defaults'); expect(result.content[0].text).toContain('Provide a project or workspace'); }); }); describe('XOR Validation', () => { it('should error when neither projectPath nor workspacePath provided', async () => { const result = await plugin.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Missing required session defaults'); expect(result.content[0].text).toContain('Provide a project or workspace'); }); it('should error when both projectPath and workspacePath provided', async () => { const result = await plugin.handler({ projectPath: '/path/to/project.xcodeproj', workspacePath: '/path/to/workspace.xcworkspace', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Mutually exclusive parameters provided'); }); it('should handle empty strings as undefined', async () => { const result = await plugin.handler({ projectPath: '', workspacePath: '', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Missing required session defaults'); expect(result.content[0].text).toContain('Provide a project or workspace'); }); }); describe('Workspace Support', () => { it('should list schemes for workspace', async () => { const mockExecutor = createMockExecutor({ success: true, output: `Information about workspace "MyWorkspace": Schemes: MyApp MyAppTests`, }); const result = await listSchemesLogic( { workspacePath: '/path/to/MyProject.xcworkspace' }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ Available schemes:', }, { type: 'text', text: 'MyApp\nMyAppTests', }, { type: 'text', text: `Next Steps: 1. Build the app: build_macos({ workspacePath: "/path/to/MyProject.xcworkspace", scheme: "MyApp" }) or for iOS: build_sim({ workspacePath: "/path/to/MyProject.xcworkspace", scheme: "MyApp", simulatorName: "iPhone 16" }) 2. Show build settings: show_build_settings({ workspacePath: "/path/to/MyProject.xcworkspace", scheme: "MyApp" })`, }, ], isError: false, }); }); it('should generate correct workspace command', async () => { const calls: any[] = []; const mockExecutor = async ( command: string[], action: string, showOutput: boolean, workingDir?: string, ) => { calls.push([command, action, showOutput, workingDir]); return { success: true, output: `Information about workspace "MyWorkspace": Schemes: MyApp`, error: undefined, process: { pid: 12345 }, }; }; await listSchemesLogic({ workspacePath: '/path/to/MyProject.xcworkspace' }, mockExecutor); expect(calls).toEqual([ [ ['xcodebuild', '-list', '-workspace', '/path/to/MyProject.xcworkspace'], 'List Schemes', true, undefined, ], ]); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/__tests__/list_sims.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor, createMockFileSystemExecutor, } from '../../../../test-utils/mock-executors.ts'; // Import the plugin and logic function import listSims, { list_simsLogic } from '../list_sims.ts'; describe('list_sims tool', () => { let callHistory: Array<{ command: string[]; logPrefix?: string; useShell?: boolean; env?: Record<string, string>; }>; callHistory = []; describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(listSims.name).toBe('list_sims'); }); it('should have correct description', () => { expect(listSims.description).toBe('Lists available iOS simulators with their UUIDs. '); }); it('should have handler function', () => { expect(typeof listSims.handler).toBe('function'); }); it('should have correct schema with enabled boolean field', () => { const schema = z.object(listSims.schema); // Valid inputs expect(schema.safeParse({ enabled: true }).success).toBe(true); expect(schema.safeParse({ enabled: false }).success).toBe(true); expect(schema.safeParse({ enabled: undefined }).success).toBe(true); expect(schema.safeParse({}).success).toBe(true); // Invalid inputs expect(schema.safeParse({ enabled: 'yes' }).success).toBe(false); expect(schema.safeParse({ enabled: 1 }).success).toBe(false); expect(schema.safeParse({ enabled: null }).success).toBe(false); }); }); describe('Handler Behavior (Complete Literal Returns)', () => { it('should handle successful simulator listing', async () => { const mockJsonOutput = JSON.stringify({ devices: { 'iOS 17.0': [ { name: 'iPhone 15', udid: 'test-uuid-123', isAvailable: true, state: 'Shutdown', }, ], }, }); const mockTextOutput = `== Devices == -- iOS 17.0 -- iPhone 15 (test-uuid-123) (Shutdown)`; // Create a mock executor that returns different outputs based on command const mockExecutor = async ( command: string[], logPrefix?: string, useShell?: boolean, env?: Record<string, string>, ) => { callHistory.push({ command, logPrefix, useShell, env }); // Return JSON output for JSON command if (command.includes('--json')) { return { success: true, output: mockJsonOutput, error: undefined, process: { pid: 12345 }, }; } // Return text output for text command return { success: true, output: mockTextOutput, error: undefined, process: { pid: 12345 }, }; }; const result = await list_simsLogic({ enabled: true }, mockExecutor); // Verify both commands were called expect(callHistory).toHaveLength(2); expect(callHistory[0]).toEqual({ command: ['xcrun', 'simctl', 'list', 'devices', '--json'], logPrefix: 'List Simulators (JSON)', useShell: true, env: undefined, }); expect(callHistory[1]).toEqual({ command: ['xcrun', 'simctl', 'list', 'devices'], logPrefix: 'List Simulators (Text)', useShell: true, env: undefined, }); expect(result).toEqual({ content: [ { type: 'text', text: `Available iOS Simulators: iOS 17.0: - iPhone 15 (test-uuid-123) Next Steps: 1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' }) 2. Open the simulator UI: open_sim({}) 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' }) 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`, }, ], }); }); it('should handle successful listing with booted simulator', async () => { const mockJsonOutput = JSON.stringify({ devices: { 'iOS 17.0': [ { name: 'iPhone 15', udid: 'test-uuid-123', isAvailable: true, state: 'Booted', }, ], }, }); const mockTextOutput = `== Devices == -- iOS 17.0 -- iPhone 15 (test-uuid-123) (Booted)`; const mockExecutor = async (command: string[]) => { if (command.includes('--json')) { return { success: true, output: mockJsonOutput, error: undefined, process: { pid: 12345 }, }; } return { success: true, output: mockTextOutput, error: undefined, process: { pid: 12345 }, }; }; const result = await list_simsLogic({ enabled: true }, mockExecutor); expect(result).toEqual({ content: [ { type: 'text', text: `Available iOS Simulators: iOS 17.0: - iPhone 15 (test-uuid-123) [Booted] Next Steps: 1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' }) 2. Open the simulator UI: open_sim({}) 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' }) 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`, }, ], }); }); it('should merge devices from text that are missing from JSON', async () => { const mockJsonOutput = JSON.stringify({ devices: { 'iOS 18.6': [ { name: 'iPhone 15', udid: 'json-uuid-123', isAvailable: true, state: 'Shutdown', }, ], }, }); const mockTextOutput = `== Devices == -- iOS 18.6 -- iPhone 15 (json-uuid-123) (Shutdown) -- iOS 26.0 -- iPhone 17 Pro (text-uuid-456) (Shutdown)`; const mockExecutor = async (command: string[]) => { if (command.includes('--json')) { return { success: true, output: mockJsonOutput, error: undefined, process: { pid: 12345 }, }; } return { success: true, output: mockTextOutput, error: undefined, process: { pid: 12345 }, }; }; const result = await list_simsLogic({ enabled: true }, mockExecutor); // Should contain both iOS 18.6 from JSON and iOS 26.0 from text expect(result).toEqual({ content: [ { type: 'text', text: `Available iOS Simulators: iOS 18.6: - iPhone 15 (json-uuid-123) iOS 26.0: - iPhone 17 Pro (text-uuid-456) Next Steps: 1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' }) 2. Open the simulator UI: open_sim({}) 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' }) 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`, }, ], }); }); it('should handle command failure', async () => { const mockExecutor = createMockExecutor({ success: false, output: '', error: 'Command failed', process: { pid: 12345 }, }); const result = await list_simsLogic({ enabled: true }, mockExecutor); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to list simulators: Command failed', }, ], }); }); it('should handle JSON parse failure and fall back to text parsing', async () => { const mockTextOutput = `== Devices == -- iOS 17.0 -- iPhone 15 (test-uuid-456) (Shutdown)`; const mockExecutor = async (command: string[]) => { // JSON command returns invalid JSON if (command.includes('--json')) { return { success: true, output: 'invalid json', error: undefined, process: { pid: 12345 }, }; } // Text command returns valid text output return { success: true, output: mockTextOutput, error: undefined, process: { pid: 12345 }, }; }; const result = await list_simsLogic({ enabled: true }, mockExecutor); // Should fall back to text parsing and extract devices expect(result).toEqual({ content: [ { type: 'text', text: `Available iOS Simulators: iOS 17.0: - iPhone 15 (test-uuid-456) Next Steps: 1. Boot a simulator: boot_sim({ simulatorUuid: 'UUID_FROM_ABOVE' }) 2. Open the simulator UI: open_sim({}) 3. Build for simulator: build_sim({ scheme: 'YOUR_SCHEME', simulatorId: 'UUID_FROM_ABOVE' }) 4. Get app path: get_sim_app_path({ scheme: 'YOUR_SCHEME', platform: 'iOS Simulator', simulatorId: 'UUID_FROM_ABOVE' })`, }, ], }); }); it('should handle exception with Error object', async () => { const mockExecutor = createMockExecutor(new Error('Command execution failed')); const result = await list_simsLogic({ enabled: true }, mockExecutor); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to list simulators: Command execution failed', }, ], }); }); it('should handle exception with string error', async () => { const mockExecutor = createMockExecutor('String error'); const result = await list_simsLogic({ enabled: true }, mockExecutor); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to list simulators: String error', }, ], }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import launchAppSim, { launch_app_simLogic } from '../launch_app_sim.ts'; describe('launch_app_sim tool', () => { beforeEach(() => { sessionStore.clear(); }); describe('Export Field Validation (Literal)', () => { it('should expose correct name and description', () => { expect(launchAppSim.name).toBe('launch_app_sim'); expect(launchAppSim.description).toBe('Launches an app in an iOS simulator.'); }); it('should expose only non-session fields in public schema', () => { const schema = z.object(launchAppSim.schema); expect( schema.safeParse({ bundleId: 'com.example.testapp', }).success, ).toBe(true); expect( schema.safeParse({ bundleId: 'com.example.testapp', args: ['--debug'], }).success, ).toBe(true); expect(schema.safeParse({}).success).toBe(false); expect(schema.safeParse({ bundleId: 123 }).success).toBe(false); expect(schema.safeParse({ args: ['--debug'] }).success).toBe(false); expect(Object.keys(launchAppSim.schema).sort()).toEqual(['args', 'bundleId'].sort()); }); }); describe('Handler Requirements', () => { it('should require simulator identifier when not provided', async () => { const result = await launchAppSim.handler({ bundleId: 'com.example.testapp' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Missing required session defaults'); expect(result.content[0].text).toContain('Provide simulatorId or simulatorName'); expect(result.content[0].text).toContain('session-set-defaults'); }); it('should validate bundleId when simulatorId default exists', async () => { sessionStore.setDefaults({ simulatorId: 'SIM-UUID' }); const result = await launchAppSim.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Parameter validation failed'); expect(result.content[0].text).toContain('bundleId: Required'); expect(result.content[0].text).toContain( 'Tip: set session defaults via session-set-defaults', ); }); it('should reject when both simulatorId and simulatorName provided explicitly', async () => { const result = await launchAppSim.handler({ simulatorId: 'SIM-UUID', simulatorName: 'iPhone 16', bundleId: 'com.example.testapp', }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Mutually exclusive parameters provided'); expect(result.content[0].text).toContain('simulatorId'); expect(result.content[0].text).toContain('simulatorName'); }); }); describe('Logic Behavior (Literal Returns)', () => { it('should launch app successfully with simulatorId', async () => { let callCount = 0; const sequencedExecutor = async (command: string[]) => { callCount++; if (callCount === 1) { return { success: true, output: '/path/to/app/container', error: '', process: {} as any, }; } return { success: true, output: 'App launched successfully', error: '', process: {} as any, }; }; const result = await launch_app_simLogic( { simulatorId: 'test-uuid-123', bundleId: 'com.example.testapp', }, sequencedExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: `✅ App launched successfully in simulator test-uuid-123. Next Steps: 1. To see simulator: open_sim() 2. Log capture: start_sim_log_cap({ simulatorUuid: "test-uuid-123", bundleId: "com.example.testapp" }) With console: start_sim_log_cap({ simulatorUuid: "test-uuid-123", bundleId: "com.example.testapp", captureConsole: true }) 3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`, }, ], }); }); it('should append additional arguments when provided', async () => { let callCount = 0; const commands: string[][] = []; const sequencedExecutor = async (command: string[]) => { commands.push(command); callCount++; if (callCount === 1) { return { success: true, output: '/path/to/app/container', error: '', process: {} as any, }; } return { success: true, output: 'App launched successfully', error: '', process: {} as any, }; }; await launch_app_simLogic( { simulatorId: 'test-uuid-123', bundleId: 'com.example.testapp', args: ['--debug', '--verbose'], }, sequencedExecutor, ); expect(commands[1]).toEqual([ 'xcrun', 'simctl', 'launch', 'test-uuid-123', 'com.example.testapp', '--debug', '--verbose', ]); }); it('should surface app-not-installed error', async () => { const mockExecutor = createMockExecutor({ success: false, output: '', error: 'App not found', }); const result = await launch_app_simLogic( { simulatorId: 'test-uuid-123', bundleId: 'com.example.testapp', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'App is not installed on the simulator. Please use install_app_sim before launching.\n\nWorkflow: build → install → launch.', }, ], isError: true, }); }); it('should return launch failure message when simctl launch fails', async () => { let callCount = 0; const sequencedExecutor = async (command: string[]) => { callCount++; if (callCount === 1) { return { success: true, output: '/path/to/app/container', error: '', process: {} as any, }; } return { success: false, output: '', error: 'Launch failed', process: {} as any, }; }; const result = await launch_app_simLogic( { simulatorId: 'test-uuid-123', bundleId: 'com.example.testapp', }, sequencedExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Launch app in simulator operation failed: Launch failed', }, ], }); }); it('should launch using simulatorName by resolving UUID', async () => { let callCount = 0; const sequencedExecutor = async (command: string[]) => { callCount++; if (callCount === 1) { return { success: true, output: JSON.stringify({ devices: { 'iOS 17.0': [ { name: 'iPhone 16', udid: 'resolved-uuid', isAvailable: true, state: 'Shutdown', }, ], }, }), error: '', process: {} as any, }; } if (callCount === 2) { return { success: true, output: '/path/to/app/container', error: '', process: {} as any, }; } return { success: true, output: 'App launched successfully', error: '', process: {} as any, }; }; const result = await launch_app_simLogic( { simulatorName: 'iPhone 16', bundleId: 'com.example.testapp', }, sequencedExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: `✅ App launched successfully in simulator "iPhone 16" (resolved-uuid). Next Steps: 1. To see simulator: open_sim() 2. Log capture: start_sim_log_cap({ simulatorName: "iPhone 16", bundleId: "com.example.testapp" }) With console: start_sim_log_cap({ simulatorName: "iPhone 16", bundleId: "com.example.testapp", captureConsole: true }) 3. Stop logs: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`, }, ], }); }); it('should return error when simulator name is not found', async () => { const mockListExecutor = async () => ({ success: true, output: JSON.stringify({ devices: {} }), error: '', process: {} as any, }); const result = await launch_app_simLogic( { simulatorName: 'Missing Simulator', bundleId: 'com.example.testapp', }, mockListExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Simulator named "Missing Simulator" not found. Use list_sims to see available simulators.', }, ], isError: true, }); }); it('should return error when simctl list fails', async () => { const mockExecutor = createMockExecutor({ success: false, output: '', error: 'simctl list failed', }); const result = await launch_app_simLogic( { simulatorName: 'iPhone 16', bundleId: 'com.example.testapp', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to list simulators: simctl list failed', }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/logging/__tests__/stop_device_log_cap.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for stop_device_log_cap plugin */ import { describe, it, expect, beforeEach } from 'vitest'; import { EventEmitter } from 'events'; import { z } from 'zod'; import plugin, { stop_device_log_capLogic } from '../stop_device_log_cap.ts'; import { activeDeviceLogSessions, type DeviceLogSession } from '../start_device_log_cap.ts'; import { createMockFileSystemExecutor } from '../../../../test-utils/mock-executors.ts'; // Note: Logger is allowed to execute normally (integration testing pattern) describe('stop_device_log_cap plugin', () => { beforeEach(() => { // Clear actual active sessions before each test activeDeviceLogSessions.clear(); }); describe('Plugin Structure', () => { it('should export an object with required properties', () => { expect(plugin).toHaveProperty('name'); expect(plugin).toHaveProperty('description'); expect(plugin).toHaveProperty('schema'); expect(plugin).toHaveProperty('handler'); }); it('should have correct tool name', () => { expect(plugin.name).toBe('stop_device_log_cap'); }); it('should have correct description', () => { expect(plugin.description).toBe( 'Stops an active Apple device log capture session and returns the captured logs.', ); }); it('should have correct schema structure', () => { // Schema should be a plain object for MCP protocol compliance expect(typeof plugin.schema).toBe('object'); expect(plugin.schema).toHaveProperty('logSessionId'); // Validate that schema fields are Zod types that can be used for validation const schema = z.object(plugin.schema); expect(schema.safeParse({ logSessionId: 'test-session-id' }).success).toBe(true); expect(schema.safeParse({ logSessionId: 123 }).success).toBe(false); }); it('should have handler as a function', () => { expect(typeof plugin.handler).toBe('function'); }); }); describe('Handler Functionality', () => { // Helper function to create a test process function createTestProcess( options: { killed?: boolean; exitCode?: number | null; } = {}, ) { const emitter = new EventEmitter(); const processState = { killed: options.killed ?? false, exitCode: options.exitCode ?? (options.killed ? 0 : null), killCalls: [] as string[], kill(signal?: string) { if (this.killed) { return false; } this.killCalls.push(signal ?? 'SIGTERM'); this.killed = true; this.exitCode = 0; emitter.emit('close', 0); return true; }, }; const testProcess = Object.assign(emitter, processState); return testProcess as typeof testProcess; } it('should handle stop log capture when session not found', async () => { const mockFileSystem = createMockFileSystemExecutor(); const result = await stop_device_log_capLogic( { logSessionId: 'device-log-00008110-001A2C3D4E5F-com.example.MyApp', }, mockFileSystem, ); expect(result.content[0].text).toBe( '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', ); expect(result.isError).toBe(true); }); it('should handle successful log capture stop', async () => { const testSessionId = 'test-session-123'; const testLogFilePath = '/tmp/xcodemcp_device_log_test-session-123.log'; const testLogContent = 'Device log content here...'; // Test active session const testProcess = createTestProcess({ killed: false, exitCode: null, }); activeDeviceLogSessions.set(testSessionId, { process: testProcess as unknown as DeviceLogSession['process'], logFilePath: testLogFilePath, deviceUuid: '00008110-001A2C3D4E5F', bundleId: 'com.example.MyApp', hasEnded: false, }); // Configure test file system for successful operation const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, readFile: async () => testLogContent, }); const result = await stop_device_log_capLogic( { logSessionId: testSessionId, }, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: `✅ Device log capture session stopped successfully\n\nSession ID: ${testSessionId}\n\n--- Captured Logs ---\n${testLogContent}`, }, ], }); expect(result.isError).toBeUndefined(); expect(testProcess.killCalls).toEqual(['SIGTERM']); expect(activeDeviceLogSessions.has(testSessionId)).toBe(false); }); it('should handle already killed process', async () => { const testSessionId = 'test-session-456'; const testLogFilePath = '/tmp/xcodemcp_device_log_test-session-456.log'; const testLogContent = 'Device log content...'; // Test active session with already killed process const testProcess = createTestProcess({ killed: true, exitCode: 0, }); activeDeviceLogSessions.set(testSessionId, { process: testProcess as unknown as DeviceLogSession['process'], logFilePath: testLogFilePath, deviceUuid: '00008110-001A2C3D4E5F', bundleId: 'com.example.MyApp', hasEnded: false, }); // Configure test file system for successful operation const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, readFile: async () => testLogContent, }); const result = await stop_device_log_capLogic( { logSessionId: testSessionId, }, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: `✅ Device log capture session stopped successfully\n\nSession ID: ${testSessionId}\n\n--- Captured Logs ---\n${testLogContent}`, }, ], }); expect(testProcess.killCalls).toEqual([]); // Should not kill already killed process }); it('should handle file access failure', async () => { const testSessionId = 'test-session-789'; const testLogFilePath = '/tmp/xcodemcp_device_log_test-session-789.log'; // Test active session const testProcess = createTestProcess({ killed: false, exitCode: null, }); activeDeviceLogSessions.set(testSessionId, { process: testProcess as unknown as DeviceLogSession['process'], logFilePath: testLogFilePath, deviceUuid: '00008110-001A2C3D4E5F', bundleId: 'com.example.MyApp', hasEnded: false, }); // Configure test file system for access failure (file doesn't exist) const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => false, }); const result = await stop_device_log_capLogic( { logSessionId: testSessionId, }, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: `Failed to stop device log capture session ${testSessionId}: Log file not found: ${testLogFilePath}`, }, ], isError: true, }); expect(activeDeviceLogSessions.has(testSessionId)).toBe(false); // Session still removed }); it('should handle file read failure', async () => { const testSessionId = 'test-session-abc'; const testLogFilePath = '/tmp/xcodemcp_device_log_test-session-abc.log'; // Test active session const testProcess = createTestProcess({ killed: false, exitCode: null, }); activeDeviceLogSessions.set(testSessionId, { process: testProcess as unknown as DeviceLogSession['process'], logFilePath: testLogFilePath, deviceUuid: '00008110-001A2C3D4E5F', bundleId: 'com.example.MyApp', hasEnded: false, }); // Configure test file system for successful access but failed read const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, readFile: async () => { throw new Error('Read permission denied'); }, }); const result = await stop_device_log_capLogic( { logSessionId: testSessionId, }, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: `Failed to stop device log capture session ${testSessionId}: Read permission denied`, }, ], isError: true, }); }); it('should handle string error objects', async () => { const testSessionId = 'test-session-def'; const testLogFilePath = '/tmp/xcodemcp_device_log_test-session-def.log'; // Test active session const testProcess = createTestProcess({ killed: false, exitCode: null, }); activeDeviceLogSessions.set(testSessionId, { process: testProcess as unknown as DeviceLogSession['process'], logFilePath: testLogFilePath, deviceUuid: '00008110-001A2C3D4E5F', bundleId: 'com.example.MyApp', hasEnded: false, }); // Configure test file system for access failure with string error const mockFileSystem = createMockFileSystemExecutor({ existsSync: () => true, readFile: async () => { throw 'String error message'; }, }); const result = await stop_device_log_capLogic( { logSessionId: testSessionId, }, mockFileSystem, ); expect(result).toEqual({ content: [ { type: 'text', text: `Failed to stop device log capture session ${testSessionId}: String error message`, }, ], isError: true, }); }); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tools/logging/stop_device_log_cap.ts: -------------------------------------------------------------------------------- ```typescript /** * Logging Plugin: Stop Device Log Capture * * Stops an active Apple device log capture session and returns the captured logs. */ import * as fs from 'fs'; import { z } from 'zod'; import { log } from '../../../utils/logging/index.ts'; import { activeDeviceLogSessions, type DeviceLogSession } from './start_device_log_cap.ts'; import { ToolResponse } from '../../../types/common.ts'; import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.ts'; import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts'; import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const stopDeviceLogCapSchema = z.object({ logSessionId: z.string().describe('The session ID returned by start_device_log_cap.'), }); // Use z.infer for type safety type StopDeviceLogCapParams = z.infer<typeof stopDeviceLogCapSchema>; /** * Business logic for stopping device log capture session */ export async function stop_device_log_capLogic( params: StopDeviceLogCapParams, fileSystemExecutor: FileSystemExecutor, ): Promise<ToolResponse> { const { logSessionId } = params; const session = activeDeviceLogSessions.get(logSessionId); if (!session) { log('warning', `Device log session not found: ${logSessionId}`); return { content: [ { type: 'text', text: `Failed to stop device log capture session ${logSessionId}: Device log capture session not found: ${logSessionId}`, }, ], isError: true, }; } try { log('info', `Attempting to stop device log capture session: ${logSessionId}`); const shouldSignalStop = !(session.hasEnded ?? false) && session.process.killed !== true && session.process.exitCode == null; if (shouldSignalStop) { session.process.kill?.('SIGTERM'); } await waitForSessionToFinish(session); if (session.logStream) { await ensureStreamClosed(session.logStream); } const logFilePath = session.logFilePath; activeDeviceLogSessions.delete(logSessionId); // Check file access if (!fileSystemExecutor.existsSync(logFilePath)) { throw new Error(`Log file not found: ${logFilePath}`); } const fileContent = await fileSystemExecutor.readFile(logFilePath, 'utf-8'); log('info', `Successfully read device log content from ${logFilePath}`); log( 'info', `Device log capture session ${logSessionId} stopped. Log file retained at: ${logFilePath}`, ); return { content: [ { type: 'text', text: `✅ Device log capture session stopped successfully\n\nSession ID: ${logSessionId}\n\n--- Captured Logs ---\n${fileContent}`, }, ], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); log('error', `Failed to stop device log capture session ${logSessionId}: ${message}`); return { content: [ { type: 'text', text: `Failed to stop device log capture session ${logSessionId}: ${message}`, }, ], isError: true, }; } } type WriteStreamWithClosed = fs.WriteStream & { closed?: boolean }; async function ensureStreamClosed(stream: fs.WriteStream): Promise<void> { const typedStream = stream as WriteStreamWithClosed; if (typedStream.destroyed || typedStream.closed) { return; } await new Promise<void>((resolve) => { const onClose = (): void => resolve(); typedStream.once('close', onClose); typedStream.end(); }).catch(() => { // Ignore cleanup errors – best-effort close }); } async function waitForSessionToFinish(session: DeviceLogSession): Promise<void> { if (session.hasEnded) { return; } if (session.process.exitCode != null) { session.hasEnded = true; return; } if (typeof session.process.once === 'function') { await new Promise<void>((resolve) => { const onClose = (): void => { clearTimeout(timeout); session.hasEnded = true; resolve(); }; const timeout = setTimeout(() => { session.process.removeListener?.('close', onClose); session.hasEnded = true; resolve(); }, 1000); session.process.once('close', onClose); if (session.hasEnded || session.process.exitCode != null) { session.process.removeListener?.('close', onClose); onClose(); } }); return; } // Fallback polling for minimal mock processes (primarily in tests) for (let i = 0; i < 20; i += 1) { if (session.hasEnded || session.process.exitCode != null) { session.hasEnded = true; break; } await new Promise((resolve) => setTimeout(resolve, 50)); } } /** * Type guard to check if an object has fs-like promises interface */ function hasPromisesInterface(obj: unknown): obj is { promises: typeof fs.promises } { return typeof obj === 'object' && obj !== null && 'promises' in obj; } /** * Type guard to check if an object has existsSync method */ function hasExistsSyncMethod(obj: unknown): obj is { existsSync: typeof fs.existsSync } { return typeof obj === 'object' && obj !== null && 'existsSync' in obj; } /** * Legacy support for backward compatibility */ export async function stopDeviceLogCapture( logSessionId: string, fileSystem?: unknown, ): Promise<{ logContent: string; error?: string }> { // For backward compatibility, create a mock FileSystemExecutor from the fileSystem parameter const fsToUse = fileSystem ?? fs; const mockFileSystemExecutor: FileSystemExecutor = { async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> { if (hasPromisesInterface(fsToUse)) { await fsToUse.promises.mkdir(path, options); } else { await fs.promises.mkdir(path, options); } }, async readFile(path: string, encoding: BufferEncoding = 'utf8'): Promise<string> { if (hasPromisesInterface(fsToUse)) { const result = await fsToUse.promises.readFile(path, encoding); return typeof result === 'string' ? result : (result as Buffer).toString(); } else { const result = await fs.promises.readFile(path, encoding); return typeof result === 'string' ? result : (result as Buffer).toString(); } }, async writeFile( path: string, content: string, encoding: BufferEncoding = 'utf8', ): Promise<void> { if (hasPromisesInterface(fsToUse)) { await fsToUse.promises.writeFile(path, content, encoding); } else { await fs.promises.writeFile(path, content, encoding); } }, async cp( source: string, destination: string, options?: { recursive?: boolean }, ): Promise<void> { if (hasPromisesInterface(fsToUse)) { await fsToUse.promises.cp(source, destination, options); } else { await fs.promises.cp(source, destination, options); } }, async readdir(path: string, options?: { withFileTypes?: boolean }): Promise<unknown[]> { if (hasPromisesInterface(fsToUse)) { if (options?.withFileTypes === true) { const result = await fsToUse.promises.readdir(path, { withFileTypes: true }); return Array.isArray(result) ? result : []; } else { const result = await fsToUse.promises.readdir(path); return Array.isArray(result) ? result : []; } } else { if (options?.withFileTypes === true) { const result = await fs.promises.readdir(path, { withFileTypes: true }); return Array.isArray(result) ? result : []; } else { const result = await fs.promises.readdir(path); return Array.isArray(result) ? result : []; } } }, async rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> { if (hasPromisesInterface(fsToUse)) { await fsToUse.promises.rm(path, options); } else { await fs.promises.rm(path, options); } }, existsSync(path: string): boolean { if (hasExistsSyncMethod(fsToUse)) { return fsToUse.existsSync(path); } else { return fs.existsSync(path); } }, async stat(path: string): Promise<{ isDirectory(): boolean }> { if (hasPromisesInterface(fsToUse)) { const result = await fsToUse.promises.stat(path); return result as { isDirectory(): boolean }; } else { const result = await fs.promises.stat(path); return result as { isDirectory(): boolean }; } }, async mkdtemp(prefix: string): Promise<string> { if (hasPromisesInterface(fsToUse)) { return await fsToUse.promises.mkdtemp(prefix); } else { return await fs.promises.mkdtemp(prefix); } }, tmpdir(): string { return '/tmp'; }, }; const result = await stop_device_log_capLogic({ logSessionId }, mockFileSystemExecutor); if (result.isError) { const errorText = result.content[0]?.text; const errorMessage = typeof errorText === 'string' ? errorText.replace(`Failed to stop device log capture session ${logSessionId}: `, '') : 'Unknown error occurred'; return { logContent: '', error: errorMessage, }; } // Extract log content from successful response const successText = result.content[0]?.text; if (typeof successText !== 'string') { return { logContent: '', error: 'Invalid response format: expected text content', }; } const logContentMatch = successText.match(/--- Captured Logs ---\n([\s\S]*)$/); const logContent = logContentMatch?.[1] ?? ''; return { logContent }; } export default { name: 'stop_device_log_cap', description: 'Stops an active Apple device log capture session and returns the captured logs.', schema: stopDeviceLogCapSchema.shape, // MCP SDK compatibility handler: createTypedTool( stopDeviceLogCapSchema, (params: StopDeviceLogCapParams) => { return stop_device_log_capLogic(params, getDefaultFileSystemExecutor()); }, getDefaultCommandExecutor, ), }; ``` -------------------------------------------------------------------------------- /src/test-utils/mock-executors.ts: -------------------------------------------------------------------------------- ```typescript /** * Mock Executors for Testing - Dependency Injection Architecture * * This module provides mock implementations of CommandExecutor and FileSystemExecutor * for testing purposes. These mocks are completely isolated from production dependencies * to avoid import chains that could trigger native module loading issues in test environments. * * IMPORTANT: These are EXACT copies of the mock functions originally in utils/command.js * to ensure zero behavioral changes during the file reorganization. * * Responsibilities: * - Providing mock command execution for tests * - Providing mock file system operations for tests * - Maintaining exact behavior compatibility with original implementations * - Avoiding any dependencies on production logging or instrumentation */ import { ChildProcess } from 'child_process'; import { CommandExecutor } from '../utils/CommandExecutor.ts'; import { FileSystemExecutor } from '../utils/FileSystemExecutor.ts'; /** * Create a mock executor for testing * @param result Mock command result or error to throw * @returns Mock executor function */ export function createMockExecutor( result: | { success?: boolean; output?: string; error?: string; process?: unknown; exitCode?: number; shouldThrow?: Error; } | Error | string, ): CommandExecutor { // If result is Error or string, return executor that rejects if (result instanceof Error || typeof result === 'string') { return async () => { throw result; }; } // If shouldThrow is specified, return executor that rejects with that error if (result.shouldThrow) { return async () => { throw result.shouldThrow; }; } const mockProcess = { pid: 12345, stdout: null, stderr: null, stdin: null, stdio: [null, null, null], killed: false, connected: false, exitCode: result.exitCode ?? (result.success === false ? 1 : 0), signalCode: null, spawnargs: [], spawnfile: 'sh', } as unknown as ChildProcess; return async () => ({ success: result.success ?? true, output: result.output ?? '', error: result.error, process: (result.process ?? mockProcess) as ChildProcess, exitCode: result.exitCode ?? (result.success === false ? 1 : 0), }); } /** * Create a no-op executor that throws an error if called * Use this for tests where an executor is required but should never be called * @returns CommandExecutor that throws on invocation */ export function createNoopExecutor(): CommandExecutor { return async (command) => { throw new Error( `🚨 NOOP EXECUTOR CALLED! 🚨\n` + `Command: ${command.join(' ')}\n` + `This executor should never be called in this test context.\n` + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + `Either fix the test to avoid this code path, or use createMockExecutor() instead.`, ); }; } /** * Create a command-matching mock executor for testing multi-command scenarios * Perfect for tools that execute multiple commands (like screenshot: simctl + sips) * * @param commandMap - Map of command patterns to their mock responses * @returns CommandExecutor that matches commands and returns appropriate responses * * @example * ```typescript * const mockExecutor = createCommandMatchingMockExecutor({ * 'xcrun simctl': { output: 'Screenshot saved' }, * 'sips': { output: 'Image optimized' } * }); * ``` */ export function createCommandMatchingMockExecutor( commandMap: Record< string, { success?: boolean; output?: string; error?: string; process?: unknown; exitCode?: number; } >, ): CommandExecutor { return async (command) => { const commandStr = command.join(' '); // Find matching command pattern const matchedKey = Object.keys(commandMap).find((key) => commandStr.includes(key)); if (!matchedKey) { throw new Error( `🚨 UNEXPECTED COMMAND! 🚨\n` + `Command: ${commandStr}\n` + `Expected one of: ${Object.keys(commandMap).join(', ')}\n` + `Available patterns: ${JSON.stringify(Object.keys(commandMap), null, 2)}`, ); } const result = commandMap[matchedKey]; const mockProcess = { pid: 12345, stdout: null, stderr: null, stdin: null, stdio: [null, null, null], killed: false, connected: false, exitCode: result.exitCode ?? (result.success === false ? 1 : 0), signalCode: null, spawnargs: [], spawnfile: 'sh', } as unknown as ChildProcess; return { success: result.success ?? true, // Success by default (as discussed) output: result.output ?? '', error: result.error, process: (result.process ?? mockProcess) as ChildProcess, exitCode: result.exitCode ?? (result.success === false ? 1 : 0), }; }; } /** * Create a mock file system executor for testing */ export function createMockFileSystemExecutor( overrides?: Partial<FileSystemExecutor>, ): FileSystemExecutor { return { mkdir: async (): Promise<void> => {}, readFile: async (): Promise<string> => 'mock file content', writeFile: async (): Promise<void> => {}, cp: async (): Promise<void> => {}, readdir: async (): Promise<unknown[]> => [], rm: async (): Promise<void> => {}, existsSync: (): boolean => false, stat: async (): Promise<{ isDirectory(): boolean }> => ({ isDirectory: (): boolean => true }), mkdtemp: async (): Promise<string> => '/tmp/mock-temp-123456', tmpdir: (): string => '/tmp', ...overrides, }; } /** * Create a no-op file system executor that throws an error if called * Use this for tests where an executor is required but should never be called * @returns CommandExecutor that throws on invocation */ export function createNoopFileSystemExecutor(): FileSystemExecutor { return { mkdir: async (): Promise<void> => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + `This executor should never be called in this test context.\n` + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, readFile: async (): Promise<string> => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + `This executor should never be called in this test context.\n` + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, writeFile: async (): Promise<void> => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + `This executor should never be called in this test context.\n` + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, cp: async (): Promise<void> => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + `This executor should never be called in this test context.\n` + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, readdir: async (): Promise<unknown[]> => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + `This executor should never be called in this test context.\n` + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, rm: async (): Promise<void> => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + `This executor should never be called in this test context.\n` + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, existsSync: (): boolean => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + `This executor should never be called in this test context.\n` + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, stat: async (): Promise<{ isDirectory(): boolean }> => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + `This executor should never be called in this test context.\n` + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, mkdtemp: async (): Promise<string> => { throw new Error( `🚨 NOOP FILESYSTEM EXECUTOR CALLED! 🚨\n` + `This executor should never be called in this test context.\n` + `If you see this error, it means the test is exercising a code path that wasn't expected.\n` + `Either fix the test to avoid this code path, or use createMockFileSystemExecutor() instead.`, ); }, tmpdir: (): string => '/tmp', }; } /** * Create a mock environment detector for testing * @param options Mock options for environment detection * @returns Mock environment detector */ export function createMockEnvironmentDetector( options: { isRunningUnderClaudeCode?: boolean; } = {}, ): import('../utils/environment.js').EnvironmentDetector { return { isRunningUnderClaudeCode: () => options.isRunningUnderClaudeCode ?? false, }; } ``` -------------------------------------------------------------------------------- /example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift: -------------------------------------------------------------------------------- ```swift // // CalculatorAppTests.swift // CalculatorAppTests // // Created by Cameron on 05/06/2025. // import XCTest import SwiftUI @testable import CalculatorApp import CalculatorAppFeature final class CalculatorAppTests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false } override func tearDownWithError() throws { // Clean up after each test } } // MARK: - App Lifecycle Tests extension CalculatorAppTests { func testAppLaunch() throws { // Test that the app launches without crashing let app = CalculatorApp() XCTAssertNotNil(app, "App should initialize successfully") } func testContentViewInitialization() throws { // Test that ContentView initializes properly let contentView = ContentView() XCTAssertNotNil(contentView, "ContentView should initialize successfully") } } // MARK: - Calculator Service Integration Tests extension CalculatorAppTests { func testCalculatorServiceCreation() throws { let service = CalculatorService() XCTAssertEqual(service.display, "0", "Calculator should start with display showing 0") XCTAssertEqual(service.expressionDisplay, "", "Calculator should start with empty expression") } func testCalculatorServiceFailure() throws { let service = CalculatorService() // This test is designed to fail to test error reporting XCTAssertEqual(service.display, "999", "This test should fail - display should be 0, not 999") } func testCalculatorServiceBasicOperation() throws { let service = CalculatorService() // Test basic addition service.inputNumber("5") service.setOperation(.add) service.inputNumber("3") service.calculate() XCTAssertEqual(service.display, "8", "5 + 3 should equal 8") } func testCalculatorServiceChainedOperations() throws { let service = CalculatorService() // Test chained operations: 10 + 5 * 2 = 30 (since calculator evaluates left to right) service.inputNumber("10") service.setOperation(.add) service.inputNumber("5") service.setOperation(.multiply) service.inputNumber("2") service.calculate() XCTAssertEqual(service.display, "30", "10 + 5 * 2 should equal 30 (left-to-right evaluation)") } func testCalculatorServiceClear() throws { let service = CalculatorService() // Set up some state service.inputNumber("123") service.setOperation(.add) service.inputNumber("456") // Clear should reset everything service.clear() XCTAssertEqual(service.display, "0", "Display should be 0 after clear") XCTAssertEqual(service.expressionDisplay, "", "Expression should be empty after clear") } } // MARK: - API Surface Tests extension CalculatorAppTests { func testCalculatorServicePublicInterface() throws { let service = CalculatorService() // Test that all expected public methods are available XCTAssertNoThrow(service.inputNumber("5")) XCTAssertNoThrow(service.inputDecimal()) XCTAssertNoThrow(service.setOperation(.add)) XCTAssertNoThrow(service.calculate()) XCTAssertNoThrow(service.toggleSign()) XCTAssertNoThrow(service.percentage()) XCTAssertNoThrow(service.clear()) } func testCalculatorServicePublicProperties() throws { let service = CalculatorService() // Test that all expected public properties are accessible XCTAssertNotNil(service.display) XCTAssertNotNil(service.expressionDisplay) XCTAssertEqual(service.hasError, false) // Test testing support properties XCTAssertEqual(service.currentValue, 0) XCTAssertEqual(service.previousValue, 0) XCTAssertNil(service.currentOperation) XCTAssertEqual(service.willResetDisplay, false) } func testCalculatorOperationsEnum() throws { // Test that all operations are available XCTAssertEqual(CalculatorService.Operation.add.rawValue, "+") XCTAssertEqual(CalculatorService.Operation.subtract.rawValue, "-") XCTAssertEqual(CalculatorService.Operation.multiply.rawValue, "×") XCTAssertEqual(CalculatorService.Operation.divide.rawValue, "÷") // Test operation calculations XCTAssertEqual(CalculatorService.Operation.add.calculate(5, 3), 8) XCTAssertEqual(CalculatorService.Operation.subtract.calculate(5, 3), 2) XCTAssertEqual(CalculatorService.Operation.multiply.calculate(5, 3), 15) XCTAssertEqual(CalculatorService.Operation.divide.calculate(6, 3), 2) XCTAssertEqual(CalculatorService.Operation.divide.calculate(5, 0), 0) // Division by zero } } // MARK: - Edge Case and Error Handling Tests extension CalculatorAppTests { func testDivisionByZero() throws { let service = CalculatorService() service.inputNumber("10") service.setOperation(.divide) service.inputNumber("0") service.calculate() XCTAssertEqual(service.display, "0", "Division by zero should return 0") } func testLargeNumbers() throws { let service = CalculatorService() // Test large number input service.inputNumber("999999999") XCTAssertEqual(service.display, "999999999", "Should handle large numbers") // Test large number calculation service.setOperation(.multiply) service.inputNumber("2") service.calculate() // Should handle the result without crashing XCTAssertNotEqual(service.display, "", "Should display some result for large calculations") } func testRepeatedEquals() throws { let service = CalculatorService() service.inputNumber("5") service.setOperation(.add) service.inputNumber("3") service.calculate() // 5 + 3 = 8 let firstResult = service.display service.calculate() // Should repeat last operation: 8 + 3 = 11 let secondResult = service.display XCTAssertEqual(firstResult, "8", "First calculation should be correct") XCTAssertEqual(secondResult, "11", "Repeated equals should repeat last operation") } } // MARK: - Performance Tests extension CalculatorAppTests { func testCalculationPerformance() throws { let service = CalculatorService() measure { // Measure performance of 100 calculations for i in 1...100 { service.clear() service.inputNumber("\(i)") service.setOperation(.multiply) service.inputNumber("2") service.calculate() } } } func testLargeNumberInputPerformance() throws { let service = CalculatorService() measure { // Measure performance of inputting large numbers service.clear() for digit in "123456789012345" { service.inputNumber(String(digit)) } } } } // MARK: - State Consistency Tests extension CalculatorAppTests { func testStateConsistencyAfterOperations() throws { let service = CalculatorService() // Perform a series of operations and verify state remains consistent service.inputNumber("10") XCTAssertEqual(service.display, "10") service.setOperation(.add) XCTAssertEqual(service.display, "10") XCTAssertTrue(service.expressionDisplay.contains("10 +")) service.inputNumber("5") XCTAssertEqual(service.display, "5") service.calculate() XCTAssertEqual(service.display, "15") } func testStateConsistencyWithDecimalNumbers() throws { let service = CalculatorService() service.inputNumber("3") service.inputDecimal() service.inputNumber("14") XCTAssertEqual(service.display, "3.14") service.setOperation(.multiply) service.inputNumber("2") service.calculate() XCTAssertEqual(service.display, "6.28") } func testMultipleDecimalPointsHandling() throws { let service = CalculatorService() service.inputNumber("1") service.inputDecimal() service.inputNumber("5") service.inputDecimal() // This should be ignored service.inputNumber("9") XCTAssertEqual(service.display, "1.59", "Multiple decimal points should be ignored") } } // MARK: - Component Integration Tests extension CalculatorAppTests { func testComplexCalculationWorkflow() throws { let service = CalculatorService() // Test complex workflow through direct service calls service.inputNumber("2") service.inputNumber("5") service.setOperation(.divide) service.inputNumber("5") service.calculate() XCTAssertEqual(service.display, "5", "Complex workflow should work correctly") // Test that we can continue with the result service.setOperation(.multiply) service.inputNumber("4") service.calculate() XCTAssertEqual(service.display, "20", "Should be able to continue with previous result") } func testPercentageCalculation() throws { let service = CalculatorService() service.inputNumber("50") service.percentage() XCTAssertEqual(service.display, "0.5", "50% should equal 0.5") } func testSignToggle() throws { let service = CalculatorService() service.inputNumber("42") service.toggleSign() XCTAssertEqual(service.display, "-42", "Should toggle to negative") service.toggleSign() XCTAssertEqual(service.display, "42", "Should toggle back to positive") } } ``` -------------------------------------------------------------------------------- /src/mcp/tools/device/__tests__/launch_app_device.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Pure dependency injection test for launch_app_device plugin (device-shared) * * Tests plugin structure and app launching functionality including parameter validation, * command generation, file operations, and response formatting. * * Uses createMockExecutor for command execution and manual stubs for file operations. */ import { describe, it, expect, beforeEach } from 'vitest'; import { z } from 'zod'; import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; import launchAppDevice, { launch_app_deviceLogic } from '../launch_app_device.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; describe('launch_app_device plugin (device-shared)', () => { beforeEach(() => { sessionStore.clear(); }); describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { expect(launchAppDevice.name).toBe('launch_app_device'); }); it('should have correct description', () => { expect(launchAppDevice.description).toBe('Launches an app on a connected device.'); }); it('should have handler function', () => { expect(typeof launchAppDevice.handler).toBe('function'); }); it('should validate schema with valid inputs', () => { const schema = z.object(launchAppDevice.schema).strict(); expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true); expect(schema.safeParse({}).success).toBe(false); expect(Object.keys(launchAppDevice.schema)).toEqual(['bundleId']); }); it('should validate schema with invalid inputs', () => { const schema = z.object(launchAppDevice.schema).strict(); expect(schema.safeParse({ bundleId: null }).success).toBe(false); expect(schema.safeParse({ bundleId: 123 }).success).toBe(false); }); }); describe('Handler Requirements', () => { it('should require deviceId when not provided', async () => { const result = await launchAppDevice.handler({ bundleId: 'com.example.app' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('deviceId is required'); }); }); describe('Command Generation', () => { it('should generate correct devicectl command with required parameters', async () => { const calls: any[] = []; const mockExecutor = createMockExecutor({ success: true, output: 'App launched successfully', process: { pid: 12345 }, }); const trackingExecutor = async ( command: string[], logPrefix?: string, useShell?: boolean, env?: Record<string, string>, ) => { calls.push({ command, logPrefix, useShell, env }); return mockExecutor(command, logPrefix, useShell, env); }; await launch_app_deviceLogic( { deviceId: 'test-device-123', bundleId: 'com.example.app', }, trackingExecutor, ); expect(calls).toHaveLength(1); expect(calls[0].command).toEqual([ 'xcrun', 'devicectl', 'device', 'process', 'launch', '--device', 'test-device-123', '--json-output', expect.stringMatching(/^\/.*\/launch-\d+\.json$/), '--terminate-existing', 'com.example.app', ]); expect(calls[0].logPrefix).toBe('Launch app on device'); expect(calls[0].useShell).toBe(true); expect(calls[0].env).toBeUndefined(); }); it('should generate command with different device and bundle parameters', async () => { const calls: any[] = []; const mockExecutor = createMockExecutor({ success: true, output: 'Launch successful', process: { pid: 54321 }, }); const trackingExecutor = async (command: string[]) => { calls.push({ command }); return mockExecutor(command); }; await launch_app_deviceLogic( { deviceId: '00008030-001E14BE2288802E', bundleId: 'com.apple.mobilesafari', }, trackingExecutor, ); expect(calls[0].command).toEqual([ 'xcrun', 'devicectl', 'device', 'process', 'launch', '--device', '00008030-001E14BE2288802E', '--json-output', expect.stringMatching(/^\/.*\/launch-\d+\.json$/), '--terminate-existing', 'com.apple.mobilesafari', ]); }); }); describe('Success Path Tests', () => { it('should return successful launch response without process ID', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'App launched successfully', }); const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', bundleId: 'com.example.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ App launched successfully\n\nApp launched successfully', }, ], }); }); it('should return successful launch response with detailed output', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'Launch succeeded with detailed output', }); const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', bundleId: 'com.example.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ App launched successfully\n\nLaunch succeeded with detailed output', }, ], }); }); it('should handle successful launch with process ID information', async () => { // Mock fs operations for JSON parsing const fs = await import('fs'); const originalReadFile = fs.promises.readFile; const originalUnlink = fs.promises.unlink; const mockReadFile = (path: string) => { if (path.includes('launch-')) { return Promise.resolve( JSON.stringify({ result: { process: { processIdentifier: 12345, }, }, }), ); } return originalReadFile(path); }; const mockUnlink = () => Promise.resolve(); // Replace fs methods fs.promises.readFile = mockReadFile; fs.promises.unlink = mockUnlink; const mockExecutor = createMockExecutor({ success: true, output: 'App launched successfully', }); const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', bundleId: 'com.example.app', }, mockExecutor, ); // Restore fs methods fs.promises.readFile = originalReadFile; fs.promises.unlink = originalUnlink; expect(result).toEqual({ content: [ { type: 'text', 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 })', }, ], }); }); it('should handle successful launch with command output', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'App "com.example.app" launched on device "test-device-123"', }); const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', bundleId: 'com.example.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: '✅ App launched successfully\n\nApp "com.example.app" launched on device "test-device-123"', }, ], }); }); }); describe('Error Handling', () => { it('should return launch failure response', async () => { const mockExecutor = createMockExecutor({ success: false, error: 'Launch failed: App not found', }); const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', bundleId: 'com.nonexistent.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to launch app: Launch failed: App not found', }, ], isError: true, }); }); it('should return command failure response with specific error', async () => { const mockExecutor = createMockExecutor({ success: false, error: 'Device not found: test-device-invalid', }); const result = await launch_app_deviceLogic( { deviceId: 'test-device-invalid', bundleId: 'com.example.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to launch app: Device not found: test-device-invalid', }, ], isError: true, }); }); it('should handle executor exception with Error object', async () => { const mockExecutor = createMockExecutor(new Error('Network error')); const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', bundleId: 'com.example.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to launch app on device: Network error', }, ], isError: true, }); }); it('should handle executor exception with string error', async () => { const mockExecutor = createMockExecutor('String error'); const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', bundleId: 'com.example.app', }, mockExecutor, ); expect(result).toEqual({ content: [ { type: 'text', text: 'Failed to launch app on device: String error', }, ], isError: true, }); }); }); }); ```