mobile-debug-mcp 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts CHANGED
@@ -132,14 +132,67 @@ export interface TapResponse {
132
132
  error?: string;
133
133
  }
134
134
 
135
- export interface TapElementResponse {
135
+ export type ActionFailureCode =
136
+ | 'ELEMENT_NOT_FOUND'
137
+ | 'ELEMENT_NOT_INTERACTABLE'
138
+ | 'TIMEOUT'
139
+ | 'NAVIGATION_NO_CHANGE'
140
+ | 'AMBIGUOUS_TARGET'
141
+ | 'STALE_REFERENCE'
142
+ | 'UNKNOWN'
143
+
144
+ export interface ActionTargetResolved {
145
+ elementId: string | null;
146
+ text: string | null;
147
+ resource_id: string | null;
148
+ accessibility_id: string | null;
149
+ class: string | null;
150
+ bounds: [number, number, number, number] | null;
151
+ index: number | null;
152
+ }
153
+
154
+ export interface ActionExecutionResult {
155
+ action_id: string;
156
+ timestamp: number;
157
+ action_type: string;
158
+ target: {
159
+ selector: Record<string, unknown> | null;
160
+ resolved: ActionTargetResolved | null;
161
+ };
162
+ success: boolean;
163
+ failure_code?: ActionFailureCode;
164
+ retryable?: boolean;
165
+ ui_fingerprint_before?: string | null;
166
+ ui_fingerprint_after?: string | null;
167
+ }
168
+
169
+ export interface TapElementResponse extends ActionExecutionResult {}
170
+
171
+ export interface ExpectScreenResponse {
172
+ success: boolean;
173
+ observed_screen: {
174
+ fingerprint: string | null;
175
+ screen: string | null;
176
+ };
177
+ expected_screen: {
178
+ fingerprint: string | null;
179
+ screen: string | null;
180
+ };
181
+ confidence: number;
182
+ }
183
+
184
+ export interface ExpectElementVisibleResponse {
136
185
  success: boolean;
137
- elementId: string;
138
- action: 'tap';
139
- error?: {
140
- code: 'element_not_found' | 'element_not_visible' | 'element_not_enabled' | 'tap_failed';
141
- message: string;
186
+ selector: {
187
+ text?: string;
188
+ resource_id?: string;
189
+ accessibility_id?: string;
190
+ contains?: boolean;
142
191
  };
192
+ element_id: string | null;
193
+ element?: ActionTargetResolved | null;
194
+ failure_code?: 'TIMEOUT' | 'ELEMENT_NOT_FOUND' | 'UNKNOWN';
195
+ retryable?: boolean;
143
196
  }
144
197
 
145
198
  export interface SwipeResponse {
@@ -0,0 +1,77 @@
1
+ import assert from 'assert'
2
+ import { ToolsInteract } from '../../../src/interact/index.js'
3
+ import * as Observe from '../../../src/observe/index.js'
4
+
5
+ async function run() {
6
+ console.log('Starting expect_* unit tests...')
7
+ const originalGetScreenFingerprintHandler = (Observe as any).ToolsObserve.getScreenFingerprintHandler
8
+ const originalGetCurrentScreenHandler = (Observe as any).ToolsObserve.getCurrentScreenHandler
9
+ const originalWaitForUIHandler = (ToolsInteract as any).waitForUIHandler
10
+
11
+ try {
12
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'fp_home', activity: 'com.example.HomeActivity' })
13
+ let expectScreen = await ToolsInteract.expectScreenHandler({ platform: 'android', fingerprint: 'fp_home' })
14
+ assert.deepStrictEqual(expectScreen, {
15
+ success: true,
16
+ observed_screen: { fingerprint: 'fp_home', screen: 'com.example.HomeActivity' },
17
+ expected_screen: { fingerprint: 'fp_home', screen: null },
18
+ confidence: 1
19
+ })
20
+
21
+ ;(Observe as any).ToolsObserve.getCurrentScreenHandler = async () => ({
22
+ activity: 'com.example.HomeActivity',
23
+ shortActivity: 'HomeActivity'
24
+ })
25
+ expectScreen = await ToolsInteract.expectScreenHandler({ platform: 'android', screen: 'HomeActivity' })
26
+ assert.strictEqual(expectScreen.success, true)
27
+ assert.strictEqual(expectScreen.observed_screen.screen, 'HomeActivity')
28
+ assert.strictEqual(expectScreen.confidence, 1)
29
+
30
+ ;(ToolsInteract as any).waitForUIHandler = async () => ({
31
+ status: 'success',
32
+ element: {
33
+ text: 'Ready',
34
+ resource_id: 'rid_ready',
35
+ accessibility_id: null,
36
+ class: 'TextView',
37
+ bounds: [0, 0, 10, 10],
38
+ index: 0,
39
+ elementId: 'el_ready'
40
+ }
41
+ })
42
+ const expectElementVisible = await ToolsInteract.expectElementVisibleHandler({
43
+ selector: { text: 'Ready' },
44
+ platform: 'android'
45
+ })
46
+ assert.strictEqual(expectElementVisible.success, true)
47
+ assert.strictEqual(expectElementVisible.element_id, 'el_ready')
48
+ assert.strictEqual(expectElementVisible.element?.resource_id, 'rid_ready')
49
+
50
+ ;(ToolsInteract as any).waitForUIHandler = async () => ({
51
+ status: 'timeout',
52
+ error: { code: 'ELEMENT_NOT_FOUND', message: 'Condition visible not satisfied within timeout' }
53
+ })
54
+ const timeoutResult = await ToolsInteract.expectElementVisibleHandler({
55
+ selector: { text: 'Missing' },
56
+ platform: 'android'
57
+ })
58
+ assert.deepStrictEqual(timeoutResult, {
59
+ success: false,
60
+ selector: { text: 'Missing' },
61
+ element_id: null,
62
+ failure_code: 'TIMEOUT',
63
+ retryable: true
64
+ })
65
+
66
+ console.log('expect_* unit tests passed')
67
+ } finally {
68
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = originalGetScreenFingerprintHandler
69
+ ;(Observe as any).ToolsObserve.getCurrentScreenHandler = originalGetCurrentScreenHandler
70
+ ;(ToolsInteract as any).waitForUIHandler = originalWaitForUIHandler
71
+ }
72
+ }
73
+
74
+ run().catch((error) => {
75
+ console.error(error)
76
+ process.exit(1)
77
+ })
@@ -5,11 +5,18 @@ import * as Observe from '../../../src/observe/index.js'
5
5
  async function run() {
6
6
  console.log('Starting tap_element unit tests...')
7
7
  const originalGetUITreeHandler = (Observe as any).ToolsObserve.getUITreeHandler
8
+ const originalGetScreenFingerprintHandler = (Observe as any).ToolsObserve.getScreenFingerprintHandler
8
9
  const originalTapHandler = (ToolsInteract as any).tapHandler
9
10
  const originalComputeElementId = (ToolsInteract as any)._computeElementId
10
11
  ;(ToolsInteract as any)._resetResolvedUiElementsForTests()
11
12
 
12
13
  try {
14
+ let fingerprintCalls = 0
15
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => {
16
+ fingerprintCalls++
17
+ return { fingerprint: 'fp_mock' }
18
+ }
19
+
13
20
  ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
14
21
  device: { platform: 'android', id: 'mock-device' },
15
22
  elements: [
@@ -34,7 +41,12 @@ async function run() {
34
41
  }
35
42
 
36
43
  const tapSuccess = await ToolsInteract.tapElementHandler({ elementId: successElementId })
37
- assert.deepStrictEqual(tapSuccess, { success: true, elementId: successElementId, action: 'tap' })
44
+ assert.strictEqual(tapSuccess.success, true)
45
+ assert.strictEqual(tapSuccess.action_type, 'tap_element')
46
+ assert.strictEqual(tapSuccess.target.selector?.elementId, successElementId)
47
+ assert.strictEqual(tapSuccess.target.resolved?.elementId, successElementId)
48
+ assert.strictEqual(tapSuccess.ui_fingerprint_before, 'fp_mock')
49
+ assert.strictEqual(tapSuccess.ui_fingerprint_after, 'fp_mock')
38
50
  assert.deepStrictEqual(tapped, { platform: 'android', x: 10, y: 10, deviceId: 'mock-device' })
39
51
 
40
52
  ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
@@ -52,7 +64,8 @@ async function run() {
52
64
  })
53
65
  const hiddenResult = await ToolsInteract.tapElementHandler({ elementId: waitHidden.element.elementId })
54
66
  assert.strictEqual(hiddenResult.success, false)
55
- assert.strictEqual(hiddenResult.error?.code, 'element_not_visible')
67
+ assert.strictEqual(hiddenResult.failure_code, 'ELEMENT_NOT_INTERACTABLE')
68
+ assert.strictEqual(hiddenResult.retryable, true)
56
69
 
57
70
  ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
58
71
  device: { platform: 'android', id: 'mock-device' },
@@ -69,7 +82,7 @@ async function run() {
69
82
  })
70
83
  const disabledResult = await ToolsInteract.tapElementHandler({ elementId: waitDisabled.element.elementId })
71
84
  assert.strictEqual(disabledResult.success, false)
72
- assert.strictEqual(disabledResult.error?.code, 'element_not_enabled')
85
+ assert.strictEqual(disabledResult.failure_code, 'ELEMENT_NOT_INTERACTABLE')
73
86
 
74
87
  ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
75
88
  device: { platform: 'android', id: 'mock-device' },
@@ -77,7 +90,7 @@ async function run() {
77
90
  })
78
91
  const notFoundResult = await ToolsInteract.tapElementHandler({ elementId: successElementId })
79
92
  assert.strictEqual(notFoundResult.success, false)
80
- assert.strictEqual(notFoundResult.error?.code, 'element_not_found')
93
+ assert.strictEqual(notFoundResult.failure_code, 'STALE_REFERENCE')
81
94
 
82
95
  ;(ToolsInteract as any)._resetResolvedUiElementsForTests()
83
96
  const targetIndex = 25
@@ -124,7 +137,7 @@ async function run() {
124
137
  })
