mobile-debug-mcp 0.33.0 → 0.35.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 (448) hide show
  1. package/.agents/skills/mcp-builder/SKILL.md +44 -64
  2. package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
  3. package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
  4. package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
  5. package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
  6. package/.agents/skills/test-authoring/SKILL.md +7 -6
  7. package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
  8. package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
  9. package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
  10. package/.github/copilot-instructions.md +14 -36
  11. package/.github/workflows/ci.yml +23 -2
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +158 -124
  15. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  16. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  17. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  18. package/agent-plugin/.mcp.json +1 -1
  19. package/agent-plugin/README.md +20 -6
  20. package/agent-plugin/mcp.json +1 -1
  21. package/agent-plugin/plugin.json +1 -1
  22. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -4
  23. package/dist/contracts/execution-controls.js +24 -0
  24. package/dist/contracts/selector.js +8 -0
  25. package/dist/contracts/target.js +15 -0
  26. package/dist/contracts/ui.js +1 -0
  27. package/dist/debugger/android.js +79 -0
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/guard.js +30 -0
  30. package/dist/debugger/protocol.js +33 -0
  31. package/dist/debugger/sessions.js +304 -0
  32. package/dist/debugger/worker.js +176 -0
  33. package/dist/interact/adjustment/adjustment-context.js +41 -0
  34. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  35. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  36. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  37. package/dist/interact/adjustment/adjustment-result.js +19 -0
  38. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  39. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  40. package/dist/interact/adjustment/adjustment.js +52 -0
  41. package/dist/interact/adjustment/control-geometry.js +87 -0
  42. package/dist/interact/adjustment/control-values.js +136 -0
  43. package/dist/interact/adjustment/slider-labels.js +84 -0
  44. package/dist/interact/assertions/screen-assertion.js +37 -0
  45. package/dist/interact/assertions/state-assertion-context.js +35 -0
  46. package/dist/interact/assertions/state-assertion.js +76 -0
  47. package/dist/interact/assertions/state-comparison.js +141 -0
  48. package/dist/interact/assertions/visibility-assertion.js +34 -0
  49. package/dist/interact/conditional-action.js +12 -7
  50. package/dist/interact/index.js +65 -3074
  51. package/dist/interact/input-service.js +1 -0
  52. package/dist/interact/interaction-trace.js +21 -0
  53. package/dist/interact/model.js +1 -0
  54. package/dist/interact/observation.js +1 -0
  55. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  56. package/dist/interact/targeting/candidate-result.js +23 -0
  57. package/dist/interact/targeting/candidate-scoring.js +50 -0
  58. package/dist/interact/targeting/element-helpers.js +216 -0
  59. package/dist/interact/targeting/find-element.js +65 -0
  60. package/dist/interact/targeting/nearby-control.js +56 -0
  61. package/dist/interact/targeting/resolved-elements.js +21 -0
  62. package/dist/interact/targeting/tap-target.js +17 -0
  63. package/dist/interact/targeting/target-scope.js +104 -0
  64. package/dist/interact/waits/legacy-log-wait.js +38 -0
  65. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  66. package/dist/interact/waits/legacy-wait-state.js +34 -0
  67. package/dist/interact/waits/legacy-wait.js +48 -0
  68. package/dist/interact/waits/screen-wait.js +49 -0
  69. package/dist/interact/waits/ui-change-helpers.js +157 -0
  70. package/dist/interact/waits/ui-change-state.js +18 -0
  71. package/dist/interact/waits/ui-change-wait.js +77 -0
  72. package/dist/interact/waits/ui-wait-condition.js +56 -0
  73. package/dist/interact/waits/ui-wait-state.js +38 -0
  74. package/dist/interact/waits/ui-wait.js +106 -0
  75. package/dist/manage/android.js +55 -51
  76. package/dist/manage/index.js +38 -75
  77. package/dist/manage/ios/build-types.js +1 -0
  78. package/dist/manage/ios/build.js +210 -0
  79. package/dist/manage/ios/project-discovery.js +29 -0
  80. package/dist/manage/ios/scheme.js +18 -0
  81. package/dist/manage/ios.js +42 -256
  82. package/dist/observe/android/context.js +1 -0
  83. package/dist/observe/android/current-screen.js +55 -0
  84. package/dist/observe/android/fingerprint.js +13 -0
  85. package/dist/observe/android/log-stream.js +132 -0
  86. package/dist/observe/android/logs.js +107 -0
  87. package/dist/observe/android/screenshot.js +70 -0
  88. package/dist/observe/android/ui-tree.js +107 -0
  89. package/dist/observe/android.js +16 -520
  90. package/dist/observe/evidence.js +122 -76
  91. package/dist/observe/index.js +16 -263
  92. package/dist/observe/ios/context.js +16 -0
  93. package/dist/observe/ios/fingerprint.js +12 -0
  94. package/dist/observe/ios/log-stream.js +105 -0
  95. package/dist/observe/ios/logs.js +168 -0
  96. package/dist/observe/ios/screenshot.js +22 -0
  97. package/dist/observe/ios/ui-elements.js +241 -0
  98. package/dist/observe/ios/ui-tree.js +137 -0
  99. package/dist/observe/ios.js +17 -715
  100. package/dist/observe/logs/model.js +1 -0
  101. package/dist/{network → observe/network}/index.js +2 -2
  102. package/dist/observe/readiness-service.js +13 -2
  103. package/dist/observe/readiness.js +44 -18
  104. package/dist/observe/screenshot-encoding.js +41 -0
  105. package/dist/observe/snapshot-collection.js +79 -0
  106. package/dist/observe/snapshot-logs.js +30 -0
  107. package/dist/observe/snapshot-metadata.js +43 -31
  108. package/dist/observe/snapshot-semantic.js +115 -0
  109. package/dist/observe/system-trace-android.js +135 -0
  110. package/dist/observe/system-trace.js +294 -0
  111. package/dist/runtime/action-results.js +270 -0
  112. package/dist/runtime/clock.js +4 -0
  113. package/dist/server/common.js +1 -256
  114. package/dist/server/definitions/assert.js +162 -0
  115. package/dist/server/definitions/diagnostic.js +118 -0
  116. package/dist/server/definitions/input.js +274 -0
  117. package/dist/server/definitions/manage.js +194 -0
  118. package/dist/server/definitions/observe.js +149 -0
  119. package/dist/server/definitions/target.js +138 -0
  120. package/dist/server/definitions/wait.js +105 -0
  121. package/dist/server/handlers/action-execution.js +163 -0
  122. package/dist/server/handlers/correlation.js +23 -0
  123. package/dist/server/handlers/debugger-handlers.js +48 -0
  124. package/dist/server/handlers/debugger-response.js +9 -0
  125. package/dist/server/handlers/diagnostic.js +69 -0
  126. package/dist/server/handlers/handler-types.js +1 -0
  127. package/dist/server/handlers/input.js +154 -0
  128. package/dist/server/handlers/interact.js +113 -0
  129. package/dist/server/handlers/journey-assertions.js +35 -0
  130. package/dist/server/handlers/journey.js +147 -0
  131. package/dist/server/handlers/manage.js +119 -0
  132. package/dist/server/handlers/observe.js +95 -0
  133. package/dist/server/handlers/payload.js +4 -0
  134. package/dist/server/handlers/tool-handlers.js +160 -0
  135. package/dist/server/schemas/condition-schema.js +2 -0
  136. package/dist/server/schemas/debugger-schema.js +47 -0
  137. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +8 -22
  138. package/dist/server/schemas/system-trace-schema.js +17 -0
  139. package/dist/server/tool-definitions.js +62 -1134
  140. package/dist/server-core.js +4 -4
  141. package/dist/server.js +34 -0
  142. package/dist/system/android-status.js +101 -0
  143. package/dist/system/android.js +37 -27
  144. package/dist/system/index.js +13 -438
  145. package/dist/system/ios-status.js +132 -0
  146. package/dist/system/ios.js +11 -8
  147. package/dist/system/model.js +1 -0
  148. package/dist/system/probes.js +105 -0
  149. package/dist/system/status-summary.js +43 -0
  150. package/dist/system/unsupported-status.js +52 -0
  151. package/dist/utils/android/commands.js +89 -0
  152. package/dist/utils/android/gradle.js +139 -0
  153. package/dist/utils/android/logs.js +103 -0
  154. package/dist/utils/android/ui-tree.js +233 -0
  155. package/dist/utils/android/utils.js +6 -564
  156. package/dist/utils/cli/idb/idb-helper.js +4 -6
  157. package/dist/utils/cli/idb/install-idb.js +13 -9
  158. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  159. package/dist/utils/exec.js +2 -0
  160. package/dist/utils/ios/utils.js +45 -46
  161. package/dist/utils/java.js +75 -57
  162. package/dist/utils/operation-budget.js +2 -0
  163. package/dist/utils/resolve-device.js +6 -2
  164. package/dist/utils/ui/index.js +26 -26
  165. package/docs/CHANGELOG.md +48 -0
  166. package/docs/README.md +38 -0
  167. package/docs/benchmarks/README.md +18 -0
  168. package/docs/benchmarks/interaction-latency/README.md +14 -0
  169. package/docs/benchmarks/interaction-latency/final.md +4 -1
  170. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  171. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  172. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  173. package/docs/code-structure.md +118 -0
  174. package/docs/specs/001-state-verification/spec.md +16 -14
  175. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  176. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  177. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  178. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  179. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  180. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  181. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  182. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  183. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  184. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  185. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  186. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  187. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  188. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  189. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  190. package/docs/specs/016-environment-tools/spec.md +20 -20
  191. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  192. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  193. package/docs/specs/020-conditional-actions/intent.md +5 -3
  194. package/docs/specs/020-conditional-actions/plan.md +8 -9
  195. package/docs/specs/020-conditional-actions/spec.md +4 -2
  196. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  197. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  198. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  199. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  200. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  201. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  202. package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
  203. package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
  204. package/docs/specs/023-android-system-traces/intent.md +41 -0
  205. package/docs/specs/023-android-system-traces/plan.md +115 -0
  206. package/docs/specs/023-android-system-traces/spec.md +122 -0
  207. package/docs/specs/024-android-heap-dump-capture/intent.md +41 -0
  208. package/docs/specs/025-android-runtime-debugger/intent.md +41 -0
  209. package/docs/specs/025-android-runtime-debugger/plan.md +106 -0
  210. package/docs/specs/025-android-runtime-debugger/spec.md +183 -0
  211. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  212. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  213. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  214. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  215. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  216. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  217. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  218. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  219. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  220. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  221. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  222. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  223. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  224. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  225. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  226. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  227. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  228. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  229. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  230. package/docs/tools/TOOLS.md +31 -16
  231. package/docs/tools/{evidence.md → correlated-evidence.md} +14 -3
  232. package/docs/tools/debugger.md +180 -0
  233. package/docs/tools/interact.md +42 -20
  234. package/docs/tools/manage.md +25 -44
  235. package/docs/tools/observe.md +64 -25
  236. package/docs/tools/system-traces.md +134 -0
  237. package/docs/tools/system.md +12 -5
  238. package/package.json +11 -8
  239. package/scripts/architecture-graph.mjs +69 -0
  240. package/scripts/check-architecture.mjs +47 -0
  241. package/scripts/code-quality-baseline.json +1 -0
  242. package/scripts/code-quality.mjs +161 -0
  243. package/src/contracts/execution-controls.ts +26 -0
  244. package/src/contracts/selector.ts +17 -0
  245. package/src/contracts/target.ts +19 -0
  246. package/src/contracts/ui.ts +94 -0
  247. package/src/debugger/android.ts +66 -0
  248. package/src/debugger/diagnostics.ts +43 -0
  249. package/src/debugger/guard.ts +31 -0
  250. package/src/debugger/java/DebuggerWorker.java +166 -0
  251. package/src/debugger/protocol.ts +37 -0
  252. package/src/debugger/sessions.ts +327 -0
  253. package/src/debugger/worker.ts +135 -0
  254. package/src/interact/adjustment/adjustment-context.ts +83 -0
  255. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  256. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  257. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  258. package/src/interact/adjustment/adjustment-result.ts +34 -0
  259. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  260. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  261. package/src/interact/adjustment/adjustment.ts +49 -0
  262. package/src/interact/adjustment/control-geometry.ts +120 -0
  263. package/src/interact/adjustment/control-values.ts +141 -0
  264. package/src/interact/adjustment/slider-labels.ts +84 -0
  265. package/src/interact/assertions/screen-assertion.ts +39 -0
  266. package/src/interact/assertions/state-assertion-context.ts +48 -0
  267. package/src/interact/assertions/state-assertion.ts +64 -0
  268. package/src/interact/assertions/state-comparison.ts +168 -0
  269. package/src/interact/assertions/visibility-assertion.ts +41 -0
  270. package/src/interact/conditional-action.ts +18 -12
  271. package/src/interact/index.ts +76 -3491
  272. package/src/interact/input-service.ts +11 -0
  273. package/src/interact/interaction-trace.ts +36 -0
  274. package/src/interact/model.ts +109 -0
  275. package/src/interact/observation.ts +7 -0
  276. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  277. package/src/interact/targeting/candidate-result.ts +24 -0
  278. package/src/interact/targeting/candidate-scoring.ts +42 -0
  279. package/src/interact/targeting/element-helpers.ts +243 -0
  280. package/src/interact/targeting/find-element.ts +47 -0
  281. package/src/interact/targeting/nearby-control.ts +46 -0
  282. package/src/interact/targeting/resolved-elements.ts +20 -0
  283. package/src/interact/targeting/tap-target.ts +22 -0
  284. package/src/interact/targeting/target-scope.ts +121 -0
  285. package/src/interact/waits/legacy-log-wait.ts +30 -0
  286. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  287. package/src/interact/waits/legacy-wait-state.ts +48 -0
  288. package/src/interact/waits/legacy-wait.ts +34 -0
  289. package/src/interact/waits/screen-wait.ts +37 -0
  290. package/src/interact/waits/ui-change-helpers.ts +184 -0
  291. package/src/interact/waits/ui-change-state.ts +26 -0
  292. package/src/interact/waits/ui-change-wait.ts +59 -0
  293. package/src/interact/waits/ui-wait-condition.ts +52 -0
  294. package/src/interact/waits/ui-wait-state.ts +55 -0
  295. package/src/interact/waits/ui-wait.ts +84 -0
  296. package/src/manage/android.ts +56 -50
  297. package/src/manage/index.ts +63 -93
  298. package/src/manage/ios/build-types.ts +14 -0
  299. package/src/manage/ios/build.ts +230 -0
  300. package/src/manage/ios/project-discovery.ts +26 -0
  301. package/src/manage/ios/scheme.ts +16 -0
  302. package/src/manage/ios.ts +65 -298
  303. package/src/observe/android/context.ts +1 -0
  304. package/src/observe/android/current-screen.ts +63 -0
  305. package/src/observe/android/fingerprint.ts +13 -0
  306. package/src/observe/android/log-stream.ts +127 -0
  307. package/src/observe/android/logs.ts +110 -0
  308. package/src/observe/android/screenshot.ts +79 -0
  309. package/src/observe/android/ui-tree.ts +113 -0
  310. package/src/observe/android.ts +17 -531
  311. package/src/observe/evidence.ts +130 -71
  312. package/src/observe/index.ts +24 -286
  313. package/src/observe/ios/context.ts +22 -0
  314. package/src/observe/ios/fingerprint.ts +12 -0
  315. package/src/observe/ios/log-stream.ts +103 -0
  316. package/src/observe/ios/logs.ts +179 -0
  317. package/src/observe/ios/screenshot.ts +26 -0
  318. package/src/observe/ios/ui-elements.ts +266 -0
  319. package/src/observe/ios/ui-tree.ts +143 -0
  320. package/src/observe/ios.ts +18 -749
  321. package/src/observe/logs/model.ts +11 -0
  322. package/src/{network → observe/network}/index.ts +2 -2
  323. package/src/observe/readiness-service.ts +10 -3
  324. package/src/observe/readiness.ts +49 -22
  325. package/src/observe/screenshot-encoding.ts +32 -0
  326. package/src/observe/snapshot-collection.ts +69 -0
  327. package/src/observe/snapshot-logs.ts +26 -0
  328. package/src/observe/snapshot-metadata.ts +51 -31
  329. package/src/observe/snapshot-semantic.ts +153 -0
  330. package/src/observe/system-trace-android.ts +133 -0
  331. package/src/observe/system-trace.ts +227 -0
  332. package/src/runtime/action-results.ts +376 -0
  333. package/src/runtime/clock.ts +9 -0
  334. package/src/server/common.ts +9 -359
  335. package/src/server/definitions/assert.ts +164 -0
  336. package/src/server/definitions/diagnostic.ts +120 -0
  337. package/src/server/definitions/input.ts +276 -0
  338. package/src/server/definitions/manage.ts +196 -0
  339. package/src/server/definitions/observe.ts +151 -0
  340. package/src/server/definitions/target.ts +140 -0
  341. package/src/server/definitions/wait.ts +107 -0
  342. package/src/server/handlers/action-execution.ts +164 -0
  343. package/src/server/handlers/correlation.ts +20 -0
  344. package/src/server/handlers/debugger-handlers.ts +39 -0
  345. package/src/server/handlers/debugger-response.ts +7 -0
  346. package/src/server/handlers/diagnostic.ts +79 -0
  347. package/src/server/handlers/handler-types.ts +7 -0
  348. package/src/server/handlers/input.ts +173 -0
  349. package/src/server/handlers/interact.ts +135 -0
  350. package/src/server/handlers/journey-assertions.ts +39 -0
  351. package/src/server/handlers/journey.ts +132 -0
  352. package/src/server/handlers/manage.ts +141 -0
  353. package/src/server/handlers/observe.ts +116 -0
  354. package/src/server/handlers/payload.ts +36 -0
  355. package/src/server/handlers/tool-handlers.ts +159 -0
  356. package/src/server/schemas/condition-schema.ts +2 -0
  357. package/src/server/schemas/debugger-schema.ts +43 -0
  358. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +8 -22
  359. package/src/server/schemas/system-trace-schema.ts +17 -0
  360. package/src/server/tool-definitions.ts +69 -1140
  361. package/src/server-core.ts +4 -4
  362. package/src/server.ts +25 -1
  363. package/src/system/android-status.ts +124 -0
  364. package/src/system/android.ts +33 -21
  365. package/src/system/index.ts +21 -523
  366. package/src/system/ios-status.ts +147 -0
  367. package/src/system/ios.ts +14 -10
  368. package/src/system/model.ts +41 -0
  369. package/src/system/probes.ts +114 -0
  370. package/src/system/status-summary.ts +47 -0
  371. package/src/system/unsupported-status.ts +55 -0
  372. package/src/types.ts +16 -106
  373. package/src/utils/android/commands.ts +81 -0
  374. package/src/utils/android/gradle.ts +132 -0
  375. package/src/utils/android/logs.ts +103 -0
  376. package/src/utils/android/ui-tree.ts +239 -0
  377. package/src/utils/android/utils.ts +16 -556
  378. package/src/utils/cli/idb/check-idb.ts +4 -4
  379. package/src/utils/cli/idb/idb-helper.ts +6 -8
  380. package/src/utils/cli/idb/install-idb.ts +15 -9
  381. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  382. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  383. package/src/utils/exec.ts +2 -0
  384. package/src/utils/ios/utils.ts +57 -61
  385. package/src/utils/java.ts +71 -51
  386. package/src/utils/operation-budget.ts +2 -0
  387. package/src/utils/resolve-device.ts +11 -6
  388. package/src/utils/ui/index.ts +28 -28
  389. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  390. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  391. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  392. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  393. package/test/device/automated/debugger/runtime.android.test.ts +219 -0
  394. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  395. package/test/device/automated/observe/system-trace.smoke.ts +104 -0
  396. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  397. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  398. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  399. package/test/fixtures/coroutine-app/README.md +48 -0
  400. package/test/fixtures/coroutine-app/build.ts +35 -0
  401. package/test/fixtures/debugger-app/MainActivity.java +67 -0
  402. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  403. package/test/fixtures/debugger-app/NoLocals.java +7 -0
  404. package/test/fixtures/debugger-app/README.md +65 -0
  405. package/test/fixtures/debugger-app/build.ts +39 -0
  406. package/test/fixtures/latency-journey-app/README.md +2 -0
  407. package/test/fixtures/readiness-app/README.md +2 -0
  408. package/test/helpers/perfetto.ts +59 -0
  409. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  410. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  411. package/test/unit/debugger/diagnostics.test.ts +80 -0
  412. package/test/unit/debugger/guards.test.ts +50 -0
  413. package/test/unit/debugger/native.test.ts +33 -0
  414. package/test/unit/debugger/preparation.test.ts +32 -0
  415. package/test/unit/debugger/protocol.test.ts +15 -0
  416. package/test/unit/debugger/sessions.test.ts +83 -0
  417. package/test/unit/interact/adjustment_service.test.ts +42 -0
  418. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  419. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  420. package/test/unit/interact/element_helpers.test.ts +19 -0
  421. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  422. package/test/unit/interact/state_comparison.test.ts +20 -0
  423. package/test/unit/interact/target_scope.test.ts +35 -0
  424. package/test/unit/network/get_network_activity.test.ts +1 -1
  425. package/test/unit/observe/readiness.test.ts +3 -1
  426. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  427. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  428. package/test/unit/observe/system-trace-android.test.ts +52 -0
  429. package/test/unit/observe/system-trace.test.ts +166 -0
  430. package/test/unit/quality/architecture.test.ts +60 -0
  431. package/test/unit/quality/code-quality.test.ts +58 -0
  432. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  433. package/test/unit/server/conditional_actions.test.ts +1 -1
  434. package/test/unit/server/conditional_schema.test.ts +2 -2
  435. package/test/unit/server/debugger-response.test.ts +63 -0
  436. package/test/unit/server/system-trace.test.ts +64 -0
  437. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  438. package/dist/server/tool-handlers.js +0 -893
  439. package/docs/ROADMAP.md +0 -646
  440. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  441. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  442. package/docs/specs/baseline-spec-v0.md +0 -312
  443. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  444. package/src/server/tool-handlers.ts +0 -923
  445. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  446. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  447. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  448. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -0,0 +1,100 @@
1
+ package dev.mobiledebugmcp.coroutinefixture
2
+
3
+ import android.app.Activity
4
+ import android.os.Bundle
5
+ import android.widget.Button
6
+ import android.widget.LinearLayout
7
+ import android.widget.TextView
8
+ import java.util.concurrent.Executors
9
+ import kotlinx.coroutines.*
10
+
11
+ class MainActivity : Activity() {
12
+ private val first = Executors.newSingleThreadExecutor { Thread(it, "fixture-first") }.asCoroutineDispatcher()
13
+ private val second = Executors.newSingleThreadExecutor { Thread(it, "fixture-second") }.asCoroutineDispatcher()
14
+ private val scope = CoroutineScope(SupervisorJob() + first)
15
+ private var gate = CompletableDeferred<Unit>()
16
+ private var job: Job? = null
17
+ private lateinit var status: TextView
18
+
19
+ override fun onCreate(saved: Bundle?) {
20
+ super.onCreate(saved)
21
+ val layout = LinearLayout(this)
22
+ layout.orientation = LinearLayout.VERTICAL
23
+ layout.setPadding(0, 160, 0, 0)
24
+ status = TextView(this)
25
+ status.text = "Coroutine ready"
26
+ layout.addView(status)
27
+ fun button(label: String, action: () -> Unit) {
28
+ val button = Button(this)
29
+ button.text = label
30
+ button.setOnClickListener { action() }
31
+ layout.addView(button)
32
+ }
33
+ button("Complete run") { start(false) }
34
+ button("Cancellation run") { start(true) }
35
+ button("Release gate") { gate.complete(Unit) }
36
+ button("Cancel job") { job?.cancel() }
37
+ setContentView(layout)
38
+ }
39
+
40
+ private fun show(text: String) { runOnUiThread { status.text = text } }
41
+
42
+ private fun start(cancel: Boolean) {
43
+ if (job?.isActive == true) { show("Coroutine busy"); return }
44
+ gate = CompletableDeferred()
45
+ job = scope.launch {
46
+ if (cancel) cancelWork() else completeWork()
47
+ }
48
+ }
49
+
50
+ private suspend fun completeWork() {
51
+ val firstThread = Thread.currentThread().name
52
+ val known = 42
53
+ check(firstThread == "fixture-first") // BREAK_BEFORE
54
+ // Queued on the same single-thread executor: runs only after gate.await yields.
55
+ first.executor.execute { show("Coroutine waiting") }
56
+ gate.await() // BREAK_SUSPEND
57
+ val after = known + 1 // BREAK_AFTER
58
+ withContext(second) {
59
+ val dispatcherThread = Thread.currentThread().name
60
+ val nested = async { helper(after) } // BREAK_DISPATCH
61
+ check(dispatcherThread == "fixture-second")
62
+ val answer = nested.await()
63
+ show("Coroutine result $answer") // BREAK_RESULT
64
+ }
65
+ }
66
+
67
+ private fun helper(value: Int): Int {
68
+ val result = increment(value) // BREAK_HELPER
69
+ return result // BREAK_HELPER_RETURN
70
+ }
71
+
72
+ private fun increment(value: Int): Int {
73
+ val result = value + 1 // BREAK_INCREMENT
74
+ return result // BREAK_INCREMENT_RETURN
75
+ }
76
+
77
+ private suspend fun cancelWork() = coroutineScope {
78
+ val child = launch(second) {
79
+ try {
80
+ // The child must yield at gate.await before this queued work can run.
81
+ second.executor.execute { show("Cancellation waiting") }
82
+ gate.await()
83
+ error("Cancelled child must not cross gate")
84
+ } finally {
85
+ show("Cancellation finally") // BREAK_CANCEL
86
+ }
87
+ }
88
+ try { child.join() } finally {
89
+ withContext(NonCancellable) { child.cancelAndJoin() }
90
+ show("Coroutine cancelled")
91
+ }
92
+ }
93
+
94
+ override fun onDestroy() {
95
+ scope.cancel()
96
+ first.close()
97
+ second.close()
98
+ super.onDestroy()
99
+ }
100
+ }
@@ -0,0 +1,48 @@
1
+ # Isolated Android coroutine debugger fixture
2
+
3
+ Separate disposable package `dev.mobiledebugmcp.coroutinefixture`; the runner
4
+ refuses to replace an existing installation. No server dependency, debug agent,
5
+ automatic download or production app instrumentation is introduced.
6
+
7
+ Requires Node 22+, JDK 17, Android SDK platform/build-tools 36, Kotlin JVM compiler
8
+ and matching stdlib 2.0.21, and kotlinx-coroutines-core-jvm 1.9.0. Provide local
9
+ paths explicitly:
10
+
11
+ ```sh
12
+ export JAVA_HOME=/path/to/jdk-17
13
+ export MCP_KOTLINC=/path/to/kotlin/bin/kotlinc
14
+ export MCP_KOTLIN_STDLIB=/path/to/kotlin/lib/kotlin-stdlib.jar
15
+ export MCP_COROUTINES_CORE=/path/to/kotlinx-coroutines-core-jvm-1.9.0.jar
16
+ npm run build
17
+ MCP_COROUTINE_DEVICE=emulator-5554 npx tsx test/device/automated/debugger/coroutine.android.test.ts
18
+ ```
19
+
20
+ `npm run test:device` discovers the runner and reports **UNRUN** unless
21
+ `MCP_COROUTINE_DEVICE` is set. A configured run with missing prerequisites fails;
22
+ it cannot silently pass. Do not run other fixture journeys concurrently on the
23
+ same serial. A physical device must be selected explicitly and reported separately.
24
+
25
+ The source owns two named single-thread dispatcher executors, an explicit gate,
26
+ nested async work and a cancellation/finally path. Readiness is posted by work
27
+ queued on the same single-thread executor behind the coroutine, so the runner
28
+ releases or cancels only after the gate wait has yielded. UI controls release the
29
+ gate only after debugger resume. Compiler line/variable tables are captured for generated
30
+ continuation classes as well as ordinary methods. The assessment compares existing
31
+ MCP stops, thread stacks and shallow locals; none represents a reconstructed
32
+ logical coroutine stack. A bounded step attempt is recorded as an observation,
33
+ not a promise to follow work across suspension. Source breakpoints are tested
34
+ independently as a workaround.
35
+
36
+ The runner checks pause guards, old frame rejection, event-wait cancellation,
37
+ completion/cancellation UI recovery and owned ADB forward cleanup. Finally it
38
+ removes its package and temporary build/signing output. The overall JSON PASS is
39
+ emitted only after cleanup; cleanup failures emit JSON FAIL and reject the run.
40
+ Forward comparisons filter the selected serial and sort entries, so another
41
+ device changing its forwards does not fail this journey. SIGKILL/host loss cannot
42
+ execute finally; inspect outstanding sessions/packages manually after such loss.
43
+ Local output includes synthetic fixture values, source/APK/server/dependency hashes
44
+ and compiler metadata. Redirect stdout/stderr to an intentional local evidence
45
+ file; no data is uploaded automatically.
46
+
47
+ See the [accepted specification](../../../docs/specs/030-android-coroutine-validation/spec.md)
48
+ and [implementation/verification record](../../../docs/specs/030-android-coroutine-validation/plan.md).
@@ -0,0 +1,35 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ export const coroutineAppId = 'dev.mobiledebugmcp.coroutinefixture'
7
+ export function buildCoroutineFixture() {
8
+ if (!process.env.MCP_COROUTINES_CORE) throw new Error('MCP_COROUTINES_CORE must identify kotlinx-coroutines-core-jvm.jar')
9
+ if (!process.env.MCP_KOTLIN_STDLIB) throw new Error('MCP_KOTLIN_STDLIB must identify the matching Kotlin standard-library jar')
10
+ const source = path.dirname(fileURLToPath(import.meta.url))
11
+ const directory = mkdtempSync(path.join(tmpdir(), 'mcp-coroutine-fixture-'))
12
+ const java = process.env.JAVA_HOME ?? (existsSync('/Applications/Android Studio.app/Contents/jbr/Contents/Home') ? '/Applications/Android Studio.app/Contents/jbr/Contents/Home' : undefined)
13
+ const run = (cmd: string, args: string[]) => execFileSync(cmd, args, { cwd: directory, encoding: 'utf8', stdio: 'pipe', timeout: 120000 })
14
+ const sdk = process.env.ANDROID_HOME ?? path.join(process.env.HOME!, 'Library/Android/sdk')
15
+ const tools = path.join(sdk, 'build-tools/36.0.0')
16
+ const androidJar = path.join(sdk, 'platforms/android-36/android.jar')
17
+ const jdk = (name: string) => java ? path.join(java, 'bin', name) : name
18
+ try {
19
+ mkdirSync(path.join(directory, 'classes'))
20
+ writeFileSync(path.join(directory, 'AndroidManifest.xml'), `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="${coroutineAppId}" android:versionCode="1" android:versionName="1.0"><uses-sdk android:minSdkVersion="26" android:targetSdkVersion="36"/><application android:debuggable="true" android:label="Debugger fixture" android:theme="@android:style/Theme.Material.Light.NoActionBar"><activity android:name=".MainActivity" android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity></application></manifest>`)
21
+ run(process.env.MCP_KOTLINC ?? 'kotlinc', ['-no-stdlib', '-no-reflect', '-jvm-target', '1.8', '-classpath', [androidJar, process.env.MCP_KOTLIN_STDLIB!, process.env.MCP_COROUTINES_CORE!].join(path.delimiter), '-d', 'classes', path.join(source, 'MainActivity.kt')])
22
+ run(jdk('jar'), ['cf', 'classes.jar', '-C', 'classes', '.'])
23
+ run(path.join(tools, 'd8'), ['--min-api', '26', '--lib', androidJar, '--output', directory, 'classes.jar', process.env.MCP_KOTLIN_STDLIB!, process.env.MCP_COROUTINES_CORE!])
24
+ run(path.join(tools, 'aapt2'), ['link', '-I', androidJar, '--manifest', 'AndroidManifest.xml', '-o', 'unsigned.apk'])
25
+ run('zip', ['-q', 'unsigned.apk', 'classes.dex'])
26
+ run(path.join(tools, 'zipalign'), ['-f', '4', 'unsigned.apk', 'aligned.apk'])
27
+ run(jdk('keytool'), ['-genkeypair', '-keystore', 'fixture.jks', '-storepass', 'android', '-keypass', 'android', '-alias', 'fixture', '-dname', 'CN=Readiness Fixture', '-keyalg', 'RSA', '-validity', '365'])
28
+ const artifact = path.join(directory, 'Debugger.apk')
29
+ run(path.join(tools, 'apksigner'), ['sign', '--ks', 'fixture.jks', '--ks-pass', 'pass:android', '--out', artifact, 'aligned.apk'])
30
+ return { artifact, directory }
31
+ } catch (error) {
32
+ rmSync(directory, { recursive: true, force: true })
33
+ throw error
34
+ }
35
+ }
@@ -0,0 +1,67 @@
1
+ package dev.mobiledebugmcp.debugfixture;
2
+ import android.app.Activity;
3
+ import android.os.Bundle;
4
+ import android.widget.Button;
5
+ import android.widget.LinearLayout;
6
+ import android.widget.TextView;
7
+
8
+ public class MainActivity extends Activity {
9
+ TextView status;
10
+ @Override public void onCreate(Bundle saved) {
11
+ super.onCreate(saved);
12
+ LinearLayout layout = new LinearLayout(this); layout.setOrientation(1); layout.setPadding(0, 160, 0, 0);
13
+ status = new TextView(this); status.setText("Ready"); layout.addView(status);
14
+ Button button = new Button(this); button.setText("Calculate");
15
+ button.setOnClickListener(v -> calculate(7)); layout.addView(button);
16
+ Button late = new Button(this); late.setText("Late class");
17
+ late.setOnClickListener(v -> {
18
+ try { Class.forName("dev.mobiledebugmcp.debugfixture.LateWork").getDeclaredMethod("work").invoke(null); }
19
+ catch (Exception e) { status.setText("Late failed"); }
20
+ }); layout.addView(late);
21
+ Button noLocals = new Button(this); noLocals.setText("No locals");
22
+ noLocals.setOnClickListener(v -> {
23
+ try { Class.forName("dev.mobiledebugmcp.debugfixture.NoLocals").getDeclaredMethod("work").invoke(null); }
24
+ catch (Exception e) { status.setText("Metadata failed"); }
25
+ }); layout.addView(noLocals);
26
+ addLevelControl(layout);
27
+ setContentView(layout);
28
+ }
29
+ void addLevelControl(LinearLayout layout) {
30
+ TextView level = new TextView(this);
31
+ level.setGravity(android.view.Gravity.CENTER);
32
+ level.setText("25 %"); layout.addView(level);
33
+ android.widget.SeekBar slider = new android.widget.SeekBar(this);
34
+ slider.setMax(100); slider.setProgress(25);
35
+ slider.setContentDescription("Fixture level");
36
+ slider.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener() {
37
+ public void onProgressChanged(android.widget.SeekBar view, int progress, boolean fromUser) { level.setText(progress + " %"); }
38
+ public void onStartTrackingTouch(android.widget.SeekBar view) {}
39
+ public void onStopTrackingTouch(android.widget.SeekBar view) {}
40
+ });
41
+ layout.addView(slider);
42
+ LinearLayout range = new LinearLayout(this);
43
+ TextView minimum = new TextView(this); minimum.setText("0 %");
44
+ TextView maximum = new TextView(this); maximum.setText("100 %"); maximum.setGravity(android.view.Gravity.RIGHT);
45
+ range.addView(minimum, new LinearLayout.LayoutParams(0, 80, 1));
46
+ range.addView(maximum, new LinearLayout.LayoutParams(0, 80, 1));
47
+ layout.addView(range);
48
+ }
49
+ void calculate(int seed) {
50
+ int known = seed + 35;
51
+ String longText = new String(new char[300]).replace('\0', 'x');
52
+ Object object = new Object();
53
+ int answer = increment(known); // BREAK_CALCULATE
54
+ status.setText("Result " + answer); // BREAK_RESULT
55
+ }
56
+ int increment(int value) {
57
+ int result = value + 1; // BREAK_INCREMENT
58
+ return result;
59
+ }
60
+ }
61
+
62
+ class LateWork {
63
+ public static void work() {
64
+ int local = 99; // BREAK_LATE
65
+ android.util.Log.i("DebugFixture", "late " + local);
66
+ }
67
+ }
@@ -0,0 +1,32 @@
1
+ package dev.mobiledebugmcp.debugfixture
2
+
3
+ import android.app.Activity
4
+ import android.os.Bundle
5
+ import android.widget.Button
6
+ import android.widget.LinearLayout
7
+ import android.widget.TextView
8
+
9
+ class MainActivity : Activity() {
10
+ private lateinit var status: TextView
11
+
12
+ override fun onCreate(saved: Bundle?) {
13
+ super.onCreate(saved)
14
+ val layout = LinearLayout(this)
15
+ layout.orientation = LinearLayout.VERTICAL
16
+ layout.setPadding(0, 160, 0, 0)
17
+ status = TextView(this)
18
+ status.text = "Kotlin ready"
19
+ layout.addView(status)
20
+ val button = Button(this)
21
+ button.text = "Kotlin calculate"
22
+ button.setOnClickListener { calculate(7) }
23
+ layout.addView(button)
24
+ setContentView(layout)
25
+ }
26
+
27
+ private fun calculate(seed: Int) {
28
+ val known = seed + 35 // BREAK_KOTLIN_BEFORE
29
+ val answer = known + 1 // BREAK_KOTLIN_KNOWN
30
+ status.text = "Kotlin result $answer" // BREAK_KOTLIN_RESULT
31
+ }
32
+ }
@@ -0,0 +1,7 @@
1
+ package dev.mobiledebugmcp.debugfixture;
2
+ public class NoLocals {
3
+ public static void work() {
4
+ int hidden = 5; // BREAK_NO_LOCALS
5
+ android.util.Log.i("DebugFixture", "metadata " + hidden);
6
+ }
7
+ }
@@ -0,0 +1,65 @@
1
+ # Isolated Android debugger fixture
2
+
3
+ [Contributing](../../../CONTRIBUTING.md) · [Documentation](../../../docs/README.md)
4
+
5
+ Package `dev.mobiledebugmcp.debugfixture`, built with `android:debuggable=true`
6
+ and Java source/line/local metadata. Calculate exposes `known=42`, an increment
7
+ call for into/over/out stepping, and a visible `Result 43` after resume. Late class
8
+ loads a second class reflectively to check pending source breakpoints. NoLocals
9
+ is compiled with source/line tables and no variable tables to verify explicit ART
10
+ metadata uncertainty.
11
+
12
+ Build helper uses SDK platform/build-tools 36, the existing direct javac/D8/aapt2
13
+ pattern, ephemeral signing material and temporary output. It does not modify other
14
+ apps or display settings. The automated journey installs/removes only this fixture
15
+ and checks session-owned ADB forwards. See [debugger tools](../../../docs/tools/debugger.md).
16
+
17
+ ## Kotlin locals validation
18
+
19
+ The opt-in Kotlin variant compiles `MainActivity.kt` instead of the Java sources
20
+ into the same disposable package. Do not run the variants concurrently on one
21
+ device. It requires a Kotlin JVM compiler (`MCP_KOTLINC`, or `kotlinc` on PATH),
22
+ the matching standard-library jar (`MCP_KOTLIN_STDLIB`), and a compatible JDK.
23
+ Kotlin 2.0.21 with JDK 17 was used for the recorded validation. The fixture does
24
+ not add Gradle, download a compiler, or change the server's dependencies.
25
+
26
+ ```sh
27
+ npm run build
28
+ export JAVA_HOME=/path/to/jdk-17
29
+ export MCP_KOTLINC=/path/to/kotlin/bin/kotlinc
30
+ export MCP_KOTLIN_STDLIB=/path/to/kotlin/lib/kotlin-stdlib.jar
31
+ MCP_KOTLIN_DEVICE=emulator-5554 npx tsx test/device/automated/debugger/kotlin.android.test.ts
32
+ ```
33
+
34
+ The compiler retains its default line/local metadata; D8 runs without shrinking
35
+ or obfuscation. The runner checks the class-file local-variable scope tables,
36
+ then reads `seed=7` before `known` is initialized, `known=42` before `answer` is
37
+ initialized, and `answer=43` afterward through MCP. Old references must fail as
38
+ stale. Resume/detach is followed by a fresh hierarchy assertion for
39
+ `Kotlin result 43`; exact owned forwards, package and temporary files are cleaned
40
+ up. Build failures also remove their temporary directory.
41
+
42
+ `npm run test:device` includes this journey only when `MCP_KOTLIN_DEVICE` is set;
43
+ otherwise it reports **UNRUN** explicitly. The existing Java journey separately
44
+ covers absent variable tables. A local outside its compiler scope and missing
45
+ metadata are different conditions; an empty ART table alone proves neither.
46
+ See the [dated validation record](../../../docs/specs/029-kotlin-locals-validation/report.md).
47
+
48
+ ## Interaction regression journey
49
+
50
+ The same isolated fixture is also used by the [interaction device check](../../device/automated/interact/interaction.android.test.ts)
51
+ for clickable waits, stored handles, selector taps, visibility and enabled-state
52
+ assertions. Run it after building the server:
53
+
54
+ ```sh
55
+ MCP_INTERACTION_DEVICE=emulator-5554 npx tsx test/device/automated/interact/interaction.android.test.ts
56
+ ```
57
+
58
+ It also runs in `npm run test:device` when `MCP_INTERACTION_DEVICE` or
59
+ `MCP_DEBUGGER_DEVICE` selects a device. Fixture journeys run sequentially and
60
+ remove their own package and temporary build outputs. This check does not attach
61
+ a debugger or modify another app's data.
62
+
63
+ The interaction journey also exercises a labelled native SeekBar. It adjusts
64
+ from 25 toward 75, checks convergence within tolerance, and confirms the displayed
65
+ value with a fresh hierarchy assertion. The same fixture cleanup applies.
@@ -0,0 +1,39 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ export const debuggerAppId = 'dev.mobiledebugmcp.debugfixture'
7
+ export function buildDebuggerFixture(kotlin = false) {
8
+ if (kotlin && !process.env.MCP_KOTLIN_STDLIB) throw new Error('MCP_KOTLIN_STDLIB must identify the matching Kotlin standard-library jar')
9
+ const source = path.dirname(fileURLToPath(import.meta.url))
10
+ const directory = mkdtempSync(path.join(tmpdir(), 'mcp-debugger-fixture-'))
11
+ const java = process.env.JAVA_HOME ?? (existsSync('/Applications/Android Studio.app/Contents/jbr/Contents/Home') ? '/Applications/Android Studio.app/Contents/jbr/Contents/Home' : undefined)
12
+ const run = (cmd: string, args: string[]) => execFileSync(cmd, args, { cwd: directory, encoding: 'utf8', stdio: 'pipe' })
13
+ const sdk = process.env.ANDROID_HOME ?? path.join(process.env.HOME!, 'Library/Android/sdk')
14
+ const tools = path.join(sdk, 'build-tools/36.0.0')
15
+ const androidJar = path.join(sdk, 'platforms/android-36/android.jar')
16
+ const jdk = (name: string) => java ? path.join(java, 'bin', name) : name
17
+ try {
18
+ mkdirSync(path.join(directory, 'classes'))
19
+ writeFileSync(path.join(directory, 'AndroidManifest.xml'), `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="${debuggerAppId}" android:versionCode="1" android:versionName="1.0"><uses-sdk android:minSdkVersion="26" android:targetSdkVersion="36"/><application android:debuggable="true" android:label="Debugger fixture" android:theme="@android:style/Theme.Material.Light.NoActionBar"><activity android:name=".MainActivity" android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity></application></manifest>`)
20
+ if (kotlin) {
21
+ run(process.env.MCP_KOTLINC ?? 'kotlinc', ['-no-stdlib', '-no-reflect', '-jvm-target', '1.8', '-classpath', [androidJar, process.env.MCP_KOTLIN_STDLIB!].join(path.delimiter), '-d', 'classes', path.join(source, 'MainActivity.kt')])
22
+ } else {
23
+ run(jdk('javac'), ['-g', '--release', '8', '-classpath', androidJar, '-d', 'classes', path.join(source, 'MainActivity.java')])
24
+ run(jdk('javac'), ['-g:source,lines', '--release', '8', '-classpath', androidJar, '-d', 'classes', path.join(source, 'NoLocals.java')])
25
+ }
26
+ run(jdk('jar'), ['cf', 'classes.jar', '-C', 'classes', '.'])
27
+ run(path.join(tools, 'd8'), ['--lib', androidJar, '--output', directory, 'classes.jar', ...(kotlin ? [process.env.MCP_KOTLIN_STDLIB!] : [])])
28
+ run(path.join(tools, 'aapt2'), ['link', '-I', androidJar, '--manifest', 'AndroidManifest.xml', '-o', 'unsigned.apk'])
29
+ run('zip', ['-q', 'unsigned.apk', 'classes.dex'])
30
+ run(path.join(tools, 'zipalign'), ['-f', '4', 'unsigned.apk', 'aligned.apk'])
31
+ run(jdk('keytool'), ['-genkeypair', '-keystore', 'fixture.jks', '-storepass', 'android', '-keypass', 'android', '-alias', 'fixture', '-dname', 'CN=Readiness Fixture', '-keyalg', 'RSA', '-validity', '365'])
32
+ const artifact = path.join(directory, 'Debugger.apk')
33
+ run(path.join(tools, 'apksigner'), ['sign', '--ks', 'fixture.jks', '--ks-pass', 'pass:android', '--out', artifact, 'aligned.apk'])
34
+ return { artifact, directory }
35
+ } catch (error) {
36
+ rmSync(directory, { recursive: true, force: true })
37
+ throw error
38
+ }
39
+ }
@@ -1,5 +1,7 @@
1
1
  # Interaction latency fixture
2
2
 
3
+ [Contributing](../../../CONTRIBUTING.md) · [Documentation](../../../docs/README.md)
4
+
3
5
  An offline native Android fixture, version 1.0. It requires Android SDK platform 36,
4
6
  build-tools 36.0.0 and JDK 21. The harness builds and signs it using those installed
5
7
  tools; no Gradle dependency download is needed.
@@ -1,5 +1,7 @@
1
1
  # Readiness fixture
2
2
 
3
+ [Contributing](../../../CONTRIBUTING.md) · [Documentation](../../../docs/README.md)
4
+
3
5
  Offline native Android/iOS simulator apps used only for explicit readiness
4
6
  verification. Ready stays visible while Show loader starts a 12-second loader;
5
7
  completion hides the loader and changes its status to Done. No permissions,
@@ -0,0 +1,59 @@
1
+ import assert from 'node:assert/strict'
2
+
3
+ // Minimal local protobuf reader for format/content evidence, not trace analysis.
4
+ // Field numbers: google/perfetto protos/perfetto/trace/{trace,trace_packet}.proto
5
+ // and trace/ftrace/{ftrace_event_bundle,ftrace_event}.proto. Unknown fields skipped.
6
+ export function protobufFields(bytes: Buffer): Array<{ field: number; wire: number; value: Buffer | bigint }> {
7
+ let offset = 0
8
+ const result: Array<{ field: number; wire: number; value: Buffer | bigint }> = []
9
+ function varint() {
10
+ let value = 0n
11
+ for (let shift = 0n; shift < 70n; shift += 7n) {
12
+ assert.ok(offset < bytes.length, 'Truncated protobuf varint')
13
+ const byte = bytes[offset++]
14
+ value |= BigInt(byte & 127) << shift
15
+ if (!(byte & 128)) return value
16
+ }
17
+ throw new Error('Oversized protobuf varint')
18
+ }
19
+ while (offset < bytes.length) {
20
+ const key = Number(varint()), field = Math.floor(key / 8), wire = key % 8
21
+ assert.ok(field > 0)
22
+ let value: Buffer | bigint
23
+ if (wire === 0) value = varint()
24
+ else {
25
+ const length = wire === 2 ? Number(varint()) : wire === 1 ? 8 : wire === 5 ? 4 : -1
26
+ assert.ok(Number.isSafeInteger(length) && length >= 0 && length <= bytes.length - offset, 'Invalid protobuf length/wire')
27
+ value = bytes.subarray(offset, offset + length); offset += length
28
+ }
29
+ result.push({ field, wire, value })
30
+ }
31
+ return result
32
+ }
33
+ export function inspectPerfetto(bytes: Buffer, duration: number) {
34
+ const root = protobufFields(bytes)
35
+ assert.ok(root.length > 0)
36
+ assert.ok(root.every(f => f.field === 1 && f.wire === 2), 'Trace.packet must be repeated length-delimited messages')
37
+ const packets = root.map(f => protobufFields(f.value as Buffer))
38
+ const config = packets.flat().find(f => f.field === 33 && f.wire === 2)
39
+ assert.ok(config, 'Missing TracePacket.trace_config')
40
+ assert.equal(protobufFields(config.value as Buffer).find(f => f.field === 3)?.value, BigInt(duration))
41
+ const bundles = packets.flat().filter(f => f.field === 1 && f.wire === 2).map(f => protobufFields(f.value as Buffer))
42
+ let events = 0, compactBundles = 0
43
+ for (const bundle of bundles) {
44
+ for (const f of bundle) {
45
+ if (f.field === 2 && f.wire === 2) {
46
+ const event = protobufFields(f.value as Buffer)
47
+ assert.ok(event.some(e => e.field === 1 && e.wire === 0), 'FtraceEvent timestamp missing')
48
+ events++
49
+ }
50
+ if (f.field === 4 && f.wire === 2) {
51
+ assert.ok(protobufFields(f.value as Buffer).some(e => [1, 7].includes(e.field) && e.wire === 2))
52
+ compactBundles++
53
+ }
54
+ }
55
+ }
56
+ assert.ok(events + compactBundles > 0, 'No captured kernel/scheduler events')
57
+ return { packets: packets.length, ftrace_bundles: bundles.length, events, compact_sched_bundles: compactBundles,
58
+ validation: 'local protobuf structure, trace config duration and timestamped ftrace/compact scheduler content; not full Perfetto analysis' }
59
+ }
@@ -0,0 +1,43 @@
1
+ import assert from 'node:assert/strict'
2
+ import { deviceForwards, reportAfterCleanup } from '../../device/automated/debugger/coroutine-lifecycle.js'
3
+
4
+ const baseline = deviceForwards('emulator-5554 tcp:2 jdwp:20\nemulator-5554 tcp:1 jdwp:10\nother-device tcp:3 jdwp:30', 'emulator-5554')
5
+ assert.deepEqual(deviceForwards('other-device tcp:4 jdwp:40\nemulator-5554 tcp:1 jdwp:10\nemulator-5554 tcp:2 jdwp:20\n', 'emulator-5554'), baseline)
6
+ assert.notDeepEqual(deviceForwards('emulator-5554 tcp:1 jdwp:10', 'emulator-5554'), baseline)
7
+ assert.deepEqual(deviceForwards('emulator-55540 tcp:1 jdwp:10', 'emulator-5554'), [])
8
+
9
+ const order: string[] = []
10
+ await reportAfterCleanup(async () => { order.push('work'); return 42 }, async () => { order.push('cleanup') }, (status, result) => {
11
+ order.push(status)
12
+ assert.equal(result, 42)
13
+ })
14
+ assert.deepEqual(order, ['work', 'cleanup', 'PASS'])
15
+
16
+ // Reproduce the review bug: cleanup rejects after work succeeds or fails.
17
+ // The runner must reject and emit only FAIL in both cases.
18
+ for (const workFails of [false, true]) {
19
+ const records: string[] = []
20
+ const cleanupError = new Error('cleanup')
21
+ const journeyError = new Error('journey')
22
+ await assert.rejects(reportAfterCleanup(async () => {
23
+ if (workFails) throw journeyError
24
+ return 42
25
+ }, async () => { throw cleanupError }, (status, evidence, error) => {
26
+ records.push(status)
27
+ assert.equal(evidence, undefined)
28
+ if (workFails) {
29
+ assert.ok(error instanceof AggregateError)
30
+ assert.deepEqual(error.errors, [journeyError, cleanupError])
31
+ } else assert.equal(error, cleanupError)
32
+ }), error => workFails ? error instanceof AggregateError : error === cleanupError)
33
+ assert.deepEqual(records, ['FAIL'])
34
+ }
35
+ let cleaned = false
36
+ const workError = new Error('journey')
37
+ await assert.rejects(reportAfterCleanup(async () => { throw workError }, async () => { cleaned = true }, (status, evidence, error) => {
38
+ assert.equal(cleaned, true)
39
+ assert.equal(status, 'FAIL')
40
+ assert.equal(evidence, undefined)
41
+ assert.equal(error, workError)
42
+ }), error => error === workError)
43
+ console.log('PASS coroutine assessment: cleanup precedes PASS; work/cleanup failures report FAIL; forwards scoped and sorted')
@@ -0,0 +1,67 @@
1
+ import assert from 'node:assert/strict'
2
+ import { DebuggerSessions } from '../../../src/debugger/sessions.js'
3
+ import type { DebugNative, DebugTarget } from '../../../src/debugger/android.js'
4
+ import { DebuggerError, type WorkerEvent } from '../../../src/debugger/worker.js'
5
+ import { diagnostic } from '../../../src/debugger/diagnostics.js'
6
+ const target: DebugTarget = { app_id: 'test.app', process_name: 'test.app', pid: 1, uid: 2, start_identity: '3' }
7
+ let removed = 0
8
+ let closed = 0
9
+ let cleanupFails = false
10
+ let mode = ''
11
+ const callbacks = new Map<string, (event: WorkerEvent) => void>()
12
+ const native: DebugNative = {
13
+ candidates: async () => [target], identity: async () => target, forward: async () => 12345,
14
+ removeForward: async () => { if (cleanupFails) throw new Error('private'); removed++ }
15
+ }
16
+ const sessions = new DebuggerSessions(native, async emit => {
17
+ callbacks.set(mode, emit)
18
+ return {
19
+ call: async command => {
20
+ if (command === 'attach' && mode === 'attach') throw new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('attach', 'connection_refused'))
21
+ if (command === 'step') {
22
+ emit({ event: 'worker_failed', diagnostic: diagnostic('ipc', 'invalid_message') })
23
+ throw new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('ipc', 'invalid_message'))
24
+ }
25
+ return {}
26
+ },
27
+ close: async () => { closed++ }
28
+ }
29
+ }, async () => ({ java: 'unused', directory: 'unused' }))
30
+ try {
31
+ mode = 'attach'
32
+ const failed = await sessions.attach('one', 'test.app')
33
+ assert.equal(failed.error.code, 'DEBUGGER_WORKER_FAILED')
34
+ assert.deepEqual(failed.error.diagnostic, diagnostic('attach', 'connection_refused'))
35
+ assert.equal(failed.cleanup.status, 'complete')
36
+ assert.equal(closed, 1); assert.equal(removed, 1)
37
+ mode = 'first'
38
+ const first = await sessions.attach('one', 'test.app')
39
+ mode = 'second'
40
+ const second = await sessions.attach('two', 'test.app')
41
+ callbacks.get('first')!({ event: 'stopped', stop_id: 'stop' })
42
+ await assert.rejects(sessions.command(first.debug_session_id, 'step', { stopId: 'stop' }), /DEBUGGER_WORKER_FAILED/)
43
+ const terminal = await sessions.command(first.debug_session_id, 'state')
44
+ assert.deepEqual(terminal.error.diagnostic, diagnostic('ipc', 'invalid_message'), 'event/rejection race retains primary evidence after cleanup')
45
+ assert.equal(terminal.cleanup.status, 'complete')
46
+ await assert.rejects(sessions.command(first.debug_session_id, 'stack', {}), (e: unknown) => e instanceof DebuggerError && e.code === 'DEBUGGER_DISCONNECTED' && e.diagnostic?.reason === 'invalid_message')
47
+ assert.equal((await sessions.command(second.debug_session_id, 'state')).success, true, 'other device is isolated')
48
+ const aborted = new AbortController(); aborted.abort()
49
+ await assert.rejects(sessions.command(second.debug_session_id, 'events', { signal: aborted.signal }), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'command' && e.diagnostic.reason === 'cancelled')
50
+ cleanupFails = true
51
+ const incomplete = await sessions.command(second.debug_session_id, 'detach')
52
+ assert.equal(incomplete.error.code, 'DEBUGGER_CLEANUP_FAILED')
53
+ assert.deepEqual(incomplete.error.diagnostic, diagnostic('cleanup', 'operation_failed'))
54
+ cleanupFails = false
55
+ assert.equal((await sessions.command(second.debug_session_id, 'detach')).cleanup.status, 'complete')
56
+ mode = 'shutdown'
57
+ const shutdown = await sessions.attach('three', 'test.app')
58
+ await sessions.shutdown()
59
+ assert.equal(sessions.summary(shutdown.debug_session_id).cleanup.status, 'complete')
60
+ } finally { await sessions.shutdown() }
61
+ const badProbes = new DebuggerSessions({ ...native, candidates: async () => { throw new Error('private-token') } }, undefined,
62
+ async () => { throw new DebuggerError('DEBUGGER_UNSUPPORTED', diagnostic('compilation', 'operation_failed')) })
63
+ const probe = await badProbes.capabilities('one', 'test.app')
64
+ assert.equal(probe.success, true); assert.equal(probe.supported, false)
65
+ assert.deepEqual(probe.diagnostics, [diagnostic('compilation', 'operation_failed'), diagnostic('target_resolution', 'unknown')])
66
+ assert.ok(!JSON.stringify(probe).includes('private-token'))
67
+ console.log('PASS diagnostic session retention/races, cleanup retry, isolation, shutdown and ordered capability diagnostics')