refactor(8/12): migrate UI automation tools to event-based handlers#326
Open
cameroncooke wants to merge 3 commits intorefactor/migrate-device-macos-toolsfrom
Open
refactor(8/12): migrate UI automation tools to event-based handlers#326cameroncooke wants to merge 3 commits intorefactor/migrate-device-macos-toolsfrom
cameroncooke wants to merge 3 commits intorefactor/migrate-device-macos-toolsfrom
Conversation
Contributor
There was a problem hiding this comment.
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
runLogichelper 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.
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.
46fdf65 to
d2e93fa
Compare
ae0e2ac to
6eabc79
Compare
d2e93fa to
56e6ae9
Compare
6eabc79 to
2a18f34
Compare
2a18f34 to
a3b5afd
Compare
d79dae4 to
9af3129
Compare
a3b5afd to
7558ea2
Compare
This was referenced Apr 9, 2026
Collaborator
Author
This was referenced Apr 9, 2026
7558ea2 to
6bf61bc
Compare
9af3129 to
08d16bd
Compare
08d16bd to
af53a77
Compare
af53a77 to
7fecbf7
Compare
3036639 to
503cc59
Compare
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
503cc59 to
cd3133c
Compare
7fecbf7 to
4e35374
Compare
4e35374 to
1678b2e
Compare
cd3133c to
106a7dc
Compare
1678b2e to
c541317
Compare
106a7dc to
f7bde95
Compare
c541317 to
f62e76a
Compare
f7bde95 to
c18c941
Compare
f62e76a to
aaa305d
Compare
c18c941 to
1b4e244
Compare
aaa305d to
9c7a22a
Compare
1b4e244 to
732814d
Compare
9c7a22a to
5c4723f
Compare
54730dd to
30ef514
Compare
Replace 11 identical local runLogic definitions with the shared import from test-helpers.ts.
5c4723f to
5574554
Compare
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



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_textNotable changes
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 anemitcallback.axe-helpers.tsandaxe/index.ts: Minor updates to work with the shared command module.screenshot.ts: Usesctx.attach()for image data instead of constructingToolResponseContentdirectly. 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
Test plan
npx vitest runpasses -- all UI automation tool tests updatedctx.attach()for image data