Skip to content

refactor(8/12): migrate UI automation tools to event-based handlers#326

Open
cameroncooke wants to merge 1 commit intorefactor/migrate-device-macos-toolsfrom
refactor/migrate-ui-automation-tools
Open

refactor(8/12): migrate UI automation tools to event-based handlers#326
cameroncooke wants to merge 1 commit intorefactor/migrate-device-macos-toolsfrom
refactor/migrate-ui-automation-tools

Conversation

@cameroncooke
Copy link
Copy Markdown
Collaborator

Summary

This is PR 8 of 12 in a stacked PR series that decouples the rendering pipeline from MCP transport. Depends on PR 7 (device/macOS migrations).

Migrates all UI automation tool handlers to the new event-based handler contract.

Tools migrated (25 files)

button, gesture, key_press, key_sequence, long_press, screenshot, snapshot_ui, swipe, tap, touch, type_text

Notable changes

  • Shared AXe command module (src/mcp/tools/ui-automation/shared/axe-command.ts): Extracted common AXe CLI invocation logic that was duplicated across all 11 UI automation tools. Each tool had its own copy of AXe process spawning, timeout handling, and error formatting. Now consolidated into one shared module that accepts an emit callback.
  • axe-helpers.ts and axe/index.ts: Minor updates to work with the shared command module.
  • screenshot.ts: Uses ctx.attach() for image data instead of constructing ToolResponseContent directly. This is the only tool that produces non-text output.

Pattern

UI automation tools are simpler than build tools -- they invoke AXe, parse the response, and emit result events. The main simplification is removing the per-tool AXe boilerplate:

```typescript
// Before: each tool had ~30 lines of AXe setup
const axeResult = await executeAxeCommand({ ... });
return toolResponse([...formatResult(axeResult)]);

// After: shared module handles AXe setup
await executeAxeAction(ctx, { ... });
ctx.emit(statusLine('success', '...'));
```

Stack navigation

  • PR 1-5/12: Foundation, utilities, runtime contract
  • PR 6-7/12: Simulator, device, macOS migrations
  • PR 8/12 (this PR): UI automation tool migrations
  • PR 9/12: Remaining tool migrations
  • PR 10-12/12: Boundaries, config, tests

Test plan

  • npx vitest run passes -- all UI automation tool tests updated
  • Screenshot tool correctly uses ctx.attach() for image data
  • Shared AXe command module correctly propagates errors via events

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Identical runLogic helper duplicated across 10 test files
    • Created shared test-helpers.ts module exporting runLogic, createMockToolHandlerContext, and allText functions, then updated all 11 UI automation test files to import from the shared module, eliminating 374 lines of duplicated code.

Create PR

Or push these changes by commenting:

