mobile-debug-mcp 0.30.0 → 0.30.2

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 (44) hide show
  1. package/dist/interact/index.js +876 -155
  2. package/dist/observe/index.js +1 -0
  3. package/dist/observe/snapshot-metadata.js +62 -1
  4. package/dist/server/tool-definitions.js +20 -11
  5. package/dist/server/tool-handlers.js +7 -5
  6. package/dist/server-core.js +1 -1
  7. package/docs/CHANGELOG.md +9 -0
  8. package/docs/ROADMAP.md +85 -209
  9. package/docs/{rfcs → specs-deltas}/013-wait-and-synchronization-reliability.md +15 -35
  10. package/docs/{rfcs → specs-deltas}/014-actionability-resolution.md +32 -30
  11. package/docs/specs-deltas/015-adjustable-control-support.md +541 -0
  12. package/package.json +1 -1
  13. package/skills/spec-review/SKILL.md +52 -0
  14. package/skills/spec-review/references/spec-review-checklist.md +13 -0
  15. package/skills/spec-review/references/spec-review-template.md +28 -0
  16. package/src/interact/index.ts +1008 -171
  17. package/src/observe/index.ts +1 -0
  18. package/src/observe/snapshot-metadata.ts +69 -2
  19. package/src/server/tool-definitions.ts +20 -11
  20. package/src/server/tool-handlers.ts +7 -5
  21. package/src/server-core.ts +1 -1
  22. package/src/types.ts +26 -0
  23. package/test/unit/interact/adjust_control.test.ts +690 -33
  24. package/test/unit/interact/subtree_collection.test.ts +24 -0
  25. package/test/unit/interact/tap_element.test.ts +71 -0
  26. package/test/unit/interact/wait_for_ui_change.test.ts +92 -1
  27. package/test/unit/observe/snapshot_metadata.test.ts +67 -0
  28. package/test/unit/server/contract.test.ts +4 -0
  29. package/skills/rfc-review/SKILL.md +0 -52
  30. package/skills/rfc-review/references/rfc-review-checklist.md +0 -12
  31. package/skills/rfc-review/references/rfc-review-template.md +0 -28
  32. /package/docs/{rfcs → specs-deltas}/001-state-verification.md +0 -0
  33. /package/docs/{rfcs → specs-deltas}/002-richer-element-identity +0 -0
  34. /package/docs/{rfcs → specs-deltas}/003-wait-and-synchronization-reliability.md +0 -0
  35. /package/docs/{rfcs → specs-deltas}/004-action-verification-routing.md +0 -0
  36. /package/docs/{rfcs → specs-deltas}/005-unified-action-execution-and-verification-model.md +0 -0
  37. /package/docs/{rfcs → specs-deltas}/006-runtime-action-instrumentation-and-binding-layer.md +0 -0
  38. /package/docs/{rfcs → specs-deltas}/007-actionability-resolution-and-executable-target-selection.md +0 -0
  39. /package/docs/{rfcs → specs-deltas}/008-adjustable-control-support-and-semantic-value-manipulation.md +0 -0
  40. /package/docs/{rfcs → specs-deltas}/009-semantic-control-modeling-for-custom-and-composite-controls.md +0 -0
  41. /package/docs/{rfcs → specs-deltas}/010-verification-stabilization-and-temporal-convergence.md +0 -0
  42. /package/docs/{rfcs → specs-deltas}/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md +0 -0
  43. /package/docs/{rfcs → specs-deltas}/011.1-recovery-contract-types-and-runtime-wiring-spec.md +0 -0
  44. /package/docs/{rfcs → specs-deltas}/012-action-trace-and-xecution-observability.md +0 -0
