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,173 @@
1
+ import type { UiSelector as ExpectElementSelectorArg } from '../../contracts/selector.js'
2
+ import { measure } from '../../utils/timing.js'
3
+ import { runWithBudget } from '../../utils/operation-budget.js'
4
+ import { tapElementControlsSchema } from '../schemas/journey-schema.js'
5
+ import { notifyActionStart } from '../../interact/conditional-action.js'
6
+
7
+ import { ToolsInteract } from '../../interact/index.js'
8
+ import { recordObservationConsumer, recordObservationEvent, withObservationContext } from '../../observe/performance-metrics.js'
9
+ import {
10
+ buildActionExecutionResult,
11
+ captureActionFingerprint,
12
+ getNumberArg,
13
+ getStringArg,
14
+ inferGenericFailure,
15
+ inferScrollFailure,
16
+ requireNumberArg,
17
+ requireObjectArg,
18
+ requireStringArg,
19
+ ToolCallArgs,
20
+ wrapResponse
21
+ } from '../common.js'
22
+ import type { PlatformArg, ScrollSelectorArg } from './handler-types.js'
23
+ import { interactionControls, actionHandler } from './action-execution.js'
24
+
25
+ export async function executeTap(args: ToolCallArgs) {
26
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
27
+ const x = requireNumberArg(args, 'x')
28
+ const y = requireNumberArg(args, 'y')
29
+ const deviceId = getStringArg(args, 'deviceId')
30
+ const { verificationMode, actionTimeoutMs } = interactionControls(args)
31
+ const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
32
+ notifyActionStart()
33
+ const res = await measure('dispatch_ms', () => ToolsInteract.tapHandler({ platform, x, y, deviceId, timeoutMs: actionTimeoutMs }))
34
+ const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
35
+ const result = buildActionExecutionResult({
36
+ actionType: 'tap',
37
+ sourceModule: 'server',
38
+ selector: { x, y },
39
+ success: !!res.success,
40
+ uiFingerprintBefore,
41
+ uiFingerprintAfter,
42
+ failure: res.success ? undefined : inferGenericFailure(res.error)
43
+ })
44
+ return wrapResponse(result)
45
+ }
46
+
47
+ export async function executeTapElement(args: ToolCallArgs) {
48
+ const controls = tapElementControlsSchema.safeParse({ elementId: args.elementId, selector: args.selector, waitFor: args.waitFor })
49
+ if (!controls.success) throw new Error(`INVALID_ARGUMENT: ${controls.error.message}`)
50
+ const elementId = controls.data.elementId
51
+ const selector = controls.data.selector as ExpectElementSelectorArg | undefined
52
+ const { verificationMode, actionTimeoutMs } = interactionControls(args)
53
+ let resolvedId = elementId
54
+ let freshTree: import('../../interact/observation.js').UiTree | undefined
55
+ if (selector) {
56
+ const waitFor = controls.data.waitFor
57
+ const targetStarted = performance.now()
58
+ const resolutionTimeoutMs = waitFor?.timeoutMs ?? actionTimeoutMs ?? 5000
59
+ const resolution = (await withObservationContext({ stepId: args._stepId as string | undefined, purpose: 'target' }, () =>
60
+ measure('target_resolution_ms', () => runWithBudget(resolutionTimeoutMs, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: resolutionTimeoutMs, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, resolveTapTarget: true, platform: getStringArg(args, 'platform') as PlatformArg | undefined, deviceId: getStringArg(args, 'deviceId') })))))!
61
+ recordObservationEvent({ step_id: args._stepId as string | undefined, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status })
62
+ if (resolution.status !== 'success' || !resolution.element?.elementId) return wrapResponse({ success: false, action_type: 'tap_element', failure_code: resolution.error?.code ?? 'ELEMENT_NOT_FOUND', retryable: true })
63
+ resolvedId = resolution.element.elementId
64
+ freshTree = resolution._tree
65
+ }
66
+ notifyActionStart()
67
+ if (freshTree) recordObservationConsumer(freshTree, 'target', args._stepId as string | undefined)
68
+ const res = await measure('dispatch_ms', () => ToolsInteract.tapElementHandler({ elementId: resolvedId!, freshTree, verificationMode, timeoutMs: actionTimeoutMs, platform: getStringArg(args, 'platform') as PlatformArg | undefined, deviceId: getStringArg(args, 'deviceId') }))
69
+ return wrapResponse(res)
70
+ }
71
+
72
+ export async function executeSwipe(args: ToolCallArgs) {
73
+ const platform = (getStringArg(args, 'platform') as PlatformArg | undefined) ?? 'android'
74
+ const x1 = requireNumberArg(args, 'x1')
75
+ const y1 = requireNumberArg(args, 'y1')
76
+ const x2 = requireNumberArg(args, 'x2')
77
+ const y2 = requireNumberArg(args, 'y2')
78
+ const duration = requireNumberArg(args, 'duration')
79
+ const deviceId = getStringArg(args, 'deviceId')
80
+ const { verificationMode, actionTimeoutMs } = interactionControls(args)
81
+ const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
82
+ notifyActionStart()
83
+ const res = await measure('dispatch_ms', () => ToolsInteract.swipeHandler({ platform, x1, y1, x2, y2, duration, deviceId, timeoutMs: actionTimeoutMs }))
84
+ const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
85
+ const result = buildActionExecutionResult({
86
+ actionType: 'swipe',
87
+ sourceModule: 'server',
88
+ selector: { x1, y1, x2, y2, duration },
89
+ success: !!res.success,
90
+ uiFingerprintBefore,
91
+ uiFingerprintAfter,
92
+ failure: res.success ? undefined : inferGenericFailure(res.error)
93
+ })
94
+ return wrapResponse(result)
95
+ }
96
+
97
+ export async function executeScrollToElement(args: ToolCallArgs) {
98
+ const platform = requireStringArg(args, 'platform') as PlatformArg
99
+ const selector = requireObjectArg<ScrollSelectorArg>(args, 'selector')
100
+ const direction = getStringArg(args, 'direction') as 'down' | 'up' | undefined
101
+ const maxScrolls = getNumberArg(args, 'maxScrolls')
102
+ const scrollAmount = getNumberArg(args, 'scrollAmount')
103
+ const deviceId = getStringArg(args, 'deviceId')
104
+ const { verificationMode } = interactionControls(args)
105
+ const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
106
+ notifyActionStart()
107
+ const res = await measure('dispatch_ms', () => ToolsInteract.scrollToElementHandler({ platform, selector, direction, maxScrolls, scrollAmount, deviceId }))
108
+ const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
109
+ const result = buildActionExecutionResult({
110
+ actionType: 'scroll_to_element',
111
+ sourceModule: 'server',
112
+ selector: selector ?? null,
113
+ resolved: res?.success && res?.element ? {
114
+ elementId: null,
115
+ text: res.element.text ?? null,
116
+ resource_id: res.element.resourceId ?? null,
117
+ accessibility_id: (res.element as typeof res.element & { contentDesc?: string }).contentDesc ?? null,
118
+ class: (res.element as typeof res.element & { className?: string }).className ?? null,
119
+ bounds: res.element.bounds ?? null,
120
+ index: null
121
+ } : null,
122
+ success: !!res.success,
123
+ uiFingerprintBefore,
124
+ uiFingerprintAfter,
125
+ failure: res.success ? undefined : inferScrollFailure(res.reason)
126
+ })
127
+ return wrapResponse(result)
128
+ }
129
+
130
+ export async function executeTypeText(args: ToolCallArgs) {
131
+ const text = requireStringArg(args, 'text')
132
+ const deviceId = getStringArg(args, 'deviceId')
133
+ const { verificationMode, actionTimeoutMs } = interactionControls(args)
134
+ const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
135
+ notifyActionStart()
136
+ const res = await measure('dispatch_ms', () => ToolsInteract.typeTextHandler({ text, deviceId, timeoutMs: actionTimeoutMs }))
137
+ const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
138
+ const result = buildActionExecutionResult({
139
+ actionType: 'type_text',
140
+ sourceModule: 'server',
141
+ selector: { text },
142
+ success: !!res.success,
143
+ uiFingerprintBefore,
144
+ uiFingerprintAfter,
145
+ failure: res.success ? undefined : inferGenericFailure(res.error)
146
+ })
147
+ return wrapResponse(result)
148
+ }
149
+
150
+ export async function executePressBack(args: ToolCallArgs) {
151
+ const deviceId = getStringArg(args, 'deviceId')
152
+ const { verificationMode, actionTimeoutMs } = interactionControls(args)
153
+ const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
154
+ notifyActionStart()
155
+ const res = await measure('dispatch_ms', () => ToolsInteract.pressBackHandler({ deviceId, timeoutMs: actionTimeoutMs }))
156
+ const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint('android', deviceId)) : null
157
+ const result = buildActionExecutionResult({
158
+ actionType: 'press_back',
159
+ sourceModule: 'server',
160
+ selector: { key: 'back' },
161
+ success: !!res.success,
162
+ uiFingerprintBefore,
163
+ uiFingerprintAfter,
164
+ failure: res.success ? undefined : inferGenericFailure(res.error)
165
+ })
166
+ return wrapResponse(result)
167
+ }
168
+ export const handleTap = actionHandler('tap', executeTap)
169
+ export const handleTapElement = actionHandler('tap_element', executeTapElement)
170
+ export const handleSwipe = actionHandler('swipe', executeSwipe)
171
+ export const handleScrollToElement = actionHandler('scroll_to_element', executeScrollToElement)
172
+ export const handleTypeText = actionHandler('type_text', executeTypeText)
173
+ export const handlePressBack = actionHandler('press_back', executePressBack)
@@ -0,0 +1,135 @@
1
+ import type { UiSelector as ExpectElementSelectorArg } from '../../contracts/selector.js'
2
+ import { markEvidenceVerification } from '../../observe/evidence.js'
3
+ import { withReadyWait } from '../../observe/readiness-service.js'
4
+
5
+ import { ToolsInteract } from '../../interact/index.js'
6
+ import { recordObservationEvent, withObservationContext } from '../../observe/performance-metrics.js'
7
+ import {
8
+ getBooleanArg,
9
+ getNumberArg,
10
+ getObjectArg,
11
+ getStringArg,
12
+ requireNumberArg,
13
+ requireObjectArg,
14
+ requireStringArg,
15
+ ToolCallArgs,
16
+ wrapResponse
17
+ } from '../common.js'
18
+ import type { PlatformArg, WaitForUiMatchArg, WaitForUiRetryArg } from './handler-types.js'
19
+
20
+ export async function handleWaitForScreenChange(args: ToolCallArgs) {
21
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
22
+ const previousFingerprint = requireStringArg(args, 'previousFingerprint')
23
+ const timeoutMs = getNumberArg(args, 'timeoutMs')
24
+ const pollIntervalMs = getNumberArg(args, 'pollIntervalMs')
25
+ const deviceId = getStringArg(args, 'deviceId')
26
+ const res = await ToolsInteract.waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs, pollIntervalMs, deviceId })
27
+ return wrapResponse(res)
28
+ }
29
+
30
+ export async function handleExpectScreen(args: ToolCallArgs) {
31
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
32
+ const fingerprint = getStringArg(args, 'fingerprint')
33
+ const screen = getStringArg(args, 'screen')
34
+ const deviceId = getStringArg(args, 'deviceId')
35
+ const res = await ToolsInteract.expectScreenHandler({ platform, fingerprint, screen, deviceId })
36
+ return wrapResponse(res)
37
+ }
38
+
39
+ export async function handleExpectElementVisible(args: ToolCallArgs) {
40
+ const selector = requireObjectArg<ExpectElementSelectorArg>(args, 'selector')
41
+ const element_id = getStringArg(args, 'element_id')
42
+ const timeout_ms = getNumberArg(args, 'timeout_ms')
43
+ const poll_interval_ms = getNumberArg(args, 'poll_interval_ms')
44
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
45
+ const deviceId = getStringArg(args, 'deviceId')
46
+ const res = await ToolsInteract.expectElementVisibleHandler({ selector, element_id, timeout_ms, poll_interval_ms, platform, deviceId })
47
+ return wrapResponse(res)
48
+ }
49
+
50
+ export async function handleExpectState(args: ToolCallArgs) {
51
+ const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
52
+ const element_id = getStringArg(args, 'element_id')
53
+ const property = requireStringArg(args, 'property')
54
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
55
+ const deviceId = getStringArg(args, 'deviceId')
56
+ if (!selector && !element_id) {
57
+ throw new Error('Missing selector or element_id argument')
58
+ }
59
+ if (!Object.prototype.hasOwnProperty.call(args, 'expected')) {
60
+ throw new Error('Missing expected argument')
61
+ }
62
+ const expected = args.expected as boolean | number | string | Record<string, unknown>
63
+ const res = await ToolsInteract.expectStateHandler({ selector: selector ?? undefined, element_id: element_id ?? undefined, property, expected, platform, deviceId })
64
+ return wrapResponse(res)
65
+ }
66
+
67
+ export async function handleAdjustControl(args: ToolCallArgs) {
68
+ const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
69
+ const element_id = getStringArg(args, 'element_id')
70
+ const property = getStringArg(args, 'property') ?? 'value'
71
+ const targetValue = requireNumberArg(args, 'targetValue')
72
+ const tolerance = getNumberArg(args, 'tolerance')
73
+ const maxAttempts = getNumberArg(args, 'maxAttempts')
74
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
75
+ const deviceId = getStringArg(args, 'deviceId')
76
+ if ((!selector && !element_id) || (selector && element_id)) {
77
+ throw new Error('Exactly one of selector or element_id argument is required')
78
+ }
79
+ const res = await ToolsInteract.adjustControlHandler({
80
+ selector: selector ?? undefined,
81
+ element_id: element_id ?? undefined,
82
+ property,
83
+ targetValue,
84
+ tolerance,
85
+ maxAttempts,
86
+ platform,
87
+ deviceId
88
+ })
89
+ markEvidenceVerification()
90
+ return wrapResponse(res)
91
+ }
92
+
93
+ export async function handleWaitForUI(args: ToolCallArgs) {
94
+ if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean') throw new Error('INVALID_ARGUMENT: requireReady must be boolean')
95
+ const selector = getObjectArg<ExpectElementSelectorArg>(args, 'selector')
96
+ const condition = (getStringArg(args, 'condition') as 'exists' | 'not_exists' | 'visible' | 'clickable' | undefined) ?? 'exists'
97
+ const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
98
+ const poll_interval_ms = getNumberArg(args, 'poll_interval_ms') ?? 300
99
+ const match = getObjectArg<WaitForUiMatchArg>(args, 'match')
100
+ const retry = getObjectArg<WaitForUiRetryArg>(args, 'retry')
101
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
102
+ const deviceId = getStringArg(args, 'deviceId')
103
+ const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIHandler({ selector, condition, timeout_ms, poll_interval_ms, match, retry, platform: targetPlatform, deviceId: targetId })
104
+ const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms,
105
+ device => execute(device.platform as PlatformArg, device.id), (code, elapsed, samples) => ({ status: 'timeout', error: { code, message: 'Readiness wait could not complete' }, metrics: { latency_ms: elapsed, poll_count: samples, attempts: samples ? 1 : 0 }, requested: { selector, condition, match }, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } })) : await execute()
106
+ return wrapResponse(res)
107
+ }
108
+
109
+ export async function handleWaitForUIChange(args: ToolCallArgs) {
110
+ if (args.requireReady !== undefined && typeof args.requireReady !== 'boolean') throw new Error('INVALID_ARGUMENT: requireReady must be boolean')
111
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
112
+ const deviceId = getStringArg(args, 'deviceId')
113
+ const timeout_ms = getNumberArg(args, 'timeout_ms') ?? 60000
114
+ const stability_window_ms = getNumberArg(args, 'stability_window_ms') ?? 300
115
+ const expected_change = getStringArg(args, 'expected_change') as 'hierarchy_diff' | 'text_change' | 'state_change' | undefined
116
+ const scope = getStringArg(args, 'scope') as 'screen' | 'subtree' | undefined
117
+ const target = getStringArg(args, 'target')
118
+ const execute = (targetPlatform = platform, targetId = deviceId) => ToolsInteract.waitForUIChangeHandler({ platform: targetPlatform, deviceId: targetId, timeout_ms, stability_window_ms, expected_change, scope, target })
119
+ const res = args.requireReady === true ? await withReadyWait({ platform, deviceId }, timeout_ms,
120
+ device => execute(device.platform as PlatformArg, device.id), (code, elapsed) => ({ success: false, timeout: true, observed_change: null, elapsed_ms: elapsed, reason: code, error: { code, message: 'Readiness wait could not complete' } })) : await execute()
121
+ return wrapResponse(res)
122
+ }
123
+
124
+ export async function handleFindElement(args: ToolCallArgs) {
125
+ const query = requireStringArg(args, 'query')
126
+ const exact = getBooleanArg(args, 'exact') ?? false
127
+ const timeoutMs = getNumberArg(args, 'timeoutMs') ?? 3000
128
+ const platform = getStringArg(args, 'platform') as PlatformArg | undefined
129
+ const deviceId = getStringArg(args, 'deviceId')
130
+ const targetStarted = performance.now()
131
+ const res = await withObservationContext({ purpose: 'target' }, () => ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId }))
132
+ recordObservationEvent({ target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: res.found ? 'found' : 'not_found', operation: 'find_element' })
133
+ return wrapResponse(res)
134
+ }
135
+
@@ -0,0 +1,39 @@
1
+ import type { UiSelector as ExpectElementSelectorArg } from '../../contracts/selector.js'
2
+
3
+ import { ToolsInteract } from '../../interact/index.js'
4
+ import { ToolsObserve } from '../../observe/index.js'
5
+
6
+
7
+ import type { PlatformArg } from './handler-types.js'
8
+
9
+ export async function expectElementAbsent({ selector, timeoutMs, pollIntervalMs, platform, deviceId }: { selector: ExpectElementSelectorArg, timeoutMs: number, pollIntervalMs: number, platform: PlatformArg, deviceId?: string }) {
10
+ const deadline = performance.now() + timeoutMs
11
+ let consecutive = 0
12
+ let observed: import('../../interact/waits/ui-wait-state.js').UIWaitResult['observed']
13
+ while (performance.now() < deadline) {
14
+ const result = await ToolsInteract.waitForUIHandler({ selector, condition: 'not_exists', timeout_ms: 0, singleObservation: true, singleAttempt: true, platform, deviceId })
15
+ observed = result!.observed
16
+ consecutive = result!.status === 'success' ? consecutive + 1 : 0
17
+ if (consecutive === 2 && performance.now() <= deadline) return { success: true, observed }
18
+ const delay = Math.max(100, pollIntervalMs)
19
+ if (performance.now() + delay >= deadline) break
20
+ await new Promise(resolve => setTimeout(resolve, delay))
21
+ }
22
+ return { success: false, failure_code: 'ASSERTION_TIMEOUT', observed }
23
+ }
24
+
25
+ export async function expectFingerprint({ fingerprint, timeoutMs, platform, deviceId }: { fingerprint: string, timeoutMs: number, platform: PlatformArg, deviceId?: string }) {
26
+ const deadline = performance.now() + timeoutMs
27
+ let consecutive = 0
28
+ let observed: string | null = null
29
+ while (performance.now() < deadline) {
30
+ const result = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId })
31
+ observed = result?.fingerprint ?? null
32
+ consecutive = observed === fingerprint ? consecutive + 1 : 0
33
+ if (consecutive === 2 && performance.now() <= deadline) return { success: true, observed_fingerprint: observed }
34
+ if (performance.now() + 100 >= deadline) break
35
+ await new Promise(resolve => setTimeout(resolve, 100))
36
+ }
37
+ return { success: false, failure_code: 'ASSERTION_TIMEOUT', observed_fingerprint: observed }
38
+ }
39
+
@@ -0,0 +1,132 @@
1
+ import type { z } from 'zod'
2
+ import { debuggerSessions } from '../../debugger/sessions.js'
3
+ import { systemTraces } from '../../observe/system-trace.js'
4
+ import { currentEvidenceCorrelationId, bindSystemTrace, refreshJourneyEvidence, completeJourneyVerification } from '../../observe/evidence.js'
5
+ import { runWithBudget } from '../../utils/operation-budget.js'
6
+ import { journeySchema } from '../schemas/journey-schema.js'
7
+ import { resolveTargetDevice, withResolvedDevice } from '../../utils/resolve-device.js'
8
+ import { ToolsInteract } from '../../interact/index.js'
9
+ import { ToolsObserve } from '../../observe/index.js'
10
+ import { withObservationContext } from '../../observe/performance-metrics.js'
11
+ import { wrapResponse, type ToolCallArgs } from '../common.js'
12
+ import { payloadOf, type ToolPayload } from './payload.js'
13
+ import { handleStartApp } from './manage.js'
14
+ import { handleTapElement } from './input.js'
15
+ import { expectElementAbsent, expectFingerprint } from './journey-assertions.js'
16
+
17
+ type Journey = z.infer<typeof journeySchema>
18
+ type Step = Journey['steps'][number]
19
+ type AssertionStep = Extract<Step, { type: 'assert' }>
20
+ interface StepResult {
21
+ id: string; type: Step['type']; status: string; success: boolean; reason?: string
22
+ result: ToolPayload | null; timing?: { total_ms: number }
23
+ }
24
+ interface Context {
25
+ input: Journey; deviceId: string; results: StepResult[]; stoppedAt: string | null; pendingLightStep: number | null
26
+ }
27
+ async function assertStep(step: AssertionStep, platform: Journey['platform'], deviceId: string): Promise<ToolPayload> {
28
+ const assertion = step.assertion
29
+ const timeoutMs = step.timeoutMs ?? 5000, pollIntervalMs = step.pollIntervalMs ?? 100
30
+ if (assertion.kind === 'element_visible') return ToolsInteract.expectElementVisibleHandler({ selector: assertion.selector, timeout_ms: timeoutMs, poll_interval_ms: pollIntervalMs, platform, deviceId })
31
+ if (assertion.kind === 'element_absent') return expectElementAbsent({ selector: assertion.selector, timeoutMs, pollIntervalMs, platform, deviceId })
32
+ if (assertion.kind === 'screen_fingerprint') return expectFingerprint({ fingerprint: assertion.fingerprint, timeoutMs, platform, deviceId })
33
+ if (assertion.kind === 'state_equals') return ToolsInteract.expectStateHandler({ selector: assertion.selector, element_id: assertion.elementId, property: assertion.property, expected: assertion.expected, platform, deviceId, stabilization_window_ms: timeoutMs, poll_interval_ms: pollIntervalMs, exact_budget: true })
34
+ return { success: false, failure_code: 'INVALID_ASSERTION' }
35
+ }
36
+ async function executeStep(s: Context, step: Step, covered: boolean): Promise<ToolPayload> {
37
+ const { platform, responseMode, defaults } = s.input, deviceId = s.deviceId
38
+ try {
39
+ if (step.type === 'tap') return payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: covered ? 'none' : step.verificationMode ?? defaults?.verificationMode ?? 'full' }))
40
+ if (step.type === 'wait') {
41
+ const result = (await withObservationContext({ stepId: step.id, purpose: 'target' }, () => ToolsInteract.waitForUIHandler({ selector: step.selector, condition: step.condition ?? 'exists', timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId })))!
42
+ return { success: result.status === 'success', ...result }
43
+ }
44
+ if (step.type === 'assert') return await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, () => assertStep(step, platform, deviceId)))
45
+ if (step.type === 'start_app') return payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }))
46
+ return { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' }
47
+ } catch (error) {
48
+ return { success: false, failure_code: step.type === 'assert' ? 'ASSERTION_TIMEOUT' : 'STEP_FAILED', error: error instanceof Error ? error.message : String(error) }
49
+ }
50
+ }
51
+ function lightVerification(s: Context, step: Step, result: ToolPayload, covered: boolean, success: boolean) {
52
+ if (covered) {
53
+ result.verification = { mode: 'light', status: success ? 'unavailable' :
54
+ (['mismatch', 'unavailable'].includes(result.condition?.status ?? '') ? 'not_requested' : 'failed') }
55
+ if (success) {
56
+ result.verification_diagnostic = { code: 'VERIFICATION_UNAVAILABLE', retryable: true }
57
+ s.pendingLightStep = s.results.length
58
+ }
59
+ }
60
+ if (step.type !== 'assert' || s.pendingLightStep === null) return
61
+ const pending = s.results[s.pendingLightStep].result!
62
+ if (typeof result.observed?.matched_count === 'number') {
63
+ pending.verification!.status = 'observed'
64
+ if (pending.evidence) completeJourneyVerification(pending.evidence.action_id!)
65
+ delete pending.verification_diagnostic
66
+ const delta = ToolsObserve.getLatestStateDelta(s.input.platform, s.deviceId)
67
+ if (delta) { pending.state_delta = delta; pending.state_delta_available = true }
68
+ }
69
+ s.pendingLightStep = null
70
+ }
71
+ function compactAssertion(result: ToolPayload, success: boolean) {
72
+ const errorCode = typeof result.error === 'object' ? result.error?.code : undefined
73
+ return { success, ...(result.failure_code ? { failure_code: result.failure_code } : {}),
74
+ ...(errorCode ? { failure_code: errorCode } : {}),
75
+ ...(result.observed?.matched_count !== undefined ? { matched_count: result.observed.matched_count } : {}),
76
+ ...(result.observed_fingerprint !== undefined ? { matched: success } : {}),
77
+ ...(result.actual_state ? { observed: result.actual_state } : {}) }
78
+ }
79
+ function coveredByAssertion(s: Context, step: Step, next?: Step) {
80
+ const verification = ('verificationMode' in step ? step.verificationMode : undefined) ?? s.input.defaults?.verificationMode ?? 'full'
81
+ return step.type === 'tap' && s.input.responseMode === 'compact' && verification === 'light' && next?.type === 'assert' && next.assertion.kind === 'element_visible'
82
+ }
83
+ async function runSteps(s: Context) {
84
+ const { platform, responseMode, steps } = s.input
85
+ for (let index = 0; index < steps.length; index++) {
86
+ const step = steps[index]
87
+ if (platform === 'android' && debuggerSessions.paused(s.deviceId)) {
88
+ s.results.push({ id: step.id, type: step.type, status: 'failed', success: false, result: { success: false, failure_code: 'DEBUGGER_PAUSED', dispatch_started: false, delivery_status: 'not_delivered' } })
89
+ s.stoppedAt = step.id
90
+ break
91
+ }
92
+ const covered = coveredByAssertion(s, step, steps[index + 1]), started = performance.now()
93
+ if (s.stoppedAt) {
94
+ s.results.push({ id: step.id, type: step.type, status: 'not_run', success: false, reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 } })
95
+ continue
96
+ }
97
+ let result = await executeStep(s, step, covered)
98
+ if (platform === 'android' && debuggerSessions.paused(s.deviceId)) { result.success = false; result.failure_code = 'DEBUGGER_PAUSED'; result.retryable = false }
99
+ if (step.type === 'assert' && !result?.success && /TIMEOUT/.test(result?.failure_code ?? '')) result.failure_code = 'ASSERTION_TIMEOUT'
100
+ const success = !!result?.success
101
+ lightVerification(s, step, result, covered, success)
102
+ if (responseMode === 'compact' && ['wait', 'assert'].includes(step.type)) result = compactAssertion(result, success)
103
+ s.results.push({ id: step.id, type: step.type, status: success ? 'passed' : 'failed', success, result, timing: { total_ms: performance.now() - started } })
104
+ if (!success) s.stoppedAt = step.id
105
+ }
106
+ }
107
+ async function journeyResponse(s: Context, started: number, finishTrace?: () => Promise<unknown>) {
108
+ const { platform, captureOnFailure, responseMode } = s.input
109
+ const failureSnapshot = s.stoppedAt && captureOnFailure && responseMode === 'debug'
110
+ ? await ToolsObserve.captureDebugSnapshotHandler({ platform, deviceId: s.deviceId, includeScreenshot: false }).catch(() => null) : undefined
111
+ const timing: Record<string, number> = {}
112
+ for (const step of s.results) for (const [key, value] of Object.entries(step.result?.timing ?? {})) {
113
+ if (key !== 'total_ms' && typeof value === 'number') timing[key] = (timing[key] ?? 0) + value
114
+ }
115
+ refreshJourneyEvidence(s.results)
116
+ return wrapResponse({ ...(finishTrace ? { system_trace: await finishTrace() } : {}), success: s.stoppedAt === null, stopped_at_step_id: s.stoppedAt, steps: s.results, ...(failureSnapshot ? { failure_snapshot: failureSnapshot } : {}), timing: responseMode === 'compact' ? { total_ms: performance.now() - started } : { ...timing, total_ms: performance.now() - started } }, responseMode === 'debug')
117
+ }
118
+ export async function handleRunJourney(args: ToolCallArgs) {
119
+ const parsed = journeySchema.safeParse(args)
120
+ if (!parsed.success) throw new Error(`INVALID_ARGUMENT: ${parsed.error.message}`)
121
+ const device = await resolveTargetDevice({ platform: parsed.data.platform, deviceId: parsed.data.deviceId })
122
+ const finishTrace = parsed.data.systemTraceId ? await systemTraces.link(parsed.data.systemTraceId, device, currentEvidenceCorrelationId()!) : undefined
123
+ if (parsed.data.systemTraceId) bindSystemTrace(parsed.data.systemTraceId)
124
+ try {
125
+ return await withResolvedDevice(device, async () => {
126
+ const started = performance.now()
127
+ const state: Context = { input: parsed.data, deviceId: device.id, results: [], stoppedAt: null, pendingLightStep: null }
128
+ await runSteps(state)
129
+ return journeyResponse(state, started, finishTrace)
130
+ })
131
+ } finally { await finishTrace?.() }
132
+ }
@@ -0,0 +1,141 @@
1
+ import { measure } from '../../utils/timing.js'
2
+ import { notifyActionStart } from '../../interact/conditional-action.js'
3
+ import type {
4
+ InstallAppResponse,
5
+ ResetAppDataResponse,
6
+ TerminateAppResponse
7
+ } from '../../types.js'
8
+ import { AndroidManage, iOSManage, ToolsManage } from '../../manage/index.js'
9
+ import {
10
+ buildActionExecutionResult,
11
+ captureActionFingerprint,
12
+ getNumberArg,
13
+ getStringArg,
14
+ inferGenericFailure,
15
+ requireStringArg,
16
+ ToolCallArgs,
17
+ wrapResponse
18
+ } from '../common.js'
19
+ import type { PlatformArg, ProjectTypeArg } from './handler-types.js'
20
+ import { interactionControls, actionHandler } from './action-execution.js'
21
+
22
+ export async function executeStartApp(args: ToolCallArgs) {
23
+ const platform = requireStringArg(args, 'platform') as PlatformArg
24
+ const appId = requireStringArg(args, 'appId')
25
+ const deviceId = getStringArg(args, 'deviceId')
26
+ const { verificationMode, actionTimeoutMs } = interactionControls(args)
27
+ const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
28
+ notifyActionStart()
29
+ const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().startApp(appId, deviceId, actionTimeoutMs ?? (verificationMode === 'full' && getStringArg(args, 'responseMode') === undefined && args._journey !== true ? undefined : 10000)) : new iOSManage().startApp(appId, deviceId, verificationMode === 'full')))
30
+ const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
31
+ const result = buildActionExecutionResult({
32
+ actionType: 'start_app',
33
+ sourceModule: 'server',
34
+ device: res.device,
35
+ selector: { appId },
36
+ success: !!res.appStarted,
37
+ uiFingerprintBefore,
38
+ uiFingerprintAfter,
39
+ failure: res.appStarted ? undefined : inferGenericFailure(res.error),
40
+ details: {
41
+ launch_time_ms: res.launchTimeMs,
42
+ ...(typeof res.output === 'string' ? { output: res.output } : {}),
43
+ ...(res.device ? { device_id: res.device.id } : {}),
44
+ ...(typeof res.error === 'string' ? { error: res.error } : {}),
45
+ ...(res.diagnostics ? { diagnostics: res.diagnostics } : {}),
46
+ ...(res.observedApp ? { observed_app: res.observedApp } : {})
47
+ }
48
+ })
49
+ return wrapResponse(result)
50
+ }
51
+
52
+ export async function handleTerminateApp(args: ToolCallArgs) {
53
+ const platform = requireStringArg(args, 'platform') as PlatformArg
54
+ const appId = requireStringArg(args, 'appId')
55
+ const deviceId = getStringArg(args, 'deviceId')
56
+ const res = await (platform === 'android' ? new AndroidManage().terminateApp(appId, deviceId) : new iOSManage().terminateApp(appId, deviceId))
57
+ const response: TerminateAppResponse = { device: res.device, appTerminated: res.appTerminated }
58
+ return wrapResponse(response)
59
+ }
60
+
61
+ export async function executeRestartApp(args: ToolCallArgs) {
62
+ const platform = requireStringArg(args, 'platform') as PlatformArg
63
+ const appId = requireStringArg(args, 'appId')
64
+ const deviceId = getStringArg(args, 'deviceId')
65
+ const { verificationMode, actionTimeoutMs } = interactionControls(args)
66
+ const uiFingerprintBefore = verificationMode === 'full' ? await measure('pre_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
67
+ notifyActionStart()
68
+ const bounded = args._journey === true || verificationMode !== 'full' || args.responseMode !== undefined || actionTimeoutMs !== undefined
69
+ const res = await measure('dispatch_ms', () => (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId, bounded, actionTimeoutMs ?? 15000) : new iOSManage().restartApp(appId, deviceId, verificationMode === 'full', bounded, actionTimeoutMs ?? 15000)))
70
+ const uiFingerprintAfter = verificationMode === 'full' ? await measure('post_observation_ms', () => captureActionFingerprint(platform, deviceId)) : null
71
+ const result = buildActionExecutionResult({
72
+ actionType: 'restart_app',
73
+ sourceModule: 'server',
74
+ device: res.device,
75
+ selector: { appId },
76
+ success: !!res.appRestarted,
77
+ uiFingerprintBefore,
78
+ uiFingerprintAfter,
79
+ failure: res.appRestarted ? undefined : inferGenericFailure(res.error),
80
+ details: {
81
+ launch_time_ms: res.launchTimeMs,
82
+ ...(typeof res.output === 'string' ? { output: res.output } : {}),
83
+ ...(typeof res.terminatedBeforeRestart === 'boolean' ? { terminated_before_restart: res.terminatedBeforeRestart } : {}),
84
+ ...(typeof res.terminateError === 'string' ? { terminate_error: res.terminateError } : {}),
85
+ ...(typeof res.error === 'string' ? { error: res.error } : {}),
86
+ ...(res.diagnostics ? { diagnostics: res.diagnostics } : {}),
87
+ ...(res.observedApp ? { observed_app: res.observedApp } : {})
88
+ }
89
+ })
90
+ return wrapResponse(result)
91
+ }
92
+
93
+ export async function handleResetAppData(args: ToolCallArgs) {
94
+ const platform = requireStringArg(args, 'platform') as PlatformArg
95
+ const appId = requireStringArg(args, 'appId')
96
+ const deviceId = getStringArg(args, 'deviceId')
97
+ const res = await (platform === 'android' ? new AndroidManage().resetAppData(appId, deviceId) : new iOSManage().resetAppData(appId, deviceId))
98
+ const response: ResetAppDataResponse = { device: res.device, dataCleared: res.dataCleared }
99
+ return wrapResponse(response)
100
+ }
101
+
102
+ export async function handleInstallApp(args: ToolCallArgs) {
103
+ const platform = requireStringArg(args, 'platform') as PlatformArg
104
+ const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
105
+ const appPath = requireStringArg(args, 'appPath')
106
+ const deviceId = getStringArg(args, 'deviceId')
107
+ const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId, projectType })
108
+ const response: InstallAppResponse = {
109
+ device: res.device,
110
+ installed: res.installed,
111
+ output: res.output,
112
+ error: res.error
113
+ }
114
+ return wrapResponse(response)
115
+ }
116
+
117
+ export async function handleBuildApp(args: ToolCallArgs) {
118
+ const platform = requireStringArg(args, 'platform') as PlatformArg
119
+ const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
120
+ const projectPath = requireStringArg(args, 'projectPath')
121
+ const variant = getStringArg(args, 'variant')
122
+ const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant, projectType })
123
+ return wrapResponse(res)
124
+ }
125
+
126
+ export async function handleBuildAndInstall(args: ToolCallArgs) {
127
+ const platform = requireStringArg(args, 'platform') as PlatformArg
128
+ const projectType = requireStringArg(args, 'projectType') as ProjectTypeArg
129
+ const projectPath = requireStringArg(args, 'projectPath')
130
+ const deviceId = getStringArg(args, 'deviceId')
131
+ const timeout = getNumberArg(args, 'timeout')
132
+ const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType })
133
+ return {
134
+ content: [
135
+ { type: 'text' as const, text: res.ndjson },
136
+ { type: 'text' as const, text: JSON.stringify(res.result, null, 2) }
137
+ ]
138
+ }
139
+ }
140
+ export const handleStartApp = actionHandler('start_app', executeStartApp)
141
+ export const handleRestartApp = actionHandler('restart_app', executeRestartApp)