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,219 @@
1
+ import { debuggerResponse } from '../../../../src/server/handlers/debugger-response.js'
2
+ import { debuggerContracts } from '../../../../src/server/schemas/debugger-schema.js'
3
+ import assert from 'node:assert/strict'
4
+ import { execFileSync, spawn } from 'node:child_process'
5
+ import { readFile, readdir, rm, writeFile } from 'node:fs/promises'
6
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
7
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
8
+ import { createHash } from 'node:crypto'
9
+ import { buildDebuggerFixture, debuggerAppId } from '../../../fixtures/debugger-app/build.js'
10
+
11
+ // Explicit opt-in: installs/removes only this isolated fixture on the selected device.
12
+ if (!process.env.MCP_DEBUGGER_DEVICE) {
13
+ console.log('UNRUN Android debugger fixture: set MCP_DEBUGGER_DEVICE to an authorized emulator serial')
14
+ } else {
15
+ const deviceId = process.env.MCP_DEBUGGER_DEVICE
16
+ const adb = (...args: string[]) => execFileSync(process.env.ADB_PATH || 'adb', ['-s', deviceId, ...args], { encoding: 'utf8', timeout: 15000 })
17
+ const client = new Client({ name: 'android-debugger-smoke', version: '1.0.0' })
18
+ const transport = new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' })
19
+ const compactMode = process.env.MCP_DEBUGGER_RESPONSE_MODE === 'compact'
20
+ const measurements: { tool: string; debug_bytes: number; compact_bytes: number }[] = []
21
+ const call = async (name: string, args: Record<string, unknown>) => {
22
+ const response = await client.callTool({ name, arguments: { ...args, ...(debuggerContracts[name] && compactMode ? { responseMode: 'compact' } : {}) } }, undefined, { timeout: 45000 })
23
+ const text = (response.content as any[]).find(c => c.type === 'text')!.text!
24
+ const payload = JSON.parse(text)
25
+ if (debuggerContracts[name]) {
26
+ const command = debuggerContracts[name].command
27
+ if (compactMode && !['attach', 'state', 'capabilities'].includes(command) && payload.debug_session_id != null) {
28
+ for (const field of ['device', 'target', 'capabilities']) assert.ok(!Object.hasOwn(payload, field))
29
+ }
30
+ if (!compactMode) {
31
+ const compact = debuggerResponse(command, 'compact', payload)
32
+ measurements.push({ tool: name, debug_bytes: Buffer.byteLength(JSON.stringify(response)), compact_bytes: Buffer.byteLength(JSON.stringify({ ...response, content: [{ type: 'text', text: JSON.stringify(compact, null, 2) }] })) })
33
+ }
34
+ }
35
+ return payload
36
+ }
37
+ const hash = createHash('sha256')
38
+ const fingerprint = async (directory: string): Promise<void> => {
39
+ for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
40
+ const file = directory + '/' + entry.name
41
+ if (entry.isDirectory()) await fingerprint(file)
42
+ else hash.update(file).update(await readFile(file))
43
+ }
44
+ }
45
+ await fingerprint('dist'); await fingerprint('src/debugger/java')
46
+ const buildHash = hash.digest('hex')
47
+ const fixture = buildDebuggerFixture()
48
+ const source = await readFile(new URL('../../../fixtures/debugger-app/MainActivity.java', import.meta.url), 'utf8')
49
+ const line = (marker: string) => source.split('\n').findIndex(l => l.includes(marker)) + 1
50
+ let debugSessionId = ''
51
+ let cursor = 0
52
+ const eventsUntilStop = async () => {
53
+ for (let i = 0; i < 10; i++) {
54
+ const result = await call('wait_for_debugger_event', { debugSessionId, afterSequence: cursor, timeoutMs: 1000 })
55
+ cursor = result.next_cursor
56
+ if (result.events.length) console.log('Debug events', JSON.stringify(result.events))
57
+ const event = result.events.find((e: any) => e.event === 'stopped')
58
+ if (event) return event
59
+ }
60
+ throw new Error('No debugger stop observed')
61
+ }
62
+ const forwardsBefore = adb('forward', '--list').trim()
63
+ try {
64
+ await client.connect(transport)
65
+ const listed = await client.listTools()
66
+ assert.ok(listed.tools.some(tool => tool.name === 'attach_debugger'))
67
+ adb('install', '-r', fixture.artifact)
68
+ adb('shell', 'am', 'start', '-W', '-n', `${debuggerAppId}/.MainActivity`)
69
+ const cap = await call('get_debugger_capabilities', { platform: 'android', deviceId, appId: debuggerAppId })
70
+ assert.equal(cap.supported, true, JSON.stringify(cap))
71
+ const timeout = await call('get_debugger_capabilities', { platform: 'android', deviceId, appId: debuggerAppId, timeoutMs: 100 })
72
+ assert.equal(timeout.error.code, 'DEBUGGER_TIMEOUT')
73
+ const unsupported = await call('get_debugger_capabilities', { platform: 'android', deviceId, appId: 'com.missing.debugfixture' })
74
+ assert.equal(unsupported.supported, false)
75
+ assert.ok(unsupported.reasons.includes('PROCESS_NOT_FOUND'))
76
+ const attached = await call('attach_debugger', { platform: 'android', deviceId, appId: debuggerAppId, timeoutMs: 30000 })
77
+ assert.equal(attached.success, true, JSON.stringify(attached)); debugSessionId = attached.debug_session_id
78
+ const bp = await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.MainActivity`, line: line('BREAK_CALCULATE') })
79
+ assert.equal(bp.result.status, 'verified', JSON.stringify(bp))
80
+ const pending = await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.LateWork`, line: line('BREAK_LATE') })
81
+ assert.equal(pending.result.status, 'pending', JSON.stringify(pending))
82
+ const unavailable = await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.MainActivity`, line: 9999 })
83
+ assert.equal(unavailable.result.status, 'rejected')
84
+ // Fixture button placement is obtained from real UI semantics, not fixed coordinates.
85
+ const tree = await call('get_ui_tree', { platform: 'android', deviceId })
86
+ const button = tree.elements.find((e: any) => /^calculate$/i.test(e.text ?? ''))
87
+ assert.ok(button, 'Calculate button visible in real UI tree: ' + JSON.stringify(tree))
88
+ console.log('Fixture target', JSON.stringify({ bounds: button.bounds, device: tree.device, resolution: tree.resolution }))
89
+ const [left, top, right, bottom] = button.bounds
90
+ // Shell input is asynchronous so suspension cannot block the test's next debugger call.
91
+ await writeFile(fixture.directory + '/fixture.png', execFileSync(process.env.ADB_PATH || 'adb', ['-s', deviceId, 'exec-out', 'screencap', '-p']))
92
+ const tap = spawn(process.env.ADB_PATH || 'adb', ['-s', deviceId, 'shell', 'input', 'tap', String(Math.floor((left + right) / 2)), String(Math.floor((top + bottom) / 2))], { stdio: 'ignore' })
93
+ tap.on('error', error => { throw error })
94
+ const stop = await eventsUntilStop()
95
+ assert.equal(stop.stops[0].location.line, line('BREAK_CALCULATE'))
96
+ const stack = await call('get_debugger_stack', { debugSessionId, stopId: stop.stop_id, threadId: stop.thread_id })
97
+ assert.equal(stack.result.frames[0].location.method, 'calculate')
98
+ const frameId = stack.result.frames[0].frame_id
99
+ const locals = await call('get_debugger_locals', { debugSessionId, stopId: stop.stop_id, frameId })
100
+ assert.equal(locals.result.locals.find((v: any) => v.name === 'known')?.value, '42', JSON.stringify(locals))
101
+ const longText = locals.result.locals.find((v: any) => v.name === 'longText')
102
+ assert.equal(longText.value.length, 256); assert.equal(longText.truncated, true)
103
+ const object = locals.result.locals.find((v: any) => v.name === 'object')
104
+ assert.equal(object.value.type, 'java.lang.Object'); assert.equal(typeof object.value.object_id, 'string')
105
+ const readiness = await call('get_app_readiness', { platform: 'android', deviceId })
106
+ assert.equal(readiness.readiness.state, 'debugger_paused')
107
+ for (const name of ['tap', 'wait_for_ui', 'terminate_app', 'run_journey']) {
108
+ const blocked = await call(name, { platform: 'android', deviceId, x: 5, y: 5, appId: debuggerAppId })
109
+ assert.equal(blocked.failure_code, 'DEBUGGER_PAUSED', name)
110
+ assert.equal(blocked.dispatch_started, false)
111
+ }
112
+ await call('remove_debugger_breakpoint', { debugSessionId, breakpointId: bp.result.breakpoint_id })
113
+ const step = await call('step_debugger', { debugSessionId, stopId: stop.stop_id, threadId: stop.thread_id, kind: 'into' })
114
+ assert.equal(step.success, true, JSON.stringify(step))
115
+ const stale = await call('get_debugger_locals', { debugSessionId, stopId: stop.stop_id, frameId })
116
+ assert.equal(stale.error.code, 'DEBUGGER_STALE_REFERENCE')
117
+ const into = await eventsUntilStop()
118
+ assert.equal(into.stops[0].location.method, 'increment')
119
+ assert.equal((await call('step_debugger', { debugSessionId, stopId: into.stop_id, threadId: into.thread_id, kind: 'over' })).success, true)
120
+ const over = await eventsUntilStop()
121
+ assert.equal(over.stops[0].location.method, 'increment')
122
+ assert.equal((await call('step_debugger', { debugSessionId, stopId: over.stop_id, threadId: over.thread_id, kind: 'out' })).success, true)
123
+ const out = await eventsUntilStop()
124
+ assert.equal(out.stops[0].location.method, 'calculate')
125
+ assert.equal((await call('resume_debugger', { debugSessionId, stopId: out.stop_id })).success, true)
126
+ let resultVisible = false
127
+ for (let i = 0; i < 10; i++) {
128
+ const observed = await call('get_ui_tree', { platform: 'android', deviceId })
129
+ if (observed.elements.some((e: any) => e.text === 'Result 43')) { resultVisible = true; break }
130
+ }
131
+ assert.ok(resultVisible, 'UI tree independently establishes resumed execution: Result 43')
132
+ const noLocalsSource = await readFile(new URL('../../../fixtures/debugger-app/NoLocals.java', import.meta.url), 'utf8')
133
+ const noLocalsLine = noLocalsSource.split('\n').findIndex(line => line.includes('BREAK_NO_LOCALS')) + 1
134
+ const removedPending = await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.NoLocals`, line: noLocalsLine })
135
+ await call('remove_debugger_breakpoint', { debugSessionId, breakpointId: removedPending.result.breakpoint_id })
136
+ await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.NoLocals`, line: noLocalsLine })
137
+ const metadataTree = await call('get_ui_tree', { platform: 'android', deviceId })
138
+ const mb = metadataTree.elements.find((e: any) => /^no locals$/i.test(e.text ?? '')).bounds
139
+ spawn(process.env.ADB_PATH || 'adb', ['-s', deviceId, 'shell', 'input', 'tap', String(Math.floor((mb[0] + mb[2]) / 2)), String(Math.floor((mb[1] + mb[3]) / 2))], { stdio: 'ignore' })
140
+ const metadataStop = await eventsUntilStop()
141
+ assert.ok(metadataStop.stops.every((stop: any) => stop.breakpoint_id !== removedPending.result.breakpoint_id), 'removed pending breakpoint never rebinds')
142
+ const metadataStack = await call('get_debugger_stack', { debugSessionId, stopId: metadataStop.stop_id, threadId: metadataStop.thread_id })
143
+ const metadataLocals = await call('get_debugger_locals', { debugSessionId, stopId: metadataStop.stop_id, frameId: metadataStack.result.frames[0].frame_id })
144
+ assert.equal(metadataLocals.result.availability, 'unknown')
145
+ assert.equal(metadataLocals.result.reason, 'EMPTY_OR_UNAVAILABLE_VARIABLE_METADATA')
146
+ await call('resume_debugger', { debugSessionId, stopId: metadataStop.stop_id })
147
+ const lateTree = await call('get_ui_tree', { platform: 'android', deviceId })
148
+ const lateButton = lateTree.elements.find((e: any) => /^late class$/i.test(e.text ?? ''))
149
+ assert.ok(lateButton)
150
+ const bounds = lateButton.bounds
151
+ spawn(process.env.ADB_PATH || 'adb', ['-s', deviceId, 'shell', 'input', 'tap', String(Math.floor((bounds[0] + bounds[2]) / 2)), String(Math.floor((bounds[1] + bounds[3]) / 2))], { stdio: 'ignore' })
152
+ const lateStop = await eventsUntilStop()
153
+ assert.equal(lateStop.stops[0].location.class_name, `${debuggerAppId}.LateWork`)
154
+ assert.equal(lateStop.stops[0].location.line, line('BREAK_LATE'))
155
+ const aborted = new AbortController()
156
+ const cancelled = client.callTool({ name: 'wait_for_debugger_event', arguments: { debugSessionId, afterSequence: cursor, timeoutMs: 1000 } }, undefined, { signal: aborted.signal, timeout: 5000 })
157
+ const cancelTimer = setTimeout(() => aborted.abort(), 100)
158
+ try { await assert.rejects(cancelled) } finally { clearTimeout(cancelTimer) }
159
+ assert.equal((await call('get_debugger_state', { debugSessionId })).state, 'paused')
160
+ if (process.env.MCP_DEBUGGER_EXTENDED === '1') {
161
+ console.log('Checking real 60-second pause expiry while polling state')
162
+ const deadline = Date.now() + 65000
163
+ let state: any
164
+ do {
165
+ await new Promise(resolve => setTimeout(resolve, 1000))
166
+ state = await call('get_debugger_state', { debugSessionId })
167
+ } while (state.state === 'paused' && Date.now() < deadline)
168
+ assert.equal(state.state, 'detached', JSON.stringify(state))
169
+ assert.equal(state.reason, 'pause_limit')
170
+ assert.equal(state.cleanup.status, 'complete')
171
+ console.log('PASS real pause expiry despite active client polling')
172
+ }
173
+ const detach = await call('detach_debugger', { debugSessionId })
174
+ assert.equal(detach.cleanup.status, 'complete', JSON.stringify(detach))
175
+ assert.equal((await call('detach_debugger', { debugSessionId })).cleanup.status, 'complete')
176
+ assert.equal(adb('forward', '--list').trim(), forwardsBefore, 'unrelated forwards preserved')
177
+ const reattached = await call('attach_debugger', { platform: 'android', deviceId, appId: debuggerAppId, timeoutMs: 30000 })
178
+ assert.equal(reattached.success, true, JSON.stringify(reattached))
179
+ debugSessionId = reattached.debug_session_id
180
+ adb('shell', 'am', 'force-stop', debuggerAppId)
181
+ let disconnected: any
182
+ for (let i = 0; i < 20; i++) {
183
+ disconnected = await call('get_debugger_state', { debugSessionId })
184
+ if (disconnected.cleanup.status === 'complete') break
185
+ await new Promise(resolve => setTimeout(resolve, 100))
186
+ }
187
+ assert.equal(disconnected.state, 'disconnected', JSON.stringify(disconnected))
188
+ assert.equal(disconnected.cleanup.status, 'complete')
189
+ assert.equal(adb('forward', '--list').trim(), forwardsBefore)
190
+ // Disconnect the MCP client with an actively paused session; shutdown owns resume/dispose.
191
+ adb('shell', 'am', 'start', '-W', '-n', `${debuggerAppId}/.MainActivity`)
192
+ const shutdownSession = await call('attach_debugger', { platform: 'android', deviceId, appId: debuggerAppId, timeoutMs: 30000 })
193
+ assert.equal(shutdownSession.success, true, JSON.stringify(shutdownSession))
194
+ debugSessionId = shutdownSession.debug_session_id; cursor = 0
195
+ await call('set_debugger_breakpoint', { debugSessionId, className: `${debuggerAppId}.MainActivity`, line: line('BREAK_CALCULATE') })
196
+ const shutdownTree = await call('get_ui_tree', { platform: 'android', deviceId })
197
+ const shutdownButton = shutdownTree.elements.find((e: any) => /^calculate$/i.test(e.text ?? ''))
198
+ const sb = shutdownButton.bounds
199
+ spawn(process.env.ADB_PATH || 'adb', ['-s', deviceId, 'shell', 'input', 'tap', String(Math.floor((sb[0] + sb[2]) / 2)), String(Math.floor((sb[1] + sb[3]) / 2))], { stdio: 'ignore' })
200
+ await eventsUntilStop()
201
+ await client.close(); debugSessionId = ''
202
+ assert.equal(adb('forward', '--list').trim(), forwardsBefore, 'MCP disconnect cleans active owned forward')
203
+ const verifier = new Client({ name: 'debugger-shutdown-verifier', version: '1.0.0' })
204
+ try {
205
+ await verifier.connect(new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' }))
206
+ const result = await verifier.callTool({ name: 'get_ui_tree', arguments: { platform: 'android', deviceId } })
207
+ const treeAfterShutdown = JSON.parse((result.content as any[]).find(c => c.type === 'text').text)
208
+ assert.ok(treeAfterShutdown.elements.some((e: any) => e.text === 'Result 43'), 'fresh UI proves paused fixture resumed after server shutdown')
209
+ } finally { await verifier.close() }
210
+ console.log(JSON.stringify({ status: 'PASS', build_sha256: buildHash, device: deviceId, api: adb('shell', 'getprop', 'ro.build.version.sdk').trim(), runtime: process.version, transport: 'MCP stdio', extended: process.env.MCP_DEBUGGER_EXTENDED === '1' }))
211
+ console.log('Debugger response measurement', JSON.stringify({ mode: compactMode ? 'compact' : 'debug', calls: measurements, debug_bytes: measurements.reduce((sum, row) => sum + row.debug_bytes, 0), compact_bytes: measurements.reduce((sum, row) => sum + row.compact_bytes, 0) }))
212
+ console.log('PASS Android debugger: explicit attach, source stop, stack/known locals, pause guards, into/over/out, stale frame, resume UI, detach/forward ownership')
213
+ } finally {
214
+ if (debugSessionId) await call('detach_debugger', { debugSessionId }).catch(() => {})
215
+ await client.close()
216
+ adb('uninstall', debuggerAppId)
217
+ await rm(fixture.directory, { recursive: true, force: true })
218
+ }
219
+ }
@@ -0,0 +1,56 @@
1
+ import assert from 'node:assert/strict'
2
+ import { execFileSync } from 'node:child_process'
3
+ import { rm } from 'node:fs/promises'
4
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
5
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
6
+ import { buildDebuggerFixture, debuggerAppId } from '../../../fixtures/debugger-app/build.js'
7
+
8
+ // Reuse the isolated fixture; the device runner executes fixture journeys sequentially.
9
+ const deviceId = process.env.MCP_INTERACTION_DEVICE ?? process.env.MCP_DEBUGGER_DEVICE
10
+ if (!deviceId) {
11
+ console.log('UNRUN Android interaction fixture: select an emulator with MCP_INTERACTION_DEVICE')
12
+ } else {
13
+ const fixture = buildDebuggerFixture()
14
+ const adb = (...args: string[]) => execFileSync(process.env.ADB_PATH || 'adb', ['-s', deviceId, ...args], { encoding: 'utf8', timeout: 30000 })
15
+ const client = new Client({ name: 'interaction-service-smoke', version: '1.0.0' })
16
+ const target = { platform: 'android', deviceId }
17
+ const call = async (name: string, args: Record<string, unknown>) => {
18
+ const response = await client.callTool({ name, arguments: args }, undefined, { timeout: 120000 })
19
+ const text = response.content.find(item => item.type === 'text')
20
+ assert(text?.type === 'text')
21
+ return JSON.parse(text.text)
22
+ }
23
+ try {
24
+ await client.connect(new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' }))
25
+ adb('install', '-r', fixture.artifact)
26
+ const started = await call('start_app', { ...target, appId: debuggerAppId, verificationMode: 'none', responseMode: 'compact' })
27
+ assert.equal(started.success, true)
28
+ const waited = await call('wait_for_ui', { ...target, selector: { text: 'Calculate' }, condition: 'clickable', timeout_ms: 20000 })
29
+ assert.equal(waited.status, 'success', JSON.stringify(waited))
30
+ assert.ok(waited.element?.elementId)
31
+ const tapped = await call('tap_element', { ...target, elementId: waited.element.elementId, verificationMode: 'none', actionTimeoutMs: 30000 })
32
+ assert.equal(tapped.success, true, JSON.stringify(tapped))
33
+ const visible = await call('expect_element_visible', { ...target, selector: { text: 'Result 43' }, timeout_ms: 20000 })
34
+ assert.equal(visible.success, true, JSON.stringify(visible))
35
+ const journey = await call('run_journey', { ...target, steps: [
36
+ { id: 'calculate-again', type: 'tap', selector: { text: 'Calculate' }, verificationMode: 'none' },
37
+ { id: 'enabled-result', type: 'assert', timeoutMs: 30000, assertion: { kind: 'state_equals', selector: { text: 'Result 43' }, property: 'enabled', expected: true } },
38
+ { id: 'visible-result', type: 'assert', timeoutMs: 20000, assertion: { kind: 'element_visible', selector: { text: 'Result 43' } } }
39
+ ], defaults: { actionTimeoutMs: 30000 } })
40
+ assert.equal(journey.success, true, JSON.stringify(journey))
41
+ const adjusted = await call('adjust_control', { ...target, selector: { accessibility_id: 'Fixture level' },
42
+ property: 'value', targetValue: 75, tolerance: 5, maxAttempts: 5 })
43
+ assert.equal(adjusted.success, true, JSON.stringify(adjusted))
44
+ assert.equal(adjusted.converged, true, JSON.stringify(adjusted))
45
+ assert(Math.abs(adjusted.actual_state.value - 75) <= 5, JSON.stringify(adjusted))
46
+ const level = await call('expect_element_visible', { ...target, selector: { text: `${adjusted.actual_state.value} %` }, timeout_ms: 20000 })
47
+ assert.equal(level.success, true, JSON.stringify(level))
48
+ console.log(JSON.stringify({ status: 'PASS', server: client.getServerVersion(), device: deviceId,
49
+ api: adb('shell', 'getprop', 'ro.build.version.sdk').trim(), runtime: process.version,
50
+ journey: 'clickable wait, resolved-handle tap, visible assertion, selector journey and stable enabled-state assertion and labelled SeekBar convergence',
51
+ evidence: 'Result 43, enabled=true and adjusted slider label established by fresh hierarchy assertions', cleanup: 'Owned fixture removed in finally; no other app data or device settings changed' }))
52
+ } finally {
53
+ await client.close()
54
+ try { adb('uninstall', debuggerAppId) } finally { await rm(fixture.directory, { recursive: true, force: true }) }
55
+ }
56
+ }
@@ -0,0 +1,104 @@
1
+ import { execCmd } from '../../../../src/utils/exec.js'
2
+ import { getAdbCmd } from '../../../../src/utils/android/utils.js'
3
+ import { createHash } from 'node:crypto'
4
+ import assert from 'node:assert/strict'
5
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
6
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
7
+ import { promises as fs } from 'node:fs'
8
+ import path from 'node:path'
9
+ import { inspectPerfetto } from '../../../helpers/perfetto.js'
10
+
11
+ async function run() {
12
+ if (process.env.SKIP_DEVICE_TESTS === '1') { console.log('System trace smoke: SKIPPED (SKIP_DEVICE_TESTS=1)'); return }
13
+ const client = new Client({ name: 'system-trace-smoke', version: '1.0.0' })
14
+ const transport = new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' })
15
+ const parse = (r: any) => JSON.parse(r.content[0].text)
16
+ const call = async (name: string, args: Record<string, unknown>) => parse(await client.callTool({ name, arguments: args }, undefined, { timeout: 120000 }))
17
+ const owned: string[] = [], artifacts: string[] = []
18
+ try {
19
+ await client.connect(transport)
20
+ const devices = await call('list_devices', {})
21
+ const device = devices.devices.find((d: any) => d.platform === 'android' && d.simulator)
22
+ assert.ok(device, 'A running Android emulator is required')
23
+ const target = { platform: 'android', deviceId: device.id }
24
+ const cap = await call('get_system_trace_capabilities', target)
25
+ assert.equal(cap.supported, true, JSON.stringify(cap))
26
+ const fixtureLaunch = await call('start_app', { ...target, appId: 'com.android.settings', verificationMode: 'none', responseMode: 'compact' })
27
+ assert.equal(fixtureLaunch.success, true)
28
+ const baseline = await call('get_ui_tree', { ...target, responseMode: 'debug' })
29
+ const settingsNode = baseline.elements?.find((e: any) => e.visible && e.resourceId?.startsWith('com.android.settings:id/'))
30
+ assert.ok(settingsNode, 'Fresh baseline must contain a visible Settings-owned UI resource')
31
+ const settingsSelector = { resource_id: settingsNode.resourceId }
32
+ const start = await call('start_system_trace', { ...target, durationMs: 30000 })
33
+ owned.push(start.trace_id)
34
+ assert.equal(start.state, 'recording', JSON.stringify(start))
35
+ const journey = await call('run_journey', { ...target, systemTraceId: start.trace_id,
36
+ steps: [
37
+ { id: 'settings', type: 'start_app', appId: 'com.android.settings', verificationMode: 'none' },
38
+ { id: 'settings-visible', type: 'assert', timeoutMs: 10000, assertion: { kind: 'element_visible', selector: settingsSelector } }
39
+ ] })
40
+ assert.equal(journey.success, true, JSON.stringify(journey))
41
+ assert.equal(journey.steps[1].status, 'passed', 'The baseline Settings-owned UI element must remain semantically visible')
42
+ assert.equal(journey.evidence.trace_context.trace_id, start.trace_id)
43
+ assert.equal(journey.steps[0].result.evidence.trace_context.trace_id, start.trace_id)
44
+ assert.equal(journey.system_trace.coverage, 'covered')
45
+ const stop = await call('stop_system_trace', { traceId: start.trace_id })
46
+ if (stop.artifact) artifacts.push(stop.artifact.path)
47
+ assert.equal(stop.state, 'completed', JSON.stringify(stop)); assert.equal(stop.cleanup.status, 'complete')
48
+ assert.deepEqual(stop.correlation_ids, [journey.evidence.correlation_id])
49
+ const content = inspectPerfetto(await fs.readFile(stop.artifact.path), 30000)
50
+ assert.equal((await call('stop_system_trace', { traceId: start.trace_id })).artifact.path, stop.artifact.path)
51
+ const rejected = await call('run_journey', { ...target, systemTraceId: start.trace_id,
52
+ steps: [{ id: 'must-not-launch', type: 'start_app', appId: 'com.invalid.trace.smoke' }] })
53
+ assert.equal(rejected.error.code, 'TRACE_NOT_FOUND')
54
+ const cancel = await call('start_system_trace', { ...target, durationMs: 30000 })
55
+ owned.push(cancel.trace_id)
56
+ const cancelled = await call('cancel_system_trace', { traceId: cancel.trace_id })
57
+ assert.equal(cancelled.state, 'cancelled', JSON.stringify(cancelled))
58
+ assert.equal((await call('get_system_trace_status', { traceId: cancel.trace_id })).artifact, null)
59
+ const short = await call('start_system_trace', { ...target, durationMs: 1001 })
60
+ owned.push(short.trace_id)
61
+ assert.equal(short.state, 'recording', JSON.stringify(short))
62
+ let ended = short
63
+ for (let i = 0; i < 30 && ended.state === 'recording'; i++) {
64
+ await new Promise(resolve => setTimeout(resolve, 200))
65
+ ended = await call('get_system_trace_status', { traceId: short.trace_id })
66
+ }
67
+ if (ended.artifact) artifacts.push(ended.artifact.path)
68
+ assert.equal(ended.state, 'completed', JSON.stringify(ended)); assert.equal(ended.completion_reason, 'duration_limit')
69
+ const durationContent = inspectPerfetto(await fs.readFile(ended.artifact.path), 1001)
70
+ const shutdownClient = new Client({ name: 'system-trace-shutdown-smoke', version: '1.0.0' })
71
+ const shutdownTransport = new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' })
72
+ let shutdownTrace: any
73
+ let shutdownDiagnostics = ''
74
+ try {
75
+ await shutdownClient.connect(shutdownTransport)
76
+ shutdownTransport.stderr?.on('data', data => { shutdownDiagnostics += String(data) })
77
+ shutdownTrace = parse(await shutdownClient.callTool({ name: 'start_system_trace', arguments: { ...target, durationMs: 30000 } }, undefined, { timeout: 120000 }))
78
+ assert.equal(shutdownTrace.state, 'recording')
79
+ } finally { await shutdownClient.close() }
80
+ const remote = shutdownTrace.cleanup.remaining_resources.find((r: string) => r.startsWith('/data/misc/perfetto-traces/mcp-'))
81
+ assert.ok(remote)
82
+ const absent = await execCmd(getAdbCmd(), ['-s', device.id, 'shell', 'test', '!', '-e', remote], { timeout: 10000 })
83
+ assert.equal(absent.exitCode, 0, `Shutdown must stop and remove its owned device capture: ${shutdownTrace.trace_id}; ${shutdownDiagnostics}; ${absent.stderr}`)
84
+ const buildHash = createHash('sha256')
85
+ for (const file of ['dist/server.js', 'dist/observe/system-trace.js', 'dist/observe/system-trace-android.js', 'dist/server/handlers/tool-handlers.js', 'dist/observe/evidence.js']) buildHash.update(await fs.readFile(file))
86
+ const report = { status: 'PASS', build_sha256: buildHash.digest('hex'), runtime: process.version, server: client.getServerVersion(), device: device.id, osVersion: device.osVersion,
87
+ starting_state: 'System Settings fixture', journey: 'start Settings, assert baseline Settings-owned resource visible in UI tree; explicit trace association',
88
+ trace_id: start.trace_id, correlation_id: journey.evidence.correlation_id, phases: {
89
+ capture_started_at_ms: stop.capture_started_at_ms, capture_completed_at_ms: stop.capture_completed_at_ms },
90
+ artifact_bytes: stop.artifact.size_bytes, content, durationContent, cancel: 'PASS', expired_journey: 'PASS', active_shutdown: 'PASS',
91
+ cleanup: 'Device capture files removed; test host artifacts removed in finally. No settings or app data changed. Settings remains foreground.' }
92
+ console.log(JSON.stringify(report))
93
+ if (process.env.SYSTEM_TRACE_EVIDENCE_PATH) await fs.writeFile(process.env.SYSTEM_TRACE_EVIDENCE_PATH, JSON.stringify(report, null, 2) + '\n')
94
+ } finally {
95
+ const cleanupErrors: string[] = []
96
+ for (const traceId of owned) {
97
+ try { const r = await call('cancel_system_trace', { traceId }); if (r.artifact && !artifacts.includes(r.artifact.path)) artifacts.push(r.artifact.path); if (r.cleanup?.status !== 'complete') cleanupErrors.push(traceId) } catch { cleanupErrors.push(traceId) }
98
+ }
99
+ await client.close()
100
+ for (const file of artifacts) await fs.rm(path.dirname(file), { recursive: true, force: true })
101
+ assert.deepEqual(cleanupErrors, [], 'Owned trace cleanup failed')
102
+ }
103
+ }
104
+ run().catch(error => { console.error(error); process.exitCode = 1 })
@@ -11,7 +11,7 @@ async function run() {
11
11
  const transport = new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' })
12
12
  const target = { platform: 'android', deviceId: 'emulator-5554' }
13
13
  const report: any = { verified_on: new Date().toISOString(), device: target, package: 'com.ideamechanics.modul8.dev',
14
- source: 'rebuilt local MCP stdio server', build_sha256: createHash('sha256').update(readFileSync('dist/server/tool-handlers.js')).digest('hex'),
14
+ source: 'rebuilt local MCP stdio server', build_sha256: createHash('sha256').update(readFileSync('dist/server/handlers/tool-handlers.js')).digest('hex'),
15
15
  steps: [], status: 'UNRUN', cleanup: 'UNRUN' }
16
16
  const call = async (name: string, args: Record<string, unknown>) => {
17
17
  const response: any = await client.callTool({ name, arguments: { ...target, ...args } }, undefined, { timeout: 120000 })