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