Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/session-aware-migration-todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ Reference: `docs/session_management_plan.md`
- [x] `src/mcp/tools/project-discovery/show_build_settings.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`.

## Device Workflows
- [ ] `src/mcp/tools/device/build_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
- [ ] `src/mcp/tools/device/test_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `deviceId`, `configuration`.
- [ ] `src/mcp/tools/device/get_device_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
- [ ] `src/mcp/tools/device/install_app_device.ts` — session defaults: `deviceId`.
- [ ] `src/mcp/tools/device/launch_app_device.ts` — session defaults: `deviceId`.
- [ ] `src/mcp/tools/device/stop_app_device.ts` — session defaults: `deviceId`.
- [x] `src/mcp/tools/device/build_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
- [x] `src/mcp/tools/device/test_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `deviceId`, `configuration`.
- [x] `src/mcp/tools/device/get_device_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
- [x] `src/mcp/tools/device/install_app_device.ts` — session defaults: `deviceId`.
- [x] `src/mcp/tools/device/launch_app_device.ts` — session defaults: `deviceId`.
- [x] `src/mcp/tools/device/stop_app_device.ts` — session defaults: `deviceId`.

## Device Logging
- [ ] `src/mcp/tools/logging/start_device_log_cap.ts` — session defaults: `deviceId`.
- [x] `src/mcp/tools/logging/start_device_log_cap.ts` — session defaults: `deviceId`.

