Skip to content

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

Open
cameroncooke wants to merge 3 commits 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 3 commits 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
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from d2e93fa to 56e6ae9 Compare April 9, 2026 07:49
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from 6eabc79 to 2a18f34 Compare April 9, 2026 07:49
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from 2a18f34 to a3b5afd Compare April 9, 2026 07:59
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch 2 times, most recently from d79dae4 to 9af3129 Compare April 9, 2026 08:45
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from a3b5afd to 7558ea2 Compare April 9, 2026 08:45
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from 08d16bd to af53a77 Compare April 9, 2026 10:39
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from af53a77 to 7fecbf7 Compare April 9, 2026 10:56
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from 3036639 to 503cc59 Compare April 9, 2026 10:56
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 7fecbf7. Configure here.

@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from 503cc59 to cd3133c Compare April 9, 2026 11:22
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from 7fecbf7 to 4e35374 Compare April 9, 2026 11:22
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from 4e35374 to 1678b2e Compare April 9, 2026 11:31
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from cd3133c to 106a7dc Compare April 9, 2026 11:31
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from 1678b2e to c541317 Compare April 9, 2026 11:48
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from 106a7dc to f7bde95 Compare April 9, 2026 11:48
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from c541317 to f62e76a Compare April 9, 2026 12:03
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from f7bde95 to c18c941 Compare April 9, 2026 12:03
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from f62e76a to aaa305d Compare April 9, 2026 14:43
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from c18c941 to 1b4e244 Compare April 9, 2026 14:43
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from aaa305d to 9c7a22a Compare April 9, 2026 15:15
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch from 1b4e244 to 732814d Compare April 9, 2026 15:15
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from 9c7a22a to 5c4723f Compare April 9, 2026 15:40
@cameroncooke cameroncooke force-pushed the refactor/migrate-device-macos-tools branch 2 times, most recently from 54730dd to 30ef514 Compare April 9, 2026 20:52
@cameroncooke cameroncooke force-pushed the refactor/migrate-ui-automation-tools branch from 5c4723f to 5574554 Compare April 9, 2026 20:52
…llback

- Add blockedMessage string to GuardResult so all 10 UI automation
  tools can correctly check and emit the guard message via statusLine
- Emit headerEvent and statusLine in the screenshot optimization-failure
  base64 path for consistent client responses
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