@@ -0,0 +1,24 @@
1
+ import assert from 'assert'
2
+ import { ToolsInteract } from '../../../src/interact/index.js'
3
+
4
+ async function run() {
5
+ console.log('Starting subtree_collection unit tests...')
6
+
7
+ const elements = [
8
+ { text: 'Root', resourceId: 'root-node', stable_id: 'root-stable', bounds: [0, 0, 100, 100], visible: true, enabled: true, clickable: true },
9
+ { text: 'Numeric child', parentId: '0', stable_id: 'numeric-child', bounds: [0, 0, 50, 50], visible: true, enabled: true, clickable: true },
10
+ { text: 'Resource child', parentId: 'root-node', stable_id: 'resource-child', bounds: [0, 50, 50, 100], visible: true, enabled: true, clickable: true },
11
+ { text: 'Stable child', parentId: 'root-stable', stable_id: 'stable-child', bounds: [50, 0, 100, 50], visible: true, enabled: true, clickable: true },
12
+ { text: 'Grandchild', parentId: 'resource-child', bounds: [50, 50, 100, 100], visible: true, enabled: true, clickable: true }
13
+ ] as any[]
14
+
15
+ const indices = (ToolsInteract as any)._collectSubtreeIndices(elements, 0)
16
+ assert.deepStrictEqual(indices, [0, 1, 2, 3, 4])
17
+
18
+ console.log('subtree_collection unit tests passed')
19
+ }
20
+
21
+ run().catch((error) => {
22
+ console.error(error)
23
+ process.exit(1)
24
+ })
@@ -84,6 +84,77 @@ async function run() {
84
84
  assert.strictEqual(disabledResult.success, false)
85
85
  assert.strictEqual(disabledResult.failure_code, 'ELEMENT_NOT_INTERACTABLE')
86
86
 
87
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
88
+ device: { platform: 'android', id: 'mock-device' },
89
+ elements: [
90
+ { text: 'Plain label', resourceId: 'txt_plain', bounds: [0, 0, 20, 20], visible: true, enabled: true, clickable: false }
91
+ ]
92
+ })
93
+ const waitPlain = await ToolsInteract.waitForUIHandler({
94
+ selector: { text: 'Plain label' },
95
+ condition: 'exists',
96
+ timeout_ms: 200,
97
+ poll_interval_ms: 50,
98
+ platform: 'android'
99
+ })
100
+ const plainResult = await ToolsInteract.tapElementHandler({ elementId: waitPlain.element.elementId })
101
+ assert.strictEqual(plainResult.success, false)
102
+ assert.strictEqual(plainResult.failure_code, 'ELEMENT_NOT_INTERACTABLE')
103
+
104
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
105
+ device: { platform: 'ios', id: 'ios-mock' },
106
+ elements: [
107
+ {
108
+ text: 'Semantic tap',
109
+ resourceId: 'ios_semantic_tap',
110
+ bounds: [10, 10, 30, 30],
111
+ visible: true,
112
+ enabled: true,
113
+ clickable: false,
114
+ semantic: {
115
+ is_clickable: true,
116
+ is_container: false,
117
+ supported_actions: ['tap']
118
+ }
119
+ }
120
+ ]
121
+ })
122
+ const iosSemanticWait = await ToolsInteract.waitForUIHandler({
123
+ selector: { text: 'Semantic tap' },
124
+ condition: 'exists',
125
+ timeout_ms: 200,
126
+ poll_interval_ms: 50,
127
+ platform: 'ios'
128
+ })
129
+ const iosSemanticTap = await ToolsInteract.tapElementHandler({ elementId: iosSemanticWait.element.elementId })
130
+ assert.strictEqual(iosSemanticTap.success, true)
131
+ assert.strictEqual(iosSemanticTap.target.resolved?.elementId, iosSemanticWait.element.elementId)
132
+ assert.deepStrictEqual(tapped, { platform: 'ios', x: 20, y: 20, deviceId: 'ios-mock' })
133
+
134
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
135
+ device: { platform: 'android', id: 'mock-device' },
136
+ elements: [
137
+ { text: 'Stable target', resourceId: 'btn_stable', bounds: [0, 0, 20, 20], visible: true, enabled: true, clickable: true, stable_id: 'stable-1' }
138
+ ]
139
+ })
140
+ const waitStable = await ToolsInteract.waitForUIHandler({
141
+ selector: { text: 'Stable target' },
142
+ condition: 'exists',
143
+ timeout_ms: 200,
144
+ poll_interval_ms: 50,
145
+ platform: 'android'
146
+ })
147
+
148
+ ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
149
+ device: { platform: 'android', id: 'mock-device' },
150
+ elements: [
151
+ { text: 'Stable target', resourceId: 'btn_stable', bounds: [0, 0, 20, 20], visible: true, enabled: true, clickable: true, stable_id: 'stable-2' }
152
+ ]
153
+ })
154
+ const stableMismatchResult = await ToolsInteract.tapElementHandler({ elementId: waitStable.element.elementId })
155
+ assert.strictEqual(stableMismatchResult.success, false)
156
+ assert.strictEqual(stableMismatchResult.failure_code, 'STALE_REFERENCE')
157
+
87
158
  ;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