@cursor push 4eca3c811f
Preview (4eca3c811f)
diff --git a/src/mcp/tools/ui-automation/__tests__/button.test.ts b/src/mcp/tools/ui-automation/__tests__/button.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/button.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/button.test.ts
@@ -8,41 +8,8 @@
 import { schema, handler, buttonLogic } from '../button.ts';
 import type { CommandExecutor } from '../../../../utils/execution/index.ts';
 import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 describe('Button Plugin', () => {
   describe('Export Field Validation (Literal)', () => {
     it('should have handler function', () => {

diff --git a/src/mcp/tools/ui-automation/__tests__/gesture.test.ts b/src/mcp/tools/ui-automation/__tests__/gesture.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/gesture.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/gesture.test.ts
@@ -8,41 +8,8 @@
 import { sessionStore } from '../../../../utils/session-store.ts';
 import { schema, handler, gestureLogic } from '../gesture.ts';
 import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 describe('Gesture Plugin', () => {
   beforeEach(() => {
     sessionStore.clear();

diff --git a/src/mcp/tools/ui-automation/__tests__/key_press.test.ts b/src/mcp/tools/ui-automation/__tests__/key_press.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/key_press.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/key_press.test.ts
@@ -9,41 +9,8 @@
 import { sessionStore } from '../../../../utils/session-store.ts';
 import { schema, handler, key_pressLogic } from '../key_press.ts';
 import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 function createDefaultMockAxeHelpers() {
   return {
     getAxePath: () => '/usr/local/bin/axe',

diff --git a/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts b/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts
@@ -8,41 +8,8 @@
 import { sessionStore } from '../../../../utils/session-store.ts';
 import { schema, handler, key_sequenceLogic } from '../key_sequence.ts';
 import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 describe('Key Sequence Tool', () => {
   beforeEach(() => {
     sessionStore.clear();

diff --git a/src/mcp/tools/ui-automation/__tests__/long_press.test.ts b/src/mcp/tools/ui-automation/__tests__/long_press.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/long_press.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/long_press.test.ts
@@ -4,41 +4,8 @@
 import { sessionStore } from '../../../../utils/session-store.ts';
 import { schema, handler, long_pressLogic } from '../long_press.ts';
 import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 describe('Long Press Plugin', () => {
   beforeEach(() => {
     sessionStore.clear();

diff --git a/src/mcp/tools/ui-automation/__tests__/screenshot.test.ts b/src/mcp/tools/ui-automation/__tests__/screenshot.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/screenshot.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/screenshot.test.ts
@@ -14,41 +14,8 @@
   detectLandscapeMode,
   rotateImage,
 } from '../screenshot.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 describe('Screenshot Plugin', () => {
   beforeEach(() => {
     sessionStore.clear();

diff --git a/src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts b/src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts
@@ -4,41 +4,8 @@
 import type { CommandExecutor } from '../../../../utils/execution/index.ts';
 import { schema, handler, snapshot_uiLogic } from '../snapshot_ui.ts';
 import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 describe('Snapshot UI Plugin', () => {
   describe('Export Field Validation (Literal)', () => {
     it('should have handler function', () => {

diff --git a/src/mcp/tools/ui-automation/__tests__/swipe.test.ts b/src/mcp/tools/ui-automation/__tests__/swipe.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/swipe.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/swipe.test.ts
@@ -6,41 +6,8 @@
 
 import { schema, handler, type AxeHelpers, swipeLogic, type SwipeParams } from '../swipe.ts';
 import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 function createMockAxeHelpers(): AxeHelpers {
   return {
     getAxePath: () => '/mocked/axe/path',

diff --git a/src/mcp/tools/ui-automation/__tests__/tap.test.ts b/src/mcp/tools/ui-automation/__tests__/tap.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/tap.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/tap.test.ts
@@ -5,41 +5,8 @@
 
 import { schema, handler, type AxeHelpers, tapLogic } from '../tap.ts';
 import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 function createMockAxeHelpers(): AxeHelpers {
   return {
     getAxePath: () => '/mocked/axe/path',

diff --git a/src/mcp/tools/ui-automation/__tests__/touch.test.ts b/src/mcp/tools/ui-automation/__tests__/touch.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/touch.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/touch.test.ts
@@ -4,41 +4,8 @@
 import { sessionStore } from '../../../../utils/session-store.ts';
 import { schema, handler, touchLogic } from '../touch.ts';
 import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 describe('Touch Plugin', () => {
   beforeEach(() => {
     sessionStore.clear();

diff --git a/src/mcp/tools/ui-automation/__tests__/type_text.test.ts b/src/mcp/tools/ui-automation/__tests__/type_text.test.ts
--- a/src/mcp/tools/ui-automation/__tests__/type_text.test.ts
+++ b/src/mcp/tools/ui-automation/__tests__/type_text.test.ts
@@ -8,41 +8,8 @@
 import { sessionStore } from '../../../../utils/session-store.ts';
 import { schema, handler, type_textLogic } from '../type_text.ts';
 import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
-import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
+import { allText, runLogic } from '../../../../test-utils/test-helpers.ts';
 
-const runLogic = async (logic: () => Promise<unknown>) => {
-  const { result, run } = createMockToolHandlerContext();
-  const response = await run(logic);
-
-  if (
-    response &&
-    typeof response === 'object' &&
-    'content' in (response as Record<string, unknown>)
-  ) {
-    return response as {
-      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
-      isError?: boolean;
-      nextStepParams?: unknown;
-    };
-  }
-
-  const text = result.text();
-  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
-  const imageContent = result.attachments.map((attachment) => ({
-    type: 'image' as const,
-    data: attachment.data,
-    mimeType: attachment.mimeType,
-  }));
-
-  return {
-    content: [...textContent, ...imageContent],
-    isError: result.isError() ? true : undefined,
-    nextStepParams: result.nextStepParams,
-    attachments: result.attachments,
-    text,
-  };
-};
-
 // Mock axe helpers for dependency injection
 function createMockAxeHelpers(
   overrides: {

diff --git a/src/test-utils/test-helpers.ts b/src/test-utils/test-helpers.ts
new file mode 100644
--- /dev/null
+++ b/src/test-utils/test-helpers.ts
@@ -1,0 +1,105 @@
+import type { PipelineEvent } from '../types/pipeline-events.ts';
+import type { ToolHandlerContext, ImageAttachment } from '../rendering/types.ts';
+import type { NextStepParamsMap } from '../types/common.ts';
+
+export interface MockToolHandlerResult {
+  getEvents(): readonly PipelineEvent[];
+  getAttachments(): readonly ImageAttachment[];
+  isError(): boolean;
+  text(): string;
+  nextStepParams?: NextStepParamsMap;
+}
+
+export interface MockToolHandlerContext {
+  ctx: ToolHandlerContext;
+  result: MockToolHandlerResult;
+  run: (logic: () => Promise<unknown>) => Promise<unknown>;
+}
+
+export function createMockToolHandlerContext(): MockToolHandlerContext {
+  const events: PipelineEvent[] = [];
+  const attachments: ImageAttachment[] = [];
+  let nextStepParams: NextStepParamsMap | undefined;
+
+  const ctx: ToolHandlerContext = {
+    emit: (event: PipelineEvent) => {
+      events.push(event);
+    },
+    attach: (image: ImageAttachment) => {
+      attachments.push(image);
+    },
+    get nextStepParams() {
+      return nextStepParams;
+    },
+    set nextStepParams(value: NextStepParamsMap | undefined) {
+      nextStepParams = value;
+    },
+  };
+
+  const result: MockToolHandlerResult = {
+    getEvents: () => events,
+    getAttachments: () => attachments,
+    isError: () => events.some((e) => e.type === 'status-line' && e.level === 'error'),
+    text: () =>
+      events
+        .filter((e) => e.type === 'status-line')
+        .map((e) => (e as { message: string }).message)
+        .join('\n'),
+    get nextStepParams() {
+      return nextStepParams;
+    },
+  };
+
+  const run = async (logic: () => Promise<unknown>) => {
+    return await logic();
+  };
+
+  return { ctx, result, run };
+}
+
+export function allText(
+  result:
+    | MockToolHandlerResult
+    | { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> },
+): string {
+  if ('content' in result) {
+    return result.content
+      .filter((item) => item.type === 'text' && item.text)
+      .map((item) => item.text)
+      .join('\n');
+  }
+  return result.text();
+}
+
+export async function runLogic(logic: () => Promise<unknown>) {
+  const { result, run } = createMockToolHandlerContext();
+  const response = await run(logic);
+
+  if (
+    response &&
+    typeof response === 'object' &&
+    'content' in (response as Record<string, unknown>)
+  ) {
+    return response as {
+      content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
+      isError?: boolean;
+      nextStepParams?: unknown;
+    };
+  }
+
+  const text = result.text();
+  const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
+  const imageContent = result.getAttachments().map((attachment) => ({
+    type: 'image' as const,
+    data: attachment.data,
+    mimeType: attachment.mimeType,
+  }));
+
+  return {
+    content: [...textContent, ...imageContent],
+    isError: result.isError() ? true : undefined,
+    nextStepParams: result.nextStepParams,
+    attachments: result.getAttachments(),
+    text,
+  };
+}

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from 46fdf65 to d2e93fa Compare April 8, 2026 21:29
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from ae0e2ac to 6eabc79 Compare April 8, 2026 21:29
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d2e93fa. Configure here.


const result = await screenshotLogic(
{
simulatorId: '12345678-1234-4234-8234-123456789012',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot command failure test removed without replacement

Medium Severity

Three screenshotLogic tests were deleted without replacement: the parameter validation test, the success-with-image test, and critically the command execution failure test (which verified that success: false with 'Simulator not found' produces the correct error message). The failure path for a failed xcrun simctl io screenshot command is now untested.

Fix in Cursor Fix in Web

Triggered by project rule: Bugbot Review Guide for XcodeBuildMCP

Reviewed by Cursor Bugbot for commit d2e93fa. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant