mobile-debug-mcp 0.34.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 (432) 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 +4 -4
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +156 -129
  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 +16 -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 +27 -2
  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 +2 -1
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/protocol.js +33 -0
  30. package/dist/debugger/sessions.js +104 -62
  31. package/dist/debugger/worker.js +53 -42
  32. package/dist/interact/adjustment/adjustment-context.js +41 -0
  33. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  34. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  35. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  36. package/dist/interact/adjustment/adjustment-result.js +19 -0
  37. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  38. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  39. package/dist/interact/adjustment/adjustment.js +52 -0
  40. package/dist/interact/adjustment/control-geometry.js +87 -0
  41. package/dist/interact/adjustment/control-values.js +136 -0
  42. package/dist/interact/adjustment/slider-labels.js +84 -0
  43. package/dist/interact/assertions/screen-assertion.js +37 -0
  44. package/dist/interact/assertions/state-assertion-context.js +35 -0
  45. package/dist/interact/assertions/state-assertion.js +76 -0
  46. package/dist/interact/assertions/state-comparison.js +141 -0
  47. package/dist/interact/assertions/visibility-assertion.js +34 -0
  48. package/dist/interact/conditional-action.js +12 -7
  49. package/dist/interact/index.js +65 -3090
  50. package/dist/interact/input-service.js +1 -0
  51. package/dist/interact/interaction-trace.js +21 -0
  52. package/dist/interact/model.js +1 -0
  53. package/dist/interact/observation.js +1 -0
  54. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  55. package/dist/interact/targeting/candidate-result.js +23 -0
  56. package/dist/interact/targeting/candidate-scoring.js +50 -0
  57. package/dist/interact/targeting/element-helpers.js +216 -0
  58. package/dist/interact/targeting/find-element.js +65 -0
  59. package/dist/interact/targeting/nearby-control.js +56 -0
  60. package/dist/interact/targeting/resolved-elements.js +21 -0
  61. package/dist/interact/targeting/tap-target.js +17 -0
  62. package/dist/interact/targeting/target-scope.js +104 -0
  63. package/dist/interact/waits/legacy-log-wait.js +38 -0
  64. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  65. package/dist/interact/waits/legacy-wait-state.js +34 -0
  66. package/dist/interact/waits/legacy-wait.js +48 -0
  67. package/dist/interact/waits/screen-wait.js +49 -0
  68. package/dist/interact/waits/ui-change-helpers.js +157 -0
  69. package/dist/interact/waits/ui-change-state.js +18 -0
  70. package/dist/interact/waits/ui-change-wait.js +77 -0
  71. package/dist/interact/waits/ui-wait-condition.js +56 -0
  72. package/dist/interact/waits/ui-wait-state.js +38 -0
  73. package/dist/interact/waits/ui-wait.js +106 -0
  74. package/dist/manage/android.js +55 -51
  75. package/dist/manage/index.js +38 -75
  76. package/dist/manage/ios/build-types.js +1 -0
  77. package/dist/manage/ios/build.js +210 -0
  78. package/dist/manage/ios/project-discovery.js +29 -0
  79. package/dist/manage/ios/scheme.js +18 -0
  80. package/dist/manage/ios.js +42 -256
  81. package/dist/observe/android/context.js +1 -0
  82. package/dist/observe/android/current-screen.js +55 -0
  83. package/dist/observe/android/fingerprint.js +13 -0
  84. package/dist/observe/android/log-stream.js +132 -0
  85. package/dist/observe/android/logs.js +107 -0
  86. package/dist/observe/android/screenshot.js +70 -0
  87. package/dist/observe/android/ui-tree.js +107 -0
  88. package/dist/observe/android.js +16 -520
  89. package/dist/observe/evidence.js +114 -75
  90. package/dist/observe/index.js +16 -263
  91. package/dist/observe/ios/context.js +16 -0
  92. package/dist/observe/ios/fingerprint.js +12 -0
  93. package/dist/observe/ios/log-stream.js +105 -0
  94. package/dist/observe/ios/logs.js +168 -0
  95. package/dist/observe/ios/screenshot.js +22 -0
  96. package/dist/observe/ios/ui-elements.js +241 -0
  97. package/dist/observe/ios/ui-tree.js +137 -0
  98. package/dist/observe/ios.js +17 -715
  99. package/dist/observe/logs/model.js +1 -0
  100. package/dist/{network → observe/network}/index.js +2 -2
  101. package/dist/observe/readiness-service.js +4 -2
  102. package/dist/observe/readiness.js +29 -10
  103. package/dist/observe/screenshot-encoding.js +41 -0
  104. package/dist/observe/snapshot-collection.js +79 -0
  105. package/dist/observe/snapshot-logs.js +30 -0
  106. package/dist/observe/snapshot-metadata.js +43 -31
  107. package/dist/observe/snapshot-semantic.js +115 -0
  108. package/dist/observe/system-trace.js +22 -13
  109. package/dist/runtime/action-results.js +270 -0
  110. package/dist/runtime/clock.js +4 -0
  111. package/dist/server/common.js +1 -256
  112. package/dist/server/definitions/assert.js +162 -0
  113. package/dist/server/definitions/diagnostic.js +118 -0
  114. package/dist/server/definitions/input.js +274 -0
  115. package/dist/server/definitions/manage.js +194 -0
  116. package/dist/server/definitions/observe.js +149 -0
  117. package/dist/server/definitions/target.js +138 -0
  118. package/dist/server/definitions/wait.js +105 -0
  119. package/dist/server/handlers/action-execution.js +163 -0
  120. package/dist/server/handlers/correlation.js +23 -0
  121. package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
  122. package/dist/server/handlers/debugger-response.js +9 -0
  123. package/dist/server/handlers/diagnostic.js +69 -0
  124. package/dist/server/handlers/handler-types.js +1 -0
  125. package/dist/server/handlers/input.js +154 -0
  126. package/dist/server/handlers/interact.js +113 -0
  127. package/dist/server/handlers/journey-assertions.js +35 -0
  128. package/dist/server/handlers/journey.js +147 -0
  129. package/dist/server/handlers/manage.js +119 -0
  130. package/dist/server/handlers/observe.js +95 -0
  131. package/dist/server/handlers/payload.js +4 -0
  132. package/dist/server/handlers/tool-handlers.js +160 -0
  133. package/dist/server/schemas/condition-schema.js +2 -0
  134. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
  135. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  136. package/dist/server/tool-definitions.js +58 -1135
  137. package/dist/server-core.js +2 -2
  138. package/dist/system/android-status.js +101 -0
  139. package/dist/system/android.js +37 -27
  140. package/dist/system/index.js +13 -439
  141. package/dist/system/ios-status.js +132 -0
  142. package/dist/system/ios.js +11 -8
  143. package/dist/system/model.js +1 -0
  144. package/dist/system/probes.js +105 -0
  145. package/dist/system/status-summary.js +43 -0
  146. package/dist/system/unsupported-status.js +52 -0
  147. package/dist/utils/android/commands.js +89 -0
  148. package/dist/utils/android/gradle.js +139 -0
  149. package/dist/utils/android/logs.js +103 -0
  150. package/dist/utils/android/ui-tree.js +233 -0
  151. package/dist/utils/android/utils.js +6 -564
  152. package/dist/utils/cli/idb/idb-helper.js +4 -6
  153. package/dist/utils/cli/idb/install-idb.js +13 -9
  154. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  155. package/dist/utils/ios/utils.js +45 -46
  156. package/dist/utils/java.js +65 -49
  157. package/dist/utils/resolve-device.js +4 -2
  158. package/dist/utils/ui/index.js +26 -26
  159. package/docs/CHANGELOG.md +44 -0
  160. package/docs/README.md +38 -0
  161. package/docs/benchmarks/README.md +18 -0
  162. package/docs/benchmarks/interaction-latency/README.md +14 -0
  163. package/docs/benchmarks/interaction-latency/final.md +4 -1
  164. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  165. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  166. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  167. package/docs/code-structure.md +118 -0
  168. package/docs/specs/001-state-verification/spec.md +16 -14
  169. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  170. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  171. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  172. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  173. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  174. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  175. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  176. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  177. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  178. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  179. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  180. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  181. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  182. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  183. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  184. package/docs/specs/016-environment-tools/spec.md +20 -20
  185. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  186. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  187. package/docs/specs/020-conditional-actions/intent.md +5 -3
  188. package/docs/specs/020-conditional-actions/plan.md +8 -9
  189. package/docs/specs/020-conditional-actions/spec.md +4 -2
  190. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  191. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  192. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  193. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  194. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  195. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  196. package/docs/specs/023-android-system-traces/intent.md +3 -2
  197. package/docs/specs/023-android-system-traces/plan.md +5 -7
  198. package/docs/specs/023-android-system-traces/spec.md +3 -1
  199. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  200. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  201. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  202. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  203. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  204. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  205. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  206. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  207. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  208. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  209. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  210. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  211. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  212. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  213. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  214. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  215. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  216. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  217. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  218. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  219. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  220. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  221. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  222. package/docs/tools/TOOLS.md +31 -18
  223. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  224. package/docs/tools/debugger.md +76 -0
  225. package/docs/tools/interact.md +42 -20
  226. package/docs/tools/manage.md +25 -44
  227. package/docs/tools/observe.md +55 -23
  228. package/docs/tools/system-traces.md +9 -1
  229. package/docs/tools/system.md +12 -5
  230. package/package.json +11 -8
  231. package/scripts/architecture-graph.mjs +69 -0
  232. package/scripts/check-architecture.mjs +47 -0
  233. package/scripts/code-quality-baseline.json +1 -0
  234. package/scripts/code-quality.mjs +161 -0
  235. package/src/contracts/execution-controls.ts +26 -0
  236. package/src/contracts/selector.ts +17 -0
  237. package/src/contracts/target.ts +19 -0
  238. package/src/contracts/ui.ts +94 -0
  239. package/src/debugger/android.ts +2 -1
  240. package/src/debugger/diagnostics.ts +43 -0
  241. package/src/debugger/guard.ts +6 -1
  242. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  243. package/src/debugger/protocol.ts +37 -0
  244. package/src/debugger/sessions.ts +314 -155
  245. package/src/debugger/worker.ts +51 -38
  246. package/src/interact/adjustment/adjustment-context.ts +83 -0
  247. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  248. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  249. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  250. package/src/interact/adjustment/adjustment-result.ts +34 -0
  251. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  252. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  253. package/src/interact/adjustment/adjustment.ts +49 -0
  254. package/src/interact/adjustment/control-geometry.ts +120 -0
  255. package/src/interact/adjustment/control-values.ts +141 -0
  256. package/src/interact/adjustment/slider-labels.ts +84 -0
  257. package/src/interact/assertions/screen-assertion.ts +39 -0
  258. package/src/interact/assertions/state-assertion-context.ts +48 -0
  259. package/src/interact/assertions/state-assertion.ts +64 -0
  260. package/src/interact/assertions/state-comparison.ts +168 -0
  261. package/src/interact/assertions/visibility-assertion.ts +41 -0
  262. package/src/interact/conditional-action.ts +18 -12
  263. package/src/interact/index.ts +76 -3492
  264. package/src/interact/input-service.ts +11 -0
  265. package/src/interact/interaction-trace.ts +36 -0
  266. package/src/interact/model.ts +109 -0
  267. package/src/interact/observation.ts +7 -0
  268. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  269. package/src/interact/targeting/candidate-result.ts +24 -0
  270. package/src/interact/targeting/candidate-scoring.ts +42 -0
  271. package/src/interact/targeting/element-helpers.ts +243 -0
  272. package/src/interact/targeting/find-element.ts +47 -0
  273. package/src/interact/targeting/nearby-control.ts +46 -0
  274. package/src/interact/targeting/resolved-elements.ts +20 -0
  275. package/src/interact/targeting/tap-target.ts +22 -0
  276. package/src/interact/targeting/target-scope.ts +121 -0
  277. package/src/interact/waits/legacy-log-wait.ts +30 -0
  278. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  279. package/src/interact/waits/legacy-wait-state.ts +48 -0
  280. package/src/interact/waits/legacy-wait.ts +34 -0
  281. package/src/interact/waits/screen-wait.ts +37 -0
  282. package/src/interact/waits/ui-change-helpers.ts +184 -0
  283. package/src/interact/waits/ui-change-state.ts +26 -0
  284. package/src/interact/waits/ui-change-wait.ts +59 -0
  285. package/src/interact/waits/ui-wait-condition.ts +52 -0
  286. package/src/interact/waits/ui-wait-state.ts +55 -0
  287. package/src/interact/waits/ui-wait.ts +84 -0
  288. package/src/manage/android.ts +56 -50
  289. package/src/manage/index.ts +63 -93
  290. package/src/manage/ios/build-types.ts +14 -0
  291. package/src/manage/ios/build.ts +230 -0
  292. package/src/manage/ios/project-discovery.ts +26 -0
  293. package/src/manage/ios/scheme.ts +16 -0
  294. package/src/manage/ios.ts +65 -298
  295. package/src/observe/android/context.ts +1 -0
  296. package/src/observe/android/current-screen.ts +63 -0
  297. package/src/observe/android/fingerprint.ts +13 -0
  298. package/src/observe/android/log-stream.ts +127 -0
  299. package/src/observe/android/logs.ts +110 -0
  300. package/src/observe/android/screenshot.ts +79 -0
  301. package/src/observe/android/ui-tree.ts +113 -0
  302. package/src/observe/android.ts +17 -531
  303. package/src/observe/evidence.ts +122 -70
  304. package/src/observe/index.ts +24 -286
  305. package/src/observe/ios/context.ts +22 -0
  306. package/src/observe/ios/fingerprint.ts +12 -0
  307. package/src/observe/ios/log-stream.ts +103 -0
  308. package/src/observe/ios/logs.ts +179 -0
  309. package/src/observe/ios/screenshot.ts +26 -0
  310. package/src/observe/ios/ui-elements.ts +266 -0
  311. package/src/observe/ios/ui-tree.ts +143 -0
  312. package/src/observe/ios.ts +18 -749
  313. package/src/observe/logs/model.ts +11 -0
  314. package/src/{network → observe/network}/index.ts +2 -2
  315. package/src/observe/readiness-service.ts +5 -3
  316. package/src/observe/readiness.ts +39 -18
  317. package/src/observe/screenshot-encoding.ts +32 -0
  318. package/src/observe/snapshot-collection.ts +69 -0
  319. package/src/observe/snapshot-logs.ts +26 -0
  320. package/src/observe/snapshot-metadata.ts +51 -31
  321. package/src/observe/snapshot-semantic.ts +153 -0
  322. package/src/observe/system-trace.ts +24 -15
  323. package/src/runtime/action-results.ts +376 -0
  324. package/src/runtime/clock.ts +9 -0
  325. package/src/server/common.ts +9 -359
  326. package/src/server/definitions/assert.ts +164 -0
  327. package/src/server/definitions/diagnostic.ts +120 -0
  328. package/src/server/definitions/input.ts +276 -0
  329. package/src/server/definitions/manage.ts +196 -0
  330. package/src/server/definitions/observe.ts +151 -0
  331. package/src/server/definitions/target.ts +140 -0
  332. package/src/server/definitions/wait.ts +107 -0
  333. package/src/server/handlers/action-execution.ts +164 -0
  334. package/src/server/handlers/correlation.ts +20 -0
  335. package/src/server/handlers/debugger-handlers.ts +39 -0
  336. package/src/server/handlers/debugger-response.ts +7 -0
  337. package/src/server/handlers/diagnostic.ts +79 -0
  338. package/src/server/handlers/handler-types.ts +7 -0
  339. package/src/server/handlers/input.ts +173 -0
  340. package/src/server/handlers/interact.ts +135 -0
  341. package/src/server/handlers/journey-assertions.ts +39 -0
  342. package/src/server/handlers/journey.ts +132 -0
  343. package/src/server/handlers/manage.ts +141 -0
  344. package/src/server/handlers/observe.ts +116 -0
  345. package/src/server/handlers/payload.ts +36 -0
  346. package/src/server/handlers/tool-handlers.ts +159 -0
  347. package/src/server/schemas/condition-schema.ts +2 -0
  348. package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
  349. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  350. package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
  351. package/src/server/tool-definitions.ts +67 -1143
  352. package/src/server-core.ts +2 -2
  353. package/src/server.ts +1 -1
  354. package/src/system/android-status.ts +124 -0
  355. package/src/system/android.ts +33 -21
  356. package/src/system/index.ts +21 -525
  357. package/src/system/ios-status.ts +147 -0
  358. package/src/system/ios.ts +14 -10
  359. package/src/system/model.ts +41 -0
  360. package/src/system/probes.ts +114 -0
  361. package/src/system/status-summary.ts +47 -0
  362. package/src/system/unsupported-status.ts +55 -0
  363. package/src/types.ts +16 -106
  364. package/src/utils/android/commands.ts +81 -0
  365. package/src/utils/android/gradle.ts +132 -0
  366. package/src/utils/android/logs.ts +103 -0
  367. package/src/utils/android/ui-tree.ts +239 -0
  368. package/src/utils/android/utils.ts +16 -556
  369. package/src/utils/cli/idb/check-idb.ts +4 -4
  370. package/src/utils/cli/idb/idb-helper.ts +6 -8
  371. package/src/utils/cli/idb/install-idb.ts +15 -9
  372. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  373. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  374. package/src/utils/ios/utils.ts +57 -61
  375. package/src/utils/java.ts +62 -43
  376. package/src/utils/resolve-device.ts +9 -6
  377. package/src/utils/ui/index.ts +28 -28
  378. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  379. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  380. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  381. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  382. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  383. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  384. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  385. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  386. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  387. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  388. package/test/fixtures/coroutine-app/README.md +48 -0
  389. package/test/fixtures/coroutine-app/build.ts +35 -0
  390. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  391. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  392. package/test/fixtures/debugger-app/README.md +52 -0
  393. package/test/fixtures/debugger-app/build.ts +25 -15
  394. package/test/fixtures/latency-journey-app/README.md +2 -0
  395. package/test/fixtures/readiness-app/README.md +2 -0
  396. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  397. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  398. package/test/unit/debugger/diagnostics.test.ts +80 -0
  399. package/test/unit/debugger/guards.test.ts +4 -0
  400. package/test/unit/debugger/preparation.test.ts +32 -0
  401. package/test/unit/debugger/protocol.test.ts +15 -0
  402. package/test/unit/debugger/sessions.test.ts +1 -1
  403. package/test/unit/interact/adjustment_service.test.ts +42 -0
  404. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  405. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  406. package/test/unit/interact/element_helpers.test.ts +19 -0
  407. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  408. package/test/unit/interact/state_comparison.test.ts +20 -0
  409. package/test/unit/interact/target_scope.test.ts +35 -0
  410. package/test/unit/network/get_network_activity.test.ts +1 -1
  411. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  412. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  413. package/test/unit/quality/architecture.test.ts +60 -0
  414. package/test/unit/quality/code-quality.test.ts +58 -0
  415. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  416. package/test/unit/server/conditional_actions.test.ts +1 -1
  417. package/test/unit/server/conditional_schema.test.ts +2 -2
  418. package/test/unit/server/debugger-response.test.ts +63 -0
  419. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  420. package/dist/server/tool-handlers.js +0 -1000
  421. package/docs/ROADMAP.md +0 -646
  422. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  423. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  424. package/docs/specs/baseline-spec-v0.md +0 -312
  425. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  426. package/src/server/debugger-handlers.ts +0 -29
  427. package/src/server/tool-handlers.ts +0 -1008
  428. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  429. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  430. /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
  431. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  432. /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
+ }
@@ -23,8 +23,29 @@ public class MainActivity extends Activity {
23
23
  try { Class.forName("dev.mobiledebugmcp.debugfixture.NoLocals").getDeclaredMethod("work").invoke(null); }
24
24
  catch (Exception e) { status.setText("Metadata failed"); }
25
25
  }); layout.addView(noLocals);
26
+ addLevelControl(layout);
26
27
  setContentView(layout);
27
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
+ }
28
49
  void calculate(int seed) {
29
50
  int known = seed + 35;
30
51
  String longText = new String(new char[300]).replace('\0', 'x');
@@ -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
+ }
@@ -1,5 +1,7 @@
1
1
  # Isolated Android debugger fixture
2
2
 
3
+ [Contributing](../../../CONTRIBUTING.md) · [Documentation](../../../docs/README.md)
4
+
3
5
  Package `dev.mobiledebugmcp.debugfixture`, built with `android:debuggable=true`
4
6
  and Java source/line/local metadata. Calculate exposes `known=42`, an increment
5
7
  call for into/over/out stepping, and a visible `Result 43` after resume. Late class
@@ -11,3 +13,53 @@ Build helper uses SDK platform/build-tools 36, the existing direct javac/D8/aapt
11
13
  pattern, ephemeral signing material and temporary output. It does not modify other