88
159
  device: { platform: 'android', id: 'mock-device' },
89
160
  elements: []
@@ -6,7 +6,18 @@ type UiTree = {
6
6
  device: { platform: 'android', id: string, osVersion: string, model: string, simulator: boolean }
7
7
  screen: string
8
8
  resolution: { width: number, height: number }
9
- elements: Array<{ text: string, type: string, bounds: number[], visible: boolean }>
9
+ elements: Array<{
10
+ text: string
11
+ type: string
12
+ bounds: number[]
13
+ visible: boolean
14
+ enabled?: boolean
15
+ clickable?: boolean
16
+ stable_id?: string
17
+ parentId?: number
18
+ children?: number[]
19
+ state?: Record<string, unknown> | null
20
+ }>
10
21
  snapshot_revision: number
11
22
  captured_at_ms: number
12
23
  }
@@ -22,6 +33,21 @@ function makeTree(screen: string, revision: number): UiTree {
22
33
  }
23
34
  }
24
35
 
36
+ function makeScopedTree(title: string, status: string, revision: number): UiTree {
37
+ return {
38
+ device: { platform: 'android', id: 'mock', osVersion: '14', model: 'Pixel', simulator: true },
39
+ screen: 'Scoped',
40
+ resolution: { width: 1080, height: 2400 },
41
+ elements: [
42
+ { text: 'Root', type: 'FrameLayout', bounds: [0, 0, 1080, 2400], visible: true, enabled: true, stable_id: 'root', children: [1, 2] },
43
+ { text: title, type: 'TextView', bounds: [0, 0, 1080, 200], visible: true, enabled: true, stable_id: 'title', parentId: 0, state: { text_value: title } },
44
+ { text: status, type: 'TextView', bounds: [0, 200, 1080, 260], visible: true, enabled: true, stable_id: 'status', parentId: 0, state: { text_value: status } }
45
+ ],
46
+ snapshot_revision: revision,
47
+ captured_at_ms: 1000 + revision
48
+ }
49
+ }
50
+
25
51
  async function runScenario({
26
52
  snapshots,
27
53
  expectedChange,
@@ -72,6 +98,62 @@ async function runScenario({
72
98
  }
73
99
  }
74
100
 
101
+ async function runScopedScenario() {
102
+ const snapshots = [
103
+ makeScopedTree('Title', 'Status 1', 1),
104
+ makeScopedTree('Title', 'Status 2', 2),
105
+ makeScopedTree('Title Ready', 'Status 2', 3),
106
+ makeScopedTree('Title Ready', 'Status 2', 4)
107
+ ]
108
+
109
+ const originalGetUITreeHandler = (ToolsObserve as any).getUITreeHandler
110
+ const originalSetTimeout = globalThis.setTimeout
111
+ const originalDateNow = Date.now
112
+ let now = 0
113
+ let calls = 0
114
+
115
+ try {
116
+ ;(Date as any).now = () => now
117
+ ;(globalThis as any).setTimeout = (callback: (...args: any[]) => void) => {
118
+ now += 1
119
+ callback()
120
+ return 0
121
+ }
122
+
123
+ ;(ToolsObserve as any).getUITreeHandler = async () => snapshots[Math.min(calls++, snapshots.length - 1)]
124
+
125
+ const resolution = await ToolsInteract.waitForUIHandler({
126
+ selector: { text: 'Title' },
127
+ condition: 'exists',
128
+ timeout_ms: 1000,
129
+ poll_interval_ms: 1,
130
+ platform: 'android',
131
+ deviceId: 'mock'
132
+ })
133
+
134
+ const targetId = resolution?.element?.elementId
135
+ assert.ok(targetId)
136
+
137
+ calls = 0
138
+ now = 0
139
+ const result = await ToolsInteract.waitForUIChangeHandler({
140
+ platform: 'android',
141
+ deviceId: 'mock',
142
+ expected_change: 'text_change',
143
+ scope: 'subtree',
144
+ target: targetId,
145
+ timeout_ms: 2000,
146
+ stability_window_ms: 1
147
+ })
148
+
149
+ return { result, targetId, calls }
150
+ } finally {
151
+ ;(ToolsObserve as any).getUITreeHandler = originalGetUITreeHandler
152
+ ;(globalThis as any).setTimeout = originalSetTimeout
153
+ ;(Date as any).now = originalDateNow
154
+ }
155
+ }
156
+
75
157
  async function run() {
76
158
  const success = await runScenario({
77
159
  snapshots: [makeTree('Loading', 1), makeTree('Loaded', 2)],
@@ -109,6 +191,15 @@ async function run() {
109
191
  assert.strictEqual(defaultWindow.calls, 4)
110
192
  assert.deepStrictEqual(defaultWindow.delays, [300, 300, 300])
111
193
 
194
+ const scoped = await runScopedScenario()
195
+ assert.strictEqual(scoped.result.success, true)
196
+ assert.strictEqual(scoped.result.scope, 'subtree')
197
+ assert.strictEqual(scoped.result.target, scoped.targetId)
198
+ assert.strictEqual(scoped.result.observed_change, 'text_change')
199
+ assert.strictEqual(scoped.result.change_summary?.added_elements, 0)
200
+ assert.strictEqual(scoped.result.change_summary?.removed_elements, 0)
201
+ assert.strictEqual(scoped.result.stability_state, 'stable')
202
+
112
203
  const resetWindow = await runScenario({
113
204
  snapshots: [makeTree('Loading', 1), makeTree('Loaded', 2), makeTree('Loaded-again', 3), makeTree('Loaded-again', 4), makeTree('Loaded-again', 5)],
114
205
  expectedChange: 'text_change',
@@ -0,0 +1,67 @@
1
+ import assert from 'assert'
2
+ import { deriveSnapshotMetadata, resetSnapshotMetadataForTests } from '../../../src/observe/snapshot-metadata.js'
3
+
4
+ async function run() {
5
+ console.log('Starting snapshot_metadata unit tests...')
6
+
7
+ resetSnapshotMetadataForTests()
8
+
9
+ const deviceKey = 'android:mock'
10
+ const first = deriveSnapshotMetadata(deviceKey, {
11
+ screen: 'Home',
12
+ resolution: { width: 100, height: 200 },
13
+ elements: [
14
+ {
15
+ text: 'Alpha',
16
+ contentDescription: null,
17
+ resourceId: 'row_1',
18
+ type: 'TextView',
19
+ clickable: false,
20
+ enabled: true,
21
+ visible: true,
22
+ bounds: [0, 0, 10, 10],
23
+ state: null,
24
+ stable_id: 'stable-row'
25
+ }
26
+ ]
27
+ }, 'ui_tree')
28
+
29
+ assert.strictEqual(first.snapshot_revision, 1)
30
+ assert.strictEqual(first.snapshot_delta, null)
31
+
32
+ const second = deriveSnapshotMetadata(deviceKey, {
33
+ screen: 'Home',
34
+ resolution: { width: 100, height: 200 },
35
+ elements: [
36
+ {
37
+ text: 'Beta',
38
+ contentDescription: null,
39
+ resourceId: 'row_1',
40
+ type: 'TextView',
41
+ clickable: false,
42
+ enabled: true,
43
+ visible: true,
44
+ bounds: [0, 0, 10, 10],
45
+ state: null,
46
+ stable_id: 'stable-row'
47
+ }
48
+ ]
49
+ }, 'ui_tree')
50
+
51
+ assert.strictEqual(second.snapshot_revision, 2)
52
+ assert.deepStrictEqual(second.snapshot_delta, {
53
+ previous_snapshot_revision: 1,
54
+ added_elements: 0,
55
+ removed_elements: 0,
56
+ mutated_elements: 1,
57
+ total_elements: 1
58
+ })
59
+
60
+ resetSnapshotMetadataForTests()
61
+ console.log('snapshot_metadata unit tests passed')
62
+ }
63
+
64
+ run().catch((error) => {
65
+ console.error(error)
66
+ process.exit(1)
67
+ })
@@ -70,6 +70,10 @@ async function run() {
70
70
  const adjustControl = toolDefinitions.find((tool) => tool.name === 'adjust_control')
71
71
  assert(adjustControl, 'adjust_control should be registered')
72
72
  assert.deepStrictEqual((adjustControl as any).inputSchema.required, ['targetValue'])
73
+ assert.deepStrictEqual((adjustControl as any).inputSchema.oneOf, [
74
+ { required: ['selector'] },
75
+ { required: ['element_id'] }
76
+ ])
73
77
  assert.strictEqual((adjustControl as any).inputSchema.properties.targetValue.type, 'number')
74
78
  assert.match((adjustControl as any).description, /numeric control value/i)
75
79
  assert.match((adjustControl as any).description, /expect_state/i)
@@ -1,52 +0,0 @@
1
- # RFC Review skill
2
-
3
- name: rfc-review
4
- version: 0.1.1
5
- summary: Reusable workflow for reviewing RFCs/specs in this repository with a consistent readiness rubric and output template.
6
-
7
- # Purpose
8
- Help an agent review an RFC for clarity, implementation readiness, and alignment with the current codebase. Use a common template so reviews stay consistent across documents and reviewers.
9
-
10
- # Activation conditions
11
- Activate when an agent needs to:
12
- - review a new or revised RFC
13
- - assess whether an RFC is implementation-ready
14
- - identify whether feedback is an RFC issue or an implementation issue
15
- - compare a spec against the current `src/` contract surface and docs
16
-
17
- # Surface area (actions)
18
- - locate-rfc
19
- - compare-against-code
20
- - assess-contract-completeness
21
- - classify-gaps
22
- - produce-review
23
-
24
- # Core guidance
25
- 1. Read the RFC first, then compare it against the relevant code, docs, and tests.
26
- 2. Separate **spec gaps** from **implementation gaps**.
27
- 3. Check for: problem clarity, scope boundaries, explicit contracts, acceptance criteria, non-goals, and consistency with existing behavior.
28
- 4. Prefer precise feedback that names the missing contract, unclear rule, or inconsistent behavior.
29
- 5. Use the shared review template in `references/rfc-review-template.md` for the final output.
30
- 6. If the RFC is not ready, say exactly what must be clarified before implementation can start.
31
- 7. Classify each blocker as either a **spec gap** or an **implementation contract gap** and stop at that boundary.
32
-
33
- # Inputs & outputs
34
- - review-rfc(input: { rfcPath, relatedPaths?, focusAreas? }) -> { verdict, risks, specGaps, implementationGaps, recommendations }
35
- - compare-against-code(input: { rfcPath, codePaths[] }) -> { matches, mismatches, notes }
36
- - produce-review(input: { rfcPath, findings[] }) -> { summary, verdict, checklist, nextStep }
37
-
38
- # Failure handling
39
- - If the RFC file is missing, stop and report the missing path explicitly.
40
- - If the RFC is ambiguous, classify each concern as either "spec" or "implementation" instead of blending them.
41
- - If the review cannot be grounded in the current repo, state that the RFC is not reviewable yet.
42
-
43
- # Progressive disclosure
44
- - Keep this file short.
45
- - Load the reference template only when writing the final review.
46
-
47
- # References
48
- - `references/rfc-review-template.md` — standard review format and verdict rubric
49
- - `references/rfc-review-checklist.md` — questions to apply while reviewing an RFC
50
-
51
- # License
52
- Same as repository (MIT).
@@ -1,12 +0,0 @@
1
- # RFC Review Checklist
2
-
3
- Ask these questions while reviewing:
4
-
5
- 1. Is the problem statement specific and grounded in current failures?
6
- 2. Are non-goals explicit?
7
- 3. Are contracts concrete enough to implement?
8
- 4. Are acceptance criteria testable?
9
- 5. Does the RFC define the source of truth for new fields or behaviors?
10
- 6. Does it match existing code paths and public tool surfaces?
11
- 7. Can each open concern be classified as a spec issue or an implementation issue?
12
- 8. Is the RFC ready to implement without further interpretation?
@@ -1,28 +0,0 @@
1
- # RFC Review Template
2
-
3
- Use this structure for every RFC review:
4
-
5
- ## Verdict
6
- - Ready / Needs clarification / Needs implementation contract / Not ready
7
-
8
- ## Summary
9
- - One short paragraph on the RFC's current quality.
10
-
11
- ## What is good
12
- - List the strongest parts of the RFC.
13
-
14
- ## Issues
15
- For each issue, include:
16
- - **Type:** spec / implementation / implementation contract / doc
17
- - **Severity:** low / medium / high
18
- - **Why it matters:** one sentence
19
- - **Fix:** exact change needed
20
-
21
- ## Missing contract surfaces
22
- - List any API shapes, response fields, state transitions, or invariants that are still undefined.
23
-
24
- ## Codebase alignment
25
- - Note whether the RFC matches current `src/`, docs, and tests.
26
-
27
- ## Next step
28
- - State the smallest next action needed to move the RFC forward.