Skip to content

Commit dec9ec3

Browse files
committed
feat(device): add session-aware handlers and streaming log capture
1 parent 4bbfe5d commit dec9ec3

17 files changed

Lines changed: 517 additions & 299 deletions

docs/session-aware-migration-todo.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ Reference: `docs/session_management_plan.md`
1212
- [x] `src/mcp/tools/project-discovery/show_build_settings.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`.
1313

1414
## Device Workflows
15-
- [ ] `src/mcp/tools/device/build_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
16-
- [ ] `src/mcp/tools/device/test_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `deviceId`, `configuration`.
17-
- [ ] `src/mcp/tools/device/get_device_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
18-
- [ ] `src/mcp/tools/device/install_app_device.ts` — session defaults: `deviceId`.
19-
- [ ] `src/mcp/tools/device/launch_app_device.ts` — session defaults: `deviceId`.
20-
- [ ] `src/mcp/tools/device/stop_app_device.ts` — session defaults: `deviceId`.
15+
- [x] `src/mcp/tools/device/build_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
16+
- [x] `src/mcp/tools/device/test_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `deviceId`, `configuration`.
17+
- [x] `src/mcp/tools/device/get_device_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`.
18+
- [x] `src/mcp/tools/device/install_app_device.ts` — session defaults: `deviceId`.
19+
- [x] `src/mcp/tools/device/launch_app_device.ts` — session defaults: `deviceId`.
20+
- [x] `src/mcp/tools/device/stop_app_device.ts` — session defaults: `deviceId`.
2121

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

2525
## macOS Workflows
2626
- [ ] `src/mcp/tools/macos/build_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`.

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

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,40 @@
44
* Using dependency injection for deterministic testing
55
*/
66

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

1113
describe('build_device plugin', () => {
14+
beforeEach(() => {
15+
sessionStore.clear();
16+
});
17+
1218
describe('Export Field Validation (Literal)', () => {
1319
it('should have correct name', () => {
1420
expect(buildDevice.name).toBe('build_device');
1521
});
1622

1723
it('should have correct description', () => {
18-
expect(buildDevice.description).toBe(
19-
"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' })",
20-
);
24+
expect(buildDevice.description).toBe('Builds an app for a connected device.');
2125
});
2226

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

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

37-
// Test optional fields
38-
expect(buildDevice.schema.configuration.safeParse('Debug').success).toBe(true);
39-
expect(buildDevice.schema.derivedDataPath.safeParse('/path/to/derived-data').success).toBe(
40-
true,
41-
);
42-
expect(buildDevice.schema.extraArgs.safeParse(['--arg1', '--arg2']).success).toBe(true);
43-
expect(buildDevice.schema.preferXcodebuild.safeParse(true).success).toBe(true);
44-
45-
// Test invalid inputs
46-
expect(buildDevice.schema.projectPath.safeParse(null).success).toBe(false);
47-
expect(buildDevice.schema.workspacePath.safeParse(null).success).toBe(false);
48-
expect(buildDevice.schema.scheme.safeParse(null).success).toBe(false);
49-
expect(buildDevice.schema.extraArgs.safeParse('not-array').success).toBe(false);
50-
expect(buildDevice.schema.preferXcodebuild.safeParse('not-boolean').success).toBe(false);
39+
const schemaKeys = Object.keys(buildDevice.schema).sort();
40+
expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild']);
5141
});
5242
});
5343

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

6050
expect(result.isError).toBe(true);
61-
expect(result.content[0].text).toContain('Either projectPath or workspacePath is required');
51+
expect(result.content[0].text).toContain('Missing required session defaults');
52+
expect(result.content[0].text).toContain('Provide a project or workspace');
6253
});
6354

6455
it('should error when both projectPath and workspacePath provided', async () => {
@@ -69,7 +60,8 @@ describe('build_device plugin', () => {
6960
});
7061

7162
expect(result.isError).toBe(true);
72-
expect(result.content[0].text).toContain('mutually exclusive');
63+
expect(result.content[0].text).toContain('Parameter validation failed');
64+
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
7365
});
7466
});
7567

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

8274
expect(result.isError).toBe(true);
83-
expect(result.content[0].text).toContain('Parameter validation failed');
84-
expect(result.content[0].text).toContain('scheme');
85-
expect(result.content[0].text).toContain('Required');
75+
expect(result.content[0].text).toContain('Missing required session defaults');
76+
expect(result.content[0].text).toContain('scheme is required');
8677
});
8778

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

9485
expect(result.isError).toBe(true);
9586
expect(result.content[0].text).toContain('Parameter validation failed');
87+
expect(result.content[0].text).toContain('projectPath');
88+
expect(result.content[0].text).toContain(
89+
'Tip: set session defaults via session-set-defaults',
90+
);
9691
});
9792
});
9893

src/mcp/tools/device/__tests__/get_device_app_path.test.ts

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,40 @@
44
* Using dependency injection for deterministic testing
55
*/
66

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

1113
describe('get_device_app_path plugin', () => {
14+
beforeEach(() => {
15+
sessionStore.clear();
16+
});
17+
1218
describe('Export Field Validation (Literal)', () => {
1319
it('should have correct name', () => {
1420
expect(getDeviceAppPath.name).toBe('get_device_app_path');
1521
});
1622

1723
it('should have correct description', () => {
1824
expect(getDeviceAppPath.description).toBe(
19-
"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' })",
25+
'Retrieves the built app path for a connected device.',
2026
);
2127
});
2228

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

27-
it('should validate schema correctly', () => {
28-
// Test project path
29-
expect(
30-
getDeviceAppPath.schema.projectPath.safeParse('/path/to/project.xcodeproj').success,
31-
).toBe(true);
32-
33-
// Test workspace path
34-
expect(
35-
getDeviceAppPath.schema.workspacePath.safeParse('/path/to/workspace.xcworkspace').success,
36-
).toBe(true);
37-
38-
// Test required scheme field
39-
expect(getDeviceAppPath.schema.scheme.safeParse('MyScheme').success).toBe(true);
40-
41-
// Test optional fields
42-
expect(getDeviceAppPath.schema.configuration.safeParse('Debug').success).toBe(true);
43-
expect(getDeviceAppPath.schema.platform.safeParse('iOS').success).toBe(true);
44-
expect(getDeviceAppPath.schema.platform.safeParse('watchOS').success).toBe(true);
45-
expect(getDeviceAppPath.schema.platform.safeParse('tvOS').success).toBe(true);
46-
expect(getDeviceAppPath.schema.platform.safeParse('visionOS').success).toBe(true);
47-
48-
// Test invalid inputs
49-
expect(getDeviceAppPath.schema.projectPath.safeParse(null).success).toBe(false);
50-
expect(getDeviceAppPath.schema.workspacePath.safeParse(null).success).toBe(false);
51-
expect(getDeviceAppPath.schema.scheme.safeParse(null).success).toBe(false);
52-
expect(getDeviceAppPath.schema.platform.safeParse('invalidPlatform').success).toBe(false);
33+
it('should expose only platform in public schema', () => {
34+
const schema = z.object(getDeviceAppPath.schema).strict();
35+
expect(schema.safeParse({}).success).toBe(true);
36+
expect(schema.safeParse({ platform: 'iOS' }).success).toBe(true);
37+
expect(schema.safeParse({ projectPath: '/path/to/project.xcodeproj' }).success).toBe(false);
38+
39+
const schemaKeys = Object.keys(getDeviceAppPath.schema).sort();
40+
expect(schemaKeys).toEqual(['platform']);
5341
});
5442
});
5543

@@ -59,7 +47,8 @@ describe('get_device_app_path plugin', () => {
5947
scheme: 'MyScheme',
6048
});
6149
expect(result.isError).toBe(true);
62-
expect(result.content[0].text).toContain('Either projectPath or workspacePath is required');
50+
expect(result.content[0].text).toContain('Missing required session defaults');
51+
expect(result.content[0].text).toContain('Provide a project or workspace');
6352
});
6453

6554
it('should error when both projectPath and workspacePath provided', async () => {
@@ -69,7 +58,27 @@ describe('get_device_app_path plugin', () => {
6958
scheme: 'MyScheme',
7059
});
7160
expect(result.isError).toBe(true);
72-
expect(result.content[0].text).toContain('mutually exclusive');
61+
expect(result.content[0].text).toContain('Parameter validation failed');
62+
expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
63+
});
64+
});
65+
66+
describe('Handler Requirements', () => {
67+
it('should require scheme when missing', async () => {
68+
const result = await getDeviceAppPath.handler({
69+
projectPath: '/path/to/project.xcodeproj',
70+
});
71+
expect(result.isError).toBe(true);
72+
expect(result.content[0].text).toContain('Missing required session defaults');
73+
expect(result.content[0].text).toContain('scheme is required');
74+
});
75+
76+
it('should require project or workspace when scheme default exists', async () => {
77+
sessionStore.setDefaults({ scheme: 'MyScheme' });
78+
79+
const result = await getDeviceAppPath.handler({});
80+
expect(result.isError).toBe(true);
81+
expect(result.content[0].text).toContain('Provide a project or workspace');
7382
});
7483
});
7584

src/mcp/tools/device/__tests__/install_app_device.test.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,48 @@
44
* Using dependency injection for deterministic testing
55
*/
66

7-
import { describe, it, expect } from 'vitest';
7+
import { describe, it, expect, beforeEach } from 'vitest';
8+
import { z } from 'zod';
89
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
910
import installAppDevice, { install_app_deviceLogic } from '../install_app_device.ts';
11+
import { sessionStore } from '../../../../utils/session-store.ts';
1012

1113
describe('install_app_device plugin', () => {
14+
beforeEach(() => {
15+
sessionStore.clear();
16+
});
17+
18+
describe('Handler Requirements', () => {
19+
it('should require deviceId when session defaults are missing', async () => {
20+
const result = await installAppDevice.handler({
21+
appPath: '/path/to/test.app',
22+
});
23+
24+
expect(result.isError).toBe(true);
25+
expect(result.content[0].text).toContain('deviceId is required');
26+
});
27+
});
28+
1229
describe('Export Field Validation (Literal)', () => {
1330
it('should have correct name', () => {
1431
expect(installAppDevice.name).toBe('install_app_device');
1532
});
1633

1734
it('should have correct description', () => {
18-
expect(installAppDevice.description).toBe(
19-
'Installs an app on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Requires deviceId and appPath.',
20-
);
35+
expect(installAppDevice.description).toBe('Installs an app on a connected device.');
2136
});
2237

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

27-
it('should validate schema correctly', () => {
28-
// Test required fields
29-
expect(installAppDevice.schema.deviceId.safeParse('test-device-123').success).toBe(true);
30-
expect(installAppDevice.schema.appPath.safeParse('/path/to/test.app').success).toBe(true);
42+
it('should require appPath in public schema', () => {
43+
const schema = z.object(installAppDevice.schema).strict();
44+
expect(schema.safeParse({ appPath: '/path/to/test.app' }).success).toBe(true);
45+
expect(schema.safeParse({}).success).toBe(false);
46+
expect(schema.safeParse({ deviceId: 'test-device-123' }).success).toBe(false);
3147

32-
// Test invalid inputs
33-
expect(installAppDevice.schema.deviceId.safeParse(null).success).toBe(false);
34-
expect(installAppDevice.schema.deviceId.safeParse(123).success).toBe(false);
35-
expect(installAppDevice.schema.appPath.safeParse(null).success).toBe(false);
48+
expect(Object.keys(installAppDevice.schema)).toEqual(['appPath']);
3649
});
3750
});
3851

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

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,50 @@
77
* Uses createMockExecutor for command execution and manual stubs for file operations.
88
*/
99

10-
import { describe, it, expect } from 'vitest';
10+
import { describe, it, expect, beforeEach } from 'vitest';
1111
import { z } from 'zod';
1212
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
1313
import launchAppDevice, { launch_app_deviceLogic } from '../launch_app_device.ts';
14+
import { sessionStore } from '../../../../utils/session-store.ts';
1415

1516
describe('launch_app_device plugin (device-shared)', () => {
17+
beforeEach(() => {
18+
sessionStore.clear();
19+
});
20+
1621
describe('Export Field Validation (Literal)', () => {
1722
it('should have correct name', () => {
1823
expect(launchAppDevice.name).toBe('launch_app_device');
1924
});
2025

2126
it('should have correct description', () => {
22-
expect(launchAppDevice.description).toBe(
23-
'Launches an app on a physical Apple device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Requires deviceId and bundleId.',
24-
);
27+
expect(launchAppDevice.description).toBe('Launches an app on a connected device.');
2528
});
2629

2730
it('should have handler function', () => {
2831
expect(typeof launchAppDevice.handler).toBe('function');
2932
});
3033

3134
it('should validate schema with valid inputs', () => {
32-
const schema = z.object(launchAppDevice.schema);
33-
expect(
34-
schema.safeParse({
35-
deviceId: 'test-device-123',
36-
bundleId: 'com.example.app',
37-
}).success,
38-
).toBe(true);
39-
expect(
40-
schema.safeParse({
41-
deviceId: '00008030-001E14BE2288802E',
42-
bundleId: 'com.apple.calculator',
43-
}).success,
44-
).toBe(true);
35+
const schema = z.object(launchAppDevice.schema).strict();
36+
expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true);
37+
expect(schema.safeParse({}).success).toBe(false);
38+
expect(Object.keys(launchAppDevice.schema)).toEqual(['bundleId']);
4539
});
4640

4741
it('should validate schema with invalid inputs', () => {
48-
const schema = z.object(launchAppDevice.schema);
49-
expect(schema.safeParse({}).success).toBe(false);
50-
expect(
51-
schema.safeParse({
52-
deviceId: null,
53-
bundleId: 'com.example.app',
54-
}).success,
55-
).toBe(false);
56-
expect(
57-
schema.safeParse({
58-
deviceId: 'test-device-123',
59-
bundleId: null,
60-
}).success,
61-
).toBe(false);
62-
expect(
63-
schema.safeParse({
64-
deviceId: 123,
65-
bundleId: 'com.example.app',
66-
}).success,
67-
).toBe(false);
42+
const schema = z.object(launchAppDevice.schema).strict();
43+
expect(schema.safeParse({ bundleId: null }).success).toBe(false);
44+
expect(schema.safeParse({ bundleId: 123 }).success).toBe(false);
45+
});
46+
});
47+
48+
describe('Handler Requirements', () => {
49+
it('should require deviceId when not provided', async () => {
50+
const result = await launchAppDevice.handler({ bundleId: 'com.example.app' });
51+
52+
expect(result.isError).toBe(true);
53+
expect(result.content[0].text).toContain('deviceId is required');
6854
});
6955
});
7056

0 commit comments

Comments
 (0)