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
8 changes: 4 additions & 4 deletions docs/session-aware-migration-todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ Reference: `docs/session_management_plan.md`
- [ ] `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`.
- [ ] `src/mcp/tools/macos/build_run_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
- [ ] `src/mcp/tools/macos/test_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
- [ ] `src/mcp/tools/macos/get_mac_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
- [x] `src/mcp/tools/macos/build_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
- [x] `src/mcp/tools/macos/build_run_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.
- [x] `src/mcp/tools/macos/test_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
- [x] `src/mcp/tools/macos/get_mac_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.

## Simulator Build/Test/Path
- [x] `src/mcp/tools/simulator/test_sim.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `simulatorId`, `simulatorName`, `configuration`, `useLatestOS`.
Expand Down
85 changes: 56 additions & 29 deletions src/mcp/tools/macos/__tests__/build_macos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,81 @@
* NO VITEST MOCKING ALLOWED - Only createMockExecutor and createMockFileSystemExecutor
*/

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 { sessionStore } from '../../../../utils/session-store.ts';
import buildMacOS, { buildMacOSLogic } from '../build_macos.ts';

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

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

it('should have correct description', () => {
expect(buildMacOS.description).toBe(
"Builds a macOS app using xcodebuild from a project or workspace. Provide exactly one of projectPath or workspacePath. Example: build_macos({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })",
);
expect(buildMacOS.description).toBe('Builds a macOS app.');
});

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