12
14
  apps or display settings. The automated journey installs/removes only this fixture
13
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.
@@ -1,10 +1,11 @@
1
1
  import { execFileSync } from 'node:child_process'
2
- import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
4
  import path from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
6
  export const debuggerAppId = 'dev.mobiledebugmcp.debugfixture'
7
- export function buildDebuggerFixture() {
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')
8
9
  const source = path.dirname(fileURLToPath(import.meta.url))
9
10
  const directory = mkdtempSync(path.join(tmpdir(), 'mcp-debugger-fixture-'))
10
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)
@@ -13,17 +14,26 @@ export function buildDebuggerFixture() {
13
14
  const tools = path.join(sdk, 'build-tools/36.0.0')
14
15
  const androidJar = path.join(sdk, 'platforms/android-36/android.jar')
15
16
  const jdk = (name: string) => java ? path.join(java, 'bin', name) : name
16
- mkdirSync(path.join(directory, 'classes'))
17
- 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>`)
18
- run(jdk('javac'), ['-g', '--release', '8', '-classpath', androidJar, '-d', 'classes', path.join(source, 'MainActivity.java')])
19
- run(jdk('javac'), ['-g:source,lines', '--release', '8', '-classpath', androidJar, '-d', 'classes', path.join(source, 'NoLocals.java')])
20
- run(jdk('jar'), ['cf', 'classes.jar', '-C', 'classes', '.'])
21
- run(path.join(tools, 'd8'), ['--lib', androidJar, '--output', directory, 'classes.jar'])
22
- run(path.join(tools, 'aapt2'), ['link', '-I', androidJar, '--manifest', 'AndroidManifest.xml', '-o', 'unsigned.apk'])
23
- run('zip', ['-q', 'unsigned.apk', 'classes.dex'])
24
- run(path.join(tools, 'zipalign'), ['-f', '4', 'unsigned.apk', 'aligned.apk'])
25
- run(jdk('keytool'), ['-genkeypair', '-keystore', 'fixture.jks', '-storepass', 'android', '-keypass', 'android', '-alias', 'fixture', '-dname', 'CN=Readiness Fixture', '-keyalg', 'RSA', '-validity', '365'])
26
- const artifact = path.join(directory, 'Debugger.apk')
27
- run(path.join(tools, 'apksigner'), ['sign', '--ks', 'fixture.jks', '--ks-pass', 'pass:android', '--out', artifact, 'aligned.apk'])
28
- return { artifact, directory }
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
+ }
29
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,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')
@@ -0,0 +1,80 @@
1
+ import assert from 'node:assert/strict'
2
+ import { mkdtemp, writeFile, rm } from 'node:fs/promises'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { diagnostic, stages, reasons, classify, parseDiagnostic } from '../../../src/debugger/diagnostics.js'
6
+ import { DebuggerError, JdiWorker, withDebuggerBudget, type WorkerEvent } from '../../../src/debugger/worker.js'
7
+ import { parseWorkerMessage } from '../../../src/debugger/protocol.js'
8
+ import { handleDebugger } from '../../../src/server/handlers/debugger-handlers.js'
9
+
10
+ const secret = 'PRIVATE_LOCALS_TOKEN_/private/path'
11
+ for (const stage of stages) for (const reason of reasons) {
12
+ const value = diagnostic(stage, reason)
13
+ assert.ok(Buffer.byteLength(JSON.stringify(value)) <= 512)
14
+ assert.ok(value.recovery.length <= 240 && /^[\x00-\x7f]*$/.test(value.recovery))
15
+ assert.deepEqual(Object.keys(value), ['stage', 'reason', 'recovery'])
16
+ }
17
+ assert.deepEqual(parseDiagnostic({ stage: 'attach', reason: 'connection_refused', recovery: secret, raw: secret }), diagnostic('attach', 'connection_refused'))
18
+ assert.equal(classify(new Error(secret), 'attach', 'DEBUGGER_ATTACH_FAILED').diagnostic?.reason, 'unknown')
19
+ assert.equal(classify(new DebuggerError('DEBUGGER_STALE_REFERENCE'), 'command', 'DEBUGGER_WORKER_FAILED').diagnostic, undefined)
20
+ for (const hostile of [secret, { stage: secret, reason: 'unknown' }, { stage: 'attach', reason: secret }]) {
21
+ assert.throws(() => parseWorkerMessage({ id: 1, error: { code: 'DEBUGGER_WORKER_FAILED', diagnostic: hostile } }), /INVALID_WORKER_DIAGNOSTIC/)
22
+ }
23
+ assert.throws(() => parseWorkerMessage({ id: 1, error: { code: secret } }), /INVALID_WORKER_ERROR/)
24
+ const cancelled = new AbortController(); cancelled.abort()
25
+ const response = await handleDebugger('get_debugger_state', { debugSessionId: 'absent' }, cancelled.signal)
26
+ assert.equal(response.isError, true)
27
+ const payload = JSON.parse(response.content[0].text)
28
+ assert.equal(payload.error.code, 'DEBUGGER_CANCELLED')
29
+ assert.deepEqual(payload.error.diagnostic, diagnostic('unknown', 'cancelled'))
30
+ assert.equal((await handleDebugger('get_debugger_state', {})).isError, true)
31
+
32
+ const directory = await mkdtemp(path.join(os.tmpdir(), 'debugger-diagnostics-test-'))
33
+ let index = 0
34
+ async function worker(body: string, onEvent: (event: WorkerEvent) => void = () => {}) {
35
+ const file = path.join(directory, `worker-${index++}`)
36
+ await writeFile(file, `#!${process.execPath}\n${body}\n`, { mode: 0o700 })
37
+ return new JdiWorker(file, directory, onEvent)
38
+ }
39
+ async function fails(body: string, stage: string, reason: string) {
40
+ const w = await worker(body)
41
+ try {
42
+ await assert.rejects(w.call('attach', {}, 3000), (error: unknown) => {
43
+ assert.ok(error instanceof DebuggerError)
44
+ assert.equal(error.code, 'DEBUGGER_WORKER_FAILED')
45
+ assert.deepEqual(error.diagnostic, diagnostic(stage as typeof stages[number], reason as typeof reasons[number]))
46
+ assert.ok(!JSON.stringify(error).includes(secret))
47
+ return true
48
+ })
49
+ } finally { await w.close() }
50
+ }
51
+ try {
52
+ const missing = new JdiWorker(path.join(directory, 'missing'), directory, () => {})
53
+ try { await assert.rejects(missing.call('attach'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'spawn_failed') } finally { await missing.close() }
54
+ await fails(`process.stderr.write(${JSON.stringify(secret)}); process.exit(7)`, 'worker_start', 'process_exited')
55
+ await fails(`process.stdout.write(${JSON.stringify(secret + '\n')}); setInterval(()=>{},1000)`, 'ipc', 'invalid_message')
56
+ await fails(`process.stdout.write('x'.repeat(1048577)+'\\n'); setInterval(()=>{},1000)`, 'ipc', 'message_limit')
57
+ const attach = await worker(`process.stdin.once('data', data => { const {id}=JSON.parse(data); console.log(JSON.stringify({id,error:{code:'DEBUGGER_WORKER_FAILED',diagnostic:{stage:'attach',reason:'connection_refused',raw:${JSON.stringify(secret)}}}})) })`)
58
+ try { await assert.rejects(attach.call('attach'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'connection_refused') } finally { await attach.close() }
59
+ let ready = () => {}
60
+ const readyPromise = new Promise<void>(resolve => { ready = resolve })
61
+ const brokenPipe = await worker(`require('node:fs').closeSync(0); console.log(JSON.stringify({event:'ready'})); setInterval(()=>{},1000)`, () => ready())
62
+ try {
63
+ await readyPromise
64
+ await assert.rejects(brokenPipe.call('state'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'write_failed')
65
+ } finally { await brokenPipe.close() }
66
+ const afterReply = await worker(`process.stdin.once('data',data=>{const {id}=JSON.parse(data); console.log(JSON.stringify({id,result:{}})); setTimeout(()=>process.exit(7),100)})`)
67
+ try {
68
+ await afterReply.call('state')
69
+ await assert.rejects(afterReply.call('state'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'ipc' && e.diagnostic.reason === 'process_exited')
70
+ } finally { await afterReply.close() }
71
+ const idle = await worker('process.stdin.resume()')
72
+ try {
73
+ await assert.rejects(idle.call('stack', {}, 50), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'command' && e.diagnostic.reason === 'timeout')
74
+ const controller = new AbortController()
75
+ const call = withDebuggerBudget(1000, controller.signal, () => idle.call('attach'))
76
+ controller.abort()
77
+ await assert.rejects(call, (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'attach' && e.diagnostic.reason === 'cancelled')
78
+ } finally { await idle.close() }
79
+ } finally { await rm(directory, { recursive: true, force: true }) }
80
+ console.log('PASS diagnostic bounds/privacy, strict IPC, real worker spawn/exit/malformed/oversize, attach failure, timeout/cancel and MCP wrapping')
@@ -38,6 +38,10 @@ try {
38
38
  assert.equal(journey.steps[1].success, false)
39
39
  assert.equal(journey.steps[1].result.failure_code, 'DEBUGGER_PAUSED')
40
40
  assert.equal(calls, 2, 'no action queued or retried after pause')
41
+ _setDeviceListersForTests({ listAndroidDevices: async () => { throw new Error('resolution unavailable') } })
42
+ const failedResolution = payload(await handleToolCall('tap', { platform: 'android', deviceId: 'fixture', x: 1, y: 1 }))
43
+ assert.equal(failedResolution.error.tool, 'tap')
44
+ assert.match(failedResolution.error.message, /resolution unavailable/, 'resolution errors retain the MCP error envelope')
41
45
  console.log('PASS debugger readiness without UI acquisition, device scoping, dispatch guard, mid-journey pause preservation')
42
46
  } finally {
43
47
  debuggerSessions.paused = originalPause; debuggerSessions.hasActiveSessions = originalActive
@@ -0,0 +1,32 @@
1
+ import assert from 'node:assert/strict'
2
+ import { execFile } from 'node:child_process'
3
+ import { promisify } from 'node:util'
4
+ import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
5
+ import os from 'node:os'
6
+ import path from 'node:path'
7
+ const exec = promisify(execFile)
8
+ const directory = await mkdtemp(path.join(os.tmpdir(), 'debugger-fake-jdk-'))
9
+ const secret = 'SECRET_COMPILER_OUTPUT'
10
+ const run = async () => {
11
+ const code = `import {prepareWorker} from './src/debugger/worker.ts'; try {await prepareWorker(); process.exitCode=1} catch(e) {console.log(JSON.stringify({code:e.code,diagnostic:e.diagnostic}))}`
12
+ const { stdout, stderr } = await exec(process.execPath, ['--import', 'tsx', '--input-type=module', '-e', code], {
13
+ env: { ...process.env, ANDROID_STUDIO_JDK: directory }, timeout: 10000
14
+ })
15
+ assert.ok(!stdout.includes(secret)); assert.ok(!stderr.includes(secret))
16
+ return JSON.parse(stdout)
17
+ }
18
+ try {
19
+ await mkdir(path.join(directory, 'bin'))
20
+ const java = path.join(directory, 'bin', 'java')
21
+ await writeFile(java, `#!${process.execPath}\nconsole.error('openjdk version "17.0.1"');`, { mode: 0o700 })
22
+ await writeFile(path.join(directory, 'bin', 'javac'), `#!${process.execPath}\nconsole.error('${secret}');process.exit(1)`, { mode: 0o700 })
23
+ const compile = await run()
24
+ assert.equal(compile.code, 'DEBUGGER_UNSUPPORTED')
25
+ assert.equal(compile.diagnostic.stage, 'compilation'); assert.equal(compile.diagnostic.reason, 'operation_failed')
26
+ // Discovery succeeds; the actual bounded version subprocess then fails.
27
+ await writeFile(java, `#!${process.execPath}\nconst fs=require('node:fs');const marker=${JSON.stringify(path.join(directory, 'seen'))};if(fs.existsSync(marker)){console.error('${secret}');process.exit(1)}fs.writeFileSync(marker,'');console.error('openjdk version "17.0.1"');`, { mode: 0o700 })
28
+ const prerequisite = await run()
29
+ assert.equal(prerequisite.code, 'DEBUGGER_ATTACH_FAILED')
30
+ assert.equal(prerequisite.diagnostic.stage, 'prerequisite'); assert.equal(prerequisite.diagnostic.reason, 'unavailable')
31
+ } finally { await rm(directory, { recursive: true, force: true }) }
32
+ console.log('PASS isolated fake-JDK compilation/prerequisite classification without compiler output leakage')