mobile-debug-mcp 0.29.0 → 0.30.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/AGENTS.md +13 -0
- package/README.md +44 -21
- package/dist/interact/index.js +30 -25
- package/dist/server/tool-definitions.js +1 -1
- package/dist/server/tool-handlers.js +1 -1
- package/dist/server-core.js +1 -1
- package/docs/CHANGELOG.md +5 -0
- package/docs/ROADMAP.md +18 -7
- package/docs/rfcs/013-wait-and-synchronization-reliability.md +890 -0
- package/docs/rfcs/014-actionability-resolution.md +392 -0
- package/docs/specs/mcp-tooling-spec-v1.md +28 -0
- package/docs/tools/interact.md +6 -0
- package/package.json +1 -1
- package/src/interact/index.ts +29 -24
- package/src/server/tool-definitions.ts +1 -1
- package/src/server/tool-handlers.ts +1 -1
- package/src/server-core.ts +1 -1
- package/test/unit/interact/wait_for_ui_change.test.ts +105 -52
|
@@ -2,74 +2,127 @@ import assert from 'assert'
|
|
|
2
2
|
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
3
3
|
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type UiTree = {
|
|
6
|
+
device: { platform: 'android', id: string, osVersion: string, model: string, simulator: boolean }
|
|
7
|
+
screen: string
|
|
8
|
+
resolution: { width: number, height: number }
|
|
9
|
+
elements: Array<{ text: string, type: string, bounds: number[], visible: boolean }>
|
|
10
|
+
snapshot_revision: number
|
|
11
|
+
captured_at_ms: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function makeTree(screen: string, revision: number): UiTree {
|
|
15
|
+
return {
|
|
16
|
+
device: { platform: 'android', id: 'mock', osVersion: '14', model: 'Pixel', simulator: true },
|
|
17
|
+
screen,
|
|
18
|
+
resolution: { width: 1080, height: 2400 },
|
|
19
|
+
elements: [{ text: screen, type: 'TextView', bounds: [0, 0, 100, 40], visible: true }],
|
|
20
|
+
snapshot_revision: revision,
|
|
21
|
+
captured_at_ms: 1000 + revision
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function runScenario({
|
|
26
|
+
snapshots,
|
|
27
|
+
expectedChange,
|
|
28
|
+
timeoutMs,
|
|
29
|
+
stabilityWindowMs,
|
|
30
|
+
stepMs
|
|
31
|
+
}: {
|
|
32
|
+
snapshots: UiTree[]
|
|
33
|
+
expectedChange: 'hierarchy_diff' | 'text_change' | 'state_change'
|
|
34
|
+
timeoutMs: number
|
|
35
|
+
stabilityWindowMs?: number
|
|
36
|
+
stepMs: number
|
|
37
|
+
}) {
|
|
6
38
|
const originalGetUITreeHandler = (ToolsObserve as any).getUITreeHandler
|
|
39
|
+
const originalSetTimeout = globalThis.setTimeout
|
|
40
|
+
const originalDateNow = Date.now
|
|
41
|
+
let now = 0
|
|
42
|
+
let calls = 0
|
|
43
|
+
const delays: number[] = []
|
|
7
44
|
|
|
8
45
|
try {
|
|
9
|
-
|
|
46
|
+
;(Date as any).now = () => now
|
|
47
|
+
;(globalThis as any).setTimeout = (callback: (...args: any[]) => void, delay?: number) => {
|
|
48
|
+
delays.push(typeof delay === 'number' ? delay : 0)
|
|
49
|
+
now += stepMs
|
|
50
|
+
callback()
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
|
|
10
54
|
;(ToolsObserve as any).getUITreeHandler = async () => {
|
|
11
55
|
calls++
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
device: { platform: 'android', id: 'mock', osVersion: '14', model: 'Pixel', simulator: true },
|
|
15
|
-
screen: 'Loading',
|
|
16
|
-
resolution: { width: 1080, height: 2400 },
|
|
17
|
-
elements: [{ text: 'Loading', type: 'TextView', bounds: [0, 0, 100, 40], visible: true }],
|
|
18
|
-
snapshot_revision: 1,
|
|
19
|
-
captured_at_ms: 1000
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
device: { platform: 'android', id: 'mock', osVersion: '14', model: 'Pixel', simulator: true },
|
|
25
|
-
screen: 'Loaded',
|
|
26
|
-
resolution: { width: 1080, height: 2400 },
|
|
27
|
-
elements: [{ text: 'Loaded', type: 'TextView', bounds: [0, 0, 100, 40], visible: true }],
|
|
28
|
-
snapshot_revision: 2,
|
|
29
|
-
captured_at_ms: 2000
|
|
30
|
-
}
|
|
56
|
+
return snapshots[Math.min(calls - 1, snapshots.length - 1)]
|
|
31
57
|
}
|
|
32
58
|
|
|
33
|
-
const
|
|
34
|
-
platform: 'android',
|
|
35
|
-
deviceId: 'mock',
|
|
36
|
-
expected_change: 'text_change',
|
|
37
|
-
timeout_ms: 1500,
|
|
38
|
-
stability_window_ms: 1
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
assert.strictEqual(success.success, true)
|
|
42
|
-
assert.strictEqual(success.observed_change, 'text_change')
|
|
43
|
-
assert.strictEqual(success.snapshot_revision, 2)
|
|
44
|
-
assert.strictEqual(success.timeout, false)
|
|
45
|
-
|
|
46
|
-
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
47
|
-
device: { platform: 'android', id: 'mock', osVersion: '14', model: 'Pixel', simulator: true },
|
|
48
|
-
screen: 'Static',
|
|
49
|
-
resolution: { width: 1080, height: 2400 },
|
|
50
|
-
elements: [{ text: 'Static', type: 'TextView', bounds: [0, 0, 100, 40], visible: true }],
|
|
51
|
-
snapshot_revision: 9,
|
|
52
|
-
captured_at_ms: 3000
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const timeout = await ToolsInteract.waitForUIChangeHandler({
|
|
59
|
+
const result = await ToolsInteract.waitForUIChangeHandler({
|
|
56
60
|
platform: 'android',
|
|
57
61
|
deviceId: 'mock',
|
|
58
|
-
expected_change:
|
|
59
|
-
timeout_ms:
|
|
60
|
-
stability_window_ms:
|
|
62
|
+
expected_change: expectedChange,
|
|
63
|
+
timeout_ms: timeoutMs,
|
|
64
|
+
stability_window_ms: stabilityWindowMs
|
|
61
65
|
})
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
assert.strictEqual(timeout.observed_change, null)
|
|
65
|
-
assert.strictEqual(timeout.timeout, true)
|
|
66
|
-
|
|
67
|
-
console.log('wait_for_ui_change tests passed')
|
|
67
|
+
return { result, calls, delays }
|
|
68
68
|
} finally {
|
|
69
69
|
;(ToolsObserve as any).getUITreeHandler = originalGetUITreeHandler
|
|
70
|
+
;(globalThis as any).setTimeout = originalSetTimeout
|
|
71
|
+
;(Date as any).now = originalDateNow
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
async function run() {
|
|
76
|
+
const success = await runScenario({
|
|
77
|
+
snapshots: [makeTree('Loading', 1), makeTree('Loaded', 2)],
|
|
78
|
+
expectedChange: 'text_change',
|
|
79
|
+
timeoutMs: 1500,
|
|
80
|
+
stabilityWindowMs: 1,
|
|
81
|
+
stepMs: 1
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
assert.strictEqual(success.result.success, true)
|
|
85
|
+
assert.strictEqual(success.result.observed_change, 'text_change')
|
|
86
|
+
assert.strictEqual(success.result.snapshot_revision, 2)
|
|
87
|
+
assert.strictEqual(success.result.timeout, false)
|
|
88
|
+
|
|
89
|
+
const timeout = await runScenario({
|
|
90
|
+
snapshots: [makeTree('Static', 9)],
|
|
91
|
+
expectedChange: 'state_change',
|
|
92
|
+
timeoutMs: 5,
|
|
93
|
+
stabilityWindowMs: 1,
|
|
94
|
+
stepMs: 1
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assert.strictEqual(timeout.result.success, false)
|
|
98
|
+
assert.strictEqual(timeout.result.observed_change, null)
|
|
99
|
+
assert.strictEqual(timeout.result.timeout, true)
|
|
100
|
+
|
|
101
|
+
const defaultWindow = await runScenario({
|
|
102
|
+
snapshots: [makeTree('Loading', 1), makeTree('Loaded', 2), makeTree('Loaded', 3), makeTree('Loaded', 4)],
|
|
103
|
+
expectedChange: 'text_change',
|
|
104
|
+
timeoutMs: 2000,
|
|
105
|
+
stepMs: 260
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
assert.strictEqual(defaultWindow.result.success, true)
|
|
109
|
+
assert.strictEqual(defaultWindow.calls, 4)
|
|
110
|
+
assert.deepStrictEqual(defaultWindow.delays, [300, 300, 300])
|
|
111
|
+
|
|
112
|
+
const resetWindow = await runScenario({
|
|
113
|
+
snapshots: [makeTree('Loading', 1), makeTree('Loaded', 2), makeTree('Loaded-again', 3), makeTree('Loaded-again', 4), makeTree('Loaded-again', 5)],
|
|
114
|
+
expectedChange: 'text_change',
|
|
115
|
+
timeoutMs: 2000,
|
|
116
|
+
stabilityWindowMs: 300,
|
|
117
|
+
stepMs: 150
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
assert.strictEqual(resetWindow.result.success, true)
|
|
121
|
+
assert.strictEqual(resetWindow.calls, 5)
|
|
122
|
+
|
|
123
|
+
console.log('wait_for_ui_change tests passed')
|
|
124
|
+
}
|
|
125
|
+
|
|
73
126
|
run().catch((error) => {
|
|
74
127
|
console.error(error)
|
|
75
128
|
process.exit(1)
|