it('should validate schema correctly', () => {
// Test required fields
expect(buildMacOS.schema.projectPath.safeParse('/path/to/MyProject.xcodeproj').success).toBe(
true,
);
const schema = z.object(buildMacOS.schema);

expect(schema.safeParse({}).success).toBe(true);
expect(
buildMacOS.schema.workspacePath.safeParse('/path/to/MyProject.xcworkspace').success,
schema.safeParse({
derivedDataPath: '/path/to/derived-data',
extraArgs: ['--arg1', '--arg2'],
preferXcodebuild: true,
}).success,
).toBe(true);
expect(buildMacOS.schema.scheme.safeParse('MyScheme').success).toBe(true);

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

// Test invalid inputs
expect(buildMacOS.schema.projectPath.safeParse(null).success).toBe(false);
expect(buildMacOS.schema.workspacePath.safeParse(null).success).toBe(false);
expect(buildMacOS.schema.scheme.safeParse(null).success).toBe(false);
expect(buildMacOS.schema.arch.safeParse('invalidArch').success).toBe(false);
expect(buildMacOS.schema.extraArgs.safeParse('not-array').success).toBe(false);
expect(buildMacOS.schema.preferXcodebuild.safeParse('not-boolean').success).toBe(false);
expect(schema.safeParse({ derivedDataPath: 42 }).success).toBe(false);
expect(schema.safeParse({ extraArgs: ['--ok', 1] }).success).toBe(false);
expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);

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

describe('Handler Requirements', () => {
it('should require scheme when no defaults provided', async () => {
const result = await buildMacOS.handler({});

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

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

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

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

it('should reject when both projectPath and workspacePath provided explicitly', async () => {
sessionStore.setDefaults({ scheme: 'MyScheme' });

const result = await buildMacOS.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');
expect(result.content[0].text).toContain('projectPath');
expect(result.content[0].text).toContain('workspacePath');
});
});

Expand Down Expand Up @@ -416,7 +443,7 @@ describe('build_macos plugin', () => {
it('should error when neither projectPath nor workspacePath provided', async () => {
const result = await buildMacOS.handler({ 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('Provide a project or workspace');
});

it('should error when both projectPath and workspacePath provided', async () => {
Expand All @@ -426,7 +453,7 @@ describe('build_macos plugin', () => {
scheme: 'MyScheme',
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('mutually exclusive');
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
});

it('should succeed with valid projectPath', async () => {
Expand Down
123 changes: 42 additions & 81 deletions src/mcp/tools/macos/__tests__/build_run_macos.test.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,75 @@
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 { sessionStore } from '../../../../utils/session-store.ts';
import tool, { buildRunMacOSLogic } from '../build_run_macos.ts';

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

describe('Export Field Validation (Literal)', () => {
it('should export the correct name', () => {
expect(tool.name).toBe('build_run_macos');
});

it('should export the correct description', () => {
expect(tool.description).toBe(
"Builds and runs a macOS app from a project or workspace in one step. Provide exactly one of projectPath or workspacePath. Example: build_run_macos({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })",
);
expect(tool.description).toBe('Builds and runs a macOS app.');
});

it('should export a handler function', () => {
expect(typeof tool.handler).toBe('function');
});

it('should validate schema with valid project inputs', () => {
const validInput = {
projectPath: '/path/to/project.xcodeproj',
scheme: 'MyApp',
configuration: 'Debug',
derivedDataPath: '/path/to/derived',
arch: 'arm64',
extraArgs: ['--verbose'],
preferXcodebuild: true,
};
it('should expose only non-session fields in schema', () => {
const schema = z.object(tool.schema);
expect(schema.safeParse(validInput).success).toBe(true);
});

it('should validate schema with valid workspace inputs', () => {
const validInput = {
workspacePath: '/path/to/workspace.xcworkspace',
scheme: 'MyApp',
configuration: 'Debug',
derivedDataPath: '/path/to/derived',
arch: 'arm64',
extraArgs: ['--verbose'],
preferXcodebuild: true,
};
const schema = z.object(tool.schema);
expect(schema.safeParse(validInput).success).toBe(true);
expect(schema.safeParse({}).success).toBe(true);
expect(
schema.safeParse({
derivedDataPath: '/tmp/derived',
extraArgs: ['--verbose'],
preferXcodebuild: true,
}).success,
).toBe(true);

expect(schema.safeParse({ derivedDataPath: 1 }).success).toBe(false);
expect(schema.safeParse({ extraArgs: ['--ok', 2] }).success).toBe(false);
expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);

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

it('should validate schema with minimal valid project inputs', () => {
const validInput = {
projectPath: '/path/to/project.xcodeproj',
scheme: 'MyApp',
};
const schema = z.object(tool.schema);
expect(schema.safeParse(validInput).success).toBe(true);
});
describe('Handler Requirements', () => {
it('should require scheme before executing', async () => {
const result = await tool.handler({});

it('should validate schema with minimal valid workspace inputs', () => {
const validInput = {
workspacePath: '/path/to/workspace.xcworkspace',
scheme: 'MyApp',
};
const schema = z.object(tool.schema);
expect(schema.safeParse(validInput).success).toBe(true);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('scheme is required');
});

it('should reject inputs with both projectPath and workspacePath', () => {
const invalidInput = {
projectPath: '/path/to/project.xcodeproj',
workspacePath: '/path/to/workspace.xcworkspace',
scheme: 'MyApp',
};
const schema = z.object(tool.schema);
expect(schema.safeParse(invalidInput).success).toBe(true); // Base schema passes, but runtime validation should fail
});
it('should require project or workspace once scheme is set', async () => {
sessionStore.setDefaults({ scheme: 'MyApp' });

it('should reject inputs with neither projectPath nor workspacePath', () => {
const invalidInput = {
scheme: 'MyApp',
};
const schema = z.object(tool.schema);
expect(schema.safeParse(invalidInput).success).toBe(true); // Base schema passes, but runtime validation should fail
});
const result = await tool.handler({});

it('should reject invalid projectPath', () => {
const invalidInput = {
projectPath: 123,
scheme: 'MyApp',
};
const schema = z.object(tool.schema);
expect(schema.safeParse(invalidInput).success).toBe(false);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Provide a project or workspace');
});

it('should reject invalid scheme', () => {
const invalidInput = {
projectPath: '/path/to/project.xcodeproj',
scheme: 123,
};
const schema = z.object(tool.schema);
expect(schema.safeParse(invalidInput).success).toBe(false);
});
it('should fail when both project and workspace provided explicitly', async () => {
sessionStore.setDefaults({ scheme: 'MyApp' });

it('should reject invalid arch', () => {
const invalidInput = {
const result = await tool.handler({
projectPath: '/path/to/project.xcodeproj',
scheme: 'MyApp',
arch: 'invalid',
};
const schema = z.object(tool.schema);
expect(schema.safeParse(invalidInput).success).toBe(false);
workspacePath: '/path/to/workspace.xcworkspace',
});

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

Expand Down
Loading
Loading