mobile-debug-mcp 0.26.3 → 0.26.4

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.
@@ -277,6 +277,31 @@ async function handleExpectState(args: ToolCallArgs) {
277
277
  return wrapResponse(res)
278
278
  }
279
279
 
280
+ async function handleAdjustControl(args: ToolCallArgs) {
281
+ const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
282
+ const element_id = getStringArg(args, 'element_id')
283
+ const property = getStringArg(args, 'property') ?? 'value'
284
+ const targetValue = requireNumberArg(args, 'targetValue')
285
+ const tolerance = getNumberArg(args, 'tolerance') ?? 0
286
+ const maxAttempts = getNumberArg(args, 'maxAttempts') ?? 3
287
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
288
+ const deviceId = getStringArg(args, 'deviceId')
289
+ if (!selector && !element_id) {
290
+ throw new Error('Missing selector or element_id argument')
291
+ }
292
+ const res = await ToolsInteract.adjustControlHandler({
293
+ selector: selector ?? undefined,
294
+ element_id: element_id ?? undefined,
295
+ property,
296
+ targetValue,
297
+ tolerance,
298
+ maxAttempts,
299
+ platform,
300
+ deviceId
301
+ })
302
+ return wrapResponse(res)
303
+ }
304
+
280
305
  async function handleWaitForUI(args: ToolCallArgs) {
281
306
  const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
282
307
  const condition = (getStringArg(args, 'condition') as 'exists' | 'not_exists' | 'visible' | 'clickable' | undefined) ?? 'exists'
@@ -496,6 +521,7 @@ export const toolHandlers: Record<string, ToolHandler> = {
496
521
  expect_screen: handleExpectScreen,
497
522
  expect_element_visible: handleExpectElementVisible,
498
523
  expect_state: handleExpectState,
524
+ adjust_control: handleAdjustControl,
499
525
  wait_for_ui: handleWaitForUI,
500
526
  find_element: handleFindElement,
501
527
  tap: handleTap,
@@ -13,7 +13,7 @@ export { wrapResponse, toolDefinitions, handleToolCall }
13
13
 
14
14
  export const serverInfo = {
15
15
  name: 'mobile-debug-mcp',
16
- version: '0.26.3'
16
+ version: '0.26.4'
17
17
  }
18
18
 
19
19
  export function createServer() {
package/src/types.ts CHANGED
@@ -413,6 +413,23 @@ export interface ExpectStateResponse {
413
413
  retryable?: boolean;
414
414
  }
415
415
 
416
+ export interface AdjustControlResponse extends ActionExecutionResult {
417
+ target_state: {
418
+ property: string;
419
+ target_value: number;
420
+ tolerance: number;
421
+ };
422
+ actual_state: {
423
+ property: string;
424
+ value: number | null;
425
+ raw_value?: number | null;
426
+ } | null;
427
+ within_tolerance: boolean;
428
+ converged: boolean;
429
+ attempts: number;
430
+ adjustment_mode: 'semantic' | 'gesture' | 'coordinate';
431
+ }
432
+
416
433
  export interface WaitForUIChangeResponse {
417
434
  success: boolean;
418
435
  observed_change: 'hierarchy_diff' | 'text_change' | 'state_change' | null;
@@ -341,7 +341,7 @@ function normalizeClassName(value: unknown): string {
341
341
  }
342
342
 
343
343
  function inferAndroidRole(className: string): string | null {
344
- if (/seekbar|slider|progress/.test(className)) return 'slider'
344
+ if (/seekbar|slider/.test(className)) return 'slider'
345
345
  if (/switch|toggle/.test(className)) return 'switch'
346
346
  if (/checkbox/.test(className)) return 'checkbox'
347
347
  if (/radiobutton|radio/.test(className)) return 'radio'
@@ -384,7 +384,7 @@ function buildAndroidSemantic(clickable: boolean, className: string): UIElementS
384
384
 
385
385
  function isSliderLikeAndroid(node: any): boolean {
386
386
  const className = String(node['@_class'] || '').toLowerCase()
387
- return /seekbar|slider|range|progress/i.test(className)
387
+ return /seekbar|slider|range/i.test(className)
388
388
  }
389
389
 
390
390
  function extractAndroidState(node: any): UIElementState | null {
@@ -0,0 +1,365 @@
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 adjust_control unit tests...')
7
+
8
+ const originalGetUITreeHandler = (Observe as any).ToolsObserve.getUITreeHandler
9
+ const originalGetScreenFingerprintHandler = (Observe as any).ToolsObserve.getScreenFingerprintHandler
10
+ const originalTapHandler = (ToolsInteract as any).tapHandler
11
+ const originalSwipeHandler = (ToolsInteract as any).swipeHandler
12
+ const originalExpectStateHandler = (ToolsInteract as any).expectStateHandler
13
+
14
+ try {
15
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
16
+ device: { platform: 'android', id: 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
17
+ screen: '',
18
+ resolution: { width: 1080, height: 2400 },
19
+ elements: [
20
+ {
21
+ text: 'Duration',
22
+ type: 'android.widget.SeekBar',
23
+ contentDescription: null,
24
+ clickable: true,
25
+ enabled: true,
26
+ visible: true,
27
+ bounds: [0, 0, 200, 40],
28
+ resourceId: 'seek_duration',
29
+ state: {
30
+ value: 10,
31
+ raw_value: 10,
32
+ value_range: { min: 0, max: 100 }
33
+ }
34
+ }
35
+ ]
36
+ })
37
+
38
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'fp_slider', activity: 'MainActivity' })
39
+
40
+ const wait = await ToolsInteract.waitForUIHandler({
41
+ selector: { text: 'Duration' },
42
+ condition: 'clickable',
43
+ timeout_ms: 200,
44
+ poll_interval_ms: 50,
45
+ platform: 'android'
46
+ })
47
+ assert.strictEqual(wait.status, 'success')
48
+ assert.ok(wait.element?.elementId)
49
+
50
+ const tapCalls: Array<{ platform?: string, x: number, y: number, deviceId?: string }> = []
51
+ const swipeCalls: Array<{ platform?: string, x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string }> = []
52
+ ;(ToolsInteract as any).tapHandler = async ({ platform, x, y, deviceId }: any) => {
53
+ tapCalls.push({ platform, x, y, deviceId })
54
+ return {
55
+ device: { platform: platform || 'android', id: deviceId || 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
56
+ success: true,
57
+ x,
58
+ y
59
+ }
60
+ }
61
+ ;(ToolsInteract as any).swipeHandler = async ({ platform, x1, y1, x2, y2, duration, deviceId }: any) => {
62
+ swipeCalls.push({ platform, x1, y1, x2, y2, duration, deviceId })
63
+ return {
64
+ device: { platform: platform || 'android', id: deviceId || 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
65
+ success: true,
66
+ start: [x1, y1],
67
+ end: [x2, y2],
68
+ duration
69
+ }
70
+ }
71
+
72
+ ;(ToolsInteract as any).expectStateHandler = async () => ({
73
+ success: true,
74
+ selector: { text: 'Duration' },
75
+ element_id: wait.element.elementId,
76
+ expected_state: { property: 'value', expected: 30 },
77
+ element: {
78
+ elementId: wait.element.elementId,
79
+ text: 'Duration',
80
+ resource_id: 'seek_duration',
81
+ accessibility_id: null,
82
+ class: 'android.widget.SeekBar',
83
+ bounds: [0, 0, 200, 40],
84
+ index: 0,
85
+ state: { value: 30, raw_value: 30, value_range: { min: 0, max: 100 } }
86
+ },
87
+ observed_state: { property: 'value', value: 30, raw_value: 30 },
88
+ reason: 'value matches expected value'
89
+ })
90
+
91
+ const adjust = await ToolsInteract.adjustControlHandler({
92
+ element_id: wait.element.elementId,
93
+ property: 'value',
94
+ targetValue: 30,
95
+ tolerance: 0.5,
96
+ maxAttempts: 2,
97
+ platform: 'android'
98
+ })
99
+
100
+ assert.strictEqual(adjust.success, true)
101
+ assert.strictEqual(adjust.converged, true)
102
+ assert.strictEqual(adjust.within_tolerance, true)
103
+ assert.strictEqual(adjust.adjustment_mode, 'coordinate')
104
+ assert.strictEqual(adjust.target_state.target_value, 30)
105
+ assert.strictEqual(adjust.attempts, 1)
106
+ assert.strictEqual(tapCalls.length, 1)
107
+ assert.strictEqual(swipeCalls.length, 0)
108
+ assert.ok(tapCalls[0].x <= 66, 'tap should bias inward from the exact target point')
109
+ assert.strictEqual(adjust.action_type, 'adjust_control')
110
+ assert.strictEqual(adjust.target.selector.elementId, wait.element.elementId)
111
+
112
+ ;(ToolsInteract as any).expectStateHandler = async () => ({
113
+ success: true,
114
+ selector: { text: 'Duration' },
115
+ element_id: wait.element.elementId,
116
+ expected_state: { property: 'value', expected: 2 },
117
+ element: {
118
+ elementId: wait.element.elementId,
119
+ text: 'Duration',
120
+ resource_id: 'seek_duration',
121
+ accessibility_id: null,
122
+ class: 'android.widget.SeekBar',
123
+ bounds: [0, 0, 200, 40],
124
+ index: 0,
125
+ state: { value: 2, raw_value: 2, value_range: { min: 0, max: 100 } }
126
+ },
127
+ observed_state: { property: 'value', value: 2, raw_value: 2 },
128
+ reason: 'value matches expected value'
129
+ })
130
+
131
+ const lowEndAdjust = await ToolsInteract.adjustControlHandler({
132
+ element_id: wait.element.elementId,
133
+ property: 'value',
134
+ targetValue: 2,
135
+ tolerance: 0.5,
136
+ maxAttempts: 2,
137
+ platform: 'android'
138
+ })
139
+
140
+ assert.strictEqual(lowEndAdjust.success, true)
141
+ assert.strictEqual(lowEndAdjust.converged, true)
142
+ assert.strictEqual(lowEndAdjust.within_tolerance, true)
143
+ assert.strictEqual(lowEndAdjust.attempts, 1)
144
+ assert.strictEqual(tapCalls.length, 2)
145
+ assert.strictEqual(swipeCalls.length, 0)
146
+ assert.ok(tapCalls[1].x >= 22, 'low-end tap should stay inside the first step instead of hugging the edge')
147
+
148
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
149
+ device: { platform: 'android', id: 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
150
+ screen: '',
151
+ resolution: { width: 1080, height: 2400 },
152
+ elements: [
153
+ {
154
+ text: 'Duration',
155
+ type: 'android.widget.SeekBar',
156
+ contentDescription: null,
157
+ clickable: true,
158
+ enabled: true,
159
+ visible: true,
160
+ bounds: [0, 0, 200, 40],
161
+ resourceId: 'seek_duration',
162
+ state: {
163
+ value: 18,
164
+ raw_value: 18,
165
+ value_range: { min: 0, max: 20 }
166
+ }
167
+ }
168
+ ]
169
+ })
170
+
171
+ ;(ToolsInteract as any).expectStateHandler = async () => ({
172
+ success: true,
173
+ selector: { text: 'Duration' },
174
+ element_id: wait.element.elementId,
175
+ expected_state: { property: 'value', expected: 20 },
176
+ element: {
177
+ elementId: wait.element.elementId,
178
+ text: 'Duration',
179
+ resource_id: 'seek_duration',
180
+ accessibility_id: null,
181
+ class: 'android.widget.SeekBar',
182
+ bounds: [0, 0, 200, 40],
183
+ index: 0,
184
+ state: { value: 20, raw_value: 20, value_range: { min: 0, max: 20 } }
185
+ },
186
+ observed_state: { property: 'value', value: 20, raw_value: 20 },
187
+ reason: 'value matches expected value'
188
+ })
189
+
190
+ const highEndAdjust = await ToolsInteract.adjustControlHandler({
191
+ element_id: wait.element.elementId,
192
+ property: 'value',
193
+ targetValue: 20,
194
+ tolerance: 0.5,
195
+ maxAttempts: 2,
196
+ platform: 'android'
197
+ })
198
+
199
+ assert.strictEqual(highEndAdjust.success, true)
200
+ assert.strictEqual(highEndAdjust.converged, true)
201
+ assert.strictEqual(highEndAdjust.within_tolerance, true)
202
+ assert.strictEqual(highEndAdjust.attempts, 1)
203
+ assert.strictEqual(tapCalls.length, 3)
204
+ assert.strictEqual(swipeCalls.length, 0)
205
+ assert.ok(tapCalls[2].x >= 180, 'high-end tap should bias into the last step without hitting the edge')
206
+
207
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
208
+ device: { platform: 'android', id: 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
209
+ screen: '',
210
+ resolution: { width: 1440, height: 3200 },
211
+ elements: [
212
+ {
213
+ text: 'Precision',
214
+ type: 'android.widget.SeekBar',
215
+ contentDescription: null,
216
+ clickable: true,
217
+ enabled: true,
218
+ visible: true,
219
+ bounds: [0, 0, 3000, 40],
220
+ resourceId: 'seek_precision',
221
+ state: {
222
+ value: 9000,
223
+ raw_value: 9000,
224
+ value_range: { min: 0, max: 10000 }
225
+ }
226
+ }
227
+ ]
228
+ })
229
+
230
+ ;(ToolsInteract as any).expectStateHandler = async () => ({
231
+ success: true,
232
+ selector: { text: 'Precision' },
233
+ element_id: wait.element.elementId,
234
+ expected_state: { property: 'value', expected: 9999 },
235
+ element: {
236
+ elementId: wait.element.elementId,
237
+ text: 'Precision',
238
+ resource_id: 'seek_precision',
239
+ accessibility_id: null,
240
+ class: 'android.widget.SeekBar',
241
+ bounds: [0, 0, 3000, 40],
242
+ index: 0,
243
+ state: { value: 9999, raw_value: 9999, value_range: { min: 0, max: 10000 } }
244
+ },
245
+ observed_state: { property: 'value', value: 9999, raw_value: 9999 },
246
+ reason: 'value matches expected value'
247
+ })
248
+
249
+ const precisionAdjust = await ToolsInteract.adjustControlHandler({
250
+ selector: { text: 'Precision' },
251
+ property: 'value',
252
+ targetValue: 9999,
253
+ tolerance: 0.5,
254
+ maxAttempts: 2,
255
+ platform: 'android'
256
+ })
257
+
258
+ assert.strictEqual(precisionAdjust.success, true)
259
+ assert.strictEqual(precisionAdjust.converged, true)
260
+ assert.strictEqual(precisionAdjust.within_tolerance, true)
261
+ assert.strictEqual(precisionAdjust.attempts, 1)
262
+ assert.strictEqual(tapCalls.length, 4)
263
+ assert.strictEqual(swipeCalls.length, 0)
264
+ assert.ok(tapCalls[3].x > 2750, 'wide, high-range control should not be clamped to a 3% endpoint margin')
265
+
266
+ let treeFetches = 0
267
+ let retryVerificationCount = 0
268
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => {
269
+ treeFetches++
270
+ return {
271
+ device: { platform: 'android', id: 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
272
+ screen: '',
273
+ resolution: { width: 1080, height: 2400 },
274
+ elements: [
275
+ {
276
+ text: 'Duration',
277
+ type: 'android.widget.SeekBar',
278
+ contentDescription: null,
279
+ clickable: true,
280
+ enabled: true,
281
+ visible: true,
282
+ bounds: [0, 0, 200, 40],
283
+ resourceId: 'seek_duration',
284
+ state: {
285
+ value: 10,
286
+ raw_value: 10,
287
+ value_range: { min: 0, max: 20 }
288
+ }
289
+ }
290
+ ]
291
+ }
292
+ }
293
+
294
+ ;(ToolsInteract as any).tapHandler = async ({ platform, x, y, deviceId }: any) => {
295
+ tapCalls.push({ platform, x, y, deviceId })
296
+ return {
297
+ device: { platform: platform || 'android', id: deviceId || 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
298
+ success: false,
299
+ error: 'tap failed'
300
+ }
301
+ }
302
+
303
+ ;(ToolsInteract as any).swipeHandler = async ({ platform, x1, y1, x2, y2, duration, deviceId }: any) => {
304
+ swipeCalls.push({ platform, x1, y1, x2, y2, duration, deviceId })
305
+ return {
306
+ device: { platform: platform || 'android', id: deviceId || 'mock-device', osVersion: '14', model: 'Pixel', simulator: true },
307
+ success: true,
308
+ start: [x1, y1],
309
+ end: [x2, y2],
310
+ duration
311
+ }
312
+ }
313
+
314
+ ;(ToolsInteract as any).expectStateHandler = async () => {
315
+ retryVerificationCount++
316
+ const value = retryVerificationCount === 1 ? 11 : 12
317
+ return {
318
+ success: true,
319
+ selector: { text: 'Duration' },
320
+ element_id: wait.element.elementId,
321
+ expected_state: { property: 'value', expected: 12 },
322
+ element: {
323
+ elementId: wait.element.elementId,
324
+ text: 'Duration',
325
+ resource_id: 'seek_duration',
326
+ accessibility_id: null,
327
+ class: 'android.widget.SeekBar',
328
+ bounds: [0, 0, 200, 40],
329
+ index: 0,
330
+ state: { value, raw_value: value, value_range: { min: 0, max: 20 } }
331
+ },
332
+ observed_state: { property: 'value', value, raw_value: value },
333
+ reason: value === 12 ? 'value matches expected value' : 'value still below target'
334
+ }
335
+ }
336
+
337
+ const cachedResolveAdjust = await ToolsInteract.adjustControlHandler({
338
+ element_id: wait.element.elementId,
339
+ property: 'value',
340
+ targetValue: 12,
341
+ tolerance: 0.5,
342
+ maxAttempts: 2,
343
+ platform: 'android'
344
+ })
345
+
346
+ assert.strictEqual(cachedResolveAdjust.success, true)
347
+ assert.strictEqual(cachedResolveAdjust.converged, true)
348
+ assert.strictEqual(cachedResolveAdjust.within_tolerance, true)
349
+ assert.strictEqual(cachedResolveAdjust.attempts, 3)
350
+ assert.strictEqual(treeFetches, 1, 'second attempt should reuse the resolved element instead of refetching the UI tree')
351
+
352
+ console.log('adjust_control unit tests passed')
353
+ } finally {
354
+ ;(Observe as any).ToolsObserve.getUITreeHandler = originalGetUITreeHandler
355
+ ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = originalGetScreenFingerprintHandler
356
+ ;(ToolsInteract as any).tapHandler = originalTapHandler
357
+ ;(ToolsInteract as any).swipeHandler = originalSwipeHandler
358
+ ;(ToolsInteract as any).expectStateHandler = originalExpectStateHandler
359
+ }
360
+ }
361
+
362
+ run().catch((error) => {
363
+ console.error(error)
364
+ process.exit(1)
365
+ })
@@ -31,6 +31,20 @@ async function run() {
31
31
  })
32
32
  assert.deepStrictEqual(androidElements[0].semantic, { is_clickable: true, is_container: false })
33
33
 
34
+ const androidProgressElements: any[] = []
35
+ traverseNode({
36
+ '@_class': 'android.widget.ProgressBar',
37
+ '@_text': 'Loading progress',
38
+ '@_content-desc': 'Loading progress',
39
+ '@_enabled': 'true',
40
+ '@_progress': '40',
41
+ '@_max': '100',
42
+ '@_bounds': '[0,0][200,40]'
43
+ }, androidProgressElements)
44
+
45
+ assert.notStrictEqual(androidProgressElements[0]?.role, 'slider')
46
+ assert.notStrictEqual(androidProgressElements[0]?.state?.value, 40)
47
+
34
48
  const androidFallbackElements: any[] = []
35
49
  traverseNode({
36
50
  '@_class': 'android.widget.Button',
@@ -70,6 +84,16 @@ async function run() {
70
84
  })
71
85
  assert.deepStrictEqual(iosElements[0].semantic, { is_clickable: true, is_container: false })
72
86
 
87
+ const iosProgressElements: any[] = []
88
+ traverseIDBNode({
89
+ AXElementType: 'ProgressIndicator',
90
+ AXLabel: 'Loading progress',
91
+ AXValue: '0.4',
92
+ AXTraits: ['UIAccessibilityTraitUpdatesFrequently']
93
+ }, iosProgressElements)
94
+
95
+ assert.notStrictEqual(iosProgressElements[0]?.role, 'slider')
96
+
73
97
  const iosFallbackElements: any[] = []
74
98
  traverseIDBNode({
75
99
  AXElementType: 'Button',
@@ -16,6 +16,7 @@ async function run() {
16
16
  assert(names.includes('capture_screenshot'))
17
17
  assert(names.includes('get_ui_tree'))
18
18
  assert(names.includes('tap_element'))
19
+ assert(names.includes('adjust_control'))
19
20
 
20
21
  const waitForUI = toolDefinitions.find((tool) => tool.name === 'wait_for_ui')
21
22
  assert(waitForUI, 'wait_for_ui should be registered')
@@ -66,6 +67,13 @@ async function run() {
66
67
  assert.match((expectElementVisible as any).description, /selector is the primary input/i)
67
68
  assert.match((expectElementVisible as any).description, /Returns structured binary success\/failure only/i)
68
69
 
70
+ const adjustControl = toolDefinitions.find((tool) => tool.name === 'adjust_control')
71
+ assert(adjustControl, 'adjust_control should be registered')
72
+ assert.deepStrictEqual((adjustControl as any).inputSchema.required, ['targetValue'])
73
+ assert.strictEqual((adjustControl as any).inputSchema.properties.targetValue.type, 'number')
74
+ assert.match((adjustControl as any).description, /numeric control value/i)
75
+ assert.match((adjustControl as any).description, /expect_state/i)
76
+
69
77
  const classifyActionOutcome = toolDefinitions.find((tool) => tool.name === 'classify_action_outcome')
70
78
  assert(classifyActionOutcome, 'classify_action_outcome should be registered')
71
79
  assert.match((classifyActionOutcome as any).description, /action_type/i)
@@ -14,6 +14,7 @@ async function run() {
14
14
  const originalExpectScreenHandler = (ToolsInteract as any).expectScreenHandler
15
15
  const originalExpectElementVisibleHandler = (ToolsInteract as any).expectElementVisibleHandler
16
16
  const originalExpectStateHandler = (ToolsInteract as any).expectStateHandler
17
+ const originalAdjustControlHandler = (ToolsInteract as any).adjustControlHandler
17
18
  const originalStartApp = AndroidManage.prototype.startApp
18
19
  const originalCaptureScreenshotHandler = (ToolsObserve as any).captureScreenshotHandler
19
20
  const originalGetUITreeHandler = (ToolsObserve as any).getUITreeHandler
@@ -191,6 +192,43 @@ async function run() {
191
192
  assert.strictEqual(expectStatePayload.expected_state.property, 'checked')
192
193
  assert.strictEqual(expectStatePayload.observed_state.value, true)
193
194
 
195
+ ;(ToolsInteract as any).adjustControlHandler = async () => ({
196
+ action_id: 'adjust_control_1',
197
+ timestamp: '2026-04-29T08:00:00.000Z',
198
+ action_type: 'adjust_control',
199
+ lifecycle_state: 'pending_verification',
200
+ source_module: 'interact',
201
+ target: {
202
+ selector: { elementId: 'el_duration' },
203
+ resolved: {
204
+ elementId: 'el_duration',
205
+ text: 'Duration',
206
+ resource_id: null,
207
+ accessibility_id: null,
208
+ class: 'android.view.View',
209
+ bounds: [0, 0, 100, 20],
210
+ index: 0
211
+ }
212
+ },
213
+ success: true,
214
+ ui_fingerprint_before: 'fp_before',
215
+ ui_fingerprint_after: 'fp_after',
216
+ target_state: { property: 'value', target_value: 30, tolerance: 0.5 },
217
+ actual_state: { property: 'value', value: 30, raw_value: 30 },
218
+ within_tolerance: true,
219
+ converged: true,
220
+ attempts: 1,
221
+ adjustment_mode: 'semantic'
222
+ })
223
+
224
+ const adjustControlResponse = await handleToolCall('adjust_control', { element_id: 'el_duration', targetValue: 30, tolerance: 0.5, property: 'value' })
225
+ const adjustControlPayload = JSON.parse((adjustControlResponse as any).content[0].text)
226
+ assert.strictEqual(adjustControlPayload.success, true)
227
+ assert.strictEqual(adjustControlPayload.action_type, 'adjust_control')
228
+ assert.strictEqual(adjustControlPayload.target_state.target_value, 30)
229
+ assert.strictEqual(adjustControlPayload.within_tolerance, true)
230
+ assert.strictEqual(adjustControlPayload.converged, true)
231
+
194
232
  ;(ToolsInteract as any).tapHandler = async () => {
195
233
  throw new Error('boom')
196
234
  }
@@ -319,6 +357,7 @@ async function run() {
319
357
  ;(ToolsObserve as any).getUITreeHandler = originalGetUITreeHandler
320
358
  ;(ToolsObserve as any).getScreenFingerprintHandler = originalGetScreenFingerprintHandler
321
359
  ;(ToolsObserve as any).captureDebugSnapshotHandler = originalCaptureDebugSnapshotHandler
360
+ ;(ToolsInteract as any).adjustControlHandler = originalAdjustControlHandler
322
361
  }
323
362
  }
324
363