mobile-debug-mcp 0.31.0 → 0.32.0

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.
Files changed (66) hide show
  1. package/dist/interact/android.js +13 -8
  2. package/dist/interact/index.js +33 -22
  3. package/dist/interact/ios.js +5 -2
  4. package/dist/manage/android.js +20 -10
  5. package/dist/manage/ios.js +19 -10
  6. package/dist/observe/android.js +6 -1
  7. package/dist/observe/index.js +22 -5
  8. package/dist/observe/ios.js +7 -2
  9. package/dist/observe/snapshot-metadata.js +94 -15
  10. package/dist/server/common.js +14 -7
  11. package/dist/server/journey-schema.js +60 -0
  12. package/dist/server/tool-definitions.js +59 -6
  13. package/dist/server/tool-handlers.js +358 -55
  14. package/dist/server-core.js +1 -1
  15. package/dist/utils/exec.js +4 -2
  16. package/dist/utils/ios/utils.js +5 -2
  17. package/dist/utils/operation-budget.js +25 -0
  18. package/dist/utils/resolve-device.js +12 -1
  19. package/dist/utils/timing.js +19 -0
  20. package/dist/utils/ui/index.js +2 -0
  21. package/docs/CHANGELOG.md +3 -0
  22. package/docs/benchmarks/interaction-latency/README.md +26 -0
  23. package/docs/benchmarks/interaction-latency/final.json +1810 -0
  24. package/docs/benchmarks/interaction-latency/final.md +17 -0
  25. package/docs/benchmarks/interaction-latency/ios-validation.md +11 -0
  26. package/docs/specs-deltas/018-interaction-latency-and-batched-journeys.md +301 -0
  27. package/docs/tools/interact.md +30 -0
  28. package/package.json +1 -1
  29. package/src/interact/android.ts +13 -8
  30. package/src/interact/index.ts +32 -23
  31. package/src/interact/ios.ts +5 -2
  32. package/src/manage/android.ts +19 -10
  33. package/src/manage/ios.ts +18 -10
  34. package/src/observe/android.ts +5 -1
  35. package/src/observe/index.ts +21 -5
  36. package/src/observe/ios.ts +6 -2
  37. package/src/observe/snapshot-metadata.ts +94 -15
  38. package/src/server/common.ts +13 -6
  39. package/src/server/journey-schema.ts +63 -0
  40. package/src/server/tool-definitions.ts +60 -7
  41. package/src/server/tool-handlers.ts +324 -53
  42. package/src/server-core.ts +1 -1
  43. package/src/types.ts +31 -2
  44. package/src/utils/exec.ts +4 -2
  45. package/src/utils/ios/utils.ts +5 -2
  46. package/src/utils/operation-budget.ts +22 -0
  47. package/src/utils/resolve-device.ts +13 -1
  48. package/src/utils/timing.ts +15 -0
  49. package/src/utils/ui/index.ts +2 -0
  50. package/test/device/automated/latency/benchmark.ts +136 -0
  51. package/test/device/automated/latency/report.ts +6 -0
  52. package/test/fixtures/latency-journey-app/AndroidManifest.xml +6 -0
  53. package/test/fixtures/latency-journey-app/README.md +10 -0
  54. package/test/fixtures/latency-journey-app/build.ts +26 -0
  55. package/test/fixtures/latency-journey-app/src/MainActivity.java +48 -0
  56. package/test/unit/manage/restart_budget.test.ts +37 -0
  57. package/test/unit/observe/capture_debug_snapshot.test.ts +6 -0
  58. package/test/unit/observe/snapshot_metadata.test.ts +20 -1
  59. package/test/unit/observe/state_delta.test.ts +15 -0
  60. package/test/unit/server/benchmark_report.test.ts +4 -0
  61. package/test/unit/server/contract.test.ts +1 -1
  62. package/test/unit/server/interaction_modes.test.ts +60 -0
  63. package/test/unit/server/operation_budget.test.ts +32 -0
  64. package/test/unit/server/response_shapes.test.ts +3 -3
  65. package/test/unit/server/run_journey.test.ts +90 -0
  66. package/test/unit/server/tap_selector.test.ts +68 -0