125
138
  const shiftedIndexResult = await ToolsInteract.tapElementHandler({ elementId: indexedWait.element.elementId })
126
139
  assert.strictEqual(shiftedIndexResult.success, false)
127
- assert.strictEqual(shiftedIndexResult.error?.code, 'element_not_found')
140
+ assert.strictEqual(shiftedIndexResult.failure_code, 'STALE_REFERENCE')
128
141
 
129
142
  ;(ToolsInteract as any)._resetResolvedUiElementsForTests()
130
143
  const cacheLimit = (ToolsInteract as any)._maxResolvedUiElements as number
@@ -151,15 +164,19 @@ async function run() {
151
164
  }
152
165
 
153
166
  assert.ok(oldestElementId, 'Oldest element ID should be captured')
167
+ const fingerprintCallsBeforeEvictedTap = fingerprintCalls
154
168
  const evictedResult = await ToolsInteract.tapElementHandler({ elementId: oldestElementId as string })
155
169
  assert.strictEqual(evictedResult.success, false)
156
- assert.strictEqual(evictedResult.error?.code, 'element_not_found')
170
+ assert.strictEqual(evictedResult.failure_code, 'STALE_REFERENCE')
171
+ assert.strictEqual(evictedResult.ui_fingerprint_before, null)
172
+ assert.strictEqual(fingerprintCalls, fingerprintCallsBeforeEvictedTap)
157
173
 
