Skip to content

Commit 6eabc79

Browse files
committed
refactor: migrate device and macOS tools to event-based handler contract
1 parent d9e9216 commit 6eabc79

31 files changed

+2732
-4107
lines changed

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

Lines changed: 143 additions & 213 deletions
Large diffs are not rendered by default.

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

Lines changed: 316 additions & 221 deletions
Large diffs are not rendered by default.

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

Lines changed: 120 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,46 @@
1-
/**
2-
* Tests for get_device_app_path plugin (unified)
3-
* Following CLAUDE.md testing standards with literal validation
4-
* Using dependency injection for deterministic testing
5-
*/
6-
71
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { DERIVED_DATA_DIR } from '../../../../utils/log-paths.ts';
83
import * as z from 'zod';
94
import {
105
createMockCommandResponse,
116
createMockExecutor,
127
} from '../../../../test-utils/mock-executors.ts';
138
import { schema, handler, get_device_app_pathLogic } from '../get_device_app_path.ts';
149
import { sessionStore } from '../../../../utils/session-store.ts';
10+
import { createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
11+
12+
const runLogic = async (logic: () => Promise<unknown>) => {
13+
const { result, run } = createMockToolHandlerContext();
14+
const response = await run(logic);
15+
16+
if (
17+
response &&
18+
typeof response === 'object' &&
19+
'content' in (response as Record<string, unknown>)
20+
) {
21+
return response as {
22+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
23+
isError?: boolean;
24+
nextStepParams?: unknown;
25+
};
26+
}
27+
28+
const text = result.text();
29+
const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
30+
const imageContent = result.attachments.map((attachment) => ({
31+
type: 'image' as const,
32+
data: attachment.data,
33+
mimeType: attachment.mimeType,
34+
}));
35+
36+
return {
37+
content: [...textContent, ...imageContent],
38+
isError: result.isError() ? true : undefined,
39+
nextStepParams: result.nextStepParams,
40+
attachments: result.attachments,
41+
text,
42+
};
43+
};
1544

1645
describe('get_device_app_path plugin', () => {
1746
beforeEach(() => {
@@ -107,12 +136,14 @@ describe('get_device_app_path plugin', () => {
107136
);
108137
};
109138

110-
await get_device_app_pathLogic(
111-
{
112-
projectPath: '/path/to/project.xcodeproj',
113-
scheme: 'MyScheme',
114-
},
115-
mockExecutor,
139+
await runLogic(() =>
140+
get_device_app_pathLogic(
141+
{
142+
projectPath: '/path/to/project.xcodeproj',
143+
scheme: 'MyScheme',
144+
},
145+
mockExecutor,
146+
),
116147
);
117148

118149
expect(calls).toHaveLength(1);
@@ -128,6 +159,8 @@ describe('get_device_app_path plugin', () => {
128159
'Debug',
129160
'-destination',
130161
'generic/platform=iOS',
162+
'-derivedDataPath',
163+
DERIVED_DATA_DIR,
131164
],
132165
logPrefix: 'Get App Path',
133166
useShell: false,
@@ -161,13 +194,15 @@ describe('get_device_app_path plugin', () => {
161194
);
162195
};
163196

164-
await get_device_app_pathLogic(
165-
{
166-
projectPath: '/path/to/project.xcodeproj',
167-
scheme: 'MyScheme',
168-
platform: 'watchOS',
169-
},
170-
mockExecutor,
197+
await runLogic(() =>
198+
get_device_app_pathLogic(
199+
{
200+
projectPath: '/path/to/project.xcodeproj',
201+
scheme: 'MyScheme',
202+
platform: 'watchOS',
203+
},
204+
mockExecutor,
205+
),
171206
);
172207

173208
expect(calls).toHaveLength(1);
@@ -183,6 +218,8 @@ describe('get_device_app_path plugin', () => {
183218
'Debug',
184219
'-destination',
185220
'generic/platform=watchOS',
221+
'-derivedDataPath',
222+
DERIVED_DATA_DIR,
186223
],
187224
logPrefix: 'Get App Path',
188225
useShell: false,
@@ -216,12 +253,14 @@ describe('get_device_app_path plugin', () => {
216253
);
217254
};
218255

219-
await get_device_app_pathLogic(
220-
{
221-
workspacePath: '/path/to/workspace.xcworkspace',
222-
scheme: 'MyScheme',
223-
},
224-
mockExecutor,
256+
await runLogic(() =>
257+
get_device_app_pathLogic(
258+
{
259+
workspacePath: '/path/to/workspace.xcworkspace',
260+
scheme: 'MyScheme',
261+
},
262+
mockExecutor,
263+
),
225264
);
226265

227266
expect(calls).toHaveLength(1);
@@ -237,6 +276,8 @@ describe('get_device_app_path plugin', () => {
237276
'Debug',
238277
'-destination',
239278
'generic/platform=iOS',
279+
'-derivedDataPath',
280+
DERIVED_DATA_DIR,
240281
],
241282
logPrefix: 'Get App Path',
242283
useShell: false,
@@ -251,29 +292,24 @@ describe('get_device_app_path plugin', () => {
251292
'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n',
252293
});
253294

254-
const result = await get_device_app_pathLogic(
255-
{
256-
projectPath: '/path/to/project.xcodeproj',
257-
scheme: 'MyScheme',
258-
},
259-
mockExecutor,
260-
);
261-
262-
expect(result).toEqual({
263-
content: [
295+
const result = await runLogic(() =>
296+
get_device_app_pathLogic(
264297
{
265-
type: 'text',
266-
text: '✅ App path retrieved successfully: /path/to/build/Debug-iphoneos/MyApp.app',
298+
projectPath: '/path/to/project.xcodeproj',
299+
scheme: 'MyScheme',
267300
},
268-
],
269-
nextStepParams: {
270-
get_app_bundle_id: { appPath: '/path/to/build/Debug-iphoneos/MyApp.app' },
271-
install_app_device: {
272-
deviceId: 'DEVICE_UDID',
273-
appPath: '/path/to/build/Debug-iphoneos/MyApp.app',
274-
},
275-
launch_app_device: { deviceId: 'DEVICE_UDID', bundleId: 'BUNDLE_ID' },
301+
mockExecutor,
302+
),
303+
);
304+
305+
expect(result.isError).toBeFalsy();
306+
expect(result.nextStepParams).toEqual({
307+
get_app_bundle_id: { appPath: '/path/to/build/Debug-iphoneos/MyApp.app' },
308+
install_app_device: {
309+
deviceId: 'DEVICE_UDID',
310+
appPath: '/path/to/build/Debug-iphoneos/MyApp.app',
276311
},
312+
launch_app_device: { deviceId: 'DEVICE_UDID', bundleId: 'BUNDLE_ID' },
277313
});
278314
});
279315

@@ -283,23 +319,18 @@ describe('get_device_app_path plugin', () => {
283319
error: 'xcodebuild: error: The project does not exist.',
284320
});
285321

286-
const result = await get_device_app_pathLogic(
287-
{
288-
projectPath: '/path/to/nonexistent.xcodeproj',
289-
scheme: 'MyScheme',
290-
},
291-
mockExecutor,
292-
);
293-
294-
expect(result).toEqual({
295-
content: [
322+
const result = await runLogic(() =>
323+
get_device_app_pathLogic(
296324
{
297-
type: 'text',
298-
text: 'Failed to get app path: xcodebuild: error: The project does not exist.',
325+
projectPath: '/path/to/nonexistent.xcodeproj',
326+
scheme: 'MyScheme',
299327
},
300-
],
301-
isError: true,
302-
});
328+
mockExecutor,
329+
),
330+
);
331+
332+
expect(result.isError).toBe(true);
333+
expect(result.nextStepParams).toBeUndefined();
303334
});
304335

305336
it('should return exact parse failure response', async () => {
@@ -308,23 +339,18 @@ describe('get_device_app_path plugin', () => {
308339
output: 'Build settings without required fields',
309340
});
310341

311-
const result = await get_device_app_pathLogic(
312-
{
313-
projectPath: '/path/to/project.xcodeproj',
314-
scheme: 'MyScheme',
315-
},
316-
mockExecutor,
317-
);
318-
319-
expect(result).toEqual({
320-
content: [
342+
const result = await runLogic(() =>
343+
get_device_app_pathLogic(
321344
{
322-
type: 'text',
323-
text: 'Failed to extract app path from build settings. Make sure the app has been built first.',
345+
projectPath: '/path/to/project.xcodeproj',
346+
scheme: 'MyScheme',
324347
},
325-
],
326-
isError: true,
327-
});
348+
mockExecutor,
349+
),
350+
);
351+
352+
expect(result.isError).toBe(true);
353+
expect(result.nextStepParams).toBeUndefined();
328354
});
329355

330356
it('should include optional configuration parameter in command', async () => {
@@ -353,13 +379,15 @@ describe('get_device_app_path plugin', () => {
353379
);
354380
};
355381

356-
await get_device_app_pathLogic(
357-
{
358-
projectPath: '/path/to/project.xcodeproj',
359-
scheme: 'MyScheme',
360-
configuration: 'Release',
361-
},
362-
mockExecutor,
382+
await runLogic(() =>
383+
get_device_app_pathLogic(
384+
{
385+
projectPath: '/path/to/project.xcodeproj',
386+
scheme: 'MyScheme',
387+
configuration: 'Release',
388+
},
389+
mockExecutor,
390+
),
363391
);
364392

365393
expect(calls).toHaveLength(1);
@@ -375,6 +403,8 @@ describe('get_device_app_path plugin', () => {
375403
'Release',
376404
'-destination',
377405
'generic/platform=iOS',
406+
'-derivedDataPath',
407+
DERIVED_DATA_DIR,
378408
],
379409
logPrefix: 'Get App Path',
380410
useShell: false,
@@ -393,47 +423,18 @@ describe('get_device_app_path plugin', () => {
393423
return Promise.reject(new Error('Network error'));
394424
};
395425

396-
const result = await get_device_app_pathLogic(
397-
{
398-
projectPath: '/path/to/project.xcodeproj',
399-
scheme: 'MyScheme',
400-
},
401-
mockExecutor,
402-
);
403-
404-
expect(result).toEqual({
405-
content: [
426+
const result = await runLogic(() =>
427+
get_device_app_pathLogic(
406428
{
407-
type: 'text',
408-
text: 'Error retrieving app path: Network error',
429+
projectPath: '/path/to/project.xcodeproj',
430+
scheme: 'MyScheme',
409431
},
410-
],
411-
isError: true,
412-
});
413-
});
414-
415-
it('should return exact string error handling response', async () => {
416-
const mockExecutor = () => {
417-
return Promise.reject('String error');
418-
};
419-
420-
const result = await get_device_app_pathLogic(
421-
{
422-
projectPath: '/path/to/project.xcodeproj',
423-
scheme: 'MyScheme',
424-
},
425-
mockExecutor,
432+
mockExecutor,
433+
),
426434
);
427435

428-
expect(result).toEqual({
429-
content: [
430-
{
431-
type: 'text',
432-
text: 'Error retrieving app path: String error',
433-
},
434-
],
435-
isError: true,
436-
});
436+
expect(result.isError).toBe(true);
437+
expect(result.nextStepParams).toBeUndefined();
437438
});
438439
});
439440
});

0 commit comments

Comments
 (0)