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.
- package/dist/interact/android.js +13 -8
- package/dist/interact/index.js +33 -22
- package/dist/interact/ios.js +5 -2
- package/dist/manage/android.js +20 -10
- package/dist/manage/ios.js +19 -10
- package/dist/observe/android.js +6 -1
- package/dist/observe/index.js +22 -5
- package/dist/observe/ios.js +7 -2
- package/dist/observe/snapshot-metadata.js +94 -15
- package/dist/server/common.js +14 -7
- package/dist/server/journey-schema.js +60 -0
- package/dist/server/tool-definitions.js +59 -6
- package/dist/server/tool-handlers.js +358 -55
- package/dist/server-core.js +1 -1
- package/dist/utils/exec.js +4 -2
- package/dist/utils/ios/utils.js +5 -2
- package/dist/utils/operation-budget.js +25 -0
- package/dist/utils/resolve-device.js +12 -1
- package/dist/utils/timing.js +19 -0
- package/dist/utils/ui/index.js +2 -0
- package/docs/CHANGELOG.md +3 -0
- package/docs/benchmarks/interaction-latency/README.md +26 -0
- package/docs/benchmarks/interaction-latency/final.json +1810 -0
- package/docs/benchmarks/interaction-latency/final.md +17 -0
- package/docs/benchmarks/interaction-latency/ios-validation.md +11 -0
- package/docs/specs-deltas/018-interaction-latency-and-batched-journeys.md +301 -0
- package/docs/tools/interact.md +30 -0
- package/package.json +1 -1
- package/src/interact/android.ts +13 -8
- package/src/interact/index.ts +32 -23
- package/src/interact/ios.ts +5 -2
- package/src/manage/android.ts +19 -10
- package/src/manage/ios.ts +18 -10
- package/src/observe/android.ts +5 -1
- package/src/observe/index.ts +21 -5
- package/src/observe/ios.ts +6 -2
- package/src/observe/snapshot-metadata.ts +94 -15
- package/src/server/common.ts +13 -6
- package/src/server/journey-schema.ts +63 -0
- package/src/server/tool-definitions.ts +60 -7
- package/src/server/tool-handlers.ts +324 -53
- package/src/server-core.ts +1 -1
- package/src/types.ts +31 -2
- package/src/utils/exec.ts +4 -2
- package/src/utils/ios/utils.ts +5 -2
- package/src/utils/operation-budget.ts +22 -0
- package/src/utils/resolve-device.ts +13 -1
- package/src/utils/timing.ts +15 -0
- package/src/utils/ui/index.ts +2 -0
- package/test/device/automated/latency/benchmark.ts +136 -0
- package/test/device/automated/latency/report.ts +6 -0
- package/test/fixtures/latency-journey-app/AndroidManifest.xml +6 -0
- package/test/fixtures/latency-journey-app/README.md +10 -0
- package/test/fixtures/latency-journey-app/build.ts +26 -0
- package/test/fixtures/latency-journey-app/src/MainActivity.java +48 -0
- package/test/unit/manage/restart_budget.test.ts +37 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +6 -0
- package/test/unit/observe/snapshot_metadata.test.ts +20 -1
- package/test/unit/observe/state_delta.test.ts +15 -0
- package/test/unit/server/benchmark_report.test.ts +4 -0
- package/test/unit/server/contract.test.ts +1 -1
- package/test/unit/server/interaction_modes.test.ts +60 -0
- package/test/unit/server/operation_budget.test.ts +32 -0
- package/test/unit/server/response_shapes.test.ts +3 -3
- package/test/unit/server/run_journey.test.ts +90 -0
- 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 })
|