158
174
  console.log('tap_element unit tests passed')
159
175
  } finally {
160
176
  ;(ToolsInteract as any)._resetResolvedUiElementsForTests()
161
177
  ;(ToolsInteract as any)._computeElementId = originalComputeElementId
162
178
  ;(Observe as any).ToolsObserve.getUITreeHandler = originalGetUITreeHandler
179
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = originalGetScreenFingerprintHandler
163
180
  ;(ToolsInteract as any).tapHandler = originalTapHandler
164
181
  }
165
182
  }
@@ -69,8 +69,101 @@ exit 0
69
69
  assert.ok(res2.output && typeof res2.output === 'string', 'Project dir install succeeded with output')
70
70
  }
71
71
 
72
+ const testOnlyAdbPath = path.join(binDir, 'adb-test-only')
73
+ const testOnlyAdbScript = `#!/bin/sh
74
+ if [ "$1" = "-s" ]; then
75
+ shift 2
76
+ fi
77
+
78
+ if [ "$1" = "shell" ] && [ "$2" = "getprop" ]; then
79
+ case "$3" in
80
+ ro.build.version.release) echo '16' ;;
81
+ ro.product.model) echo 'sdk_gphone64_arm64' ;;
82
+ ro.kernel.qemu) echo '1' ;;
83
+ esac
84
+ exit 0
85
+ fi
86
+
87
+ if [ "$1" = "install" ]; then
88
+ if [ "$2" = "-r" ] && [ "$3" = "-t" ]; then
89
+ echo 'Performing Streamed Install'
90
+ echo 'Success'
91
+ exit 0
92
+ fi
93
+ echo 'Performing Streamed Install'
94
+ echo 'adb: failed to install test.apk: Failure [INSTALL_FAILED_TEST_ONLY: Failed to install test-only apk. Did you forget to add -t?]' 1>&2
95
+ exit 1
96
+ fi
97
+
98
+ echo 'Success'
99
+ exit 0
100
+ `
101
+ await fs.writeFile(testOnlyAdbPath, testOnlyAdbScript, { mode: 0o755 })
102
+ process.env.ADB_PATH = testOnlyAdbPath
103
+
104
+ const { dir: d2, file: testOnlyApk } = await makeTempFile('.apk')
105
+ const testOnlyRes = await ai.installApp(testOnlyApk, 'emulator-5554')
106
+ console.log('testOnlyRes', testOnlyRes)
107
+ assert.strictEqual(testOnlyRes.installed, true, 'Test-only APK should retry with -t and install successfully')
108
+
109
+ const cleanupLog = path.join(binDir, 'pm-cleanup.log')
110
+ const pmFallbackAdbPath = path.join(binDir, 'adb-pm-fallback')
111
+ const pmFallbackAdbScript = `#!/bin/sh
112
+ if [ "$1" = "-s" ]; then
113
+ shift 2
114
+ fi
115
+
116
+ if [ "$1" = "shell" ] && [ "$2" = "getprop" ]; then
117
+ case "$3" in
118
+ ro.build.version.release) echo '16' ;;
119
+ ro.product.model) echo 'sdk_gphone64_arm64' ;;
120
+ ro.kernel.qemu) echo '1' ;;
121
+ esac
122
+ exit 0
123
+ fi
124
+
125
+ if [ "$1" = "install" ]; then
126
+ echo 'adb install failed' 1>&2
127
+ exit 1
128
+ fi
129
+
130
+ if [ "$1" = "push" ]; then
131
+ echo 'pushed'
132
+ exit 0
133
+ fi
134
+
135
+ if [ "$1" = "shell" ] && [ "$2" = "pm" ] && [ "$3" = "install" ]; then
136
+ if [ "$4" = "-r" ] && [ "$5" = "-t" ]; then
137
+ echo 'Failure [INSTALL_FAILED_VERSION_DOWNGRADE]'
138
+ exit 1
139
+ fi
140
+ echo 'Failure [INSTALL_FAILED_TEST_ONLY: Failed to install test-only apk. Did you forget to add -t?]'
141
+ exit 1
142
+ fi
143
+
144
+ if [ "$1" = "shell" ] && [ "$2" = "rm" ]; then
145
+ echo cleanup >> "${cleanupLog}"
146
+ exit 0
147
+ fi
148
+
149
+ echo 'unexpected args:' "$@" 1>&2
150
+ exit 1
151
+ `
152
+ await fs.writeFile(pmFallbackAdbPath, pmFallbackAdbScript, { mode: 0o755 })
153
+ process.env.ADB_PATH = pmFallbackAdbPath
154
+
155
+ const { dir: d3, file: pmFallbackApk } = await makeTempFile('.apk')
156
+ const pmFallbackRes = await ai.installApp(pmFallbackApk, 'emulator-5554')
157
+ console.log('pmFallbackRes', pmFallbackRes)
158
+ assert.strictEqual(pmFallbackRes.installed, false, 'Failed pm fallback should surface as install failure')
159
+ assert.match(pmFallbackRes.error || '', /INSTALL_FAILED_VERSION_DOWNGRADE/, 'Final pm retry failure should be reported')
160
+ const cleanupCount = (await fs.readFile(cleanupLog, 'utf8')).trim().split('\n').filter(Boolean).length
161
+ assert.strictEqual(cleanupCount, 1, 'pm fallback cleanup should run once')
162
+
72
163
  // cleanup