@@ -0,0 +1,90 @@
1
+ import { _setDeviceListersForTests, _resetDeviceListersForTests } from '../../../src/utils/resolve-device.js'
2
+ import assert from 'assert'
3
+ import { handleToolCall } from '../../../src/server-core.js'
4
+ import { ToolsInteract } from '../../../src/interact/index.js'
5
+ import { ToolsObserve } from '../../../src/observe/index.js'
6
+
7
+ async function run() {
8
+ const originalWait = (ToolsInteract as any).waitForUIHandler
9
+ const originalExpectVisible = (ToolsInteract as any).expectElementVisibleHandler
10
+ const originalTap = (ToolsInteract as any).tapElementHandler
11
+ const originalTree = (ToolsObserve as any).getUITreeHandler
12
+ let resolutions = 0
13
+ _setDeviceListersForTests({ listAndroidDevices: async () => { resolutions++; return [{ id: 'fixture', platform: 'android' } as any] } })
14
+ try {
15
+ const calls: string[] = []
16
+ ;(ToolsInteract as any).waitForUIHandler = async () => {
17
+ calls.push('wait')
18
+ return { status: 'success', element: { elementId: 'ready' } }
19
+ }
20
+ ;(ToolsInteract as any).expectElementVisibleHandler = async () => {
21
+ calls.push('assert')
22
+ return { success: false, failure_code: 'TIMEOUT' }
23
+ }
24
+
25
+ const response: any = await handleToolCall('run_journey', {
26
+ platform: 'android',
27
+ steps: [
28
+ { id: 'wait-ready', type: 'wait', selector: { text: 'Ready' } },
29
+ { id: 'assert-ready', type: 'assert', assertion: { kind: 'element_visible', selector: { text: 'Ready' } } },
30
+ { id: 'not-run', type: 'wait', selector: { text: 'Later' } }
31
+ ]
32
+ })
33
+ const result = JSON.parse(response.content[0].text)
34
+ assert.strictEqual(resolutions, 1)
35
+ assert.deepStrictEqual(calls, ['wait', 'assert'])
36
+ await handleToolCall('run_journey', { platform: 'android', steps: [{ id: 'valid', type: 'wait', selector: { text: 'Ready' } }, { id: 'bad', type: 'tap' }] })
37
+ assert.strictEqual(resolutions, 1, 'Invalid later steps must fail before device work')
38
+ assert.deepStrictEqual(calls, ['wait', 'assert'])
39
+ assert.strictEqual(result.success, false)
40
+ assert.strictEqual(result.stopped_at_step_id, 'assert-ready')
41
+ assert.strictEqual(result.steps[0].status, 'passed')
42
+ assert.strictEqual(result.steps[1].status, 'failed')
43
+ assert.deepStrictEqual(result.steps[2], {
44
+ id: 'not-run', type: 'wait', status: 'not_run', success: false,
45
+ reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 }
46
+ })
47
+
48
+ let postActionReads = 0
49
+ ;(ToolsObserve as any).getUITreeHandler = async () => { postActionReads++; throw new Error('Unexpected duplicate observation') }
50
+ ;(ToolsInteract as any).waitForUIHandler = async () => ({ status: 'success', element: { elementId: 'target' }, _tree: {} })
51
+ ;(ToolsInteract as any).tapElementHandler = async (args: any) => {
52
+ assert.strictEqual(args.verificationMode, 'none')
53
+ return { success: true, action_id: 'tap', action_type: 'tap_element' }
54
+ }
55
+ ;(ToolsInteract as any).expectElementVisibleHandler = async () => ({ success: true, observed: { matched_count: 1 } })
56
+ const lightResponse: any = await handleToolCall('run_journey', {
57
+ platform: 'android', responseMode: 'compact', defaults: { verificationMode: 'light' },
58
+ steps: [
59
+ { id: 'tap-target', type: 'tap', selector: { accessibility_id: 'target' } },
60
+ { id: 'assert-result', type: 'assert', assertion: { kind: 'element_visible', selector: { accessibility_id: 'result' } } }
61
+ ]
62
+ })
63
+ const light = JSON.parse(lightResponse.content[0].text)
64
+ assert.strictEqual(light.success, true)
65
+ assert.deepStrictEqual(Object.keys(light.timing), ['total_ms'])
66
+ assert.deepStrictEqual(light.steps[0].result.verification, { mode: 'light', status: 'observed' })
67
+ assert.strictEqual(light.steps[0].result.verification_diagnostic, undefined)
68
+ assert.strictEqual(postActionReads, 0, 'The following assertion supplies the fresh observation')
69
+ ;(ToolsInteract as any).expectElementVisibleHandler = async () => ({ success: false, failure_code: 'TIMEOUT', observed: { matched_count: 0 } })
70
+ const failedAssertionResponse: any = await handleToolCall('run_journey', {
71
+ platform: 'android', responseMode: 'compact', defaults: { verificationMode: 'light' },
72
+ steps: [
73
+ { id: 'tap-target', type: 'tap', selector: { accessibility_id: 'target' } },
74
+ { id: 'assert-result', type: 'assert', assertion: { kind: 'element_visible', selector: { accessibility_id: 'result' } } }
75
+ ]
76
+ })
77
+ const failedAssertion = JSON.parse(failedAssertionResponse.content[0].text)
78
+ assert.strictEqual(failedAssertion.success, false)
79
+ assert.deepStrictEqual(failedAssertion.steps[0].result.verification, { mode: 'light', status: 'observed' })
80
+ assert.strictEqual(postActionReads, 0)
81
+ } finally {
82
+ _resetDeviceListersForTests()
83
+ ;(ToolsInteract as any).waitForUIHandler = originalWait
84
+ ;(ToolsInteract as any).expectElementVisibleHandler = originalExpectVisible
85
+ ;(ToolsInteract as any).tapElementHandler = originalTap
86
+ ;(ToolsObserve as any).getUITreeHandler = originalTree
87
+ }
88
+ }
89
+
90
+ run().catch((error) => { console.error(error); process.exit(1) })
@@ -0,0 +1,68 @@
1
+ import assert from 'node:assert/strict'
2
+ import { handleToolCall } from '../../../src/server-core.js'
3
+ import { ToolsInteract } from '../../../src/interact/index.js'
4
+
5
+ async function run() {
6
+ const originalWait = ToolsInteract.waitForUIHandler
7
+ const originalTap = ToolsInteract.tapElementHandler
8
+ let tapCount = 0
9
+ let lastWait: any
10
+ let resolution: any
11
+ ToolsInteract.waitForUIHandler = async (args: any) => { lastWait = args; return resolution }
12
+ ToolsInteract.tapElementHandler = async (args: any) => {
13
+ tapCount++
14
+ assert.equal(args.elementId, 'selected')
15
+ assert.deepEqual(args.freshTree, { elements: [] })
16
+ return { action_id: 'tap', action_type: 'tap_element', success: true, lifecycle_state: 'pending_verification' } as any
17
+ }
18
+ try {
19
+ const selector = { accessibility_id: 'save' }
20
+ resolution = { status: 'success', element: { elementId: 'selected' }, _tree: { elements: [] } }
21
+ const success = JSON.parse((await handleToolCall('tap_element', {
22
+ selector, platform: 'android', verificationMode: 'none', responseMode: 'compact'
23
+ })).content[0].text!)
24
+ assert.equal(success.success, true)
25
+ assert.equal(tapCount, 1)
26
+ assert.equal(lastWait.condition, 'clickable')
27
+ assert.equal(lastWait.singleObservation, true)
28
+ assert.equal(lastWait.singleAttempt, true)
29
+ assert.equal(lastWait.rejectAmbiguous, true)
30
+
31
+ await handleToolCall('tap_element', {
32
+ selector, waitFor: { condition: 'visible', timeoutMs: 700, pollIntervalMs: 80 },
33
+ platform: 'android', verificationMode: 'none', responseMode: 'compact'
34
+ })
35
+ assert.equal(lastWait.condition, 'visible')
36
+ assert.equal(lastWait.timeout_ms, 700)
37
+ assert.equal(lastWait.poll_interval_ms, 80)
38
+ assert.equal(lastWait.singleAttempt, false)
39
+ assert.equal(lastWait.rejectAmbiguous, false)
40
+ assert.equal(tapCount, 2)
41
+
42
+ resolution = { status: 'timeout', error: { code: 'AMBIGUOUS_TARGET' } }
43
+ const ambiguous = JSON.parse((await handleToolCall('tap_element', {
44
+ selector, platform: 'android', verificationMode: 'none', responseMode: 'compact'
45
+ })).content[0].text!)
46
+ assert.equal(ambiguous.failure_code, 'AMBIGUOUS_TARGET')
47
+ assert.equal(tapCount, 2)
48
+
49
+ resolution = { status: 'timeout', error: { code: 'ELEMENT_NOT_FOUND' } }
50
+ const missing = JSON.parse((await handleToolCall('tap_element', {
51
+ selector, platform: 'android', verificationMode: 'none', responseMode: 'compact'
52
+ })).content[0].text!)
53
+ assert.equal(missing.failure_code, 'ELEMENT_NOT_FOUND')
54
+ assert.equal(missing.dispatch_started, false)
55
+ assert.equal(missing.retryable, true)
56
+ assert.equal(tapCount, 2)
57
+
58
+ const invalid = await handleToolCall('tap_element', {
59
+ elementId: 'old', selector, waitFor: {}, platform: 'android'
60
+ })
61
+ assert.match(JSON.parse(invalid.content[0].text!).error.message, /Invalid input/)
62
+ assert.equal(tapCount, 2)
63
+ } finally {
64
+ ToolsInteract.waitForUIHandler = originalWait
65
+ ToolsInteract.tapElementHandler = originalTap
66
+ }
67
+ }
68
+ run().catch(error => { console.error(error); process.exitCode = 1 })