## macOS Workflows
- [ ] `src/mcp/tools/macos/build_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
Expand Down
55 changes: 25 additions & 30 deletions src/mcp/tools/device/__tests__/build_device.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,40 @@
* Using dependency injection for deterministic testing
*/

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

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

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

it('should have correct description', () => {
expect(buildDevice.description).toBe(
"Builds an app from a project or workspace for a physical Apple device. Provide exactly one of projectPath or workspacePath. Example: build_device({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })",
);
expect(buildDevice.description).toBe('Builds an app for a connected device.');
});

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

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

// Test optional fields
expect(buildDevice.schema.configuration.safeParse('Debug').success).toBe(true);
expect(buildDevice.schema.derivedDataPath.safeParse('/path/to/derived-data').success).toBe(
true,
);
expect(buildDevice.schema.extraArgs.safeParse(['--arg1', '--arg2']).success).toBe(true);
expect(buildDevice.schema.preferXcodebuild.safeParse(true).success).toBe(true);

// Test invalid inputs
expect(buildDevice.schema.projectPath.safeParse(null).success).toBe(false);
expect(buildDevice.schema.workspacePath.safeParse(null).success).toBe(false);
expect(buildDevice.schema.scheme.safeParse(null).success).toBe(false);
expect(buildDevice.schema.extraArgs.safeParse('not-array').success).toBe(false);
expect(buildDevice.schema.preferXcodebuild.safeParse('not-boolean').success).toBe(false);
const schemaKeys = Object.keys(buildDevice.schema).sort();
expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild']);
});
});

Expand All @@ -58,7 +48,8 @@ describe('build_device plugin', () => {
});

expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Either projectPath or workspacePath is required');
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 () => {
Expand All @@ -69,7 +60,8 @@ describe('build_device plugin', () => {
});

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

Expand All @@ -80,9 +72,8 @@ describe('build_device plugin', () => {
});

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

it('should return Zod validation error for invalid parameter types', async () => {
Expand All @@ -93,6 +84,10 @@ describe('build_device plugin', () => {

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

Expand Down
69 changes: 39 additions & 30 deletions src/mcp/tools/device/__tests__/get_device_app_path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,40 @@
* Using dependency injection for deterministic testing
*/

import { describe, it, expect } from 'vitest';
import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
import getDeviceAppPath, { get_device_app_pathLogic } from '../get_device_app_path.ts';
import { sessionStore } from '../../../../utils/session-store.ts';

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

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

it('should have correct description', () => {
expect(getDeviceAppPath.description).toBe(
"Gets the app bundle path for a physical device application (iOS, watchOS, tvOS, visionOS) using either a project or workspace. Provide exactly one of projectPath or workspacePath. Example: get_device_app_path({ projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme' })",
'Retrieves the built app path for a connected device.',
);
});

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

it('should validate schema correctly', () => {
// Test project path
expect(
getDeviceAppPath.schema.projectPath.safeParse('/path/to/project.xcodeproj').success,
).toBe(true);

// Test workspace path
expect(
getDeviceAppPath.schema.workspacePath.safeParse('/path/to/workspace.xcworkspace').success,
).toBe(true);

// Test required scheme field
expect(getDeviceAppPath.schema.scheme.safeParse('MyScheme').success).toBe(true);

// Test optional fields
expect(getDeviceAppPath.schema.configuration.safeParse('Debug').success).toBe(true);
expect(getDeviceAppPath.schema.platform.safeParse('iOS').success).toBe(true);
expect(getDeviceAppPath.schema.platform.safeParse('watchOS').success).toBe(true);
expect(getDeviceAppPath.schema.platform.safeParse('tvOS').success).toBe(true);
expect(getDeviceAppPath.schema.platform.safeParse('visionOS').success).toBe(true);

// Test invalid inputs
expect(getDeviceAppPath.schema.projectPath.safeParse(null).success).toBe(false);
expect(getDeviceAppPath.schema.workspacePath.safeParse(null).success).toBe(false);
expect(getDeviceAppPath.schema.scheme.safeParse(null).success).toBe(false);
expect(getDeviceAppPath.schema.platform.safeParse('invalidPlatform').success).toBe(false);
it('should expose only platform in public schema', () => {
const schema = z.object(getDeviceAppPath.schema).strict();
expect(schema.safeParse({}).success).toBe(true);
expect(schema.safeParse({ platform: 'iOS' }).success).toBe(true);
expect(schema.safeParse({ projectPath: '/path/to/project.xcodeproj' }).success).toBe(false);

const schemaKeys = Object.keys(getDeviceAppPath.schema).sort();
expect(schemaKeys).toEqual(['platform']);
});
});

Expand All @@ -59,7 +47,8 @@ describe('get_device_app_path plugin', () => {
scheme: 'MyScheme',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Either projectPath or workspacePath is required');
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 () => {
Expand All @@ -69,7 +58,27 @@ describe('get_device_app_path plugin', () => {
scheme: 'MyScheme',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('mutually exclusive');
expect(result.content[0].text).toContain('Parameter validation failed');
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
});
});

describe('Handler Requirements', () => {
it('should require scheme when missing', async () => {
const result = await getDeviceAppPath.handler({
projectPath: '/path/to/project.xcodeproj',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Missing required session defaults');
expect(result.content[0].text).toContain('scheme is required');
});

it('should require project or workspace when scheme default exists', async () => {
sessionStore.setDefaults({ scheme: 'MyScheme' });

const result = await getDeviceAppPath.handler({});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Provide a project or workspace');
});
});

Expand Down
37 changes: 25 additions & 12 deletions src/mcp/tools/device/__tests__/install_app_device.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,48 @@
* Using dependency injection for deterministic testing
*/

import { describe, it, expect } from 'vitest';
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 physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Requires deviceId and appPath.',
);
expect(installAppDevice.description).toBe('Installs an app on a connected device.');
});

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

it('should validate schema correctly', () => {
// Test required fields
expect(installAppDevice.schema.deviceId.safeParse('test-device-123').success).toBe(true);
expect(installAppDevice.schema.appPath.safeParse('/path/to/test.app').success).toBe(true);
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);

// Test invalid inputs
expect(installAppDevice.schema.deviceId.safeParse(null).success).toBe(false);
expect(installAppDevice.schema.deviceId.safeParse(123).success).toBe(false);
expect(installAppDevice.schema.appPath.safeParse(null).success).toBe(false);
expect(Object.keys(installAppDevice.schema)).toEqual(['appPath']);
});
});

Expand Down
60 changes: 23 additions & 37 deletions src/mcp/tools/device/__tests__/launch_app_device.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,64 +7,50 @@
* Uses createMockExecutor for command execution and manual stubs for file operations.
*/

import { describe, it, expect } from 'vitest';
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 physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Requires deviceId and bundleId.',
);
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);
expect(
schema.safeParse({
deviceId: 'test-device-123',
bundleId: 'com.example.app',
}).success,
).toBe(true);
expect(
schema.safeParse({
deviceId: '00008030-001E14BE2288802E',
bundleId: 'com.apple.calculator',
}).success,
).toBe(true);
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);
expect(schema.safeParse({}).success).toBe(false);
expect(
schema.safeParse({
deviceId: null,
bundleId: 'com.example.app',
}).success,
).toBe(false);
expect(
schema.safeParse({
deviceId: 'test-device-123',
bundleId: null,
}).success,
).toBe(false);
expect(
schema.safeParse({
deviceId: 123,
bundleId: 'com.example.app',
}).success,
).toBe(false);
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');
});
});

Expand Down
Loading
Loading