73
164
  await fs.rm(d1, { recursive: true, force: true }).catch(() => {})
165
+ await fs.rm(d2, { recursive: true, force: true }).catch(() => {})
166
+ await fs.rm(d3, { recursive: true, force: true }).catch(() => {})
74
167
  await fs.rm(dirGradle, { recursive: true, force: true }).catch(() => {})
75
168
 
76
169
  // restore PATH and ADB_PATH
@@ -87,4 +180,4 @@ exit 0
87
180
  }
88
181
  }
89
182
 
90
- run().catch((e) => { console.error(e); process.exit(1) })
183
+ run().catch((e) => { console.error(e); process.exit(1) })
@@ -8,6 +8,8 @@ async function run() {
8
8
  assert.strictEqual(serverInfo.name, 'mobile-debug-mcp')
9
9
  assert.strictEqual(names.length, uniqueNames.size, 'tool names should be unique')
10
10
  assert(names.includes('wait_for_ui'))
11
+ assert(names.includes('expect_screen'))
12
+ assert(names.includes('expect_element_visible'))
11
13
  assert(names.includes('capture_screenshot'))
12
14
  assert(names.includes('get_ui_tree'))
13
15
  assert(names.includes('tap_element'))
@@ -16,6 +18,14 @@ async function run() {
16
18
  assert(waitForUI, 'wait_for_ui should be registered')
17
19
  assert.strictEqual((waitForUI as any).inputSchema.properties.timeout_ms.default, 60000)
18
20
  assert.strictEqual((waitForUI as any).inputSchema.properties.condition.default, 'exists')
21
+ assert.match((waitForUI as any).description, /resolve elements/i)
22
+ assert.match((waitForUI as any).description, /must not be used alone to confirm action success/i)
23
+ assert.match((waitForUI as any).description, /follow with expect_\*/i)
24
+
25
+ const waitForScreenChange = toolDefinitions.find((tool) => tool.name === 'wait_for_screen_change')
26
+ assert(waitForScreenChange, 'wait_for_screen_change should be registered')
27
+ assert.match((waitForScreenChange as any).description, /does not verify correctness of the resulting state/i)
28
+ assert.match((waitForScreenChange as any).description, /follow with expect_screen/i)
19
29
 
20
30
  const captureDebugSnapshot = toolDefinitions.find((tool) => tool.name === 'capture_debug_snapshot')
21
31
  assert(captureDebugSnapshot, 'capture_debug_snapshot should be registered')
@@ -33,6 +43,22 @@ async function run() {
33
43
  const tapElement = toolDefinitions.find((tool) => tool.name === 'tap_element')
34
44
  assert(tapElement, 'tap_element should be registered')
35
45
  assert.deepStrictEqual((tapElement as any).inputSchema.required, ['elementId'])
46
+ assert.match((tapElement as any).description, /RESOLVE → ACT → WAIT \(if needed\) → EXPECT/)
47
+ assert.match((tapElement as any).description, /If needed, wait for transition using wait_for_\*/)
48
+ assert.match((tapElement as any).description, /Verify outcome using expect_\*/)
49
+
50
+ const expectScreen = toolDefinitions.find((tool) => tool.name === 'expect_screen')
51
+ assert(expectScreen, 'expect_screen should be registered')
52
+ assert.match((expectScreen as any).description, /Primary and authoritative verification tool/i)
53
+ assert.match((expectScreen as any).description, /final verification step/i)
54
+ assert.match((expectScreen as any).description, /Returns structured binary success\/failure only/i)
55
+
56
+ const expectElementVisible = toolDefinitions.find((tool) => tool.name === 'expect_element_visible')
57
+ assert(expectElementVisible, 'expect_element_visible should be registered')
58
+ assert.deepStrictEqual((expectElementVisible as any).inputSchema.required, ['selector'])
59
+ assert.match((expectElementVisible as any).description, /Primary and authoritative verification tool/i)
60
+ assert.match((expectElementVisible as any).description, /selector is the primary input/i)
61
+ assert.match((expectElementVisible as any).description, /Returns structured binary success\/failure only/i)
36
62
 
37
63
  await assert.rejects(() => handleToolCall('unknown_tool'), /Unknown tool: unknown_tool/)
38
64
 
@@ -1,6 +1,7 @@
1
1
  import assert from 'assert'
2
2
  import { handleToolCall } from '../../../src/server-core.js'
3
3
  import { ToolsManage } from '../../../src/manage/index.js'
4
+ import { AndroidManage } from '../../../src/manage/index.js'
4
5
  import { ToolsInteract } from '../../../src/interact/index.js'
5
6
  import { ToolsObserve } from '../../../src/observe/index.js'
6
7
 
@@ -8,8 +9,13 @@ async function run() {
8
9
  const originalInstallAppHandler = (ToolsManage as any).installAppHandler
9
10
  const originalWaitForUIHandler = (ToolsInteract as any).waitForUIHandler
10
11
  const originalTapElementHandler = (ToolsInteract as any).tapElementHandler
12
+ const originalTapHandler = (ToolsInteract as any).tapHandler
13
+ const originalExpectScreenHandler = (ToolsInteract as any).expectScreenHandler
14
+ const originalExpectElementVisibleHandler = (ToolsInteract as any).expectElementVisibleHandler
15
+ const originalStartApp = AndroidManage.prototype.startApp
11
16
  const originalCaptureScreenshotHandler = (ToolsObserve as any).captureScreenshotHandler
12
17
  const originalGetUITreeHandler = (ToolsObserve as any).getUITreeHandler
18
+ const originalGetScreenFingerprintHandler = (ToolsObserve as any).getScreenFingerprintHandler
13
19
 
14
20
  try {
15
21
  ;(ToolsManage as any).installAppHandler = async () => ({
@@ -40,16 +46,70 @@ async function run() {
40
46
  assert.strictEqual(waitForUIPayload.element.elementId, 'el_ready')
41
47
 
42
48
  ;(ToolsInteract as any).tapElementHandler = async () => ({
49
+ action_id: 'tap_element_1',
50
+ timestamp: 1234567890,
51
+ action_type: 'tap_element',
52
+ target: {
53
+ selector: { elementId: 'el_ready' },
54
+ resolved: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'Button', bounds: [0, 0, 10, 10], index: 0 }
55
+ },
43
56
  success: true,
44
- elementId: 'el_ready',
45
- action: 'tap'
57
+ ui_fingerprint_before: 'fp_before',
58
+ ui_fingerprint_after: 'fp_after'
46
59
  })
47
60
 
48
61
  const tapElementResponse = await handleToolCall('tap_element', { elementId: 'el_ready' })
49
62
  const tapElementPayload = JSON.parse((tapElementResponse as any).content[0].text)
50
63
  assert.strictEqual(tapElementPayload.success, true)
51
- assert.strictEqual(tapElementPayload.elementId, 'el_ready')
52
- assert.strictEqual(tapElementPayload.action, 'tap')
64
+ assert.strictEqual(tapElementPayload.action_type, 'tap_element')
65
+ assert.strictEqual(tapElementPayload.target.resolved.elementId, 'el_ready')
66
+ assert.strictEqual(tapElementPayload.ui_fingerprint_before, 'fp_before')
67
+
68
+ ;(ToolsObserve as any).getScreenFingerprintHandler = async () => ({ fingerprint: 'fp_mock', activity: 'MainActivity' })
69
+ ;(ToolsInteract as any).tapHandler = async () => ({ success: true, x: 1, y: 2 })
70
+ const tapResponse = await handleToolCall('tap', { platform: 'android', x: 1, y: 2 })
71
+ const tapPayload = JSON.parse((tapResponse as any).content[0].text)
72
+ assert.strictEqual(tapPayload.success, true)
73
+ assert.strictEqual(tapPayload.action_type, 'tap')
74
+ assert.deepStrictEqual(tapPayload.target.selector, { x: 1, y: 2 })
75
+ assert.strictEqual(tapPayload.ui_fingerprint_before, 'fp_mock')
76
+
77
+ AndroidManage.prototype.startApp = async function () {
78
+ return {
79
+ device: { platform: 'android', id: 'emulator-5554', osVersion: '14', model: 'Pixel', simulator: true },
80
+ appStarted: true,
81
+ launchTimeMs: 123
82
+ } as any
83
+ }
84
+ const startAppResponse = await handleToolCall('start_app', { platform: 'android', appId: 'com.example.app' })
85
+ const startAppPayload = JSON.parse((startAppResponse as any).content[0].text)
86
+ assert.strictEqual(startAppPayload.success, true)
87
+ assert.strictEqual(startAppPayload.action_type, 'start_app')
88
+ assert.deepStrictEqual(startAppPayload.target.selector, { appId: 'com.example.app' })
89
+
90
+ ;(ToolsInteract as any).expectScreenHandler = async () => ({
91
+ success: true,
92
+ observed_screen: { fingerprint: 'fp_after', screen: 'MainActivity' },
93
+ expected_screen: { fingerprint: 'fp_after', screen: null },
94
+ confidence: 1
95
+ })
96
+
97
+ const expectScreenResponse = await handleToolCall('expect_screen', { fingerprint: 'fp_after' })
98
+ const expectScreenPayload = JSON.parse((expectScreenResponse as any).content[0].text)
99
+ assert.strictEqual(expectScreenPayload.success, true)
100
+ assert.strictEqual(expectScreenPayload.confidence, 1)
101
+
102
+ ;(ToolsInteract as any).expectElementVisibleHandler = async () => ({
103
+ success: true,
104
+ selector: { text: 'Ready' },
105
+ element_id: 'el_ready',
106
+ element: { elementId: 'el_ready', text: 'Ready', resource_id: null, accessibility_id: null, class: 'TextView', bounds: [0, 0, 10, 10], index: 0 }
107
+ })
108
+
109
+ const expectElementResponse = await handleToolCall('expect_element_visible', { selector: { text: 'Ready' } })
110
+ const expectElementPayload = JSON.parse((expectElementResponse as any).content[0].text)
111
+ assert.strictEqual(expectElementPayload.success, true)
112
+ assert.strictEqual(expectElementPayload.element_id, 'el_ready')
53
113
 
54
114
  ;(ToolsObserve as any).captureScreenshotHandler = async () => ({
55
115
  device: { platform: 'ios', id: 'booted', osVersion: '18.0', model: 'Simulator', simulator: true },
@@ -82,8 +142,13 @@ async function run() {
82
142
  ;(ToolsManage as any).installAppHandler = originalInstallAppHandler
83
143
  ;(ToolsInteract as any).waitForUIHandler = originalWaitForUIHandler
84
144
  ;(ToolsInteract as any).tapElementHandler = originalTapElementHandler
145
+ ;(ToolsInteract as any).tapHandler = originalTapHandler
146
+ ;(ToolsInteract as any).expectScreenHandler = originalExpectScreenHandler
147
+ ;(ToolsInteract as any).expectElementVisibleHandler = originalExpectElementVisibleHandler
148
+ AndroidManage.prototype.startApp = originalStartApp
85
149
  ;(ToolsObserve as any).captureScreenshotHandler = originalCaptureScreenshotHandler
86
150
  ;(ToolsObserve as any).getUITreeHandler = originalGetUITreeHandler
151
+ ;(ToolsObserve as any).getScreenFingerprintHandler = originalGetScreenFingerprintHandler
87
152
  }
88
153
  }
89
154